Using Foundry to give yourself $1bn

so you want to be a billionaire?

Okay, okay, so you won’t technically become a billionaire from this method (sorry for the clickbait). You will be a pretend billionaire though. And hopefully, this post helps you develop a sick DeFi app that takes the world by storm that takes you a step closer to actually become one. This post outlines how to use forge-std to improve your experience using mainnet forks and existing protocols & tokens. Using forge-std, we can remove the need of performing a swap or impersonating an account to obtain arbitrary tokens - in just 3 lines of code. Even if you never write mainnet fork tests, you will find value in this post as writing token balances is just one application of the tools in the stdlib.

If you don’t know what mainnet forking is, or why you might want to use it, check out this great write up of using mainnet forks in Forge:

forge cheatcodes

Forge, the testing framework built into foundry, takes queues from dapptools with giving users special abilities to interact with the EVM that otherwise is impossible. These abilities are called “cheatcodes” - and they are truly OP. You can see a brief overview of them here:

This book also has a guide for getting setup with Foundry, if you haven’t already.

enough preamble, lets rival Bezos in wealth

First, add forge-std as a dependency in your forge repo:

forge install brockelmore/forge-std

Create a test contract and add the following:

import "ds-test/test.sol";
import "forge-std/stdlib.sol";
import "forge-std/Vm.sol";

interface IERC20 {
    function balanceOf(address) external view returns (uint256);
}

contract TestContract is DSTest {
    using stdStorage for StdStorage;
    StdStorage stdstore;

    function writeTokenBalance(address who, address token, uint256 amt) internal {
        stdstore
            .target(token)
            .sig(IERC20(token).balanceOf.selector)
            .with_key(who)
            .checked_write(amt);
    }
}

(okay technically this is more than 3 lines of code, but if you flattened out the stdstore call the function itself would be 3 lines)

But boom! This function will now let you write the balance of any address for (almost) any token. All you have to do is call something like:

writeTokenBalance(address(this), address(dai), 1_000_000_000 * 1e18);

You are now a billionaire - congrats!

You’re encouraged to stop reading and go try this out. I am now going to go into how this works, why its so cool, and some limitations.

how it works

This will get a bit technical. For you as a user, all you have to know is:

  1. Set the target contract with stdstore.target(target)
  2. Set the function signature with .sig(MyContractInterface(target).myFunc.selector)
  3. Set the inputs with .with_key(an_address_or_uint_or_whatever)
  4. if multiple inputs, just add more with_key

This is basically a nicer ux for doing a low level abi.encodeWithSelector. As a user, just look at what the function takes as input and think “okay param 1 of the function is an address that is read and I want to give this test contract some tokens so I will do with_key(address(this))”.

forge-std does some magic under the hood that will be explained here. First let me explain what inside forge makes this possible.

There are 2 cheatcodes that give us this crazy power. The first is the record() cheatcode. This tells forge to record all SLOADs and SSTOREs for each address. The second is accesses(address), which takes an address and returns 2 lists, one of the SLOADs that occurred on that address, the other the SSTOREs that occurred on that address. With this, the stdlib does the following:

    stdstore_vm.record();
    bytes32 fdat;
    {
        (, bytes memory rdat) = who.staticcall(cald);
        fdat = bytesToBytes32(rdat, 32*field_depth);
    }
    
    (bytes32[] memory reads, ) = stdstore_vm.accesses(address(who));

This is rather unreadable (sorry), so lets break it down. We tell forge to record by making a call to the cheatcode address (stdstore_vm). We then make a call to who (the specified target), passing in the abi encoded calldata (cald). The calldata is constructed from the .sig(bytes4) call we did above (i.e. IERC20(token).balanceOf.selector) and the keys, or inputs (i.e. .with_key(address(this))). This is rather low level, but basically it constructs the data needed to do a low level call to the target, recording the slots read. The stdlib then uses the accesses cheatcode to get all the storage slots read.

That was a little complex. Basically, the flow is:

  1. Parameterization of the call you are going to make (sig and with_key functions)
  2. Start recording
  3. Make the call to the target smart contract
  4. Grab the SLOADs
  5. If multiple slots are read, use the store cheatcode to set the storage slot to a known magic number and make the call again, checking if its equal to the magic number
  6. if it is, we have identified the slot, if not continue iterating thru the reads

We have now found the exact storage slot that is used to store information for a particular function call! Now, if the user used checked_write(value), just do a vm.store(target, slot, value), or if they used find(), just return the slot to them. And presto-chango, the balance of the user has been updated to the value passed into checked_write.

why this is so cool

First, it is guaranteed to work for any view function that reads a single storage slot & isn’t a packed storage slot. It doesn’t matter if it is a mapping, flat variable, or extremely deeply nested mapping. So if you want to change the admin of a contract, an internal balance in a protocol, or manipulate an oracle price, you can use the stdlib to do so.

For example, take the follow contract as an example:

contract StorageTest {
    function hidden() public view returns (bytes32 t) {
        bytes32 slot = keccak256("my.random.var");
        assembly {
            t := sload(slot)
        }
    }
}

We would never be able to find the above storage slot without the pattern described above. But with our pattern its extremely easy:

function testStorageHidden() public {
    uint256 slot = stdstore.target(address(test)).sig("hidden()").find();
    assert(uint256(keccak256("my.random.var")) == slot);
}

We just set the target as the test address, tell the stdlib what signature to use, and call find. And since we record the accesses, we are able to find even a custom slot like the hash of “my.random.var”. We could write to it just by swapping out find with checked_write(100), writing 100 to the slot.

Sometimes, you may want to write to a specific field of a struct that is in storage. You can accomplish this by specifying the depth of the field, i.e:

struct A {
    uint256 a; // depth 0
    uint256 b; // depth 1
}

Just add a .depth(1) to your call to stdstore like this:

stdstore
    .target(address(test))
    .sig(test.MyStructA.selector)
    .depth(1)
    .checked_write(100);

And it would write to the b field in struct A.

limitations

As mentioned above, when we don’t satisfy the above constraints (view, single non-packed slot), we don’t have guarantees that it will work. For most standard ERC20s, this isn’t an issue. Most of the time if it doesn’t work, it won’t silently not work. It detects most errors and will revert. If it doesn’t work for a particular token, you can tap into the record + accesses yourself and try to find the slot yourself. Then just use the store cheatcode and you are off to the races.

closing thoughts

This pattern is crazy powerful, not just for mainnet forks, but also generally in your tests. I personally use it a ton for giving test “users” (i.e. addresses that interact with my contracts via the prank cheatcode) tokens, instead of transferring from another address (to guarantee I don’t run out of tokens). Additionally, as mentioned before, the record, accesses(address), load, and store cheatcodes are incredible tools for writing secure contracts. I use load all the time for reading internal/private variable state when testing.

Thanks for reading. Build shit, make money

Brock

Subscribe to Brock
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.