Setup and Foundry


Topics Covered:

In this primer we cover the basics of how to setup foundry and pull the contracts we need for each challenge, and go through an example setup.

Setting Up the Dev Environment

Foundry can be installed by following the direction on their site. Once installed, you can use the foundryup command to update all the tools we need for this course.

Assuming you have foundry and git installed: (Windows users may want to use WSL)

# update forge, chisel, cast and anvil
foundryup

My repo below has the full code and solutions. You can use it as a reference if you wish, or just copy the contracts from OpenZeppelin as we go.

Building a new repo

# create a new directory and initialize forge
mkdir {PROJECT NAME}
forge init

Copying From Me

# clone the repo and open the new directory
git clone https://github.com/jordaniza/assemblynaut.git
cd assemblynaut

# build the repo
forge build

You should be good to go!

Example Structure

Before we start, let’s lay a foundation for how we are going to structure these tests.

For this example, let’s use the Fallback contract in Ethernaut.

Our basic project structure should look like this:

lib/                          # dependencies
src/
  |-- Fallback.sol            # target contract
test/
  |-- Fallback.t.sol          # attack contract
foundry.toml                  # foundry config

Our source code (the contract we are attacking) will live in the src folder, and our hacks will live in the test folder.

Fallback.sol:

The first contract in Ethernaut is a relatively simple starting point, copy this code into the Fallback.sol file in the src folder:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {
    mapping(address => uint256) public contributions;
    address public owner;

    constructor() {
        owner = msg.sender;
        contributions[msg.sender] = 1000 * (1 ether);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "caller is not the owner");
        _;
    }

    function contribute() public payable {
        require(msg.value < 0.001 ether);
        contributions[msg.sender] += msg.value;
        if (contributions[msg.sender] > contributions[owner]) {
            owner = msg.sender;
        }
    }

    function getContribution() public view returns (uint256) {
        return contributions[msg.sender];
    }

    function withdraw() public onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    receive() external payable {
        require(msg.value > 0 && contributions[msg.sender] > 0);
        owner = msg.sender;
    }
}

Fallback.t.sol

Our Fallback.t.sol will have the following structure:

pragma solidity ^0.8.0;

// import the forge test utilities
import "forge-std/Test.sol";

// import our target contract
import "../src/Fallback.sol";

// ensure our test contract has the forge test utils enabled
contract ASMFallback is Test {

    // here we will declare any state variables

    function setUp() public {
        // this function runs before each of our test
        // functions, and is where we will set up
        // our contract and state
    }

    function testAttackFallback() public {
        // this is where our attack will go
        // forge tests must always start with the word
        // 'test' and be marked as `public` or `external`
    }
}

ASMFallback is Test says that our contract is inheriting the foundry test suite, giving us access the forge test utilities in our test contract.

Writing the Test Case

Let’s fill this in: first let’s define 2 state variables:

  1. Our target contract, which is of type Fallout
  2. Our attacker which is a nominal address
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Fallback.sol";

contract ASMFallback is Test {
    Fallback public target;
    address payable public attacker = payable(address(420));

    // ...
}

Now, let’s create our setUp hook, recall the aim of this challenge:

You will beat this level if:

You claim ownership of the contract

You reduce its balance to 0

So we need to make sure the target is deployed and has some ether. For good measure, let’s also give the attacker some ether.

We can use foundry’s vm.deal(address who, uint256 wei) to mint arbitrary amounts of wei to any address:

function setUp() public {
    target = new Fallback();
    vm.deal(attacker, 1 ether);
    vm.deal(address(target), 1000 ether);
}

Lastly, we want to execute our test as the attacker. We can do this using foundry’s vm.startPrank(address who) which will execute all subsequent functions as the address who.

function testAttackFallback() public attack {
  vm.startPrank(attacker);

  // our exploit goes here

  vm.stopPrank();
}

Personal preference here, but this pattern is so common I like to mark the attack function as such with a modifier:

modifier attack() {
    vm.startPrank(attacker);
    _;
    vm.stopPrank();
}

function testAttackFallback() public attack {
    // our exploit goes here
}

Finally, set’s complete the setup by defining our assertions: what must hold true for this challenge to be considered “Complete”?

function testAttackFallback() public attack {
  // ... exploit
  // you claim ownership of the contract
  assertEq(target.owner(), attacker);
  // you reduce its balance to 0
  assertEq(address(target).balance, 0);
}

With that, we are good to go, putting it all together:

pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Fallback.sol";

contract ASMFallback is Test {
    Fallback public target;
    address payable public attacker = payable(address(420));

    function setUp() public {
        target = new Fallback();
        vm.deal(attacker, 1 ether);
        vm.deal(address(target), 1000 ether);
    }

    modifier attack() {
        vm.startPrank(attacker);
        _;
        vm.stopPrank();
    }

    function testAttackFallback() public attack {
        // assertions
        assertEq(target.owner(), attacker);
        assertEq(address(target).balance, 0);
    }
}

Checking it all works

Let’s test everything is functioning as intended. We can run our tests using foundry’s forge test command:

Failing tests:
Encountered 1 failing test in test/Fallback.t.sol:ASMFallback
[FAIL. Reason: Assertion failed.] testAttackFallback() (gas: 40534)

Test failing is fine in this case - we haven’t written any logic. If you’re not seeing the above for any reason then you should feel bad you should have more specific errors that tell you what’s wrong.

Assuming you ARE seeing the above, then all is good and you’re ready to go.

Prev

Top