Delegatecall

Delegatecall allows a contract to execute code from another contract while preserving its context (storage, caller, etc.). However, incorrect usage or misunderstanding of delegatecall can lead to devastating results.

When using delegatecall, it’s crucial to keep two things in mind:

  1. delegatecall preserves context (storage, caller, etc.).

  2. The storage layout must be the same for the contract calling delegatecall and the contract being called.

Exploiting Delegatecall: An Example

Consider the following contract, HackMe, which uses delegatecall to execute code:

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

contract Lib {
    address public owner;

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

contract HackMe {
    address public owner;
    Lib public lib;

    constructor(Lib _lib) {
        owner = msg.sender;
        lib = Lib(_lib);
    }

    fallback() external payable {
        address(lib).delegatecall(msg.data);
    }
}

In this contract, it’s not immediately obvious that the owner of HackMe can be changed, since there’s no function inside HackMe to do so. However, an attacker can hijack the contract by exploiting delegatecall.

Here’s how the attack works:

  1. Alice deploys Lib.

  2. Alice deploys HackMe with the address of Lib.

  3. Eve deploys Attack with the address of HackMe.

  4. Eve calls Attack.attack().

  5. Attack is now the owner of HackMe.

In step 4, when Eve calls Attack.attack(), Attack calls the fallback function of HackMe, sending the function selector of pwn(). HackMe forwards the call to Lib using delegatecall. This tells Solidity to call the function pwn() inside Lib, which updates the owner to msg.sender. Because delegatecall runs the code of Lib using the context of HackMe, HackMe’s storage was updated to msg.sender, where msg.sender is the caller of HackMe - in this case, Attack.

A More Sophisticated Exploit

The previous example was relatively straightforward but delegatecall can be exploited in more sophisticated ways. For instance, consider the following contracts:

In this example, the state variables are not defined in the same manner in Lib and HackMe. This means that calling Lib.doSomething() will change the first state variable inside HackMe, which happens to be the address of lib.

The Attack

Here’s how the attack works:

  1. Alice deploys Lib.

  2. Alice deploys HackMe with the address of Lib.

  3. Eve deploys Attack with the address of HackMe.

  4. Eve calls Attack.attack().

  5. Attack is now the owner of HackMe.

In step 4, when Eve calls Attack.attack(), the Attack contract does two things:

  • First, it calls HackMe.doSomething() with the address of Attack cast to a uint. This causes HackMe to execute Lib.doSomething() via delegatecall, but because the storage layouts of Lib and HackMe are different, it ends up overwriting the lib address in HackMe with the address of Attack.

  • Second, it calls HackMe.doSomething() again, but this time with an arbitrary number as input. Because the lib address in HackMe has been overwritten with the address of Attack, this causes HackMe to execute Attack.doSomething() via delegatecall, which changes the owner of HackMe to the msg.sender (which is Attack).

Preventing Delegatecall Exploits

Preventing delegatecall exploits primarily involves understanding how delegatecall works and being aware of its potential dangers. Always remember that delegatecall preserves the context of the calling contract and that the storage layout must be the same for both the calling contract and the called contract. Be cautious when using delegatecall and always thoroughly test your contracts to ensure they are secure.

Using Stateless Libraries

One of the most effective ways to prevent delegatecall exploits is to use stateless libraries. A stateless library in Solidity is a library that does not have any state variables. It only contains functions.

Here’s why stateless libraries can help prevent delegatecall exploits:

  • Since stateless libraries do not have any state variables, there’s no risk of a delegatecall unintentionally overwriting state variables in the calling contract.

  • Functions in stateless libraries are often designed to be pure or view, meaning they do not modify the state and only operate on their input arguments. This makes them safer to use with delegatecall.

Here’s an example of what a stateless library in Solidity might look like:

In this example, the SafeMath library provides safe versions of the addition and subtraction operations. These functions can be used in other contracts via delegatecall without risking any state variable overwrites because SafeMath does not have any state variables

Last updated