DEV Community

Erhan Tezcan
Erhan Tezcan

Posted on

Ethernaut: 12. Privacy

Play the level

// 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
Enter fullscreen mode Exit fullscreen mode

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 the bool public locked = true which is stored as 1.
  • 1: 0x0000000000000000000000000000000000000000000000000000000062bc6f36 This is the uint256 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 in 6f36ff0a:
    • uint8 private flattening = 10 which is 0a
    • uint8 private denomination = 255 which is ff
    • uint16 private awkwardness = uint16(now) which is 6f36. Well, that awkwardness variable is just the block.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 became 6f36 (4 x 4-bit hexadecimals).
  • 3: 0x0ec18718027136372f96fb04400e05bac5ba7feda24823118503bff40bc5eb55 This is data[0].
  • 4: 0x61a99635e6d4b7233a35f3d0d5d8fadf2981d424110e8bca127d64958d1e68c0 This is data[1].
  • 5: 0x46b7d5d54e84dc3ac47f57bea2ca5f79c04dadf65d3a0f3581dcad259f9480cf This is data[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:

Top comments (0)