0

I'm playing Ethernaut Gatekeeper Two:

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

import "forge-std/Test.sol";

contract GatekeeperTwo is Test {
    address public entrant;

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

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

    modifier gateThree(bytes8 _gateKey) {
        emit log_named_uint("part1", uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))));

        require(
            uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^
                uint64(_gateKey) ==
                uint64(0) - 1, "err3"
        );
        _;
    }

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

contract Attack is Test {
    constructor() public {
        GatekeeperTwo gk = new GatekeeperTwo();
        address g = address(gk);
        
        bytes4 sig = bytes4(keccak256("enter(bytes8)"));
        log_named_bytes("first method",  abi.encode(sig, key()));
        log_named_bytes("second method", abi.encodePacked(sig, key()));
        log_named_bytes("third method",  abi.encodeWithSignature("enter(bytes8)", key()));

        (bool success, ) = g.call(abi.encodePacked(sig, key()));
        require(success);
    }

    function key() private returns (bytes8) {
        uint64 val = uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^
            (type(uint64).max);
        return bytes8(val);
    }
}

contract ContractTest is Test {
    function setUp() public {}

    function testExample() public {
        new Attack();
    }
}

In my test code, these three lines,

log_named_bytes("first method",  abi.encode(sig, key()));
log_named_bytes("second method", abi.encodePacked(sig, key()));
log_named_bytes("third method",  abi.encodeWithSignature("enter(bytes8)", key()));

Produces different output:

Logs:
  first method: 0x3370204e00000000000000000000000000000000000000000000000000000000f719f96375b9c994000000000000000000000000000000000000000000000000
  second method: 0x3370204ef719f96375b9c994
  third method: 0x3370204ef719f96375b9c994000000000000000000000000000000000000000000000000

And only the third one produces the correct call data and cracks the level. Why won't the other two work?

1 Answer 1

2

A contract function call expects a bytes4 function selector along with abi-encoded parameters (each padded to 32 bytes).

abi.encode(sig, key());

pads both, sig (bytes4) and key() (bytes8) to 32 bytes. Because sig is treated as bytes32, this producing an incorrect encoding.

abi.encodePacked(sig, key());

packs both, sig (bytes4) and key() (bytes8) tightly without any extra padding. Because key() is not in the correct (bytes32) format, this is an incorrect encoding.

abi.encodeWithSelector(sig, key());

returns the correct encoding, a bytes4 sig, along with a 32 bytes padded value key().

The following are equivalent:

abi.encodeWithSelector(sig, key());
abi.encodeWithSignature("enter(bytes8)", key());
abi.encodePacked(sig, abi.encode(key()));

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.