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:

alt text

Now, we can read the slot 5 using this command:

cast storage 0x0aD53A9bc8Ef9294FCA38B93acb2d23DbD1A5E43 5 --rpc-url $SEPOLIA_RPC_URL

alt text

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]))

alt text

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

alt text

alt text