The delegatecall Function in Solidity

ayoashy

Ayo Ashiru

Posted on November 18, 2024

The delegatecall Function in Solidity

The delegatecall method is a powerful low-level function that enables one contract (the "Caller") to execute code in another contract (the "Callee") while preserving the storage and context of the Caller. Unlike the call method, which executes code in a different contract but does not retain the Caller’s state, delegatecall maintains the context of the Caller’s storage, allowing for more flexible contract design. However, as with any low-level operation, improper use of delegatecall can introduce significant security risks. For a deeper understanding of the call method, be sure to check out my previous article, The call Function in Solidity.

What is delegatecall?

The delegatecall function in Solidity enables a contract to call another contract’s function but retain its own storage, sender, and msg.value. This is useful when you want to separate logic from storage, allowing the Caller to use the Callee’s logic while maintaining its own state.

Common Use Cases:

  • Upgradeable Contracts: The Proxy pattern uses delegatecall to upgrade contract logic without affecting stored data.

  • Modular Contracts: Break down complex functionality into separate contracts to improve code reusability and maintainability.

Basic Example of delegatecall

In this example, the Caller contract calls setNum in the Callee contract to set a number. The state change happens in the Caller’s storage rather than the Callee’s.

Callee Contract

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

contract Callee {
    uint public num;

    function setNum(uint _num) public {
        num = _num;
    }
}
Enter fullscreen mode Exit fullscreen mode

Caller Contract

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

contract Caller {
    uint public num;
    address public calleeAddress;

    uint public num; // State variable that MUST be in storage slot 0 to matche the storage layout of the Callee contract

    function setCalleeAddress(address _calleeAddress) public {
        calleeAddress = _calleeAddress;
    }

        /**
     * @dev Calls the setNum function of the Callee contract using delegatecall.
     * The logic of the Callee's setNum function is executed in the context of the Caller contract.
     * This updates the `num` variable in the Caller contract's storage, NOT the Callee's.
     * 
     * NOTE: The `num` variable in both the Caller and Callee contracts MUST occupy the same storage slot.
     * In this case, `num` is the first declared state variable in both contracts, which means it is in slot 0.
     * 
     * @param _num The value to set the Caller contract's `num` variable to.
     */
    function delegateSetNum(uint _num) public {
        (bool success, ) = calleeAddress.delegatecall(
            abi.encodeWithSignature("setNum(uint256)", _num)
        );
        require(success, "delegatecall failed");
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. The setCalleeAddress function stores the address of the Callee contract.

  2. delegateSetNum uses delegatecall to execute setNum in Callee, but any state changes are applied in Caller’s storage.

Risks and Best Practices of Using delegatecall

While delegatecall is powerful, improper use can introduce serious security risks. Here’s what to consider:

  1. Storage Layout Consistency

    • The storage layout between the Caller and Callee must match exactly. Any mismatch can cause unexpected behavior and data corruption.
  2. Reentrancy Attacks

    • Using delegatecall with untrusted contracts can expose vulnerabilities, such as reentrancy attacks. Apply the checks-effects-interactions pattern to minimize risk.
  3. Avoid Unauthorized Access

    • Ensure delegatecall does not inadvertently expose sensitive functions. Use access control to limit function calls as needed.

Example of a Security Vulnerability

In the following example, HackMe calls Lib's pwn function with delegatecall, allowing anyone to take over the HackMe contract by changing the owner.

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

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

    // Delegate all external calls to Lib contract, including unauthorized access to pwn
    fallback() external payable {
        address(lib).delegatecall(msg.data);
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Anyone calling pwn can change the owner of HackMe because HackMe uses Lib’s storage layout, unintentionally exposing its owner variable.

Practical Tips for Using delegatecall Safely

To maximize the benefits of delegatecall while minimizing risks, follow these tips:

  1. Check Storage Compatibility: Ensure that the Caller and Callee contracts have matching storage layouts to avoid corruption.

  2. Validate delegatecall Targets: Only use trusted contracts as targets for delegatecall.

  3. Use Access Control: Restrict access to functions that use delegatecall to avoid unauthorized actions.

Conclusion

The delegatecall function in Solidity provides flexibility for modular and upgradeable smart contracts. However, due to its powerful nature, it’s essential to use delegatecall with caution, carefully managing storage layouts and implementing security checks. Proper use of delegatecall can open up many possibilities for flexible contract development, but with poor implementation, it can introduce serious vulnerabilities.

💖 💪 🙅 🚩
ayoashy
Ayo Ashiru

Posted on November 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related