Skip to content

Latest commit

 

History

History
194 lines (127 loc) · 6.1 KB

README.md

File metadata and controls

194 lines (127 loc) · 6.1 KB

Solidity Game - Gatekeeper Attack

Inspired by OpenZeppelin's Ethernaut, Gatekeeper One Level

⚠️Do not try on mainnet!

Task

Make it past the gatekeeper and register as an entrant.

Hint:

  1. Remember what you've learned from the Telephone and Token games.
  2. You can learn more about the special function gasleft(), in Solidity's documentation (see here and here).

What will you learn?

  1. How to count gas

    In Ethereum, computations cost money. This is calculated by gas * gas price, where gas is a unit of computation and gas price scales with the load on Ethereum network. The transaction sender needs to pay the resulting ethers for every transaction she/it invokes.

    Complex transactions (like contract creation) costs more than easier transactions (like sending someone some Ethers). Storing data to the blockchain costs more than reading the data, and reading constant variables costs less than reading storage values.

    Specifically, gas is assigned at the assembly level, i.e. each time an operation happens on the call stack. For example, these are arithmetic operations and their current gas costs, from the Ethereum Yellow Paper (Appendix H):

    gas1

    Tip: Use Remix to play gas

    Important to know

    Different Solidity compiler versions will calculate gas differently. And whether or not optimization is enabled will also affect gas usage. Try changing the compiler defaults in Settings tab to see how remaining gas will change.

    Before starting this game, make sure you have configured Remix to the correct compiler version.

  2. Datatype conversions

    The second piece of knowledge you need to solve this level is around data conversions. Whenever you convert a datapoint with larger storage space into a smaller one, you will lose and corrupt your data.

    gas2

  3. Byte masking

    Conversely, if you want to intentionally achieve the above result, you can perform byte masking. Solidity allows such bitwise operations for bytes and ints as follows:

bytes4 a = 0xffffffff;
bytes4 mask = 0xf0f0f0f0;
bytes4 result = a & mask ;   // 0xf0f0f0f0

What is the most difficult challenge?

  1. Pass Gate 1

    Similar to Telephone, you can pass Gate 1 by simply letting your contract be the middleman.

  2. Pass Gate 3

    Gate 3 takes in an 8 byte key, and has the following requirements:

require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));

This means that the integer key, when converted into various byte sizes, need to fulfil the following properties:

  • 0x11111111 == 0x1111, which is only possible if the value is masked by 0x0000FFFF
  • 0x1111111100001111 != 0x00001111, which is only possible if you keep the preceding values, with the mask 0xFFFFFFFF0000FFFF

Calculate the key using the 0xFFFFFFFF0000FFFF mask:

bytes8 key = bytes8(tx.origin) & 0xFFFFFFFF0000FFFF;

UPDATE:

Due to Solidity v0.8.0 changes, type conversion address to bytes8 is not allowed. Use as following:

bytes8 key = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
  1. Pass Gate 2

    Finally, to pass Gate 2’s require(msg.gas % 8191 == 0), you have to ensure that your remaining gas is an integer multiple of 8191, at the particular moment when msg.gas % 8191 is executed in the call stack.

Security Considerations

  • Abstain from asserting gas consumption in your smart contracts, as different compiler settings will yield different results.
  • Be careful about data corruption when converting data types into different sizes.
  • Save gas by not storing unnecessary values. Pushing a value to state MSTORE, MLOAD is always less gas intensive than store values to the blockchain with SSTORE, SLOAD
  • Save gas by using appropriate modifiers to get functions calls for free, i.e. external pure or external view function calls are free!
  • Save gas by masking values (less operations), rather than typecasting

Source Code

⚠️This contract contains a bug or risk. Do not use on mainnet!

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

contract GatekeeperOne {
  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(uint160(tx.origin)),
      "GatekeeperOne: invalid gateThree part three"
    );
    _;
  }

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

Configuration

Install Truffle cli

Skip if you have already installed.

npm install -g truffle

Install Dependencies

yarn install

Test and Attack!💥

Run Tests

truffle develop
test

You should pass three gatekeepers successfully.

truffle(develop)> test
Using network 'develop'.


Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



  Contract: Hacker
    √ should pass three gatekeepers (239ms)


  1 passing (328ms)