Foundry is a fast, portable toolkit for Ethereum development. It’s an improvement on the amazing Dapptools framework, written in Rust (even God is rewriting the human genetic code in Rust and you have doubts anon?).
If you’re hearing about Foundry for the first time, then I suggest you first check out
Mainnet forking is when you fork the state of a live network and use it locally, usually to test your contracts against the live state. This can be extremely helpful while testing and can get you started without having to deploy a bunch of contracts to a local testnet to set up the environment.
In this article, we’re gonna locally simulate a swap on Uniswap. I chose Uniswap because playing around with the live state of something we all use sounded the most fun. I have intentionally chosen an approach that requires the least code to simulate the swap, so we can just focus on forge mainnet forking. We will be simulating the swap on the Rinkeby
network since it’s easier for this example, but the steps should be identical while forking from Mainnet.
In Ethereum, all you need to interact with a contract is the contract address, the function you’re calling, and the input parameters. The function name and the input parameters are encoded and set as the call data for the transaction.
The easiest way is to just attempt the same swap on the Uniswap interface and get the generated data from MetaMask. Let’s do that. In case you don’t have Rinkeby Eth, if not you can get some from Paradigm’s awesome MultiFaucet.
Go on to the Uniswap interface, make sure you’re on the Rinkeby
network, and initiate a swap for 1 Eth to Dai. It should look like this.
Initiate the swap, and you should get the familiar MetaMask popup. Now instead of confirming the transaction, check out the Data and Hex tab.
We can see that we are calling the Multicall
function with uint256
and bytes[]
as input parameters. (Ignore the Fetch failed).
On the HEX tab, you’ll see parameter information and a lot of Hex data (cropped out here). This is the hex encoding of the function signature and the input parameters.
Now we have
We have everything to simulate the swap locally.
Before we start, let’s quickly see what the call data encodes. For this, we can use this ETH Calldata Decoder by Apoorv Lathey. After decoding the calldata, we see that the uint256
parameter is deadline
which is a UNIX timestamp. We can assume that’s it’s the deadline before the swap must take place, we will play with this later.
Go to your terminal, and run the following commands to initialize a forge project and cd
into the newly created folder.
forge init simulate-swap
cd simulate-swap
Navigate to src/test/Contract.t.sol
, this is where we will be writing our simulation.
Paste this snippet in Contract.t.sol
.
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.6;
import "ds-test/test.sol";
// for cheatcodes
interface Vm {
function prank(address) external;
function warp(uint256) external;
}
// to verfiy that we recevied Dai
interface Dai {
function balanceOf(address) external view returns (uint256);
}
contract ContractTest is DSTest {
Vm vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
Dai dai = Dai(0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735);
// your address
address constant myAddress = "your ethereum address";
// contract address from MetaMask
address constant swapDai = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
function testSwap() public {
// to verify if we are actually forking
emit log_named_uint("Current ether balance of myAddress", myAddress.balance);
emit log_named_uint("Dai balance of myAddress before", dai.balanceOf(myAddress));
// type casting hex call data to bytes
bytes
memory data = hex"paste the hex call data here. Remove the leading '0x' ";
// setting the next call's msg.sender as myAddress
vm.prank(myAddress);
(bool sent, ) = payable(swapDai).call{value: 1 ether}(data);
require(sent, "failed");
emit log_named_uint("Dai balance of myAddress after", dai.balanceOf(myAddress));
}
}
Dai
interface is to get the balance of our address before and after the swap.Vm
interface is to use cheatcodes.
vm.prank(myAddress)
sets the msg.sender of the next call to myAddress
vm.warp(uint)
allows you to set the block.timestamp.call
We just need to set one environment variable, ETH_RPC_URL
to your RPC provider endpoint. Make sure it’s the endpoint for the network you want to fork.
Run this command in your terminal.
ETH_RPC_URL="paste your rpc url here"
Now that’s everything is set up, let’s run the simulation. Run this command in your terminal.
forge test --fork-url $ETH_RPC_URL -vv
You should see an output like this
Well done! You just successfully forked the state of a live network and played God with it.
If you are experimenting with this swap for a considerable amount of time, you might notice that after a while the swap doesn’t work. This is because of the deadline parameter that we saw earlier. After the deadline the swap is invalid. What do can we do? You know where I’m going with this, cheatcodes!
This can be a fun exercise, using warp(uint)
+ the decoded deadline
timestamp, play around and see when the swap is valid/invalid. Happy forging!