More EVM Puzzles - Part 2
May 26, 2022 by patrickd
This continues the series on Dalton Sweeney (opens in a new tab)'s "10 more EVM puzzles" (opens in a new tab) collection. If you haven't read it yet, you should probably check out Part 1 first.
Puzzle #3
00 36 CALLDATASIZE
01 6000 PUSH1 00
03 6000 PUSH1 00
05 37 CALLDATACOPY
06 36 CALLDATASIZE
07 6000 PUSH1 00
09 6000 PUSH1 00
0B F0 CREATE
0C 6000 PUSH1 00
0E 80 DUP1
0F 80 DUP1
10 80 DUP1
11 93 SWAP4
12 5A GAS
13 F4 DELEGATECALL
14 6005 PUSH1 05
16 54 SLOAD
17 60AA PUSH1 AA
19 14 EQ
1A 601E PUSH1 1E
1C 57 JUMPI
1D FE INVALID
1E 5B JUMPDEST
1F 00 STOP
? Enter the calldata:
Just like in the previous puzzle, there's only one JUMPDEST
we have to reach located at offset 0x1E
and all we have to make sure to get there is, that the EQ
comparison before the JUMP
succeeds.
This is actually the first time that storage is touched during these puzzles: SLOAD
will consume one item from the stack as "key" and push the "value" it found there to stack. It's a simple read operation from a key-value store.
We can see that the jump-condition can be written as: SLOAD(0x05) == 0xAA
, basically we have to make sure that the value for the storage key 0x05
equals 0xAA
. There's no SSTORE
opcode writing to storage though, but there is a DELEGATECALL
, meaning that we can run code of another smart contract on our context, which includes storage.
The entire first part of the bytecode before the CREATE
opcode is the same as before, so we'll again be able to deploy a contract via the calldata we send. Similarly, the bytecode before the DELEGATECALL
is almost the same as it was for the CALL
in the previous puzzle. The only difference is that one less 0x00
is duplicated, because there's no "value" to send here - that wouldn't make sense since any wei we send with the call would end up at the same contract again due to delegation.
In summary: We have to send construction bytecode that returns runtime bytecode of a contract that'll store the value 0xAA
at the key 0x05
. After it was deployed that runtime bytecode will be delegate-called and we'll write to the puzzle's storage instead of the storage of the contract that was deployed. This storage value is then checked and if it was correctly set we'll successfully jump to the end.
Runtime Bytecode
00 60AA PUSH1 AA [0xAA]
02 6005 PUSH1 05 [0x05, 0x0A] (key, value)
04 55 SSTORE []
This time we don't actually have to RETURN
anything, instead we can just STOP
(which is implicit at the end of bytecode) after writing to storage.
Runtime bytecode: 0x60AA600555
Construction Bytecode
00 6460AA600555 PUSH5 60AA600555 [0x60AA600555]
06 6000 PUSH1 00 [0x0, 0x60AA600555] (offset, value)
08 52 MSTORE []
00 600A PUSH1 0A [0x0A]
02 601B PUSH1 1B [0x1B, 0x0A] (offset, size)
04 F3 RETURN []
Like last time, we write the runtime bytecode to memory so we can RETURN
it during initiation.
Construction Bytecode: 0x6460AA600555600052600A601BF3
Solution
? Enter the calldata: 0x6460AA600555600052600A601BF3
Puzzle solved!
This was relatively easy to solve since we could mostly re-purpose the simple solution from Puzzle #2.
Puzzle #4
00 30 ADDRESS
01 31 BALANCE
02 36 CALLDATASIZE
03 6000 PUSH1 00
05 6000 PUSH1 00
07 37 CALLDATACOPY
08 36 CALLDATASIZE
09 6000 PUSH1 00
0B 30 ADDRESS
0C 31 BALANCE
0D F0 CREATE
0E 31 BALANCE
0F 90 SWAP1
10 04 DIV
11 6002 PUSH1 02
13 14 EQ
14 6018 PUSH1 18
16 57 JUMPI
17 FD REVERT
18 5B JUMPDEST
19 00 STOP
? Enter the value to send:
? Enter the calldata:
We can see a new combination of opcodes here: First ADDRESS
which pushes the puzzle contract's own address onto the stack, and then BALANCE
which consumes this address and returns the amount of ether the account at said address has, in this case: The puzzle's balance which we can manipulate by sending value when prompted.
Ignoring the first balance that is pushed onto the stack, what follows is the same CREATE
operation as in the previous puzzles with the only difference that this time the entire puzzle's balance is forwarded to the contract being created.
00 30 ADDRESS [AddressOfPuzzle]
01 31 BALANCE [BalanceOfPuzzle]
... Balance of Puzzle is sent to new contract...
0D F0 CREATE [AddressOfNewContract, OldBalanceOfPuzzle]
0E 31 BALANCE [BalanceOfNewContract, OldBalanceOfPuzzle]
0F 90 SWAP1 [OldBalanceOfPuzzle, BalanceOfNewContract]
10 04 DIV [OldBalanceOfPuzzle//BalanceOfNewContract]
11 6002 PUSH1 02 [0x02, OldBalanceOfPuzzle//BalanceOfNewContract]
13 14 EQ [OldBalanceOfPuzzle//BalanceOfNewContract == 0x02]
Immediately after, the returned address of the newly created contract is then consumed to obtain its balance. The old balance of the puzzle is then integer-divided by the current balance of the new contract and the result of this division must be 2 in order for the jump to happen. But without doing anything the result would currently always be 1.
So to solve this puzzle the construction bytecode we send as calldata has to send away half of its balance. For example, if we send 4 wei to the puzzle, our contract receiving them has to burn 2 wei so that 4//2 == 2. The construction bytecode doesn't have to return any runtime bytecode since that's not necessary for it to simply hold on the the remaining balance.
0C 6000 PUSH1 00 [0x0]
0E 80 DUP1 [0x0, 0x0]
0E 80 DUP1 [0x0, 0x0, 0x0]
0F 80 DUP1 [0x0, 0x0, 0x0, 0x0]
0C 6002 PUSH1 02 [2, 0x0, 0x0, 0x0, 0x0]
11 81 DUP2 [0x0, 2, 0x0, 0x0, 0x0, 0x0]
13 5A GAS [GAS, 0x0, 2, 0x0, 0x0, 0x0, 0x0] (gas, address, value, argsOffset, argsSize, retOffset, retSize)
00 F1 CALL []
07 6000 PUSH1 00 [0x0]
09 80 DUP1 [0x0, 0x0] (offset, size)
0B F3 RETURN []
The above construction bytecode sends 2 wei to the 0x0 address in order to burn it and then returns nothing as runtime bytecode.
? Enter the value to send: 4
? Enter the calldata: 0x60008080806002815AF1600080F3
Puzzle solved!
Initially I forgot that the old balance of the puzzle is used. Assuming that it's the current one I thought about having to send some of the balance back to the puzzle. Since sending value with CALL
would trigger the puzzle's bytecode again though the only way to do that would be deploying another contract that uses SELFDESTRUCT
to inject value into the puzzle without that happening. Surprised by how convoluted this solution seemed, I later noticed the challenge was actually way easier than that - took me a while though.