Ethereum x Foundry - 0x6 Delegation

Ethereum x Foundry - 0x6 Delegation

ยท

3 min read

Overview

This level introduces us to the following concept(s):

  • The delegatecall low-level function in Solidity

GitHub Repository available at: github.com/0xEval/ethernaut-x-foundry

Objective

To complete the challenge, we will need to:

  • Claim ownership of the instance

Contract Analysis

When the instance is initialized, two contracts will be deployed Delegate and Delegation.

A Delegate is simply identified by a public address owner and has a function pwn() which will change ownership to the sender.

A Delegation contains both an owner and a reference to a Delegate. It has a fallback() function which uses the delegatecall() low-level function on the delegate instance, forwarding msg.data.

contract Delegate {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }

    function pwn() public {
        owner = msg.sender;
    }
}

contract Delegation {
    address public owner;
    Delegate delegate;

    constructor(address _delegateAddress) {
        delegate = Delegate(_delegateAddress);
        owner = msg.sender;
    }

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

Understanding the Delegatecall() function

๐Ÿ“ Quick reminder on the call() function: solidity-by-example.org/call

There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.

This tells us the main different between call() and delegatecall() is data/execution context.

Let's draft a simple example:

// B is deployed at address 0x0B...
contract B {
  uint public value;
  address public sender;

  function set(uint256 _value) public {
    value = _value;
  }
}

// A is deployed at address 0x0A...
contract A {
  uint public value;
  address public sender;

  function set(uint256 _value, address _addr) public {
    _addr.delegatecall(
       abi.encodeWithSignature("set(uint256)", _value)
     );
  }
}

A.set(10, 0x0B) will trigger delegatecall() which in turn will execute the set() function, as defined in B's code but using A's storage

A.value = 10 A.sender = 0x0A

B.value = 0 B.sender = 0

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.

In fact, this pattern allows the implementation of libraries (such as SafeMath) in Solidity!

๐Ÿ’ก Recommended reads:

Knowing this should be enough for us to hack the contract, let's see how.

Attacking The Contract

Make sure to read through Part 0 for setup instructions.

Create the Delegation.t.sol test file in the test/ directory that will contain the attack logic (level setup and submission is truncated for clarity):

  1. We send a call to Delegation passing the signature of Delegate.pwn(). Since the function is not present in Delegation's code, thefallback method is triggered.

  2. This in turn will execute the low-level delegatecall() function and pass the signature in parameter as part of msg.data

  3. Since it is a delegated call, the executed code will be Delegate.pwn() but in context of Delegation's storage.

After the call, the owner of Delegation should set to our attacker EoA.

function testDelegationHack() public {
   address(delegationContract).call(abi.encodeWithSignature("pwn()"));
   assertEq(delegationContract.owner(), attacker);
}

Run the attack using the forge test subcommand: image.png

ย