This is the level 12 of OpenZeppelin Ethernaut web3/solidity based game.
Pre-requisites
- Fixed size byte arrays
- Layout of State Variables in a Solidity contract
- Reading storage at a slot
Hack
Given contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
constructor(bytes32[3] memory _data) public {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
player has to set locked state variable to false.
This is similar to level 8: Vault where any state variable (irrespective of whether it is private) can be read given it's slot number.
unlock uses the third entry (index 2) of data which is a bytes32 array. Let's determined data's third entry's slot number (each slot can accommodate at most 32 bytes) according to storage rules:
-
lockedis 1 byteboolin slot 0 -
IDis a 32 byteuint256. It is 1 byte extra big to be inserted in slot 0. So it goes in & totally fills slot 1 -
flattening- a 1 byteuint8,denomination- a 1 byteuint8andawkwardness- a 2 byteuint16totals 4 bytes. So, all three of these go into slot 2 - Array data always start a new slot, so
datastarts from slot 3. Since it isbytes32array each value takes 32 bytes. Hence value at index 0 is stored in slot 3, index 1 is stored in slot 4 and index 2 value goes into slot 5
Alright so key is in slot 5 (index 2 / third entry). Read it.
key = await web3.eth.getStorageAt(contract.address, 5)
// Output: '0x5dd89f7b81030395311dd63330c747fe293140d92dbe7eee1df2a8c233ef8d6d'
This key is 32 byte. But require check in unlock converts the data[2] 32 byte value to a byte16 before matching.
byte16(data[2]) will truncate the last 16 bytes of data[2] and return only the first 16 bytes.
Accordingly convert key to a 16 byte hex (with prefix - 0x):
key = key.slice(0, 34)
// Output: 0x5dd89f7b81030395311dd63330c747fe
Call unlock with key:
await contract.unlock(key)
Unlocked! Verify by:
await contract.locked()
// Output: false
Learned something awesome? Consider starring the github repo 😄
and following me on twitter here 🙏
Top comments (0)