Halborn Logo

// Blog

Delegatecall Vulnerabilities In Solidity


profile

Rob Behnke

March 10th, 2023


As one of the first smart contract-oriented programming languages, Solidity is indispensable for developers building decentralized applications. However, Solidity also has certain quirks that can produce unpredictable behavior at runtime. 

In this article, we discuss how the delegatecall in Solidity can introduce vulnerabilities in smart contracts. We also highlight measures for preventing security issues associated with using delegatecall in your code. 

What is delegatecall? 

In Solidity, call and delegatecall are low-level interfaces for interacting with contracts (by sending external message calls to functions). Triggering the call function in a contract causes the code at that address to execute in the context of the target contract. This means whatever state is modified as part of the operation will always belong to the called contract. 

delegatecall works differently because execution occurs in the context (programming environment) of the caller contract. For example, a delegatecall from contract A to contract B would modify contract B’s storage using functions in contract A.

Delegatecall Solidity[Image source]

The image above further shows two features of the delegatecall function:

  • The execution of delegatecall always alters the state of the contract account that initiated the operation (instead of the target contact's state as in call). 

  • delegatecall preserves the context of the original message call—that is, the values of msg.sender, msg.data, and msg.value don’t change—unlike call where those values might change during execution. 

The main use case for delegatecall is to allow a contract to delegate the execution of a particular message call to another contract. Since the target contract in a delegatecall writes to storage of the calling contract, it's possible to have contracts that run code stored inside another contract as though calling an internal function. 

For example, you can deploy a library contract with functions/utilities intended for use across a bunch of different contracts. Delegatecall enables those contracts to reuse the same library at runtime—removing the need to duplicate code and keeping contracts modular and gas-efficient. 

Delegatecall further supports so-called “proxy patterns” used in upgrading smart contracts. In this case, a proxy contract (holding contract state) delegates execution to logic contract (holding business logic). The proxy contract remains immutable, but it is possible to point it to a new logic contract—thereby simulating an upgrade to a protocol.

Understanding vulnerabilities related to delegatecall 

As explained, delegatecall preserves the context of the original call—that is, variables like msg.sender, msg.value, and msg.data don't change. This feature is useful in cases like proxy patterns where users interact with an immutable contract that delegates execution to upgradable proxies, or when using libraries. 

However, this pattern may cause certain security issues, especially around access control in smart contracts. To illustrate, consider the following example from the Ethernaut CTF wargame:

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

        }

    }

}

Delegation is a contract that forwards message calls to Delegate. Delegation is initialized with the deployer’s address and has no function for updating the value of owner. However, this doesn’t make it impossible for an attacker to claim unauthorized ownership of Delegation

Here’s how the attack works in practice:

1. The attacker creates a contract that calls Delegation and passes the function selector of pwn() in the calldata. Since no function in Delegation matches pwn(), it catches the error using the fallback function and forwards the call to Delegate. pwn()` exists in Delegate, so the call—which tells Delegate to update owner to msg.sender—can run.

2. Delegate executes the call using the original values of msg.sender and msg.data and updates the owner variable in Delegation to the value of msg.sender (ie. ie. the attacker’s contract address). The attacker gains control of Delegation and proceeds to trigger malicious operations, like draining funds stored in the contract. 

The first Parity multisig wallet hack is an example of a real-world exploit involving the delegatecall vulnerability. The attacker re-initialized the wallet using functions stored in another library and claimed ownership of the multisig wallet, before draining funds from it. (A similar vulnerability was also exploited in the Punk Protocol hack, allowing the attacker to hijack the protocol’s contract and steal user deposits.)

How to prevent delegatecall vulnerabilities 

The delegatecall vulnerability described in this article can be prevented by using a “stateless library” (ie. a library contract that only exposes pure or view functions and doesn’t modify state in client contracts). If the library contract in the earlier example (or the Parity wallet library) had no state-modifying functions, attackers cannot exploit delegatecall to hijack the contract.

© Halborn 2024. All rights reserved.