A collection of EVM puzzles. Each puzzle consists on sending a successful transaction to a contract. The bytecode of the contract is provided, and you need to fill the transaction data that won't revert the execution.
Clone this repository and install its dependencies (npm install
or yarn
). Then run:
npx hardhat play
And the game will start.
In some puzzles you only need to provide the value that will be sent to the contract, in others the calldata, and in others both values.
You can use evm.codes
's reference and playground to work through this.
Following are the solutions for each puzzle. But first, a high level overview how this actually works:
For each new puzzle, the play.js script is creating a smart contract with the code of the puzzle, e.g. for puzzle #1 a smart contract with code 0x3456FDFDFDFDFDFD5B00
is being created. If you enter a solution, a transaction with your calldata/callvalue is being sent to that generted smart contract. If the tx is successfull, you passed, if the tx reverts, you failed.
First opcode is CALLVALUE
, which moves the msg value on top of the stack.
Second opcode is JUMP
, which reads the topmost value of the stack and jumps to that destination.
We can see that JUMPDEST
is at 08, so callvalue has to be 8.
First opcode is CALLVALUE
again, but this time the second opcode is CODESIZE
, which puts the size of the code (so last address + 1) on the stack. The third opcode SUB
subtracts CALLVALUE
from CODESIZE
.
The result of SUB
has to be 6, which is the jump destination. CODESIZE
returns 10, so CALLVALUE
has to be 4.
First opcode is CALLDATASIZE
, which puts the byte size of the calldata on top of the stack, e.g. 0x01FF
-> 2
JUMPDEST
is at address 4, so CALLDATASIZE
has to return 4, e.g. correct calldata could be 0x01020304
This puzzle takes the CALLVALUE
and CODESIZE
and uses them as arguments for a bitwise exclusive or (XOR) operation.
Truth table of A XOR B
A | B | Y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
0 | 0 | 1 |
0 | 1 | 0 |
JUMPDEST
is at address 0A (10), so the result of our XOR operation has to be 10.
CODESIZE
is 0x0B + 1 = 0x0C => 12 => 0b1100
, our task is to submit the correct calldata so that 12 XOR CALLVALUE
returns 10, which is 0b0110 => 6
.
CODESIZE | 1 | 1 | 0 | 0 |
---|---|---|---|---|
CALLVALUE | 0 | 1 | 1 | 0 |
XOR | 1 | 0 | 1 | 0 |
This puzzle puts the callvalue on the stack, duplicates it and multiplicates it by itself. After that it pushes the value 0x0100 = 256
to the stack and checks if the result of the multiplication before is equal.
So the puzzle basically squares the callvalue and checks if the result is equal to 256. If yes, it jumps to the desired jumpdestination, if not it reverts.
The JUMPI
opcode is used to implement conditions, it takes two values from the stack, the first (topmost) value is the jump destination, the second value is the conditional value: If its different from 0, it jumps to JUMPDEST
, if not it will simply continue.
The solution is the square root of 256, which is 16.
This puzzle takes the calldata via CALLDATALOAD
and pads it to 32 bytes, the result is being used as JUMPDEST
.
To solve this puzzle we have to send the jump destination 0x0A padded to 32 bytes:
0x000000000000000000000000000000000000000000000000000000000000000A
This puzzle is one of the most difficult so far.
CALLDATACOPY
saves the calldata to the memory. It takes three input arguments: The memory offset, the calldata offset, and the length to copy. In our puzzle CALLDATACOPY
copies the entire calldata to memory, so the memory and calldata content is now identical.
It continues with the same three opcodes, but then executes CREATE
. What this opcode does is creating a new contract, with the calldata as creation bytecode.
CREATE
returns the address of the new contract, which is consumed by EXTCODESIZE
. The following opcodes basically check if the bytecode size of the newly created contract is equal to one, if yes, jump to the jump destination.
To achieve this, we have to submit a creation bytecode which just returns a single byte as calldata
0x60016000F3
-> push a single byte to memory and return it via RETURN
.
For more info on creation vs runtime bytecode, check out this link or this.