乐闻世界logo
搜索文章和话题

What are the differences between delegatecall and call in Solidity? How to implement proxy contracts?

3月6日 21:55

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.

solidity
contract 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.

solidity
contract 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

Featurecalldelegatecall
Execution contextTarget contractCaller contract
msg.senderCurrent contract addressOriginal caller address
msg.valuePassed valueOriginal call value
storage accessTarget contract's storageCaller contract's storage
Code execution locationTarget contract addressTarget contract address
Use casesRegular calls, transfersProxy contracts, library contracts

4. Proxy Contract Implementation

Basic Proxy Contract (EIP-1967)

solidity
contract 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.

solidity
contract 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

  1. Use standard storage slots: Follow EIP-1967 standard to avoid storage conflicts
  2. Maintain consistent storage layout: Cannot change the order of existing variables when upgrading
  3. Use OpenZeppelin: Use audited standard implementations
  4. Test upgrade process: Fully test upgrade process on testnet
  5. 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 {} }

标签:Solidity