Solutions to Ethernaut, EVM puzzles and More EVM Puzzles in Foundry, migrated & improved from my previous solutions with Hardhat.
To get started:
forge install
Then, see the solutions in action via:
forge test
Note
We are making use of the following libraries for Ethernaut, shown via their installation commands:
forge install OpenZeppelin/ethernaut
forge install openzeppelin-contracts-08=OpenZeppelin/openzeppelin-contracts@v4.7.3
We use the original Ethernaut levels whenever possible, but there are a few exceptions as noted below. We also provide scripts to automatically solve & submit each problem, see scripts section below.
- Hello Ethernaut
- Fallback
- Fallout* uses v0.8 instead of v0.6
- CoinFlip
- Telephone
- Token* using v0.8 instead of v0.6
- Delegation
- Force
- Vault
- King
- Reentrance* using v0.8 instead of v0.6
- Elevator
- Privacy
- Gatekeeper One
- Gatekeeper Two
- Naught Coin
- Preservation
- Recovery
- Magic Number
- Alien Codex* requires v0.5, so we deploy bytecode via
CREATE
- Denial
- Shop
- Dex
- Dex Two
- Puzzle Wallet
- Motorbike* using v0.8 instead of v0.7 or below
- Double Entry Point
- Good Samaritan
- Gatekeeper Three
- Switch
Tip
For my old write-ups using Hardhat, see here.
We have a script for each Ethernaut level that can check if it is solved, or actually solve & submit it given an instance.
Important
We do not encourage running these scripts without even looking at the questions & trying them yourself. We only want these to be used so that you don't write everything from the console from scratch in the website!
So please, try & solve the questions yourself before running these scripts!
First, write your credentials within an .env
file, as shown in the .env.example
. Then, on the Ethernaut website for some challenge, get a new instance and keep note of the level & instance address.
You can run scripts as shown below, with <Level>
corresponding to the level name as it appears in the file name. These scripts will run the attack on actual contracts on the blockchain.
# Check if the instance is solved
source .env && forge script ./scripts/<Level>.s.sol:Check -f=$RPC_URL
# Solve & submit instance
source .env && forge script ./scripts/<Level>.s.sol:Solve -f=$RPC_URL --private-key=$PRIVATE_KEY
Tip
To actually do the transactions on-chain, just add --broadcast
flag to the command.
If things go right, you should see a tick-mark on Ethernaut website with the same wallet, meaning that your submitted solution was accepted!
In the CoinFlip level, we must deploy an attacker contract first and then call flip()
on it on 10 different blocks. Foundry does not support this kind of behavior in a single script, as discussed in this issue.
For this reason, we must run the scripts for this level as follows:
# (1) deploy attacker
source .env && forge script ./scripts/CoinFlip.s.sol:Solve -f=$RPC_URL --private-key=$PRIVATE_KEY -s="deploy()" --broadcast
# (2) save attacker address to .env as:
ATKR_COINFLIP=<attacker-address-here>
# (3) attack (do this 10 times)
source .env && forge script ./scripts/CoinFlip.s.sol:Solve -f=$RPC_URL --private-key=$PRIVATE_KEY -s="flip()" --broadcast
# (4) run the script as usual to submit the instance
source .env && forge script ./scripts/CoinFlip.s.sol:Solve -f=$RPC_URL --private-key=$PRIVATE_KEY --broadcast
In the Motorbike level, we use selfdestruct
and the instance verification checks for the contract size of the self-destructed contract to see if it is 0. Due to how selfdestruct
works, the contract size is only made 0 after the block is mined for that transaction, so in a single Foundry script we will not be able to see that effect, as per the issue here.
For this reason, the attack is in two steps:
# (1) deploy attacker
source .env && forge script ./scripts/Motorbike.s.sol:Solve -f=$RPC_URL --private-key=$PRIVATE_KEY -s="pwn()" --broadcast
# (2) run the script as usual to submit the instance
source .env && forge script ./scripts/Motorbike.s.sol:Solve -f=$RPC_URL --private-key=$PRIVATE_KEY --broadcast
Here is what a script looks like for some challenge called LevelName
:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {CheckScript} from "./common/Check.sol";
import {SolveScript} from "./common/Solve.sol";
import {LevelName} from "ethernaut/levels/LevelName.sol";
contract Check is CheckScript("LEVEL_NAME") {}
contract Solve is SolveScript("LEVEL_NAME") {
LevelName target;
constructor() {
target = LevelName(instance);
}
function attack() public override {
// the attack code within the test can be copy-pasted here!
}
}
Here, you just have to write the attack within attack
function, and it should "just work". The SolveScript
also exposes a variable called player
that holds your address as well.
Tip
For my write-ups see here.
Tip
For my write-ups see here.