# A Solidity Symphony: Testing Events with Foundry

By [JJGarcia.eth](https://paragraph.com/@jjgarcia) · 2023-07-12

---

![](https://storage.googleapis.com/papyrus_images/cd9a54405f1c9b5c09e99ab74fd56f11e8a91bc824527e5b8deb373a43085810.png)

* * *

### To Begin…

In the world of blockchain, transaction execution and onchain persistent storage modifications incur a fundamentally important cost. One way to circumvent the costs of onchain interactions for data retrieval is through the observance of emitted events. [Solidity events](https://docs.soliditylang.org/en/v0.6.7/contracts.html#events) allow for a generally free and persistent way of capturing onchain activities. Given the potential dependency of events by offchain parties, careful orchestration and diligent testing of event emissions becomes a necessity to ensure the robustness of our smart contracts.

In this example, we’ll conduct both a trivial and non-trivial approaches (Part 1 and Part 2 respectively) to testing emitted events with Foundry. The trivial approach will be with a direct function call and Foundry’s `vm.expecteEmit()` cheatcode. The non-trivial approach will be with Solidity’s low-level call method and Foundry’s `vm.recordLogs()` and `vm.getRecordedLogs()` cheatcodes. We’ll be using our simple (**and unsecure**) `PiggyBank` contract for this test.

There are also bonus subsections that cover analyzing Foundry’s stack trace printouts provided by verbose test runs.

Please note: This article will assume you have basic knowledge of the Solidity language, events, and Foundry testing. If you need a refresher on events please read through the official [Solidity documentation](https://docs.soliditylang.org/en/v0.8.20/contracts.html#events).

Okay. Let’s begin!

* * *

### The Events

We’ll start by taking a first look at the events.

    contract PiggyBankEvents {
        event Deposited(address indexed from, address indexed to, uint256 amount);
        event Withdrawn(address indexed to, uint256 amount);
    }
    

For the `Deposited` event we have two variables that make up our `topics`:

*   `from`: the sender account
    
*   `to`: the receiver account
    

And one variable that makes up our `data`:

*   `amount`: the amount to deposit
    

For the `Withdrawn` event we have one variable that makes up our `topics`:

*   `to`: the receiver account
    

And one variable that makes up our `data`:

*   `amount`: the amount to withdraw
    

### The Contract

    pragma solidity 0.8.20;
    
    contract PiggyBank is PiggyBankEvents {
        uint256 public totalBalance;
        mapping(address account => uint256) public balances;
    
        function deposit(address _account) external payable {
            require(msg.value != 0, "invalid deposit");
    
            // Increment record
            totalBalance += msg.value;
            balances[_account] += msg.value;
    
            // Emit event
            emit Deposited(msg.sender, _account, msg.value);
        }
    
        function withdraw(uint256 _amount) external {
            require(balances[msg.sender] >= _amount, "balance too low");
    
            // Decrement record
            totalBalance -= _amount;
            balances[msg.sender] -= _amount;
    
            payable(msg.sender).transfer(_amount);
    
            // Emit event
            emit Withdrawn(msg.sender, _amount);
        }
    }
    

Here is our `PiggyBank` contract. In summary, it allows for deposits and withdraws of ether. Please note that this is not a secure contract and is purely for demo purposes.

For this contract, the important takeaways are:

1.  The inheritance of the `PiggyBankEvents` contract.
    
2.  The `Deposited` event emission within the `deposit()` function.
    
3.  The `Withdrawn` event emission within the `withdraw()` function.
    

* * *

### The Test: Part 1

We’re going to start off somewhat backwards. We’re going to begin by testing the `Withdrawn` event. You’ll see why when we get to the `Deposited` event test section (Part 2).

Here we go 🌪️…

Below is the portion of our Foundry test for validating withdrawals. Please note 📝 this test contract also inherits the `PiggyBankEvents` contract which gives it access to the same events as our `PiggyBank` contract.

    pragma solidity 0.8.20;
    
    import {Test, Vm} from "forge-std/Test.sol";
    
    contract PiggyBankTest is Test, PiggyBankEvents {
        function testPiggyBank_Withdraw() public {
            // Create PiggyBank contract
            PiggyBank piggyBank = new PiggyBank();
            uint256 _amount = 1000;
    
            // Deposit
            vm.deal(msg.sender, _amount);
            vm.startPrank(msg.sender);
            (bool _success, ) = address(piggyBank).call{value: _amount}(
                abi.encodeWithSignature("deposit(address)", msg.sender)
            );
            assertTrue(_success, "deposited payment.");
            vm.stopPrank();
    
            // Set withdraw event expectations
            vm.expectEmit(true, false, false, true, address(piggyBank));
            emit Withdrawn(msg.sender, 1000);
    
            // Withdraw
            vm.startPrank(msg.sender);
            piggyBank.withdraw(_amount);
            vm.stopPrank();
        }
    
        ...
    }
    

Starting from the top we can see we’re creating a new instance of our `PiggyBank` contract and setting the `_amount` value, which will be the amount we are depositing and withdrawing.

    // Create PiggyBank 
    PiggyBank piggyBank = new PiggyBank();
    uint256 _amount = 1000;
    

Next, we are conducting a deposit with an ether transfer.

    // Deposit
    vm.deal(msg.sender, _amount);
    vm.startPrank(msg.sender);
    (bool _success, ) = address(piggyBank).call{value: _amount}(
        abi.encodeWithSignature("deposit(address)", msg.sender)
    );
    assertTrue(_success, "deposited payment.");
    vm.stopPrank();
    

Notice here that we are only verifying the success of the transaction with `assertTrue(_success, ...)`. There is no check for the `Deposited` event emission. Just hold onto that 🤔.

Continuing on, we’ll conduct our withdrawal of our funds using our contact’s `withdraw()` function.

    // Set withdraw event expectations
    vm.expectEmit(true, false, false, true, address(piggyBank));
    emit Withdrawn(msg.sender, _amount);
    
    // Withdraw
    vm.startPrank(msg.sender);
    piggyBank.withdraw(_amount);
    vm.stopPrank();
    

For this the effort, we will start by explaining the use of Foundry’s `startPrank()` and `stopPrank()` cheatcodes. As noted in the [official Foundry documentation](https://book.getfoundry.sh/cheatcodes/start-prank), `startPrank` “Sets `msg.sender` **for all subsequent calls** until `stopPrank` is called.”

Now for the important cheatcode 🤗

Notice the `vm.expectEmit()` cheatcode. We’ll break this down a bit, but also feel free to following along with [Foundry’s documentation on this](https://book.getfoundry.sh/cheatcodes/expect-emit).

For `vm.expectEmit()`, the first three arguments are for the three possible `topics` of the event. According to the Solidity documentation, events allow us to “add the attribute `indexed` to up to **three** parameters which adds them to a special data structure known as `topics` instead of the data part of the log". Since our `Withdrawn` event is only making use of the first `topic`, we only want to set the first argument to `true`. Setting this argument to `false` would tell Foundry that you do not care if the actual emitted results match up.

For the fourth argument, this will represent the data portion of our emitted event. For this test we want this to match, so we will set this to `true`.

For the fifth argument, this will check the emitter address (i.e. the contract’s address that the event was emitted from). We will specify this as the `PiggyBank` contract’s address since that is where we expect the event to be emitted from.

Following the `expectEmit` cheatcode, we must follow it with the emission of the actual expected event from within our test:

    vm.expectEmit(true, false, false, true, address(piggyBank));
    emit Withdrawn(msg.sender, _amount);
    

Finally, we can call our `withdraw()` function.

    // Withdraw
    ...
    piggyBank.withdraw(_amount);
    ...
    

Using Foundry’s builtin command-line interface `forge`, we will run the test:

    $ forge test --match-test testPiggyBank_Withdraw -vvvvv
    [⠔] Compiling...
    No files changed, compilation skipped
    
    Running 1 test for test/Scratch__test.sol:PiggyBankTest
    [PASS] testPiggyBank_Withdraw() (gas: 218500)
    Traces:
      [218500] PiggyBankTest::testPiggyBank_Withdraw() 
        ├─ [155802] → new PiggyBank@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
        │   └─ ← 778 bytes of code
        ├─ [0] VM::deal(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, 1000) 
        │   └─ ← ()
        ├─ [0] VM::startPrank(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38) 
        │   └─ ← ()
        ├─ [46612] PiggyBank::deposit{value: 1000}(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38) 
        │   ├─ emit Deposited(from: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, to: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, amount: 1000)
        │   └─ ← ()
        ├─ [0] VM::stopPrank() 
        │   └─ ← ()
        ├─ [0] VM::startPrank(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38) 
        │   └─ ← ()
        ├─ [0] VM::expectEmit(true, false, false, true, PiggyBank: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) 
        │   └─ ← ()
        ├─ emit Withdrawn(to: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, amount: 1000)
        ├─ [7533] PiggyBank::withdraw(1000) 
        │   ├─ [0] 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38::fallback{value: 1000}() 
        │   │   └─ ← ()
        │   ├─ emit Withdrawn(to: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, amount: 1000)
        │   └─ ← ()
        ├─ [0] VM::stopPrank() 
        │   └─ ← ()
        └─ ← ()
    

Okay. That’s a lot to tell us we had a successful test. Let’s break this down some more!

Starting at the top:

    $ forge test --match-test testPiggyBank_Withdraw -vvvvv
    

This is our command which specifies we’re running a `test` named `testPiggyBank_Withdraw` with level 5 verbosity (stack traces and setup traces are always displayed). Note that `testPiggyBank_Withdraw` is the name of our function with the test. That is not a coincidence.

Next we have our test completion status:

    [PASS] testPiggyBank_Withdraw() (gas: 218500)
    

For this we just care that our `testPiggyBank_Withdraw()` test status is `[PASS]`. Congratulations! We passed our first test 🥳.

#### Call stack analysis:

Going forward, you can skip to Part 2, or dive deeper into the stack trace 🧠.

To stay on track and remain focused on our withdraw function, we’ll focus on only the necessary lines within Foundry’s stack trace.

    [218500] PiggyBankTest::testPiggyBank_Withdraw()
    ├─ [155802] → new PiggyBank@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    .
    .
    .
    ├─ [0] VM::expectEmit(true, false, false, true, PiggyBank: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) 
        │   └─ ← ()
        ├─ emit Withdrawn(to: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, amount: 1000)
    

Starting off, we can see that the address for the `PiggyBank` contract is `0x5615...b72f`. Below, we notice our test is specifying the event to watch for within the `expectEmit` cheatcode. We see the expected values for `to` as `0x1804...1f38` and `1000` for `amount`, and we also see the `PiggyBank` address matches the address above.

Next is the call to our `withdraw()` function:

    ├─ [7533] PiggyBank::withdraw(1000) 
        │   ├─ [0] 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38::fallback{value: 1000}() 
        │   │   └─ ← ()
        │   ├─ emit Withdrawn(to: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, amount: 1000)
        │   └─ ← ()
    

On the second line we see the actual transfer to account `0x1804...1f38`. This is followed by the emission of the `Withdrawn` event with the matching values for `to` as `0x1804...1f38` and `1000` for `amount`. And that’s it. We’ve verified through the stack trace what Foundry’s forge has already told us 🤓.

Now we continue on to check the `Deposited` event’s emission.

* * *

### The Test: Part 2

Alright, we’ve made it this far. No turning back now!

Now we have our `deposit()` function that emits the `Deposited` event. We also see it is a `payable` function that is intended to be called with a `msg.value` that is not 0. Therefore, our `deposit()` test can be written as follows:

    
    pragma solidity 0.8.20;
    
    import {Test, Vm} from "forge-std/Test.sol";
    
    contract PiggyBankTest is Test, PiggyBankEvents {
        ...
    
        address internal constant RECEIVER =
            address(uint160(uint256(keccak256("piggy bank test receiver"))));
    
        function testPiggyBank_Deposit() public {
            PiggyBank piggyBank = new PiggyBank();
            uint256 _amount = 1000;
    
            // Start recording all emitted events
            vm.recordLogs();
    
            // Deposit
            vm.deal(msg.sender, _amount);
            vm.startPrank(msg.sender);
            (bool _success, ) = address(piggyBank).call{value: _amount}(
                abi.encodeWithSignature("deposit(address)", RECEIVER)
            );
            vm.stopPrank();
    
            assertTrue(_success, "deposited payment failure.");
    
            // Consume the recorded logs
            Vm.Log[] memory entries = vm.getRecordedLogs();
    
            // Check logs
            bytes32 deposited_event_signature = keccak256(
                "Deposited(address,address,uint256)"
            );
    
            for (uint256 i; i < entries.length; i++) {
                if (entries[i].topics[0] == deposited_event_signature) {
                    assertEq(
                        address(uint160(uint256((entries[i].topics[1])))),
                        msg.sender,
                        "emitted sender mismatch."
                    );
                    assertEq(
                        address(uint160(uint256((entries[i].topics[2])))),
                        RECEIVER,
                        "emitted receiver mismatch."
                    );
                    assertEq(
                        abi.decode(entries[i].data, (uint256)),
                        _amount,
                        "emitted amount mismatch."
                    );
                    assertEq(
                        entries[i].emitter,
                        address(piggyBank),
                        "emitter contract mismatch."
                    );
    
                    break;
                }
    
                if (i == entries.length - 1)
                    fail("emitted deposited event not found.");
            }
        }
    }
    

Like before, we’ll start at the top. The first noticeable line is the creation of a generic receiver:

    address internal constant RECEIVER =
            address(uint160(uint256(keccak256("piggy bank test receiver"))));
    

Overall, there’s not much to take away from this line 🐒. It’s simply creating our receiver address.

Next, you’ll notice familiar lines for contract creation and the variable storing the amount for transfer:

    PiggyBank piggyBank = new PiggyBank();
    int256 _amount = 1000;
    

For the following lines, take a look at the differences and similarities from our previous test with the `Withdrawn` event. I want to point out two specific important differences.

1.  We are not using the `vm.expectEmit()` cheatcode nor are we emitting the expected event from within our test contract.
    
2.  Instead of invoking the `PiggyBank` contract’s `deposit()` function directly, we are using the low-level `call` function to send ether to the payable function.
    

Now let’s proceed down the test function with one of the most important lines within the test:

    // Start recording all emitted events
    vm.recordLogs();
    

This line uses Foundry’s `vm.recordLogs()` cheatcode to initiate recording of all emitted events. This will help us later to assess the `Withdrawn` event emitted by the low-level call of the `deposit()` function.

Now we can perform the actual deposit:

    // Deposit
    vm.deal(msg.sender, _amount);
    vm.startPrank(msg.sender);
    (bool _success, ) = address(piggyBank).call{value: _amount}(
        abi.encodeWithSignature("deposit(address)", RECEIVER)
    );
    vm.stopPrank();
    
    assertTrue(_success, "deposited payment.");
    

Here, we introduce the `vm.deal()` cheatcode. This cheatcode allows us to fund the `msg.sender` account with ether 💸.

We won’t go into `vm.startPrank()` and `vm.stopPrank()` given we covered them in Part 1.

For the low-level call of the `deposit` function, we can see that we are sending `_amount` ether to `RECEIVER`.

And finally, we have a quick check to make sure the call was successful.

Now! Onto the good stuff 🤩!

    // Consume the recorded logs
    Vm.Log[] memory entries = vm.getRecordedLogs();
    

Let’s break this down a bit. If we navigate into Foundry’s contracts, we’ll find the `VmSafe` interface within `Vm.sol`. Here we can see the makeup of the `Log` struct:

    interface VmSafe {
        struct Log {
            bytes32[] topics;
            bytes data;
            address emitter;
        }
    ...
    }
    

We’ll these look familiar! So it looks like we have fields for the event `topics`, `data`, and `emitter` contract. As we continue on the same line, we can see that the `vm.getRecordedLogs()` is called to consume the recorded logs. Thank you `vm.recordLogs()` 🤜🏽🤛🏾! Later we will take a deep dive into what this looks like with Foundry’s call stack analysis.

Now onto the actual verification:

    // Check logs
    bytes32 deposited_event_signature = keccak256(
        "Deposited(address,address,uint256)"
    );
    
    for (uint256 i; i < entries.length; i++) {
        if (entries[i].topics[0] == deposited_event_signature) {
            assertEq(
                address(uint160(uint256((entries[i].topics[1])))),
                msg.sender,
                "emitted sender mismatch."
            );
            assertEq(
                address(uint160(uint256((entries[i].topics[2])))),
                RECEIVER,
                "emitted receiver mismatch."
            );
            assertEq(
                abi.decode(entries[i].data, (uint256)),
                _amount,
                "emitted amount mismatch."
            );
            assertEq(
                entries[i].emitter,
                address(piggyBank),
                "emitter contract mismatch."
            );
    
            break;
        }
    
        if (i == entries.length - 1)
            fail("emitted deposited event not found.");
        }
    }
    

There’s a bit more to this section, but I promise it’s not bad.

The first line creates our `Deposited` event signature, which will be used for identifying our event from within the `entries` struct array.

Next we have a loop. In all honestly, this is not needed, but I wanted to include it to show you how you can use this with more complex implementations where multiple events are triggered within a single call. If the loop bothers you, pretend there is no loop and every `i` is `0`.

The following conditional checks to see if our `entries[i].topics[0]` matches our event’s signature. I know what you’re thinking 💡. Exactly! “That’s what Foundry’s documentation of the `vm.expectEmit()` cheatcode means by ‘Topic 0 is always checked’” 🤯. It only makes sense that Foundry’s `vm.expectEmit()` would always check that the event emitted by the contract is the same as the event emitted within the test contract.

So assuming our topic 0 matches our `Deposited` signature, we’ll continue down to comparing topics 1 and 2. Remember that topics 1 and 2 are addresses. We also can see from the `Log` struct that `entries[i].topics` is a bytes32 array. Therefore, we need to convert the bytes32 values of topics 1 and 2 to addresses, as shown in the example.

If we had an additional topic (i.e. topic 3), we would simply access it with `entries[i].topics[3]`.

Now for the `data` field. As we saw above, the `entries[i].data` is a byte array. Given we know the data type expected is a `uint256`, as seen within the declaration of the `Deposit` event, we can simply use solidity’s builtin `decode` method to parse the `amount` value from the `data` field.

    abi.decode(entries[i].data, (uint256))
    

If there were more data fields, we would have to handle this line a bit differently.

The next line simply breaks the loop and the last line is a sanity check that should never execute. The last line fails the test should no `Deposited` event have been emitted.

Alright! Time to actually test this!

    $ forge test --match-test testPiggyBank_Deposit -vvvvv
    [⠃] Compiling...
    No files changed, compilation skipped
    
    Running 1 test for test/Scratch__test.sol:PiggyBankTest
    [PASS] testPiggyBank_Deposit() (gas: 263253)
    Traces:
      [263253] PiggyBankTest::testPiggyBank_Deposit() 
        ├─ [169214] → new PiggyBank@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
        │   └─ ← 845 bytes of code
        ├─ [0] VM::recordLogs() 
        │   └─ ← ()
        ├─ [0] VM::deal(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, 1000) 
        │   └─ ← ()
        ├─ [0] VM::startPrank(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38) 
        │   └─ ← ()
        ├─ [46634] PiggyBank::deposit{value: 1000}(0xfb64bE75D69E2850c43758e8a2684031f753204c) 
        │   ├─ emit Deposited(from: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, to: 0xfb64bE75D69E2850c43758e8a2684031f753204c, amount: 1000)
        │   └─ ← ()
        ├─ [0] VM::stopPrank() 
        │   └─ ← ()
        ├─ [0] VM::getRecordedLogs() 
        │   └─ ← [([0x8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7, 0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38, 0x000000000000000000000000fb64be75d69e2850c43758e8a2684031f753204c], 0x00000000000000000000000000000000000000000000000000000000000003e8)]
        └─ ← ()
    

I hope this looks familiar. We have our command for specifying our `test` named `testPiggyBank_Deposit` with level 5 verbosity (stack traces and setup traces are always displayed).

    $ forge test --match-test testPiggyBank_Deposit -vvvvv
    

Next we have our test completion status:

    [PASS] testPiggyBank_Deposit() (gas: 263253)
    

This tells us our test status was `[PASS]`🥳.

#### Call stack analysis:

Now onto the good stuff 👩🏿‍💻!

    [263253] PiggyBankTest::testPiggyBank_Deposit() 
        ├─ [169214] → new PiggyBank@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    .
    .
    .
    ├─ [0] VM::recordLogs()
    ├─ [0] VM::startPrank(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38) 
        │   └─ ← ()
    

Here we can see that the address for the `PiggyBank` contract is `0x5615...b72f`. Below, we notice our test is **NOT** specifying the event to watch for within the `expectEmit` cheatcode. This should make sense.

Then we have our call to start event log recording and our setting of `0x1804...1f38` as our active `msg.sender`.

Next is the low-level call to our `deposit()` function:

    ├─ [46634] PiggyBank::deposit{value: 1000}(0xfb64bE75D69E2850c43758e8a2684031f753204c) 
        │   ├─ emit Deposited(from: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, to: 0xfb64bE75D69E2850c43758e8a2684031f753204c, amount: 1000)
    

At the low-level call, we can see that our `msg.value` is `1000` and our `to` address is `0xfb64...204c`.

Then we can see our event being emitted 🧃. From here, it’s clear that our `from` matches the expected sender, `to` matches the expected receiver, and `amount` matches the expected amount.

We’ll that’s a warm and fuzzy!

Now onto the nitty-gritty, our collected log.

    ├─ [0] VM::getRecordedLogs() 
        │   └─ ← [([0x8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7, 0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38, 0x000000000000000000000000fb64be75d69e2850c43758e8a2684031f753204c], 0x00000000000000000000000000000000000000000000000000000000000003e8)]
        └─ ← ()
    

I hope this line makes it clear on how multiple events would be packaged within this array.

Moving inward, we’ll take a look at the first topic within the first and only `Log` struct of the array.

Let’s verify this on the fly with another great Foundry tool, cast:

    $ cast keccak "Deposited(address,address,uint256)"
    0x8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7
    

Well, that looks like a match!

Next we’ll take a look at the next two topics:

Those two look like exact matches to our expected `from` and `to` addresses respectively. Of course they are padded out to bytes32, hence the `address(uint160(uint256(...)` conversions.

The next and final verification is done on the data portion of the recorded logs. Given we only have one value within the data field, this is relatively trivial:

And the decimal conversion of `0x03e8` is the expected `1000`!

* * *

### Finale!

Wow! Congrats to you and me if you’re still here. That was a quick run through of event testing with Foundry. If you have any suggestions, comments, or requests for clarification, please do reach out.

If this made you want to throw money into a mostly empty pocket, please feel free to toss it here: 0x0b1928F5EbCFF7d9d2c8d72c608479d27117b14D.

If you’re a LinkedIn connection master, please reach out to me on [LinkedIn](https://www.linkedin.com/in/24-jason-j-garcia/) with a note from this article if you’d like to connect.

Thank you for taking time out of your day to read my work and I hope it was fun.

---

*Originally published on [JJGarcia.eth](https://paragraph.com/@jjgarcia/a-solidity-symphony-testing-events-with-foundry)*
