Call and Delegatecall in Solidity
Two low-level functions that let contracts interact with each other directly, bypassing the usual function call safety checks. Call executes code in the target contract's context, while delegatecall runs the target's code in your contract's context.
Published on 08 November, 2025
Categories:
SolidityWhat These Functions Actually Do
When you need a contract to talk to another contract, Solidity gives you regular function calls. But sometimes you need more control or flexibility. That's where
call and delegatecall come in.Think of
call like calling someone on the phone. They do something on their end, in their house, with their stuff. You just get the result back.Think of
delegatecall like inviting someone to your house to use your tools and materials. They bring the instructions, but everything happens with your resources.The Call Function
call is a low-level function that triggers code in another contract. The code runs in that external contract's context, meaning it uses that contract's storage, balance, and address.Basic Syntax
(bool success, bytes memory data) = targetAddress.call{value: amount}( abi.encodeWithSignature("functionName(uint256)", argument) );
The function returns two things:
success: true if the call worked, false if it faileddata: whatever the called function returned
Simple 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, bytes memory data) = _receiver.call{value: msg.value}( abi.encodeWithSignature("setValue(uint256)", _value) ); require(success, "Call failed"); } }
When
Caller uses call to trigger setValue, the value variable that gets updated is the one inside Receiver. The Ether sent goes to Receiver's balance.When to Use Call
You should use
call when:- Sending Ether to addresses (it's the recommended way now)
- Interacting with contracts you don't have the interface for
- You need all remaining gas forwarded to the target
- You're building proxy patterns or generic contract interactions
contract SendEther { function sendViaCall(address payable _to) public payable { (bool sent, ) = _to.call{value: msg.value}(""); require(sent, "Failed to send Ether"); } }
The Delegatecall Function
delegatecall is where things get interesting. It runs another contract's code, but in your contract's context. This means the code uses your contract's storage, balance, and address.The Context Switch
Normal Call:
Caller Contract --call--> Target Contract
(uses Target's storage)
Delegatecall:
Caller Contract --delegatecall--> Target Contract's code
(uses Caller's storage) (just borrows logic)
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 does a delegatecall to Logic.setNumber(), 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 Matters
This is critical. The storage layout must match between contracts using delegatecall.
// Correct - matching layouts 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; } }
If the layouts don't match, delegatecall will corrupt your storage:
// Dangerous - mismatched layouts contract A { address public owner; // slot 0 uint256 public value; // slot 1 } contract B { uint256 public value; // slot 0 address public owner; // slot 1 } // delegatecall between these will swap the values
Proxy Pattern Example
The most common use of delegatecall is in upgradeable contracts:
contract Proxy { address public implementation; uint256 public value; 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; uint256 public value; function setValue(uint256 _value) public { value = _value; } }
Users interact with
Proxy, but the logic lives in Implementation. You can upgrade by pointing to a new implementation contract.Key Differences
Call vs Delegatecall:
msg.sender:
- call: becomes the calling contract
- delegatecall: stays as original caller
msg.value:
- call: is the value sent with call
- delegatecall: is the original transaction value
Storage modified:
- call: target contract's storage
- delegatecall: calling contract's storage
this:
- call: refers to target contract
- delegatecall: refers to calling contract
Security Example
contract Target { address public owner; function changeOwner(address _newOwner) public { owner = _newOwner; } } contract Attacker { address public owner; function attack(address _target) public { // Using call - changes Target's owner (bool success, ) = _target.call( abi.encodeWithSignature("changeOwner(address)", address(this)) ); // Using delegatecall - changes Attacker's owner (bool success2, ) = _target.delegatecall( abi.encodeWithSignature("changeOwner(address)", address(this)) ); } }
Error Handling
Both functions return success/failure instead of reverting automatically:
contract ErrorHandler { function safeCall(address target) public { (bool success, bytes memory returnData) = target.call( abi.encodeWithSignature("someFunction()") ); if (!success) { if (returnData.length > 0) { // The call reverted with a reason assembly { revert(add(returnData, 32), mload(returnData)) } } else { revert("Call failed without reason"); } } } }
Gas Considerations
Before Solidity 0.6.2, you had to manually specify gas limits. Now they forward all available gas by default:
// Old way (before 0.6.2) target.call.gas(10000)(data); // Current way (forwards all gas) target.call(data); // Or specify gas explicitly target.call{gas: 10000}(data);
Real World Usage Pattern
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; } }
Use
call when you want standard contract interaction with safety and isolation. Use delegatecall when building upgradeable systems or borrowing logic while maintaining your own state, but understand the storage layout requirements and security implications first.