Ethernaut Lvl 13 Gatekeeper 1 Walkthrough: How to calculate smart contract gas consumption (and byte masking)

This is a in-depth series around Zeppelin team’s smart contract security puzzles. We learn key Solidity concepts to solve the puzzles 100% on your own.

Nicole Zhu
Coinmonks
Published in
5 min readSep 4, 2018

--

In this level, you estimate gas and mask your bytes to pass three different gates. The concepts behind Gate 1 is explained here in detail.

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.

Stepping through Solidity assembly (Gate 2)

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):

Where δ: gas to remove from the stack; α: gas to add to the stack

Let’s use Remix IDE to step through the following simple contract:

pragma solidity ^0.4.24;
contract SimpleContract {
function add() public pure returns (uint) {
uint a = 1;
uint b = 2;
return (a+b);
}
}

In Javascript VM, deploy this contract, invoke add(), and click the on the debug enabler in Remix console to bring up debugger UI:

Enable both Instructions and Step detail drop downs as you step into each assembly opcode.

Notice how the gas values in Step detail correspond to the opcodes chart in the yellow paper. In this case, the opcode ADD costs 3 gas, as predicted.

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 level, make sure you have configured Remix to the correct compiler version.

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.

Byte masking (Gate 3)

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

You are now ready to solve this level!

Detailed Walkthrough

Pass Gate 1

  1. Similar to Ethernaut level 4, you can pass Gate 1 by simply letting your contract be the middleman. Create a contract called Hack.sol which accesses your level instance:
contract Hack {
GatekeeperOne gate = GatekeeperOne(//YOUR ADDR);
...
}

Pass Gate 3

2. 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

3. Calculate the key using the0xFFFFFFFF0000FFFF mask:

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

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.

4. Since you’ll be calculating gas, first figure out your contract instance’s compiler version & settings. In Etherscan, look up your contract instance by its address.

5. Notice GatekeeperOne was compiled with version v0.4.18 with no optimization enabled. Update your Remix settings accordingly.

6. Create a function which will call enter() and allocate a specified amount of gas. You should invoke enter() with the lower level call function, which gives you more control over gas usage. Allocate some arbitrary amount of gas:

function hackGate() public {
gate.call.gas(99999)(bytes4(keccak256('enter(bytes8)')), key);
}

7. Step through the Remix debugger in JavaScript VM until you reach the correct opcode (when the Remix IDE highlights over msg.gas % 8191).

8. Count the remaining gas and work backwards to arrive at the correct original gas allocation. And replace the call gas allocation with your new value.

Note: You may have to do step 8 for 1–2 iterations before your remaining gas % 8191 becomes 0.

6. Toggle Remix back to Injected Web3 and you should now be able to pass GatekeeperOne!

Key Security Takeaways

  • 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

More Levels

Get Best Software Deals Directly In Your Inbox

--

--