EVM Puzzles are a collection of puzzles by Franco Victorio. In this Markdown file, I wrote down the walkthrough for each of them.
It is crucial to understand the storage structure of EVM, I refer to this article as a good resource on this. Most importantly, you should be familiar on how the stack works, and how EVM instructions use it. See the original documentation about EVM at https://ethereum.org/en/developers/docs/evm/.
Whenever you see a new instruction, you should immediately go and see what it is and what it does. I have used https://www.evm.codes/ and https://www.ethervm.io/ as my resources. For more information about EVM, you can also see their pages: https://www.evm.codes/about and https://www.ethervm.io/#overview.
Throughout this walkthrough all numbers that start with 0x are hexadecimal, and all that do not are decimal. Furthermore, bytecodes are given in hexadecimal, and two hexadecimals are equal to a byte, as 4 bits x 2 = 8 bits = 1 byte.
In all puzzles, the objective is to have the code STOP, rather than REVERT. You basically have to get to the end!
One final remark: please try to solve each of these before looking at the solution! This is a great way to learn about EVM, and solving these is a rewarding feeling. Once you learn an instruction on your own, the rest will be easier; it is the first steps that is hard to take at such a low-level context. Good luck!
Puzzle 1
00 0x34 CALLVALUE
01 0x56 JUMP
02 0xFD REVERT
03 0xFD REVERT
04 0xFD REVERT
05 0xFD REVERT
06 0xFD REVERT
07 0xFD REVERT
08 0x5B JUMPDEST
09 0x00 STOP
JUMP jumps to the destination specified by the top value in the stack. CALLVALUE pushes the call value to the stack. Looking at the code, there is a JUMPDEST at 8, so our call value must be 8.
Puzzle 2
00 0x34 CALLVALUE
01 0x38 CODESIZE
02 0x03 SUB
03 0x56 JUMP
04 0xFD REVERT
05 0xFD REVERT
06 0x5B JUMPDEST
07 0x00 STOP
08 0xFD REVERT
09 0xFD REVERT
Here is a new instruction: CODESIZE. It pushes the size of code to the stack. Which code? Well, it is the puzzle code itself, which we can see has 10 bytes (the last line is 09 but remember that it starts from 00).
The SUB instruction takes the top two values, subtracting the second one from the first. So basically, it calculates CODESIZE - CALLVALUE. We can see a JUMPDEST at 06. So, we have the equation CODESIZE - CALLVALUE = 10 - CALLVALUE = 6.
Our CALLVALUE must be 4 Wei.
Puzzle 3
00 0x36 CALLDATASIZE
01 0x56 JUMP
02 0xFD REVERT
03 0xFD REVERT
04 0x5B JUMPDEST
05 0x00 STOP
CALLDATASIZE pushes the size of calldata (bytes) to the stack. There is a JUMPDEST at 4, so the size should be 4 bytes. Any arbitrary 4-byte calldata would suffice: 0x11223344.
Puzzle 4
00 0x34 CALLVALUE
01 0x38 CODESIZE
02 0x18 XOR
03 0x56 JUMP
04 0xFD REVERT
05 0xFD REVERT
06 0xFD REVERT
07 0xFD REVERT
08 0xFD REVERT
09 0xFD REVERT
10 0x5B JUMPDEST
11 0x00 STOP
CODESIZE is 12 and JUMPDEST is at 10. XOR is a bitwise operation that stands for exclusive-or operation. Here is its logic table:
| a | b | a ^ b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Denoting XOR as ^ (as is the case in many programming languages) we need some value such that CALLVALUE ^ 12 = 10. Simple arithmethic yields CALLVALUE = 10 ^ 12 = 6.
Puzzle 5
00 0x34 CALLVALUE
01 0x80 DUP1
02 0x02 MUL
03 0x610100 PUSH2 0x0100
06 0x14 EQ
07 0x600C PUSH1 0x0C
09 0x57 JUMPI
10 0xFD REVERT
11 0xFD REVERT
12 0x5B JUMPDEST
13 0x00 STOP
14 0xFD REVERT
15 0xFD REVERT
Here we have a JUMPI which is a conditional jump. PUSH1 0x0C above it provides the correct destination address, so all we have to care about is that the condition value be non-zero.
Looking at the lines above in order:
-
CALLVALUEpushes the value to the stack. -
DUP1duplicates it, so there are two of the same value in the stack. -
MULmultiplies these two, so we basically squared the call value. -
PUSH2 0x0100pushes0x0100to the stack, which is16 ^ 2in decimals. -
EQcompares the top two items in the stack, which is16 ^ 2and the square of our callvalue! Therefore, giving a callvalue of 16 is the winning move.
Puzzle 6
00 0x6000 PUSH1 0x00
02 0x35 CALLDATALOAD
03 0x56 JUMP
04 0xFD REVERT
05 0xFD REVERT
06 0xFD REVERT
07 0xFD REVERT
08 0xFD REVERT
09 0xFD REVERT
10 0x5B JUMPDEST
11 0x00 STOP
CALLDATALOAD loads a 32-byte value at the specified byte offset. If 32-bytes go beyond the length of calldata, the overflowing bytes are set to 0.
The offset to be loaded is given by the top value in the stack, which is given by PUSH1 0x00 above. Basically, the calldata itself should have the destination address for JUMP. Our JUMPDEST is at 0x0A, so that is our calldata!
But wait, remember that overflowing bytes are set to 0. So if we just send 0x0A, the remaining 31 bytes will be 00 and we will have 0x0A00000000000000000000000000000000000000000000000000000000000000 which is a huge number!
Instead, we must do zero-padding to the left, and send 0x000000000000000000000000000000000000000000000000000000000000000A as our calldata. This way, reading 32-bytes from the zero offset will yield 0x0A.
Puzzle 7
00 0x36 CALLDATASIZE
01 0x6000 PUSH1 0x00
03 0x80 DUP1
04 0x37 CALLDATACOPY
05 0x36 CALLDATASIZE
06 0x6000 PUSH1 0x00
08 0x6000 PUSH1 0x00
10 0xF0 CREATE
11 0x3B EXTCODESIZE
12 0x6001 PUSH1 0x01
14 0x14 EQ
15 0x6013 PUSH1 0x13
17 0x57 JUMPI
18 0xFD REVERT
19 0x5B JUMPDEST
20 0x00 STOP
The first 4 lines basically copy the entire calldata into memory. The next 4 lines create a contract, where the initialization code is taken from the memory at the position our calldata was just loaded. In other words, the first 10 lines create a contract with our calldata.
Afterwards, the next 3 lines check if the EXTCODESIZE is equal 1 byte. The contract that we are checking the code size is the one we just created above with CREATE, as CREATE pushes the address to the stack. EXTCODESIZE is the size of the runtime code of a contract, not the initialization code! The puzzle expects this to be 1 byte (lines 0C and 0E). So we just have to write our own initialization code to do all this.
The instruction for this is CODECOPY, which works similar to CALLDATACOPY. The initialization code will be as follows:
PUSH1 0x01 // 1 byte
PUSH1 ;;;; // position in bytecode, we dont know yet
PUSH1 0x00 // write to memory position 0
CODECOPY // copies the bytecode
PUSH1 0x01 // 1 byte
PUSH1 0x00 // read from memory position 0
RETURN // returns the code copied above
In terms of bytecode, this results in 0x60 01 60 ;; 60 00 39 60 01 60 00 F3. This is a total of 12 bytes, so the ;;;; position will be 12, which is 0x0C. The final bytecodes are: 0x6001600C60003960016000F3.
This code copies 1 byte code into memory, and returns it to EVM so that contract creation is completed. The actual runtime code is arbitrary, it just has to be 1 byte. Furthermore, runtime code comes after the initialization code (starting at 12th position in this case), so we just have to append one byte to the end of the bytecodes above.
Let's add 0xEE for no reason: 0x6001600C60003960016000F3EE. That should do it!
Puzzle 8
00 0x36 CALLDATASIZE
01 0x6000 PUSH1 0x00
03 0x80 DUP1
04 0x37 CALLDATACOPY
05 0x36 CALLDATASIZE
06 0x6000 PUSH1 0x00
08 0x6000 PUSH1 0x00
10 0xF0 CREATE
11 0x6000 PUSH1 0x00
13 0x80 DUP1
14 0x80 DUP1
15 0x80 DUP1
16 0x80 DUP1
17 0x94 SWAP5
18 0x5A GAS
19 0xF1 CALL
20 0x6000 PUSH1 0x00
22 0x14 EQ
23 0x601B PUSH1 0x1B
25 0x57 JUMPI
26 0xFD REVERT
27 0x5B JUMPDEST
28 0x00 STOP
Similar to the previous puzzle, the first 4 lines copy the entire calldata into memory. The next 4 lines create a contract, the initialization code is taken from the memory at the position that calldata was just loaded.
Afterwards, 5 of 0x00 are pushed to the stack. SWAP5 will exchange the 1st and 6th stack items, and the 6th item at that point is the address yielded from CREATE. Next, the remaining gas amount is pushed to stack with GAS. All of this was done for the sake of CALL instruction:
CALL
gas // given by GAS the previous line
address // is the address from CREATE
value // 0
argOffset // 0
argSize // 0
retOffset // 0
retSize // 0
After CALL, a boolean result is pushed to the stack indicating its success. Looking at the following lines we see that this is expected to be 0 (PUSH1 00 and EQ with JUMPI afterwards). So we can create a contract with a REVERT instruction.
PUSH1 0x00
PUSH1 0x00
REVERT
This shall be our runtime code, which in bytecode is 0x60006000FD at 5 bytes total. We will write the initialization code ourselves too, similar to what we did in the previous puzzle.
PUSH1 0x05 // 5 bytes
PUSH1 0x0C // position of runtime code in bytecode
PUSH1 0x00 // write to memory position 0
CODECOPY // copies the bytecode
PUSH1 0x05 // 5 bytes
PUSH1 0x00 // read from memory position 0
RETURN // returns the code copied above
Again the position is 0x0C because the initialization code is 12 bytes. So our initialization bytecodes are 0x6005600C60003960056000F3 and the runtime bytecodes are 0x60006000FD. The calldata will be these concatenated: 0x6005600C60003960056000F360006000FD.
Puzzle 9
00 0x36 CALLDATASIZE
01 0x6003 PUSH1 0x03
03 0x10 LT
04 0x6009 PUSH1 0x09
06 0x57 JUMPI
07 0xFD REVERT
08 0xFD REVERT
09 0x5B JUMPDEST
10 0x34 CALLVALUE
11 0x36 CALLDATASIZE
12 0x02 MUL
13 0x6008 PUSH1 0x08
15 0x14 EQ
16 0x6014 PUSH1 0x14
18 0x57 JUMPI
19 0xFD REVERT
20 0x5B JUMPDEST
21 0x00 STOP
We start with a small JUMPI that requires 3 < CALLDATASIZE so our calldata is larger than 3 bytes. Afterwards, we multiply our CALLVALUE and CALLDATASIZE, which is expected to be 8. Simply, we will send a 4 byte calldata with 2 Wei call value.
Puzzle 10
00 0x38 CODESIZE
01 0x34 CALLVALUE
02 0x90 SWAP1
03 0x11 GT
04 0x6008 PUSH1 0x08
06 0x57 JUMPI
07 0xFD REVERT
08 0x5B JUMPDEST
09 0x36 CALLDATASIZE
10 0x610003 PUSH2 0x0003
13 0x90 SWAP1
14 0x06 MOD
15 0x15 ISZERO
16 0x34 CALLVALUE
17 0x600A PUSH1 0x0A
19 0x01 ADD
20 0x57 JUMPI
21 0xFD REVERT
22 0xFD REVERT
23 0xFD REVERT
24 0xFD REVERT
25 0x5B JUMPDEST
26 0x00 STOP
The first CODESIZE is the size of this puzzle itself, which is 1B (28 bytes). Next it swaps the CALLVALUE with it, and runs GT. In effect, this checks if CODESIZE > CALLVALUE.
After the successfull JUMPI, we are doing a CALLDATASIZE MOD 0x003 == 0 operation. We want this to be true for the next JUMPI to work, so our calldata size must be a multiple of 3.
The destination of JUMPI is defined by CALLVALUE ADD 0x0A, which should add up to 0x19. In decimals, 0x0A is 10 and 0x19 is 25, so our CALLVALUE should be 15.
Top comments (0)