✅MagicNumber
EVM opcodes
Last updated
EVM opcodes
Last updated
To solve this level, you only need to provide the Ethernaut with a Solver
, a contract that responds to whatIsTheMeaningOfLife()
with the right number.
Easy right? Well... there's a catch.
The solver's code needs to be really tiny. Really reaaaaaallly tiny. Like freakin' really really itty-bitty tiny: 10 opcodes at most.
Hint: Perhaps its time to leave the comfort of the Solidity compiler momentarily, and build this one by hand O_o. That's right: Raw EVM bytecode.
Good luck!
The ASCII art is 42, which is the answer to whatIsTheMeaningOfLife()
. Our objective is to build a set of opcodes (<= 10 bytes) that answers 42 when whatIsTheMeaningOfLife()
is called.
We have to prepare 3 things to tackle this challenge:
Runtime code
Creation code
Factory contract
The naive approach is writing a contract containing only one function that returns 42. For example:
This approch would fail because it is going to exceed 10 bytes. This is because Solidity has lots of overhead for such contract initialization and function declaration. We want to implement a minimal contract using only opcodes.
Returning 42 is equivalent to the following opcode sequence:
You can lookup the usage of each opcode at evm.codes. At EVM Playground, we can compile the mnemonic we just wrote into bytecode squence. The result is:
This is just 10 bytes so we are good to go.
The bytecode sequence above is our "runtime code". Recall that contract creation is made of two parts:
The bytecodes for these two parts are concatenated together. When a transaction is sent to the zero address, the "creation code" is run until a STOP
or RETURN
is encountered. At that moment the contract address is set and only runtime code is left on the callstack, so we are ready to go.
Now we have runtime code, and we should be constructing the creation code. The idea is similar: we shall store the runtime code bytecode sequence into memory, then return it. Again, the creation code will contain the "storing into memory" part and the "returning" part.
Here is the caveat: when returning the runtime code, we have to start from memory offset 32 - 10 = 22 instead of offset 0. This is because the runtime code is not a number and we can't append any 0x00
to its left. Recall that 0x00
is the opcode for STOP
, which is something that we should avoid. Since the runtime code is 10-byte long, it starts from memory offset 22:
Go back to EVM Playground and compile it:
In the very last step, we are going to deploy the bytecode we generated in step 2. This can be done with a "factory contract".
The factory contract uses CREATE
to deploy the creation code. CREATE
takes 3 inputs:
value
: value in wei to send to the new account.
offset
: byte offset in the memory in bytes, the initialisation code for the new account.
size
: byte size to copy (size of the initialisation code).
Here is the factory contract:
Pay attention to the create()
function call: the second parameter is add(bytecode, 0x20)
, not bytecode
. Here bytecode
is a pointer to the memory location and we are getting the location 32 bytes after that pointer. But Why? It is because the datatype bytes
is made of two parts. The first 32 bytes of it is the length of the byte string, and the actual value of the byte string comes after that 32 bytes. The datatype string
works the same.
You can find an example in Solidity doc:
In the final version of our solution contract, we make a few changes to fit the context of this challenge:
Congratulations! If you solved this level, consider yourself a Master of the Universe.
Go ahead and pierce a random object in the room with your Magnum look. Now, try to move it from afar; Your telekinesis habilities might have just started working.