Delegation — Ethernaut Level 6

Joshua Moore
DraftKings Engineering
6 min readNov 14, 2022

Two incredibly useful tools in solidity are fallback functions and the use of delegatecall. As with any topic touched on in The Ethernaut, with great power comes great responsibility. The Ethernaut Level 6 — Delegation is a› great example on the way each of these work, and how proper planning is required when making use of them. This article gives an overview of the problem, and goes into details on how both fallback functions work and what delegatecall does, and how it affects the contract.

Understanding delegatecall and fallback

Before getting into the problem, it is important to understand how delegatecall and fallback work, since these are the two main things this problem deals with. Both are widely used in Solidity development, but can create security risks if not used properly.

Use of delegatecall

Delegatecall is incredibly useful and straightforward once understood.

The purpose of a contract using delegatecall is to execute another contract’s code, while using and writing to its own state. This means any change of state in the function of the contract being called, is actually being changed in the contract using delegatecall. This is how upgradeable proxy contracts are able to change implementation while retaining all storage. Take a look at ContractA in the following example:

delegatecall of ContractB implementation.

By passing in the address of ContractB to callContractB() we will change the state variable name of ContractA to “Contract B” and the ContractA update will increment.

Please note, this is an example to show storage layout. It is unsafe to open up delegatecall to any address.

It is also important to note that when using delegatecall, msg.sender does not change, nor does address(this) or msg.value. For example, the address of the externally owned account (EOA) that calls callContractB() in ContractA, will be the msg.sender address in ContractB. This is particularly useful for the Ethernaut problem that we will be solve later on.

Safety First

To use delegatecall safely, there are a few major things to pay particular attention to:

State Variable Layout
Both the calling contract and the contract being called using delegatecall must have the same storage layout. Storage in Solidity works by using numerical address space. Delegating a call to another contract has a risk of storage collision if a state variable is not in the same storage slot as the calling contract. Let’s take a look at the previous example, but swap the storage layout:

Storage Collision

By swapping the order of updated and name in ContractA it changed their storage slot positions, creating a potential storage collision. Using delegatecall to call ContractB’s changeName() function now sets updated to name and tries to increment name as if it were an integer, effectively bricking ContractA’s storage as name will now revert.

Note: If you take a look at OpenZeppelin’s upgradeable versions of contracts, ie. ERC721Upgradeable, you’ll notice storage __gap(s) at the end of each file. These are in place to allow for reserving storage space if new state variables need to be appended in a new implementation.

Only delegatecall Contracts You Know
An important rule of thumb is to control where delegatecall happens and who has the ability to pass in a contract to call. If left open, a malicious attack can be made to make unintended changes to the state of your contract.

Keep in mind, using delegatecall on a non-contract will return true.

Control Who and What delegatecall Has Access to
It is important to limit where state variables can be changed, and who is able to call these functions. When using delegatecall, thoughtfully craft your functions to be called only by intended EOAs. Although delegatecall is used in proxy pattern’s fallback functions, it is a pattern that should be carefully utilized. Smart coding of your implementation contract is necessary to prevent unintended state changes.

One of the most infamous hacks involving delegatecall was The Parity Multisig Wallet Hack (#2), which resulted in an attacker stealing over 150,000 ether (valued at ~255 Million USD at the time of writing). The attacker utilized an open library function to claim ownership of multi-sigs, and executed withdrawals resulting in stolen millions.

Details on fallback

Built into Solidity, there is a fallback mechanism in place to handle:

  • Data received, but no function signature existing in the contract to handle it
  • No data provided within the function call

A common use of fallback functions is a proxy contract, which uses fallback functions to delegate a call to an implementation contract.

For a more detailed explanation of fallback(), check out our post on Fallback and Receive — Ethernaut Level 1.

The Problem

The goal of this Ethernaut level is to gain ownership of the Delegation contract by use of the delegatecall within the fallback function. Let’s take a look at the contracts:

Delegation

The following is the contract to take ownership of by use of fallback and delegatecall.

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}

Delegate

The following is the contract which Delegation calls using delegatecall.

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Delegate {
address public owner;
constructor(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}

Breaking it Down

To start, take note of where ownership changes within these contracts. Outside of the constructor, the only place ownership changes is in the Delegate contract’s public function pwn(). Calling pwn() directly will take ownership of the Delegate contract, but the goal is to take ownership of the Delegation contract.

Examining fallback()

The first line executed in the fallback function in Delegation is a delegatecall using msg.data to the Delegate contract address. By passing in the function selector for pwn() in the msg.data, the ownership will change to msg.sender. As stated earlier in this article, msg.sender will not be the address of Delegation, but instead will be the address of the EOA that calls Delegation.

Refresher on delegatecall

Because Delegation is calling Delegate using delegatecall, the state of Delegation is what will be changed. Examining the state, we see that both contracts contain address owner in the first memory slot, allowing us to effectively change the owner of Delegation.

Using web3js to Complete the Mission

For this problem, we’re going to stick to using the web3js library included in the Ethernaut console (accessed by opening the developer tools in your browser on the problem’s page). Although it’s possible to write a smart contract to claim ownership of the Delegation contract, the goal here is to claim ownership with the EOA.

To start, generate the function selector of pwn() to trigger the fallback function of Delegation. Function selectors are 4 bytes that represent the function. Do this by running the following command:

> web3.eth.abi.encodeFunctionSignature("pwn()")
'0xdd365b8b'

With the selector, you can now call the Delegation contract with this data to claim ownership by utilizing the fallback() and delegatecall. Do so by running the following command:

> await contract.sendTransaction({data: "0xdd365b8b"})

Once the transaction is verified, running the following command should show ownership has been claimed:

> await contract.owner()

Submit the instance to complete Ethernaut Level 6.

In Conclusion

Once understood, delegatecall can be an incredibly useful tool to use in your smart contract development. It is widely used in upgradable proxy patterns contracts and provides incredible flexibility in design. When planned accordingly, this tool can elevate smart contract development to a new level.

The next problem, Ethernaut level 7 — Force, will focus on preventing funds from being received by your contract and utilizing the selfdestruct operation.

--

--