DEV Community

Cover image for The delegatecall Function in Solidity
Ayo Ashiru
Ayo Ashiru

Posted on

2

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.

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

Learn more

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay