More EVM Puzzles - Part 3
June 6, 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 this is all new to you, you should probably check out Part 1 first.
Puzzle #5
00 6020 PUSH1 20
02 36 CALLDATASIZE
03 11 GT
04 6008 PUSH1 08
06 57 JUMPI
07 FD REVERT
08 5B JUMPDEST
09 36 CALLDATASIZE
0A 6000 PUSH1 00
0C 6000 PUSH1 00
0E 37 CALLDATACOPY
0F 36 CALLDATASIZE
10 59 MSIZE
11 03 SUB
12 6003 PUSH1 03
14 14 EQ
15 6019 PUSH1 19
17 57 JUMPI
18 FD REVERT
19 5B JUMPDEST
1A 00 STOP
? Enter the calldata:
This one starts easy: The calldata we send must be longer than 32 (0x20
) bytes for the first JUMPI
to succeed. Then all of the calldata is copied to memory at offset 0x0
. Then CALLDATASIZE
is subtracted from MSIZE
and the result of this subtraction must be 3 for the final jump to succeed as well.
evm.codes (opens in a new tab) tells us that MSIZE
returns "the size of active memory in bytes". But there's an important detail: "The size is always a multiple of a word (32 bytes)". So even if we only sent 33 bytes as calldata which were copied to memory, MSIZE
would instead return 64 as value.
This also means the solution is quite simple: Sending 61 bytes will yield a difference of 3 from the subtraction and is also of a greater size than 32 to make the first jump.
? Enter the calldata: 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Puzzle solved!
Knowing this little fact on how memory expansion works in the EVM made solving this puzzle quite easy.
Puzzle #6
00 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 PUSH32 FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0
21 34 CALLVALUE
22 01 ADD
23 6001 PUSH1 01
25 14 EQ
26 602A PUSH1 2A
28 57 JUMPI
29 FD REVERT
2A 5B JUMPDEST
2B 00 STOP
? Enter the value to send:
Seems like the "gimmick" on this one is integer overflows. Right at the beginning, a really large number is pushed onto the stack, so large in fact that just adding 16 will make it overflow and flip to 0. But for the jump condition to succeed it needs to be a 1 - so we need to send 17!
? Enter the value to send: 17
Puzzle solved!
Puzzle #7
00 5A GAS
01 34 CALLVALUE
02 5B JUMPDEST
03 6001 PUSH1 01
05 90 SWAP1
06 03 SUB
07 80 DUP1
08 6000 PUSH1 00
0A 14 EQ
0B 6011 PUSH1 11
0D 57 JUMPI
0E 6002 PUSH1 02
10 56 JUMP
11 5B JUMPDEST
12 5A GAS
13 90 SWAP1
14 91 SWAP2
15 03 SUB
16 60A6 PUSH1 A6
18 14 EQ
19 601D PUSH1 1D
1B 57 JUMPI
1C FD REVERT
1D 5B JUMPDEST
1E 00 STOP
? Enter the value to send:
This one's got a loop! Right after pushing the initial amount of GAS
onto the stack, we enter the first loop:
# It gets the call value and subtracts 1 from it.
01 34 CALLVALUE
02 5B JUMPDEST
03 6001 PUSH1 01
05 90 SWAP1
06 03 SUB
# It duplicates the result of the subtraction for the next loop round (instead of using call value again).
07 80 DUP1
# If the result of the subtraction was zero we exit the loop.
08 6000 PUSH1 00
0A 14 EQ
0B 6011 PUSH1 11
0D 57 JUMPI
# If not, we keep looping.
0E 6002 PUSH1 02
10 56 JUMP
If we'd send a value of 0 it would end up looping until it runs out of gas, because the integer will underflow resulting in the largest possible number to count down from. But as long as we send any value that ensures we have enough gas left at the end of the loop, we should be fine. If we'd for example send a 1 as a value, the result of the subtraction would immediately yield a 0 and it would never loop to begin with. Sending a 2 would loop exactly once and then exit, and so on..
00 5A GAS [initialGas]
... [0, initialGas]
12 5A GAS [gasAfterLoop, 0, initialGas]
13 90 SWAP1 [0, gasAfterLoop, initialGas]
14 91 SWAP2 [initialGas, gasAfterLoop, 0]
15 03 SUB [initialGas-gasAfterLoop, 0]
16 60A6 PUSH1 A6 [0xa6, initialGas-gasAfterLoop, 0]
18 14 EQ [0xa6==initialGas-gasAfterLoop, 0]
19 601D PUSH1 1D [0x1d, 0xa6==initialGas-gasAfterLoop, 0]
1B 57 JUMPI [0]
1C FD REVERT
1D 5B JUMPDEST [0]
1E 00 STOP [0]
Immediately after the loop it pushes the now still left gas onto the stack again. You can also see that the final value from the loop, which will always be 0, is still left on the stack even though it's never used again, which is why there are a bunch of confusing swaps happening. But all that's actually being done here is determining the gas that was consumed during the loop by subtracting the second GAS
instruction's value from the first one. The result of this subtraction is then compared with 0xA6
, or 166 in decimal.
In summary: We have to send a transaction value
that'll cause the loop to consume exactly 166 gas.
00 5A GAS 2gas │
01 34 CALLVALUE 2gas │
02 5B JUMPDEST 1gas ┢━<━┓
03 6001 PUSH1 01 3gas ┃ ┃
05 90 SWAP1 3gas ┃ ┃
06 03 SUB 3gas ┃ ┃
07 80 DUP1 3gas ┃ ┃
08 6000 PUSH1 00 3gas ┃ ┃
0A 14 EQ 3gas ┃ ┃
0B 6011 PUSH1 11 3gas ┃ ┃
0D 57 JUMPI 10gas ┠──────┐
0E 6002 PUSH1 02 3gas ┃ ┃ │
10 56 JUMP 8gas ┗━>━┛ │
11 5B JUMPDEST 1gas <──────┘
The instructions around the loop will consume 5gas while a normal loop execution will cost 43gas. The final loop skips a few instructions, pricing it at 32gas.
So what we have to determine is the CALLVALUE
we have to send for 5 + 43*(CALLVALUE-1) + 32 = 166
, which is 4.
? Enter the value to send: 4
Puzzle solved!
It should be noted though that gas prices for instructions aren't completely fixed. Their cost might change in the future which would likely break this puzzle.