Table of Contents
Introduction to Delegatecall
Ethereum founder Vitalik Buterin introduced delegatecall in EIP-7. When Contract A uses delegatecall to call Contract B, it uses Contract A's storage (not Contract B's). This means if the called function modifies state variables, these changes will actually affect Contract A.
Key applications of delegatecall include: 1) upgradeable contracts, and 2) Solidity's Linked Library. This guide focuses on upgradeable contracts.
Delegatecall Demonstration
Consider these two contracts:
ContractA:
pragma solidity 0.6.12;
contract ContractA {
address public contractB;
constructor(address _contractB) public {
contractB = _contractB;
}
function delegatecallChangeZ(uint256 _z) public {
contractB.delegatecall(abi.encodeWithSignature("changeZ(uint256)", _z));
}
function delegatecallChangeX(uint256 _x) public {
contractB.delegatecall(abi.encodeWithSignature("changeX(uint256)", _x));
}
function getValue(uint256 slot) view public returns(uint256 value) {
assembly { value := sload(slot) }
}
}ContractB:
pragma solidity 0.6.12;
contract ContractB {
uint256 public x;
uint256 public y;
uint256 public z;
function changeX(uint256 _x) public { x = _x; }
function changeY(uint256 _y) public { y = _y; }
function changeZ(uint256 _z) public { z = _z; }
}After calling delegatecallChangeZ(5), ContractB's z remains unchanged. Instead, ContractA's storage slot 2 (third slot) is modified to 5, which can be verified by calling getValue(2).
๐ Learn more about delegatecall implementation
Avoiding Storage Clashes
Calling delegatecallChangeX would modify ContractA's first state variable (contractB address) instead of ContractB's x. This demonstrates the danger of storage clashes in delegatecall operations.
Differences Between Call and Delegatecall
The key difference is that delegatecall uses the caller's storage context. Figure 3 illustrates how address(this) and msg.sender behave differently in call vs delegatecall operations.
Upgradeable Contracts
The fundamental principle is using ContractA as a Proxy contract and ContractB as an Implementation contract. The Proxy stores state variables while the Implementation contains business logic. To upgrade, simply change the Implementation address in the Proxy.
Proxy Contracts
A basic Proxy contract implementation:
contract Proxy {
bytes32 private constant _IMPLEMENTATION_SLOT = 0x36089...;
bytes32 private constant _ADMIN_SLOT = 0xb5312...;
constructor() {
address admin = msg.sender;
assembly { sstore(_ADMIN_SLOT, admin) }
}
function _delegate() internal {
address _implementation = implementation();
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()) }
}
}
fallback() payable external { _delegate(); }
receive() payable external { _delegate(); }
}Solution to Problem 1: EIP-1967
EIP-1967 defines specific storage slots for Proxy contracts to avoid clashes:
0x36089...for implementation address0xb5312...for admin address
Solution to Problem 2: Transparent Proxy Pattern
The Transparent Proxy Pattern implements:
- All end-user calls are forwarded to the Implementation contract
- Admin calls are never forwarded
This prevents "Proxy selector clashing" where calls to Proxy methods bypass the Implementation.
๐ Best practices for upgradeable contracts
Upgradeable Contract Architecture
Architecture Summary
The transparent proxy architecture is shown in Figure 6, where users interact with the Proxy contract which forwards requests to the Implementation.
Unified Upgrades for Multiple Proxies (Beacon Proxy Pattern)
For scenarios with multiple Proxy contracts sharing one Implementation, the Beacon Proxy Pattern introduces a Beacon contract that stores the Implementation address. Upgrading the Implementation requires just one change to the Beacon contract.
OpenZeppelin Contracts
OpenZeppelin provides robust implementations of these patterns that handle upgrades securely.
Other Upgradeable Architectures
UUPS Standard
The UUPS pattern (EIP-1822) differs by placing upgrade logic in the Implementation contract rather than the Proxy. Benefits include:
- More gas-efficient Proxy deployment
- Ability to remove upgradeability in future versions
Example Implementation:
contract MyToken is UUPSUpgradeable, AccessControlEnumerableUpgradeable {
function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}
}Diamond (Multi-Facet Proxy)
EIP-2535 enables upgrading individual functions through a modular approach, though it's more complex to implement.
Architecture Comparison
| Pattern | Pros | Cons |
|---|---|---|
| Transparent | Simple, widely used | Higher gas costs |
| UUPS | Gas efficient, flexible | Newer, requires careful access control |
| Diamond | Modular, avoids size limits | Complex implementation |
Limitations
No Constructors
Instead of constructors, use initializer functions:
contract MyContract is Initializable {
function initialize() public initializer {
// Initialization code
}
}Base Contract Initializers
Remember to call base contract initializers:
function initialize() public initializer {
BaseContract.initialize();
}No Initial Values
Move initial values to initializer functions:
uint256 public hasInitialValue;
function initialize() public initializer {
hasInitialValue = 42;
}Modification Restrictions
When modifying contracts:
- Don't change state variable types or order
- Don't delete existing state variables
- Don't insert new variables before existing ones
- Be careful adding variables to base contracts
FAQs
What is delegatecall in Ethereum?
Delegatecall is a low-level function that executes code from another contract while maintaining the caller's storage context. This enables patterns like upgradeable contracts.
Why can't upgradeable contracts use constructors?
Constructors only execute during deployment and don't affect the proxy's storage. Initializer functions must be used instead to properly initialize state.
What's the difference between UUPS and transparent proxies?
UUPS places upgrade logic in the implementation contract, making proxies more lightweight. Transparent proxies contain upgrade logic but are simpler to implement.
How can I upgrade multiple proxy contracts at once?
The Beacon Proxy Pattern allows upgrading multiple proxies simultaneously by having them reference a single beacon contract that stores the implementation address.
What are the main risks with upgradeable contracts?
Storage clashes and accidental overrides are key risks. Following EIP-1967 for storage slots and using established patterns like Transparent Proxy or UUPS mitigates these risks.