✅
MagicNumber
EVM opcodes
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!

Ethernaut Lvl 19 MagicNumber Walkthrough: How to deploy contracts using raw assembly opcodes
Medium
0xSage MagicNumber Walkthrough
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MagicNum {
address public solver;
constructor() {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
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:
- 1.Runtime code
- 2.Creation code
- 3.Factory contract
The naive approach is writing a contract containing only one function that returns 42. For example:
contract Hack {
function whatIsTheMeaningOfLife() external pure returns (uint) {
return 42;
}
}
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:
// The "storing into memory" part
PUSH1 0x2a // [42]
PUSH1 0x00 // [0, 42]
MSTORE // mstore(0, 42) -> store the number 42 starting from memory offset 0
// The "returning" part
PUSH1 0x20 // [32]
PUSH1 0x00 // [0, 32]
RETURN // return(0, 32) -> return 32 bytes starting from memory offset 0
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:
602a60005260206000f3
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:
creation code + runtime code
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:// The "storing into memory" part
PUSH10 0x602a60005260206000f3 // [0x602a60005260206000f3]
PUSH1 0x00 // [0, 0x602a60005260206000f3]
MSTORE // mstore(0, 0x602a60005260206000f3) -> store the runtime code 0x602a60005260206000f3 starting from memory offset 0
// The "returning" part
PUSH1 0x0a // [10]
PUSH1 0x16 // [22, 10]
RETURN // return(22, 10) -> return 10 bytes starting from memory offset 22
Go back to EVM Playground and compile it:
69602a60005260206000f3600052600a6016f3
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:- 1.
value
: value in wei to send to the new account. - 2.
offset
: byte offset in the memory in bytes, the initialisation code for the new account. - 3.
size
: byte size to copy (size of the initialisation code).
Here is the factory contract:
contract Factory {
function deploy() external {
// len(bytecode) = 19 = 0x13
bytes memory bytecode = hex"69602a60005260206000f3600052600a6016f3";
address addr;
assembly {
// create(value, offset, size)
addr := create(0, add(bytecode, 0x20), 0x13)
}
// Verify if the contract was successfully deployed
require(addr != address(0));
}
}
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.
"Hello, world!" byte string ABI encoding example
In the final version of our solution contract, we make a few changes to fit the context of this challenge:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MagicNumberHack {
constructor(MagicNum challenge) {
// len(bytecode) = 19 = 0x13
bytes memory bytecode = hex"69602a60005260206000f3600052600a6016f3";
address addr;
assembly {
// create(value, offset, size)
addr := create(0, add(bytecode, 0x20), 0x13)
}
// Verify if the contract was successfully deployed
require(addr != address(0));
// Interact with the challenge contract
challenge.setSolver(addr);
}
}
contract MagicNum {
address public solver;
constructor() {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
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.
Last modified 6mo ago