Table of contents
What are delegate calls ?
Delegate calls is a low-level functions that enables a smart contract to execute code from another contract within the context of the calling contract. This means, a contract can execute code from another contract without importing or inheriting its logic, by using delegatecall
. This allows shared logic execution, while storage and state changes are applied in the calling contract's context. This approach can reduce deployment costs but requires careful handling of storage layout and security considerations..
Why is delegatecall
important ?
The importance of delegatecall
depends on the use case you want it for which will be listed below and explained in detail.
- Gas Efficiency:
Why it matters: Imagine you have several contracts that need the same logic (e.g., math functions or token operations). Copying that code into each contract wastes gas because each contract gets larger, making deployment more expensive.
How
delegatecall
helps: Instead of copying the logic into every contract, you keep the logic in one "library" contract and usedelegatecall
to access it. This saves gas during deployment because the calling contract doesn’t grow in size.
Example: You deploy a MathLibrary
with complex mathematical functions. Instead of adding these functions to every contract, you use delegatecall
to access them, keeping your contracts smaller and deployment costs lower.
- Proxy Contracts:
Why it matters: Upgrading smart contracts is tricky because blockchain contracts are immutable once deployed. If you want to fix a bug or add features, you usually need to deploy a new contract—causing inconvenience for users and disrupting the contract’s address.
How
delegatecall
helps: Proxy contracts usedelegatecall
to forward function calls to an "implementation contract." If you need to upgrade the logic, you only replace the implementation contract while keeping the proxy contract's address and storage intact.
Example: A DeFi platform uses a proxy contract to handle user balances and transactions. If the platform updates its fee structure, they only need to deploy a new implementation contract and point the proxy to it. Users interact with the same proxy address, and their data stays safe.
Lets Pick a Scenerio as an Example:
Imagine Contract B is already deployed on the blockchain and has a function that you want to use in Contract A, but you don’t want to copy all of Contract B's code into Contract A. Instead, you can use delegatecall
to access Contract B’s functionality directly.
Here’s how Contract A can achieve this:
Pass Contract B's Address: Contract A will receive Contract B's address in its constructor.
Use
delegatecall
: Contract A will call the specific function in Contract B usingdelegatecall
, ensuring that the called function executes in the context of Contract A, using Contract A's storage.
Lets Illustrate Below:
Code Example
Contract B (Deployed)
Contract B has a simple function that increments a value stored at a specific slot.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract B {
// Function to increment a value in storage
function incrementValue(uint256 _amount) external {
assembly {
// Load the second storage slot (slot 1)
let slot := 1
let current := sload(slot)
let updated := add(current, _amount)
sstore(slot, updated)
}
}
}
Contract A (Uses Contract B via delegatecall
)
Contract A delegates the logic of incrementing a value to Contract B.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract A {
address public bAddress; // Address of Contract B
uint256 public value; // Value stored in Contract A's storage
// Constructor accepts Contract B's address
constructor(address _bAddress) {
bAddress = _bAddress;
}
// Function to interact with Contract B using delegatecall
function increment(uint256 _amount) external {
// Prepare the function signature for `incrementValue(uint256)`
bytes memory data = abi.encodeWithSignature("incrementValue(uint256)", _amount);
// Use delegatecall to execute `incrementValue` in the context of Contract A
(bool success, ) = bAddress.delegatecall(data);
require(success, "delegatecall failed");
}
}
Step-by-Step Explanation:
Deploy Contract B: First, deploy Contract B. Take note of its deployed address.
Deploy Contract A: Deploy Contract A, passing the address of Contract B to its constructor.
Call
increment
in Contract A:When you call
increment
on Contract A, it usesdelegatecall
to execute Contract B’sincrementValue
function.The function runs in the context of Contract A, so the
value
variable in Contract A’s storage is updated.
Here is How to test delegatecall
using Foundry
1. Initialize Your Foundry Project
Run the following command to initialize your Foundry project:
forge init
You should see an output like this:
2. Set Up Your Contracts
- Place
ContractA
andContractB
in thesrc
folder of your project.
3. Create a Test File
Create a test file named delegateCall.t.sol
(or any name ending with .t.sol
) in the test
folder. Foundry identifies files with the .t.sol
suffix as test files.
4. Add the Test Code
Paste the following code into your test file:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/ContractA.sol";
import "../src/ContractB.sol";
contract TestDelegateCall is Test {
A aContract;
B bContract;
function setUp() public {
//Deploy contractB
bContract = new B();
//Deploy contractA passing the address of contractB
aContract = new A(address(bContract));
}
// To test if the delagatecall updates the storage
function testDelegateCallUpdateStorage() public {
// Increment the value in Contract A's context
aContract.increment(10);
// Verify that Contract A's storage is updated
uint256 aValue = aContract.value();
assertEq(aValue, 10, "ContractA Value not updated correctly, it should be 10");
// Verify that Contract B's storage is not updated
bytes32 slotB = vm.load(address(bContract), bytes32(0));
assertEq(slotB, 0, "ContractB storage should remain unchainged");
}
//To test if it fails if a nonexistent function is called
function testDelegateCallFail() public {
//Verify that Contract A's storage is not updated
bytes memory invalidData = abi.encodeWithSignature("nonexistentFunction()");
(bool success,) = address(aContract).call(invalidData);
assertEq(success, false, "Delegatecall should fail as the function is not defined");
}
}
5. Run the Tests
Execute the following command to run the tests:
forge test
You should see output similar to this:
Here is the Github repository to the full code: https://github.com/damboy0/Delegatecall-Guide
Conclusion
Delegatecall
is a powerful feature in Solidity, enabling contracts to execute external code while maintaining their own storage and state. This feature is critical for gas-efficient contract logic sharing, implementing upgradable proxy contracts, and dynamic code execution. However, its flexibility comes with challenges, such as strict storage alignment and heightened vulnerability to exploits.
In this guide, we explored the mechanics of delegatecall
, why it matters, and how to test it effectively using Foundry. By understanding its nuances and testing thoroughly, you can leverage delegatecall
to build more modular, efficient, and scalable Solidity applications while minimizing risks.