Ethereum x Foundry - 0x8 Vault
Introduction to storage slots and how to access smart contract parameters
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:
and it does!
Run the attack using the forge test
subcommand: