delegatecall and call are two important low-level functions in Solidity for inter-contract calls. Understanding their differences is crucial for implementing proxy contracts and upgradeable contracts.
1. Basic Usage of call
call is the most commonly used low-level calling function that executes code in the target contract's context.
soliditycontract CallExample { // Use call to send ETH function sendEther(address payable recipient) public payable { (bool success, ) = recipient.call{value: msg.value}(""); require(success, "Transfer failed"); } // Use call to call functions function callFunction(address target, uint256 value) public returns (bool) { // Encode function call: function setValue(uint256) bytes memory data = abi.encodeWithSignature("setValue(uint256)", value); (bool success, bytes memory returnData) = target.call(data); require(success, "Call failed"); return success; } // Use call and get return value function callAndGetResult(address target) public view returns (uint256) { bytes memory data = abi.encodeWithSignature("getValue()"); (bool success, bytes memory returnData) = target.staticcall(data); require(success, "Call failed"); return abi.decode(returnData, (uint256)); } }
2. Basic Usage of delegatecall
delegatecall executes the target contract's code in the caller's context, keeping msg.sender and msg.value unchanged.
soliditycontract DelegatecallExample { uint256 public value; // This variable will be modified by target contract code // Use delegatecall to execute logic contract code function executeLogic(address logicContract, uint256 newValue) public { bytes memory data = abi.encodeWithSignature("setValue(uint256)", newValue); (bool success, ) = logicContract.delegatecall(data); require(success, "Delegatecall failed"); } } // Logic contract contract LogicContract { uint256 public value; // Note: This variable layout must be consistent with proxy contract function setValue(uint256 newValue) public { value = newValue; // Modifies the storage of the caller (proxy contract) } function getValue() public view returns (uint256) { return value; } }
3. Comparison of call and delegatecall
| Feature | call | delegatecall |
|---|---|---|
| Execution context | Target contract | Caller contract |
| msg.sender | Current contract address | Original caller address |
| msg.value | Passed value | Original call value |
| storage access | Target contract's storage | Caller contract's storage |
| Code execution location | Target contract address | Target contract address |
| Use cases | Regular calls, transfers | Proxy contracts, library contracts |
4. Proxy Contract Implementation
Basic Proxy Contract (EIP-1967)
soliditycontract Proxy { // EIP-1967 standard storage slots // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; // bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) bytes32 private constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; constructor(address _implementation) { _setImplementation(_implementation); _setAdmin(msg.sender); } modifier onlyAdmin() { require(msg.sender == _getAdmin(), "Not admin"); _; } // Get implementation contract address function _getImplementation() internal view returns (address impl) { assembly { impl := sload(IMPLEMENTATION_SLOT) } } // Set implementation contract address function _setImplementation(address _implementation) internal { assembly { sstore(IMPLEMENTATION_SLOT, _implementation) } } // Get admin address function _getAdmin() internal view returns (address adm) { assembly { adm := sload(ADMIN_SLOT) } } // Set admin address function _setAdmin(address _admin) internal { assembly { sstore(ADMIN_SLOT, _admin) } } // Upgrade implementation contract function upgradeTo(address _newImplementation) public onlyAdmin { _setImplementation(_newImplementation); } // Get current implementation address (for querying) function implementation() public view returns (address) { return _getImplementation(); } // Fallback function: delegate all calls to implementation contract fallback() external payable { _delegate(_getImplementation()); } receive() external payable { _delegate(_getImplementation()); } // Core delegation logic function _delegate(address _implementation) internal { assembly { // Copy msg.data calldatacopy(0, 0, calldatasize()) // Execute delegatecall let result := delegatecall( gas(), _implementation, 0, calldatasize(), 0, 0 ) // Copy return data returndatacopy(0, 0, returndatasize()) // Return or revert based on result switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } }
Logic Contract Example
solidity// Logic Contract V1 contract LogicV1 { uint256 public value; address public implementation; // Consistent with proxy contract storage layout address public admin; function setValue(uint256 _value) public { value = _value; } function getValue() public view returns (uint256) { return value; } } // Logic Contract V2 (Upgraded version) contract LogicV2 { uint256 public value; address public implementation; address public admin; // New functionality function setValue(uint256 _value) public { value = _value * 2; // New logic: double the value } function getValue() public view returns (uint256) { return value; } // New function function increment() public { value++; } }
5. Transparent Proxy Pattern
Transparent proxy solves the function selector conflict between proxy and implementation contracts.
soliditycontract TransparentProxy { bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; bytes32 private constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; constructor(address _implementation, address _admin) { _setImplementation(_implementation); _setAdmin(_admin); } modifier ifAdmin() { if (msg.sender == _getAdmin()) { _; } else { _fallback(); } } // Admin functions (only callable by admin) function upgradeTo(address _newImplementation) external ifAdmin { _setImplementation(_newImplementation); } function admin() external ifAdmin returns (address) { return _getAdmin(); } function implementation() external ifAdmin returns (address) { return _getImplementation(); } // Change admin function changeAdmin(address _newAdmin) external ifAdmin { _setAdmin(_newAdmin); } // Non-admin calls will execute here function _fallback() internal { _delegate(_getImplementation()); } fallback() external payable { _fallback(); } receive() external payable { _fallback(); } // Storage operation functions function _getImplementation() internal view returns (address impl) { assembly { impl := sload(IMPLEMENTATION_SLOT) } } function _setImplementation(address _implementation) internal { assembly { sstore(IMPLEMENTATION_SLOT, _implementation) } } function _getAdmin() internal view returns (address adm) { assembly { adm := sload(ADMIN_SLOT) } } function _setAdmin(address _admin) internal { assembly { sstore(ADMIN_SLOT, _admin) } } function _delegate(address _implementation) internal { assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } }
6. UUPS Proxy Pattern
UUPS (Universal Upgradeable Proxy Standard) puts upgrade logic in the implementation contract.
solidity// UUPS Proxy contract UUPSProxy { bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; constructor(address _implementation) { _setImplementation(_implementation); } function _getImplementation() internal view returns (address impl) { assembly { impl := sload(IMPLEMENTATION_SLOT) } } function _setImplementation(address _implementation) internal { assembly { sstore(IMPLEMENTATION_SLOT, _implementation) } } fallback() external payable { _delegate(_getImplementation()); } receive() external payable { _delegate(_getImplementation()); } function _delegate(address _implementation) internal { assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } } // UUPS Implementation Contract Interface interface IUUPS { function upgradeTo(address newImplementation) external; function proxiableUUID() external view returns (bytes32); } // UUPS Implementation Contract contract UUPSImplementation is IUUPS { bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; uint256 public value; address public implementation; address public owner; modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } constructor() { owner = msg.sender; } function upgradeTo(address newImplementation) external onlyOwner { _authorizeUpgrade(newImplementation); _upgradeTo(newImplementation); } function proxiableUUID() external pure returns (bytes32) { return IMPLEMENTATION_SLOT; } function _authorizeUpgrade(address newImplementation) internal virtual { // Can add additional upgrade verification logic here } function _upgradeTo(address newImplementation) internal { assembly { sstore(IMPLEMENTATION_SLOT, newImplementation) } } function setValue(uint256 _value) public { value = _value; } }
7. Storage Layout Considerations
solidity// Wrong storage layout example contract WrongLayoutV1 { uint256 public value; // slot 0 address public owner; // slot 1 } contract WrongLayoutV2 { address public owner; // slot 0 - Wrong! Should be slot 1 uint256 public value; // slot 1 - Wrong! Should be slot 0 uint256 public newValue; // slot 2 } // Correct storage layout contract CorrectLayoutV1 { uint256 public value; // slot 0 address public owner; // slot 1 } contract CorrectLayoutV2 { uint256 public value; // slot 0 - Keep same address public owner; // slot 1 - Keep same uint256 public newValue; // slot 2 - New variables at the end }
8. Best Practices
- Use standard storage slots: Follow EIP-1967 standard to avoid storage conflicts
- Maintain consistent storage layout: Cannot change the order of existing variables when upgrading
- Use OpenZeppelin: Use audited standard implementations
- Test upgrade process: Fully test upgrade process on testnet
- Consider diamond pattern: For ultra-large contracts, consider using EIP-2535 Diamond Standard
solidity// Use OpenZeppelin proxies import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; // Recommended: Use UUPS pattern import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MyContract is UUPSUpgradeable, Ownable { function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} }