Call and Delegatecall in Solidity

Solidity gives you regular function calls for most contract-to-contract interactions. But two low-level functions, call and delegatecall, let you go further. They bypass the usual function call safety checks and give you direct control over how code runs and where state is stored.

This post explains what each function does, how they differ, when to use them, and what can go wrong.

What call Does

call is a low-level function that triggers a function on another contract. The code runs in the target contract's context. That means the target contract's storage gets modified, the target's address is used as this, and any Ether sent goes to the target's balance.

Basic syntax:

(bool success, bytes memory data) = targetAddress.call{value: amount}(
    abi.encodeWithSignature("functionName(uint256)", argument)
);

The function returns two values. success is true if the call worked and false if it failed. data holds the return value from the called function.

Here is a working example:

contract Receiver {
    uint256 public value;

    function setValue(uint256 _value) public payable {
        value = _value;
    }
}

contract Caller {
    function callSetValue(address _receiver, uint256 _value) public payable {
        (bool success, ) = _receiver.call{value: msg.value}(
            abi.encodeWithSignature("setValue(uint256)", _value)
        );
        require(success, "Call failed");
    }
}

When Caller uses call to trigger setValue, the value variable inside Receiver gets updated. The Ether sent goes to Receiver's balance. Caller is not affected.

When to Use call

Use call in these situations:

  • Sending Ether to an address. It is the recommended method since transfer and send have a fixed gas stipend that can cause failures.
  • Interacting with a contract you do not have the interface or ABI for.
  • Building generic contract interactions where you need to forward all remaining gas.
  • Implementing proxy patterns.
contract SendEther {
    function sendViaCall(address payable _to) public payable {
        (bool sent, ) = _to.call{value: msg.value}("");
        require(sent, "Failed to send Ether");
    }
}

call does not revert automatically on failure. Always check the success boolean and handle failures explicitly. Ignoring it is a common source of bugs.

What delegatecall Does

delegatecall runs another contract's code inside your contract's context. The target contract provides the logic, but your contract's storage, address, and balance are used. The msg.sender and msg.value from the original transaction are also preserved.

This is the key distinction: call runs code in the target's world, while delegatecall borrows the target's code and runs it in your world.

call:
  Caller --> Target
             Target's storage is modified
             Target's address is `this`

delegatecall:
  Caller --> borrows Target's code
  Caller's storage is modified
  Caller's address is `this`

Here is a basic example:

contract Logic {
    uint256 public number;

    function setNumber(uint256 _number) public {
        number = _number;
    }
}

contract Storage {
    uint256 public number;

    function delegateSetNumber(address _logic, uint256 _number) public {
        (bool success, ) = _logic.delegatecall(
            abi.encodeWithSignature("setNumber(uint256)", _number)
        );
        require(success, "Delegatecall failed");
    }
}

When Storage calls delegateSetNumber, it runs Logic.setNumber using delegatecall. The number variable in Storage gets updated, not the one in Logic. The code from Logic runs, but it operates on Storage's data.

Storage Layout Must Match

This is the most important rule when using delegatecall. The calling contract and the logic contract must declare their state variables in the same order.

When delegatecall writes to number, it writes to slot 0 in storage. It does not know the variable name. It only knows the slot number.

Matching layout (correct):

contract A {
    uint256 public num;     // slot 0
    address public sender;  // slot 1
}

contract B {
    uint256 public num;     // slot 0
    address public sender;  // slot 1

    function setVars(uint256 _num) public {
        num = _num;
        sender = msg.sender;
    }
}

Mismatched layout (dangerous):

contract A {
    address public owner;   // slot 0
    uint256 public value;   // slot 1
}

contract B {
    uint256 public value;   // slot 0 -- mismatched
    address public owner;   // slot 1 -- mismatched
}

If you use delegatecall between A and B in the mismatched example, writing to value in the logic contract writes to slot 0 in A. That overwrites owner. Your storage gets corrupted silently.

Storage layout mismatch with delegatecall does not throw an error. The transaction succeeds, but the wrong variables get overwritten. This is one of the most dangerous bugs in Solidity.

The Proxy Pattern

The most common use of delegatecall in production is the upgradeable proxy pattern. The idea: users interact with a proxy contract, but the actual logic lives in a separate implementation contract. You can upgrade the contract by pointing the proxy to a new implementation, without changing the address users interact with.

contract Proxy {
    address public implementation; // slot 0
    uint256 public value;          // slot 1

    constructor(address _implementation) {
        implementation = _implementation;
    }

    fallback() external payable {
        address impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

contract Implementation {
    address public implementation; // slot 0 -- must match Proxy
    uint256 public value;          // slot 1 -- must match Proxy

    function setValue(uint256 _value) public {
        value = _value;
    }
}

Any call to Proxy that does not match a defined function falls through to the fallback, which forwards the call to Implementation using delegatecall. The proxy's storage is modified, not the implementation's.

To upgrade, deploy a new implementation contract and update implementation in the proxy. Users never need to change the address they call.

Real-world proxy patterns like OpenZeppelin's UUPS and Transparent Proxy use more sophisticated storage slot management (EIP-1967) to avoid layout collisions between proxy admin variables and logic variables.

Key Differences Side by Side

Propertycalldelegatecall
msg.senderCalling contract's addressOriginal transaction sender
msg.valueValue sent with the callOriginal transaction value
thisTarget contractCalling contract
Storage modifiedTarget contract's storageCalling contract's storage
Typical useSending Ether, generic callsProxy patterns, upgradeable contracts

Security: The Difference Matters

The distinction between call and delegatecall has direct security implications. Consider this:

contract Target {
    address public owner; // slot 0

    function changeOwner(address _newOwner) public {
        owner = _newOwner;
    }
}

contract Victim {
    address public owner; // slot 0

    function doCall(address _target) public {
        // call: changes Target's owner, Victim is unaffected
        (bool s1, ) = _target.call(
            abi.encodeWithSignature("changeOwner(address)", address(this))
        );

        // delegatecall: changes Victim's owner, Target is unaffected
        (bool s2, ) = _target.delegatecall(
            abi.encodeWithSignature("changeOwner(address)", address(this))
        );
    }
}

The call version modifies Target.owner. The delegatecall version modifies Victim.owner. If an attacker controls the logic contract passed to a delegatecall, they can overwrite any storage slot in your contract, including ownership and access control variables.

Never use delegatecall with an untrusted address.

Error Handling

Neither call nor delegatecall reverts automatically on failure. Both return a success boolean. If you ignore it, your contract continues executing after a failed call, which leads to subtle and hard-to-debug bugs.

contract ErrorHandler {
    function safeCall(address target) public {
        (bool success, bytes memory returnData) = target.call(
            abi.encodeWithSignature("someFunction()")
        );

        if (!success) {
            if (returnData.length > 0) {
                // Bubble up the revert reason from the failed call
                assembly {
                    revert(add(returnData, 32), mload(returnData))
                }
            } else {
                revert("Call failed without reason");
            }
        }
    }
}

The assembly block re-throws the original revert reason from the failed call. Without it, you lose the reason and get a generic error instead.

Gas Forwarding

Before Solidity 0.6.2, you had to manually specify gas when using call. Now it forwards all available gas by default. You can still override this when needed.

// Forwards all available gas (default since 0.6.2)
target.call(data);

// Forwards all gas with Ether
target.call{value: 1 ether}(data);

// Explicit gas limit
target.call{gas: 10000}(data);

Forwarding all gas is usually what you want. Setting an explicit gas limit can cause calls to fail if the target needs more gas than you allowed.

A Practical Pattern: MultiCall

One common real-world use of call is batching multiple contract interactions into a single transaction:

contract MultiCall {
    function multiCall(
        address[] calldata targets,
        bytes[] calldata data
    ) external returns (bytes[] memory) {
        require(targets.length == data.length, "Length mismatch");

        bytes[] memory results = new bytes[](targets.length);

        for (uint256 i = 0; i < targets.length; i++) {
            (bool success, bytes memory result) = targets[i].call(data[i]);
            require(success, "Call failed");
            results[i] = result;
        }

        return results;
    }
}

This pattern is used in DeFi protocols to batch reads or writes, reducing the number of round trips from the client.

Summary

Use call for standard contract interactions, sending Ether, and interacting with contracts you do not have an interface for. It runs code in the target's context and keeps your state isolated.

Use delegatecall when you need to borrow logic from another contract while keeping state in your own contract. The proxy upgrade pattern is the primary use case. When you use it, storage layout between contracts must match exactly, and the logic contract must be trusted code you control.

Both functions skip automatic revert on failure. Always check the returned success value.