Ethernaut Lvl 14 Gatekeeper 2 Walkthrough: How contracts initialize (and how to do bitwise operations)
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.
This levels requires you to get familiar with the Ethereum yellow paper and pass three more gates.
Inner workings of contract creation
The yellow paper formally denotes contract creation as:
Here’s a simplified flow of how contracts are created and what these variables mean:
- First, a transaction to create a contract is sent to the Ethereum network. This transaction contains input variables, notably:
- Sender (s): this is the address of the immediate contract or external wallet that wants to create a new contract.
- Original transactor (o): this is the original external wallet (a user) who created the contract. Notice that
o != s
if the user used a factory contract to create more contracts! - Available gas (g): this is the user specified, total gas allocated for contract creation.
- Gas price (p): this is the market rate for a unit of gas, which converts the transaction cost into Ethers.
- Endowment (v): this is the
value
(in Wei) that’s being transferred to seed this new contract. The default value is zero. - Initialization EVM code (i): this is everything inside your new contract’s
constructor
function and the initialization variables, in bytecode format.
2. Second, based on just the transaction’s input data, the new contract’s designated address is (pre)calculated. At this stage, the input state values are modified, but the new contract’s state is still empty.
3. Third, the initialization code kickstarts in the EVM and creates an actual contract.
4. During the process, state variables are changed, data is stored, and gas is consumed and deducted.
5. Once the contract finishes initializing, it stores its own code in association with its (pre)calculated address.
6. Finally, the remaining gas and a success/failure message is asynchronously returned to the sender s
.
Hint: Notice that up until step 5, no code previously existed at the new contract’s address!
In the footnote of the yellow paper:
During initialization code execution, EXTCODESIZE on the address should return zero, which is the length of the code of the account while CODESIZE should return the length of the initialization code (as defined in H.2
Put simply, if you try to check for a smart contract’s code size before or during contract construction, you will get an empty value. This is because the smart contract hasn’t been made yet, and thus cannot be self cognizant of its own code size.
Bitwise Operations
Solidity supports the following logic gate operations:
&
: and(x, y) bitwise and of x and y; where1010 & 1111 == 1010
|
: or(x, y) bitwise or of x and y; where1010 | 1111 == 1111
^
: xor(x, y) bitwise xor of x and y; where1010 ^ 1111 == 0101
~
: not(x) bitwise not of x; where~1010 == 0101
Notice
- If
A xor B = C
, thenA xor C = B
- In Solidity, exponentiation is handled by
**
, not^
You now have all the knowledge to solve the level!
Detailed Walkthrough
- Pass Gate 1 by creating a smart contract middleman, in Remix IDE:
contract Hack {
}
Next, let’s pass Gate 3 before 2, as the key
value is a prerequisite to invoking the function at Gate 2. To solve Gate 3, notice:
uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) — 1)
2. Recall that if If A xor B = C
, then A xor C = B
. This means the key is just the bytes8
typecast of:
uint64(_gateKey) = uint64(keccak256(msg.sender)) ^ uint64(0) — 1)
Finally, notice that Gate 3 uses an assembly function to check that the size of the calling contract is zero, i.e. contains 0 code. But, your calling contract has to have code in the first place in order to invoke GatekeeperTwo.
assembly { x := extcodesize(caller) }
So how is this possible?
Recall that during a contract’s construction, it deploys code from its (pre)calculated address, but that code is not yet stored in association with the contract itself!
This means extcodesize(sender)
should return 0, if extcodesize
is a subroutine inside the sender contract’s original constructor
function.
3. Put the enter()
function call inside your Hack.sol
’s contract construction:
Congrats on passing both gates!
Key Security Takeaways
- In addition to contract blackholes, you can also create Zombie contracts by stopping contract initialization. The resulting contract has an address, but permanently no code, and will never be able to return you the initial endowment.