Magic Number - The Ethernaut - Writeup
To solve this level, you only need to provide the Ethernaut with a Solver, a contract that responds to whatIsTheMeaningOfLife() with the right 32 byte 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 bytes at most.
Analysis
The code of the contract is:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MagicNum {
address public solver;
constructor() {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
So, we need to deploy a contract, whose runtime code size is less than or equal to 10 bytes. This contract needs to return 42 when the function whatIsTheMeaningOfLife() gets called on it.
We could think of deploying this contract:
contract Exploit {
function whatIsTheMeaningOfLife() returns (uint256) public {
return 42;
}
}
But if we compile this contract using standard tools like forge, we will see that the bytecode generated is huge:

Definately bigger than 10 bytes. We will need to create the EVM bytecode by hand.
Contract deployment bytecode
When we compile a contract, the compiler generates the bytecode for us. This bytecode will have two main parts: the init code and the runtime code.
The init code is the bytecode responsible for preparing the contract: configuring the “free memory pointer”, initializing global variables and the logic for the constructor(). The bytecode corresponding to this stub does not get stored in the contract’s code. It is executed once and never saved in the blockchain.
The runtime code will be the one saved in the contract’s code and it has the logic for itself.
Function selectors
When we interact with a contract, we do so through the function name we want to execute, passing the attribute we’d like. When generating the bytecode, the function name is never stored. Instead, its function selector is stored. For instance, the function selector for the function test(address, uint256) would be 0xba14d606. We can compute these using cast:
cast sig "test(address, uint256)"

So, when we call the test(address, uint256) function, we are actually sending its selector, 0xba14d606. The compiler will generate bytecode to execute one function or other. It will look something like this (in pseudocode):
if (function_selector == 0xba14d606){
jump(address_of_ba14d606))
}
else if (function_selector == 0xbb29998e){
jump(other_address)
}
Creating the runtime code
But we are only allowed to write 10 bytes. We are not going to waste space by including if statements and function selectors. We can just write bytecode, without caring about the function being called. This bytecode will be executed independently of that.
So, let’s generate code that simply returns 42. We will need these instructions:
| OPCODE | Meaning | Stack input | Expression |
|---|---|---|---|
| 0x60 | PUSH1 | PUSH(uint8) | |
| 0x52 | MSTORE | offset, value | memory[offset:offset+32] = value |
| 0xF3 | RETURN | offset, length | return memory[offset:offset+length] |
In order to be able to return something, we need to write into memory, because that’s how it is defined. To store in the memory, we can use, for instance, MSTORE. All the input that these opcodes need are extracted from the stack. In order to push into the stack, we need a PUSH opcode. As we are only going to push 1-byte data, we can just use PUSH1.
To return 42, we need to store in the memory the number 42. Let’s prepare the stack and call MSTORE:
PUSH1 0x2a ----- 0x2a == 42
PUSH1 0x00
MSTORE
Now, we prepare the stack again to call RETURN:
PUSH1 0x20
PUSH1 0x00
RETURN
So that our final bytecode will be:
0x602a60005260206000f3
Which, by the way, is exactly 10 bytes long.
Creating the init code
We now need to create the init code. We don’t need anything too fancy, just the most basic init code will do the trick. The init code ends with a RETURN opcode. Before that, the runtime code needs to be loaded in order to be deployed. the opcode used for that is CODECOPY:
| OPCODE | Meaning | Stack input | Expression |
|---|---|---|---|
| 0x39 | CODECOPY | destOffset, offset, length | memory[destOffset:destOffset+length] = address(this).code[offset:offset+length] |
We know that our runtime code length is 10. To know what offset should be, we need to write our whole init code first. We do know what destOffset should be. It can simply be 0. All in all, calling this opcode will look something like this:
PUSH1 0x0a
PUSH1 0x??
PUSH1 0x00
CODECOPY
As said before, we also need to prepare the stack to call RETURN. As we are returning 10 bytes of bytecode, its length attribute would be 10. The offset would be 0:
PUSH1 0x0a
PUSH1 0x00
RETURN
All in all, our total init code would be 12 bytes long, so that our final init code would look like:
0x600a600c600039600a6000f3
Deploying
So our whole bytecode is:
0x600a600c600039600a6000F3602a60005260206000F3
We can use cast to deploy a contract with this bytecode:
cast send --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --create 0x600a600c600039600a6000F3602a60005260206000F3
After deploying, we can verify that it works as expected and returns 0x2a. We can use any function for that, as it doesn’t have any function selectors:
cast call 0xc5FF86B639f7AA4BfaE0020e28E2B1983D4a1701 "test(uint256)" 4 --rpc-url $SEPOLIA_RPC_URL -vvvv

