Discover more from Dissecting DeFi Protocols
Learn enough Foundry to be dangerous
Writing integration tests with Foundry
In the last episode of Dissecting DeFi protocols, I looked at the difference between LooksRare and Zora. This time, I am going to build a simple integration test with some happy and unhappy paths to demonstrate how Foundry works.
Yes, I am late to the party, but better late than never.
You can find the repository here.
Foundry tests are written in Solidity. To create a test, the contracts that need to be tested have to be deployed inside the test contract. They need to be stored as storage variables to be used by each test. Any reusable variables should also be defined as such.
Creating some personas
Well, what is a protocol good for if there are no users? We will need a seller and a buyer at least to test the order matching strategy.
Foundry provides a way for you to simulate different situations through the concept of cheat codes. In the code above, I first assign the special address constant
HEVM_ADDRESS to a cheat codes interface, and then compute the seller/buyer addresses with the private key 1 and 2 (private keys are just numbers). Normally you don’t need to create addresses to impersonate an address this way, but because we will need the private key to sign maker orders later, we are going to use the addr method. You can find more information about cheat codes here.
Printing some ultrasound money 🔊🦇
How are we going to buy NFTs if we don’t have ETH!? Let’s give the buyer 1 ETH and also wrap it, as we want to test the buy with ERC-20 functionality.
The function deal sets the buyer balance to 1 ETH and the function startPrank sets
msg.sender for all subsequent calls until stopPrank is called. If you come from hardhat, this is basically Foundry’s version of hardhat_impersonateAccount. There is another function prank that only sets
msg.sender for the next call. When I used the
prank function, I spent a long time debugging some tests only to realize the
msg.sender was wrong because I forgot it was only valid for one call!
MINT DAT NFT
We need an NFT to sell! For test purposes I have created a
TestERC721 contract and minted token ID 0 to the seller. I have also impersonated the seller to approve LooksRare’s ERC-721 transfer manager to operate on the seller’s token.
Happy path - a successful sale
All tests are functions that start with the prefix “test”. When you call
forge test in the terminal, Foundry knows automatically which tests to run.
You can also specify a test to run.
Or a test contract.
You get the idea.
I’ve started the test with some initial assertions to make sure the seller owns the NFT and has no ETH, the buyer has 1 ETH, and the fee recipients have no ETH.
A maker order signature is required for the transaction and I’ve created 2 helper functions
signOrder to create the signature. The
OrderTypes.MakerOrder comes from the protocol, I just set it as an 1 ETH ask for
TestERC721’s token ID 0, valid from the current block til 1 day from now. The cheat code sign is then used to create a signature.
With the signature created, we can now match a taker bid with the maker ask and submit the transaction on-chain.
The transaction didn’t revert, and we can now test our assertions!
The buyer received his NFT, the seller and the fee recipients got paid, awesome!
A reverted test
This reverted test is very similar to the happy path test, except I’ve cancelled the maker ask before matching it with the taker bid, called expectRevert to test the revert error message and tested that no assets changed hands after the
expectRevert is similar to hardhat’s
HALP! I NEED TO DEBUG
Often you will want to log information into the terminal for debug purposes. While Foundry does not have
console.log like hardhat does, there is an option to emit events with the data you want to see as arguments. The following events are available.
If you want to see the logs, you will have to run the tests with the verbose flag (
-vv) turned on.
And that’s it…! You now know enough Foundry to be dangerous!