DEV Community

Cover image for Ethernaut - Lvl 6: Delegation
pacelliv
pacelliv

Posted on

Ethernaut - Lvl 6: Delegation

Requirements: understanding of delegatecall, fallback special function and methods ID.

The challenge 🤼‍♀️🤼

Claim ownership of Delegation:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Inspecting the contracts 🔎🔍

Delegate.sol contains:

  • owner: address of the account that owns the contract.
  • constructor: receives an address and set it as the owner of the contract at creation time.
  • pwn: public function to update owner as msg.sender.

Delegation.sol contains:

  • owner: address of the account that owns the contract.
  • delegate: instance of Delegate, this variable is of type Delegate.
  • constructor: receives the address of delegate and initiliazes an instance of Delegate. Set msg.sender as owner.
  • fallback: special function that makes a delegatecall to Delegate.

Delegatecall and Fallback ☎️▶️

Delegation does not have a method that updates owner after this variable has been initiliazed inside the constructor. But we can see that in Delegate there is a method to update the owner in that contract.

The fallback function in Delegation contains logic to make a delegatecall to Delegate with some encoded data.

delegatecall is a low-level function in Solidity used to make external calls to another contracts. Let's say we call a function in a contract A which delegates the call to a function in a contract B. delegatecall will load and run the logic of the function in contract B in the context of A, using the storage of contract A and for this to work contract A needs to share the same storage layout as B, otherwise we will end up writing to the incorrect slots messing with the storage of contract A.

Let's review the following example:

contract Delegatecall {
    bool public status; // slot 0
    uint256 public num; // slot 1

    function updateNum(uint256 _num) external {
        num = _num;
    }
}

contract BadDelegatecall {
    uint256 public num; // slot 0
    bool public status; // slot 1

    // A `delegatecall` would fail because `BadDelegatecall`
    // and `Delegate` does not share the same storage layout.
}

contract GoodDelegatecall {
    bool public status; // slot 0
    uint256 public num; // slot 1

    // A `delegatecall` would succeed because `GoodDelegatecall`
    // and `Delegate` share the same storage layout. 
}
Enter fullscreen mode Exit fullscreen mode

If we make a delegatecall from BadDelegatecall to update num, this would fail because the variable is in different slots. We would end up writing the value of num to status in BadDelegatecall. delegatecall will succeed in GoodDelegatecall because in this case num is in the same slot in both contracts.

delegatecall also maintains msg.sender between external calls. Let's review the following simple chain call:

EOA ---> contract A ---> contract B

`msg.sender` in A and B is the EOA.
Enter fullscreen mode Exit fullscreen mode

The fallback according to the Solidity docs:

The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable.

The hack 🌌

We need to trigger the fallback so that it makes a delegatecall to Delegate to run the logic of pwn in Delegation to update owner with our address.

We just need to figure out how to target the function pwn.

When smart contracts are compiled into bytecode, the functions are encoded with an method id, called as selector, that acts as an unique identifier so that the EVM can identify the method to call. The signature of a function consists of its name and the types of its parameters, for example, for function transfer(address _to, uint _amount) the function signature is transfer(address,uint), no spaces are used.

The selector of pwn is what we need to send in our payload for the EVM.

Ok, let's exploit this level. Request a new instance.

Compute the selector of pwn:

selector = web3.eth.abi.encodeFunctionSignature("pwn()")
Enter fullscreen mode Exit fullscreen mode

Send a raw transaction to Delegation with the selector as data:

await sendTransaction({from: player, to: contract.address, data: selector})
Enter fullscreen mode Exit fullscreen mode

After the transaction in mined, check the owner of Delegation:

await contract.owner() // should return your address
Enter fullscreen mode Exit fullscreen mode

Submit the instance to complete the level.

Conclusion 💯

Delegatecall is a powerful function in Solidity, it allow us to dynamically load code at runtime from a different address, maintaining the msg.sender between external calls.

Not properly understanding or usage of delegatecall can make a contract vulnerable to exploits because its storage is used by a different contract to execute external logic.

We also need to be careful when calling methods in a contract that implement delegatecall because we will be the msg.sender in the callee contract authorizing it to execute its logic with our signature.

Further reading 👀

Top comments (0)