Inspired by OpenZeppelin's Ethernaut, Gatekeeper One Level
Make it past the gatekeeper and register as an entrant.
Hint:
- Remember what you've learned from the Telephone and Token games.
- You can learn more about the special function
gasleft()
, in Solidity's documentation (see here and here).
-
How to count gas
In Ethereum, computations cost money. This is calculated by
gas * gas price
, wheregas
is a unit of computation andgas 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):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.
-
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
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
-
Pass Gate 1
Similar to Telephone, you can pass Gate 1 by simply letting your contract be the middleman.
-
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 by0x0000FFFF
0x1111111100001111 != 0x00001111
, which is only possible if you keep the preceding values, with the mask0xFFFFFFFF0000FFFF
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;
-
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 of8191
, at the particular moment whenmsg.gas % 8191
is executed in the call stack.
- 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 withSSTORE
,SLOAD
- Save gas by using appropriate modifiers to get functions calls for free, i.e.
external pure
orexternal view
function calls are free! - Save gas by masking values (less operations), rather than typecasting
// 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;
}
}
Skip if you have already installed.
npm install -g truffle
yarn install
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)