You are currently viewing Ethernaut: GatekeeperTwo.  This gatekeeper introduces a few new… |  by Balaji Shetty Pachai |  Coinmonks |  Sep 2022

Ethernaut: GatekeeperTwo. This gatekeeper introduces a few new… | by Balaji Shetty Pachai | Coinmonks | Sep 2022

This gatekeeper introduces a few new challenges. Register as an entrant to pass this level.

Things that might help:

  1. The assembly keyword in the second gate allows a contract to access functionality that is not native to vanilla Solidity. See here for more information. The extcodesize call in this gate will get the size of a contract’s code at a given address — you can learn more about how and when this is set in section 7 of the yellow paper.
  2. The ^ character in the third gate is a bitwise operation (XOR), and is used here to apply another common bitwise operation.

Problem Statement: In order to beat this level, you will have to set the value of entrant which is a public state variable in the GatekeeperTwo contract. This value can only be set via the enter(bytes8 _gateKey) function. This function has 3 modifiers which must be passed only then the value of entrant can be set as expected.

Modifier gateOne(): msg.sender != tx.origin

Modifier gateTwo(): extcodesize(caller()) == 0

Modifier gateThree(): Find the value of _gateKey such that the bitwise operation XOR must evaluate to uint64(0) — 1

Problem Contract:

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

contract GatekeeperTwo {

address public entrant;

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

modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);

modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;


  1. Modifier gateOne() can be easily passed by creating an AttackGateKeeperTwo contract and from that contract calling the enter function. This ensures that the tx.origin is EOA (Externally Owned Account) whereas the msg.sender is the address of AttackGateKeeperTwo contract. Thus, the require statement msg.sender != tx.origin is passed successfully.
  2. Modifier gateTwo() for this to execute successfully, the code size of the contract must be 0. You may think of how can this be achieved? We are calling enter from AttackGateKeeperTwo contract this contract does have code attached to it, so it’s code size will be non-zero), you may come to a conclusion that we can never bypass the modifier gateTwo(), however, that is not the case. This is where the subtleties of Solidity comes in handy and as described in section 7 of the Ethereum Yellow Paper “During initialization code execution, EXTCODESIZE on the address should return zero.” Thus, by making use of the subtlety we invoke enter right from the AttackGateKeeperTwo’s constructor as during that time the code size for the contract returns 0. In this way we have bypassed the modifier gateTwo() as well.
  3. Modifier gateThree() for this to pass the XOR result must be . This is achieved by the following as a ^ b = c implies a ^c = b. In our case `b` is `gateKey`

AttackeGateKeeperTwo Contract on Rinkeby:

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

contract AttackGateKeeperTwo {
GatekeeperTwo gTwo = GatekeeperTwo("GateKeeperTwo Deployed Address");
event GateTwoCompromised(address who, uint256 timestamp);
* modifierOne will be passed as tx.origin = EOA and msg.sender = address(this)
* modifierTwo will be passed as during contract construction the extcodesize is 0
* Reference from YellowPaper: During initialization code execution, EXTCODESIZE on the address should return zero, which is the length of the code of the account.
* modifirerThree do calculation inside the constructor and pass it as gateKey
constructor() public {
// GatekeeperTwo's enter function must be called from here
// Since a ^ b = c implies a ^c = b. In our case `b` is `gateKey`
bytes8 gateKey = bytes8((uint64(0) - 1)) ^ bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))));
emit GateTwoCompromised(address(this), block.timestamp);


Security Takeaway:

  1. Checking for code size of an address can be zero during initialization.

If you have learned something from this post, do share it and follow for more such contents.

Leave a Reply