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!
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.
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
SUB instruction takes the top two values, subtracting the second one from the first. So basically, it calculates
CODESIZE - CALLVALUE. We can see a
06. So, we have the equation
CODESIZE - CALLVALUE = 10 - CALLVALUE = 6.
CALLVALUE must be 4 Wei.
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:
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|
^ (as is the case in many programming languages) we need some value such that
CALLVALUE ^ 12 = 10. Simple arithmethic yields
CALLVALUE = 10 ^ 12 = 6.
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.
0x0100to the stack, which is
16 ^ 2in decimals.
EQcompares the top two items in the stack, which is
16 ^ 2and the square of our callvalue! Therefore, giving a callvalue of 16 is the winning move.
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
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
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 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
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:
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.
0xEE for no reason:
0x6001600C60003960016000F3EE. That should do it!
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 gas // given by GAS the previous line address // is the address from CREATE value // 0 argOffset // 0 argSize // 0 retOffset // 0 retSize // 0
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
JUMPI afterwards). So we can create a contract with a
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:
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
CALLDATASIZE, which is expected to be 8. Simply, we will send a 4 byte calldata with 2 Wei call value.
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
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)