Privacy - The Ethernaut - Writeup
The creator of this contract was careful enough to protect the sensitive areas of its storage.
Unlock this contract to beat the level.
This is the contract code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;
constructor(bytes32[3] memory _data) {
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
*/
}
As we can see, the contract defines a bunch of different variables. We need to understand type casting and storage structure to compute bytes16(data[2]) and unlock the contract.
In order to compact the storage and not waste too much space, some optimization rules are used. Storage is divided into slots. Slots are 32-byte sized. A simple optimization would be storing two 128-bit numbers in the same slot. We wouldn’t be wasting any space.
The contract Privacy first defines a bool variable, right before a uint256 variable. Because the second one needs a whole slot for itself, the boolean value cannot be optimized and will use a whole slot for itself as well. Variables are not reorganized to be more efficient.
Then, 2 8-bit numbers and a 16-bit number are defined. These can be grouped into a single slot.
Then, an array of 3 32-bytes variables is defined. Each one of these will use a whole slot. Summing up the slots used up until now, we know that data[2] uses the slot 5 (starting from 0). Because the blockchain storage is public, we can always read that value.
Let’s generate a new instance of the challenge to verify this:

Now, we can read the slot 5 using this command:
cast storage 0x0aD53A9bc8Ef9294FCA38B93acb2d23DbD1A5E43 5 --rpc-url $SEPOLIA_RPC_URL

And that’s data[2]. To unlock the contract, we need to know bytes16(data[2])). Casting to a smaller-sized bytes datatype preserves the left-most bytes. So, we can precompute that using Python
from Crypto.Util.number import bytes_to_long, long_to_bytes
hex(bytes_to_long(long_to_bytes(0x7e6f6248cbe04e4e809f62a89089426dc1e68e70236cc2e06bce2fcc60cfe371)[:16]))

So that’s the value we need to send:
cast send 0x0aD53A9bc8Ef9294FCA38B93acb2d23DbD1A5E43 "unlock(bytes16)" 0x7e6f6248cbe04e4e809f62a89089426d --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY
We can also verify that the locked variable (slot 0) is now false:
cast storage 0x0aD53A9bc8Ef9294FCA38B93acb2d23DbD1A5E43 0 --rpc-url $SEPOLIA_RPC_URL

