Ethernaut: 12. Privacy
Erhan Tezcan
Posted on July 16, 2022
// 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
*/
}
This is similar to the 8th level Vault, where we read the EVM storage. Here in addition to that, we learn about a small optimization of EVM and how casting works.
EVM stores state variables in chunks of 32 bytes. If consecutive variables make up a 32-byte space (such as in this case 8 + 8 + 16 = 32) they are stored in the same chunk. If you were to write them elsewhere, this optimization may not have happened. Let us check the results of await web3.eth.getStorageAt(contract.address, i)
for the following values of i
:
-
0: 0x0000000000000000000000000000000000000000000000000000000000000001
This is thebool public locked = true
which is stored as 1. -
1: 0x0000000000000000000000000000000000000000000000000000000062bc6f36
This is theuint256 public ID = block.timestamp
which is the UNIX timestamp in hex,62bc6f36
(of this block in my instance]) -
2: 0x000000000000000000000000000000000000000000000000000000006f36ff0a
This is the 32 byte chunk of 3 variables all captures in6f36ff0a
:-
uint8 private flattening = 10
which is0a
-
uint8 private denomination = 255
which isff
-
uint16 private awkwardness = uint16(now)
which is6f36
. Well, thatawkwardness
variable is just theblock.timestamp
casted to 16-bits. We already know the actual 256-bit (32-byte) value of timestamp above:62bc6f36
. When casted down 16-bits, it became6f36
(4 x 4-bit hexadecimals).
-
-
3: 0x0ec18718027136372f96fb04400e05bac5ba7feda24823118503bff40bc5eb55
This isdata[0]
. -
4: 0x61a99635e6d4b7233a35f3d0d5d8fadf2981d424110e8bca127d64958d1e68c0
This isdata[1]
. -
5: 0x46b7d5d54e84dc3ac47f57bea2ca5f79c04dadf65d3a0f3581dcad259f9480cf
This isdata[2]
.
Now we just need data[2]
casted down to bytes16
. Here is how casting works in very few words:
- Conversion to smaller type costs more signficant bits. (e.g.
uint32 -> uint16
) - Conversion to higher type adds padding bits to the left. (e.g.
uint16 -> uint32
) - Conversion to smaller byte costs less significant bits. (e.g.
bytes32 -> bytes16
) - Conversion to larger byte add padding bits to the right. (e.g.
bytes16 -> bytes32
)
So, when we cast down data[2]
we will get the left-half of it: '0x46b7d5d54e84dc3ac47f57bea2ca5f79c04dadf65d3a0f3581dcad259f9480cf'.slice(0, 2 + 32)
and then await contract.unlock('0x46b7d5d54e84dc3ac47f57bea2ca5f79')
. That is all! Here is a good article on reading storage: https://medium.com/@dariusdev/how-to-read-ethereum-contract-storage-44252c8af925.
Posted on July 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.