DEAL20% OFF
Pre Black FridaySALE
--
Days
--
Hours
--
Minutes
--
Seconds
Book a call
DEAL20% OFF
Pre Black FridaySALE
--
Days
--
Hours
--
Minutes
--
Seconds
Book a call
GLITCH40% OFF
Price Glitch40% OFF
00
Days
00
Hours
05
Minutes
00
Seconds
Claim 40% Off
GLITCH40% OFF
Price Glitch40% OFF
00
Days
00
Hours
05
Minutes
00
Seconds
Claim 40% Off
All articles

Ethernaut Level 18 Walkthrough: Magic Number

Ana Rojo 8 min read
Ethernaut Level 18 Walkthrough: Magic Number

As you may already know, The Ethernaut, from OpenZeppelin, is a series of challenges designed to teach and test your understanding of smart contract security. Well, today was the time for Ethernaut Level 18, titled "Magic Number". Let's walk through this code challenge.

Objective

The objective of the Magic Number challenge is to deploy a smart contract that returns the number 42 when the whatIsTheMeaningOfLife() function is called. The contract must be as small as possible, occupying no more than 10 opcodes.

ethernaut level 18 magic number walkthrough challenge
Source

The Instinctive Approach

My first thought was to write a simple contract 'Approach' with the inquired function 'whatIsTheMeaningOfLife' that returns 42.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

contract Approach {

    function whatIsTheMeaningOfLife() external pure returns (uint256) {

        return 42;

    }

}

But it was too easy to be right. Actually, when I checked the compiled bytecode of the contract, it was way too big, blowing past the 10-opcode limit. Then, it became clear that the trick here was in the 10-opcode limit of the contract.

Therefore, we had to find a strategy to generate a really tiny code. One way to tackle it is by crafting the contract directly in EVM bytecode, to keep it super minimal and efficient.

Just in case, let's review what is an opcode and how they are generated in Solidity.

What Are Opcodes?

Opcodes, or operation codes, are the low-level instructions executed by the Ethereum Virtual Machine (EVM). Each opcode performs a specific operation, such as adding numbers, storing data, or interacting with contracts. In Solidity, high-level code is compiled into these opcodes, which the EVM can then execute. 

For example, a simple addition in Solidity translates to several opcodes that handle pushing values to the stack, performing the addition, and storing the result. Understanding opcodes is crucial for optimizing and writing minimalistic contracts directly in EVM bytecode.

Our EVM Bytecode

Let me now back to the issue of writing the contract directly in EVM bytecode. This involves knowing the sequence of opcodes that the contract will require. In our case, we need a runtime bytecode that runs these operations:

  1. Push the value 42 onto the stack.
  2. Store it in memory.
  3. Return it.

This translates to the following opcodes:

  • PUSH1 0x2a (Push 42 onto the stack) --> '60 2a'
  • PUSH1 0x00 (Push 0 onto the stack)--> '60 00'
  • MSTORE (Store 42 in memory at position 0)--> '52'
  • PUSH1 0x20 (Push 32, length of return data, onto the stack)--> '60 20'
  • PUSH1 0x00 (Push 0, the start position of return data, onto the stack)--> '60 00'
  • RETURN (Return 32 bytes from memory starting at position 0) --> 'f3'

Then, the corresponding runtime bytecode is: '602a60005260206000f3'.

But, the deployment bytecode needed to deploy the contract on the blockchain (which includes both the constructor of the contract and the runtime bytecode) is the following:

  • PUSH10 0x602a60005260206000f3 (Push the runtime bytecode onto the stack) --> '69 602a60005260206000f3'
  • PUSH1 0x00 (Push 0 onto the stack)--> '60 00'
  • MSTORE (Store the runtime bytecode in memory at position 0)--> '52'
  • PUSH1 0x0a (Push 10, length of runtime bytecode, onto the stack)--> '60 0a'
  • PUSH1 0x16 (Push 22, position where the runtime bytecode starts, onto the stack)--> '60 16'
  • RETURN (Return 10 bytes from memory starting at position 0) --> 'f3'

Then, the corresponding deployment bytecode is: '69602a60005260206000f3600052600a6016f3'.

Coding the Solution

Once the deployment bytecode was found, it was time to deploy this minimal contract on the blockchain. Then, the DeployAndSetSolver contract was implemented. This contract consists of its constructor, which is responsible for deploying the minimalistic solver contract and then setting it in the MagicNum contract. It includes the runtime bytecode to return 42 and handles deploying this bytecode and registering the contract's address with MagicNum.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import "./MagicNum.sol";

contract DeployAndSetSolver {

    constructor(address magicNumAddress) {

        // Deployment bytecode: 69602a60005260206000f3600052600a6016f3

        bytes memory deploymentBytecode = hex"69602a60005260206000f3600052600a6016f3";

        address minimalContractAddress;

        // Deploy the minimal contract manually using assembly

        assembly {

            minimalContractAddress := create(0, add(deploymentBytecode, 0x20), mload(deploymentBytecode))

        }

        // Set the solver in the MagicNum contract

        MagicNum(magicNumAddress).setSolver(minimalContractAddress);

    }

}

The MagicNum contract provided by the challenge had a simple purpose: to store the address of another contract (the solver). For this reason, it includes the setSolver function to set this address. This separation allows us to deploy a minimalistic solver contract and then register its address in MagicNum.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

contract MagicNum {

    address public solver;

    function setSolver(address _solver) public {

        solver = _solver;

    }

}

Deployment and Testing

In my case, the implementation of the contracts was carried out in the VSCode IDE and for the deployment I used Hardhat and the Polygon Amoy testnet, so I leave below the step by step of how to do it. However, you can use REMIX to compile and deploy the contracts and use the MagicNumber contract address of the challenge.

Deployment with Hardhat

1. Install Hardhat in your project:

npm install --save-dev hardhat

2. Install hardhat-waffle in your project:

npm install --save-dev @nomiclabs/hardhat-waffle

3. Create a Hardhat project:

npx hardhat init

4. Configure Hardhat. Select Create an empty hardhat.config.js:

Now, go to file named hardhat.config.js and add the necessary configuration. In may case, I setted polygon_amoy as network and I copied the Infura URL for polygon-amoy with my Infura Api Key, and also the private key of my Metamask account. :

require("@nomiclabs/hardhat-waffle");

 module.exports = {

     solidity: "0.8.20",

     networks: {

       hardhat: {},

       polygon_amoy: {

         url: [INFURA_URL],

         accounts: [YOUR_PRIVATE_KEY]

       }

     }

   };

5. Create the deployment script in scripts/deploy.js:

The deploy.js script starts by deploying the MagicNum contract, which serves as the registry for our solver contract. Next, it deploys the DeployAndSetSolver contract, which handles deploying the minimalistic solver and setting its address in the MagicNum contract. The addresses of both deployed contracts are logged for verification.

const hre = require("hardhat");

  async function main() {

//--------------- DEPLOYMENT ---------------

     const [deployer] = await hre.ethers.getSigners();

     console.log("Deploying contracts with the account:", deployer.address);

     console.log("Account balance:", (await deployer.getBalance()).toString());

     // Deploy MagicNum contract

     const MagicNum = await hre.ethers.getContractFactory("MagicNum");

     const magicNum = await MagicNum.deploy();

     await magicNum.deployed();

     console.log("MagicNum contract deployed at address:", magicNum.address);

     // Deploy DeployAndSetSolver contract

     const DeployAndSetSolver = await hre.ethers.getContractFactory("DeployAndSetSolver");

     const deployAndSetSolver = await DeployAndSetSolver.deploy(magicNum.address);

     await deployAndSetSolver.deployed();

     console.log("DeployAndSetSolver contract deployed at address:", deployAndSetSolver.address);

//--------------------- TESTING -----------------------

     // Test the solver contract

     // ABI of the MagicNum contract

     const MagicNumABI = [

       "function solver() public view returns (address)"

     ];

     // Create a new instance of the MagicNum contract

     const magicNumContract = new hre.ethers.Contract(magicNum.address, MagicNumABI, deployer);

     // Get the solver address

     const solverAddress = await magicNumContract.solver();

     console.log("Solver contract address:", solverAddress);

     // ABI of the solver contract

     const SolverABI = [

       "function whatIsTheMeaningOfLife() public view returns (uint256)"

     ];

     // Create a new instance of the solver contract

     const solverContract = new hre.ethers.Contract(solverAddress, SolverABI, deployer);

     // Call the function whatIsTheMeaningOfLife

     const result = await solverContract.whatIsTheMeaningOfLife();

     console.log("The meaning of life is:", result.toString()); // Should log 42

   }

   main()

     .then(() => process.exit(0))

     .catch((error) => {

       console.error(error);

       process.exit(1);

     });

6. Compile the contracts:

  npx hardhat compile

7. Deploy and test the contracts on the Polygon Amoy test network:

  npx hardhat run scripts/deploy.js --network polygon_amoy

Reviewing the deploy.js code, it can be observed that after deployment, we conduct a quick test to check if we passed the challenge. Then, it was retrieved the solver contract's address from the MagicNum contract. Consequently, it was created an instance of the solver contract using its address and the ABI. Finally, it called the whatIsTheMeaningOfLife function on the solver contract and logged the result, which was 42.

Console logs after running deploy.js script.

Conclusion: Ethernaut Level 18 - Magic Number

Solving the Ethernaut Challenge 18: Magic Number requires a deep understanding of EVM bytecode and smart contract deployment mechanics. By leveraging low-level opcodes, we can create a minimal contract that meets the challenge's requirements.

faq

FAQs

Why is minimizing the number of opcodes important in this challenge?

  • The challenge specifically requires the contract to be as minimal as possible, using no more than 10 opcodes. This constraint tests the ability to optimize and write efficient low-level code.

Why is understanding opcodes and bytecode important for Ethereum developers?

  • Understanding opcodes and bytecode is crucial for optimizing smart contracts, writing highly efficient code, and ensuring security. It allows developers to have greater control over the execution and performance of their contracts on the Ethereum blockchain.

What is the difference between runtimeBytecode and deploymentBytecode?

  • runtimeBytecode is the code that runs when the contract is deployed and functions are called. deploymentBytecode includes both the constructor that runs when the contract is deployed and the runtimeBytecode that will be stored on the blockchain for future execution.

How do you deploy the minimal contract and set it as the solver?

  • We deploy the minimal contract using the DeployAndSetSolver contract, which includes the runtimeBytecode and sets it as the solver in the MagicNum contract by calling the setSolver function with the deployed contract's address.

How can I verify that the contract returns 42?

  • After deploying the contracts, use a script to call the whatIsTheMeaningOfLife() function from the solver contract and check the returned value. The script should log the result, confirming that the contract returns 42.
Share
Ana Rojo

Written by

Ana Rojo

Ana Rojo is a blockchain security analyst focused on EVM internals, contract bytecode, and storage layout vulnerabilities. Through step-by-step security walkthroughs on challenges like Ethernaut's Alien Codex and Magic Number, Ana teaches developers how low-level execution quirks can lead to protocol exploits and how to write secure smart contracts.

You may also like

Ethernaut Walkthrough

Ethernaut Level 7 Walkthrough: Force

Being relatively new to the Web3 space and having a keen interest in Security has naturally led to me participating the Ethernaut Challenge hosted by OpenZeppelin. For those unfamiliar with these challenges, each…

Jarrod PyneMay 8, 2024Read
Ethernaut Walkthrough

Ethernaut Level 6 Walkthrough: Delegation

In this level, we will familiarize ourselves with delegatecall and fallback . This serves as a preliminary step towards understanding more advanced concepts, like the proxy pattern.This article will clarify delegatecall…

Vinh TranMay 3, 2024Read
Ethernaut Walkthrough

Ethernaut Level 14 Walkthrough: Gatekeeper Two

Ethernaut challenges , comparable to a Web3-themed hacking Capture The Flag (CTF) competition, offer a dynamic environment for diving into Ethereum and Solidity programming. Each level, including Ethernaut Level 14 ,…

Janos BarnaJune 22, 2024Read
Ethernaut Walkthrough

Ethernaut Level 9 Walkthrough: King

In this challenge , we'll dive into smart contracts' payment vulnerability and essential security practices to prevent such issues. By the end of ethernaut level 9 , you'll be equipped to both exploit and safeguard the…

Vinh TranMay 20, 2024Read
Ethernaut Walkthrough

Ethernaut Level 1 Walkthrough: Fallback

This Web3/Solidity-based wargame is akin to a hacking Capture The Flag (CTF) challenge, where each level presents a smart contract puzzle waiting to be ‘hacked’. It’s an immersive and interactive way to learn about…

Sven DaneelFebruary 20, 2024Read
Ethernaut Walkthrough

Ethernaut Level 4 Walkthrough: Telephone

This Web3 /Solidity-based wargame is akin to a hacking Capture The Flag (CTF) challenge, where each level presents a smart contract puzzle waiting to be ‘hacked’. It’s an immersive and interactive way to learn about…

Sven DaneelMay 1, 2024Read
More Ethernaut Walkthrough articles →

Talk to an advisor

Book a call with a real human before the next cohort fills.

Speak with an advisor. We'll map the right program to your goals - and lock in your enrollment offer.

20% off enrollment offer

20% OFF - limited-time enrollment offer.

Applied automatically when you book a call before the timer hits zero.

--
Days
--
Hours
--
Minutes
--
Seconds
  • Guarantee: Job or 100% money back
  • Expert-curated curriculum
  • On-demand mentor support
Advisor Elena RodriguezAdvisor Sarah JenningsAdvisor David Chen
Real advisors · reply within 2 hours

Book your call

Free