Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

A Solidity Symphony: Testing Events With Foundry

JJGarcia.ethFollowBetter Programming--ListenShareIn the blockchain world, transaction execution and on-chain persistent storage modifications incur a fundamentally important cost. Observing emitted events is one way to circumvent the costs of on-chain interactions for data retrieval. Solidity events allow for a generally free and persistent way of capturing on-chain activities. Given the potential dependency of events by off-chain parties, careful orchestration and diligent testing of event emissions becomes necessary to ensure our smart contracts' robustness.In this example, we’ll conduct trivial and non-trivial approaches (Part 1 and Part 2, respectively) to testing emitted events with Foundry. The trivial approach will involve a direct function call and Foundry’s vm.expecteEmit() Cheat Code. The non-trivial approach will be with Solidity’s low-level call method and Foundry’s vm.recordLogs() and vm.getRecordedLogs() cheat codes. We’ll be using our simple (and unsecure) PiggyBank contract for this test.Bonus subsections also 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 these topics, we recommend reviewing the official Solidity documentation and the Foundry book’s Test section.Okay. Let’s begin!To begin, we’ll create our Foundry environment by running the following command:Your project’s structure and the Foundry configuration should now be similar to the below (btw, please feel free to remove that last line). The only notable difference you’ll likely see is the solc version within the foundry.toml configuration file. Please set your solc to match what I have below:Next, remove all the Counter*.sol contracts. You can also completely remove the script directory if you like. We will not be using it here.As previously mentioned, we will be using the PiggyBank contract within the PiggyBank.sol file. So, we’ll need to create this file within the ./src/ folder and copy the below events interface and contract into the file.We’ll start by taking a first look at the events.For the Deposited event, we have two variables that make up our topics:And one variable that makes up our data:For the Withdrawn event, we have one variable that makes up our topics:And one variable that makes up our data: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:We’re going to start somewhat backward. We’re going to begin by testing the Withdrawn event. You’ll see why when we reach 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.Beginning with the setUp() function, this function is a specific optional function known to forge that is invoked before each test case’s run. In this function, you’ll notice the vm.label() Foundry cheat code️. In this use case, this helper cheat code instructs forge to use the alias MSG_SENDER in place of the address of msg.sender in test traces. This will help us later in our assessments of the stack trace printouts.Now onto our testPiggyBank_Withdraw() test function.Here, 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.Next, we are conducting a deposit with an ether transfer.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 the withdrawal of our funds using our contact’s withdraw() function.For this effort, we will start by explaining the use of Foundry’s vm.startPrank() and vm.stopPrank() cheat codes. As noted in the official Foundry documentation, startPrank “Sets msg.sender for all subsequent calls until stopPrank is called.”Now for the important cheat code!Notice the vm.expectEmit() cheat code. We’ll break this down a bit, but also feel free to follow along with Foundry’s documentation on this.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.The fourth argument will represent the data portion of our emitted event. For this test, we want this to match, so that we will set this to true.For the fifth argument, this will check the emitter address (i.e., the contract’s address from which the event was emitted). 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 cheat code, we must follow it with the emission of the actual expected event from within our test:Finally, we can call our withdraw() function.Using Foundry’s built-in command-line interface forge, we will run the test:OK. That’s a lot to tell us we had a successful test. Let’s break this down some more!Starting at the top: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:For this we just care that our testPiggyBank_Withdraw() test status is [PASS]. Congratulations! We passed our first test 🥳.Going forward, you can skip to Part 2 or dive deeper into the stack trace.We’ll focus on only the necessary lines within Foundry’s stack trace to stay on track and remain focused on our withdraw function.Starting off, we can see that the address for the PiggyBank contract is 0x5615...b72f. Below, we notice our test specifies the event to watch for within the vm.expectEmit() cheat code. We see the expected value for to is our MSG_SENDER (0x1804...1f38) and 1000 for the expectedamount. We also can verify the PiggyBank address matches the address above.Next is the call to our withdraw() function:On the second line, we see the actual transfer to our MSG_SENDER. This is then followed by the emission of the Withdrawn event with the matching values for to as MSG_SENDER 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 to check the Deposited event’s emission.All right, 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:Like before, we’ll start at the top. The first noticeable line is the creation of a generic receiver:Overall, there’s not much to take away from this line. It’s simply creating our receiver address.Next, you may have noticed the introduction of a new vm.label() within our setUp() function. This label will be used to alias our account, which will be the recipient address for our withdraw() function.Within our testPiggyBank_Deposit() test function, you’ll notice familiar lines for contract creation and the variable storing the amount for transfer: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.Now, let’s proceed down the test function with one of the most important lines within the test:This line uses Foundry’s vm.recordLogs() cheat code to initiate the 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:Here, we introduce the vm.deal() cheat code. This cheat code 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 ensure the call was successful.Now! Onto the good stuff!Let’s break this down a bit. If we navigate into Foundry’s contracts, we’ll find the VmSafe interface within forge-std/Vm.sol. Here we can see the makeup of the Log struct: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 deeply dive into what this looks like with Foundry’s call stack analysis.Now onto the actual verification: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. Honestly, this is unnecessary, but I wanted to include it to show you how to use it 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() cheat code 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 that emitted within the test contract.So, assuming our topic 0 matches our Deposited signature, we’ll continue 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, as shown in the example, we need to convert the bytes32 values of topics 1 and 2 to addresses.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.If there were more data fields, we would have to handle this line a bit differently.The next line 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.All right! Time to actually test this!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).Next, we have our test completion status:This tells us our test status was [PASS].Now onto the good stuff!First, we notice the address for the PiggyBank contract is 0x5615...b72f. Next, we can see our initiation of event log recording with vm.recordLogs(). This is then followed by dealing 1000 ETH to MSG_SENDER and setting it as our active msg.sender.Continuing on, we can confirm our test is NOT specifying the event to watch for with the vm.expectEmit() cheat code. This should make sense.Here, we have the low-level call to our deposit() function where we can confirm our msg.value is 1000 and our to address is our RECEIVER (0xfb64...204c). As expected, we then notice our event being emitted 🧃. From here, it’s clear that our from matches the expected MSG_SENDER, to matches the expected RECEIVER, and amount matches the expected amount.Well, that’s warm and fuzzy!Now onto the nitty-gritty: our collected log.I hope this line makes it clear how multiple events would be packaged within this array.Moving inward, we’ll 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:Well, that looks like a match!Next, we’ll take a look at the following 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!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.Thank you for reading, and I hope it was fun.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 with a note from this article if you’d like to connect.----Better ProgrammingJJGarcia.ethinBetter Programming--Sergei SavvovinBetter Programming--5Sami MaameriinBetter Programming--7Dmitry KruglovinBetter Programming--31Chirag AgrawalinInfoSec Write-ups--1Harsh Winder--James LiminCoinsBench--Cayo TorinCoinmonks--kiecodes--Alexander Zammit--HelpStatusWritersBlogCareersPrivacyTermsAboutText to speechTeams



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

A Solidity Symphony: Testing Events With Foundry

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×