Page cover image

Delegation

delegatecall

Description

The goal of this level is for you to claim ownership of the instance you are given.

Things that might help:

  • Look into Solidity's documentation on the delegatecall low level function, how it works, how it can be used to delegate operations to on-chain libraries, and what implications it has on execution scope.

  • Fallback methods

  • Method ids

Background Knowledge

delegatecall

My comment: It is important to understand why delegatecall is useful. Recall that if a smart contract is deployed to the blockchain then we can't be modified its code no more. All the logic implemented in the contract will stay there forever, unless we selfdestruct the contract and deploy a new one. To get around this, we can use delegatecall to call a "library" contract where new logic is implemented.

delegatecall vulnerability

Code Audit

// 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;
  }
}

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;
    }
  }
}

Two key points:

  • The contract Delegation implements a fallback() function that executes delegatecall().

  • The contract Delegate implements a suspicious function pwn() that sets msg.sender as the owner.

For convenience, lets define the following aliases:

  • The attacker -> A

  • Delegation -> B

  • Delegate -> C

The idea is A calls B (delegate)calls C. Symbolically, A -> B -> C. When A -> B, we send a transaction to trigger the fallback() function in B. Here B initializes the delegatecall B -> C. If you watch Smart Contract Programmer's video, you should know that:

  • The delegatecall preserves A's context. In this case, we have msg.sender == A.

  • The delegatecall acts on B's state variable (in contrast, a normal "call" acts on C's state variable).

In C, when pwn() is called, the owner state variable refers to B's owner (instead of C's owner, since it is a delegatecall). The msg.sender here is A. In other words, the function pwn() sets A as B's owner, and we are done.

Solution

Send a transaction to Delegation and calls the pwn() function:

await contract.sendTransaction({data: web3.utils.keccak256("pwn()")})

Since pwn() is not defined in Delegation, the fallback() function will be triggered. After that, Delegation does delegatecall() and calls the pwn() function in Delegate. When pwn() is called, it updates owner to msg.sender, which is the attacker.

Note that this owner refers to the owner of Delegation since Delegation is doing the delegatecall().

Confirm the owner:

await contract.owner()

Click "Submit instance" and move on to the next level.

Summary

Usage of delegatecall is particularly risky and has been used as an attack vector on multiple historic hacks. With it, your contract is practically saying "here, -other contract- or -other library-, do whatever you want with my state". Delegates have complete access to your contract's state. The delegatecall function is a powerful feature, but a dangerous one, and must be used with extreme care.

Please refer to the The Parity Wallet Hack Explained article for an accurate explanation of how this idea was used to steal 30M USD.

Last updated