Understanding how delegatecall works and its application in building upgradeable smart contracts is essential for modern blockchain developers. This guide dives into the mechanics of delegatecall, explores its role in creating upgradable contract architectures, and outlines key constraints and best practices to ensure secure and efficient implementations.
What Is Delegatecall?
In Ethereum, delegatecall is a low-level function that allows one contract to execute code from another while maintaining the context (storage, balance, etc.) of the calling contract. Introduced by Vitalik Buterin in EIP-7, delegatecall enables powerful patterns such as library reuse and, more importantly, upgradeable smart contracts.
When Contract A uses delegatecall to invoke a function in Contract B:
- The code of Contract B is executed.
- The storage used belongs to Contract A.
msg.senderremains unchanged.address(this)refers to Contract A.
This means any state changes made during the call affect Contract A’s storage, not Contract B's.
👉 Discover how secure smart contract design boosts dApp resilience.
Practical Example of Delegatecall
Let’s consider two simple contracts:
ContractA (Caller):
contract ContractA {
address public contractB;
constructor(address _contractB) public {
contractB = _contractB;
}
function delegatecallChangeZ(uint256 _z) public {
contractB.delegatecall(abi.encodeWithSignature("changeZ(uint256)", _z));
}
function getValue(uint256 slot) view public returns(uint256 value) {
assembly {
value := sload(slot)
}
}
}ContractB (Logic):
contract ContractB {
uint256 public x;
uint256 public y;
uint256 public z;
function changeZ(uint256 _z) public {
z = _z;
}
}After calling delegatecallChangeZ(5) on ContractA:
- ContractB.z remains unchanged.
- ContractA’s storage slot 2 (where
zwould be stored) is updated to5.
You can verify this using getValue(2) in ContractA — it returns 5. This demonstrates that storage modifications occur within the proxy (caller), not the logic contract.
Avoiding Storage Clashes
A critical risk with delegatecall is storage collision. If both contracts declare variables in the same storage slots, unintended overwrites can occur.
For example, calling delegatecallChangeX() on ContractA could overwrite its own contractB address — since both are stored in slot 0 — leading to irreversible corruption.
⚠️ Always ensure storage layout compatibility between proxy and implementation contracts.
Call vs Delegatecall: Context Differences
| Feature | call | delegatecall |
|---|---|---|
| Code execution | In target contract | In target contract |
| Storage used | Target contract | Calling contract |
msg.sender | Preserved | Preserved |
address(this) | Points to target | Points to caller |
Use delegatecall when you want logic reuse without duplicating state or redeploying contracts.
Building Upgradeable Contracts Using Delegatecall
The primary use case of delegatecall is enabling upgradeable smart contracts. This architecture separates:
- Proxy Contract: Holds all persistent data (user balances, configurations).
- Implementation (Logic) Contract: Contains business logic and functions.
Users interact only with the proxy. The proxy forwards calls via delegatecall to the current implementation. When an upgrade is needed, administrators simply point the proxy to a new logic contract — all data remains intact.
Core Components of a Proxy System
Proxy Contract
- Manages storage.
- Forwards external calls via
fallback()orreceive()usingdelegatecall. - Stores the address of the current implementation.
Implementation Contract
- Contains executable logic.
- No constructor logic allowed (explained later).
- Can be replaced seamlessly.
Admin Role
- Authorized entity that triggers upgrades.
- Often implemented via access control mechanisms like OpenZeppelin’s
Ownable.
Solving Common Proxy Challenges
Challenge 1: Preventing Storage Collisions — EIP-1967
To prevent accidental overwrites, EIP-1967 standardizes special storage slots for proxy metadata:
bytes32 private constant _IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 private constant _ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;These predefined slots reduce collision risks and improve interoperability across tools and platforms.
Challenge 2: Function Selector Clashing — Transparent Proxy Pattern
If a user calls a function that exists in both the proxy and implementation, which one executes?
Without safeguards, this leads to selector clashing, where admin functions might be exposed to regular users — or vice versa.
The Transparent Proxy Pattern solves this by:
- Forwarding all non-admin calls to the implementation.
- Handling admin calls directly in the proxy (no forwarding).
This ensures clean separation of governance and user operations.
👉 Learn how leading protocols implement secure upgrade patterns.
Implementation Snippet (Transparent Proxy)
fallback() external payable {
require(msg.sender != admin(), "admin cannot fallback");
_delegate();
}
function _delegate() internal {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), sload(_IMPLEMENTATION_SLOT), 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) }
}
}Administrators interact directly with admin functions; all others are delegated.
Advanced Upgradeable Architectures
Beacon Proxy Pattern
Ideal for systems with many proxies sharing one logic contract (e.g., wallet services), the Beacon pattern introduces a central Beacon contract that stores the latest implementation address.
Each proxy delegates to the Beacon to fetch the current logic address dynamically. Upgrading becomes a single transaction: update the Beacon, and all proxies instantly use the new version.
Use cases: ERC-4337 account abstraction wallets, NFT mints with per-user proxies.
Universal Upgradeable Proxy Standard (UUPS)
Defined in EIP-1822, UUPS moves upgrade logic into the implementation contract itself, making proxies extremely lightweight.
Benefits:
- Lower deployment cost (gas savings).
- Ability to "lock" upgrades permanently by removing upgrade functions in a final version.
However, security depends heavily on proper access control in the logic layer — a misconfigured _authorizeUpgrade() can lead to irreversible loss.
Sample UUPS Implementation
contract MyToken is UUPSUpgradeable, AccessControlEnumerableUpgradeable {
function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}
}Upgrade via:
proxy.upgradeToAndCall(newImplementationAddress, data);Diamond (Multi-Facet) Proxy
Based on EIP-2535, Diamond proxies support modular logic distribution across multiple "facets". Each facet contains specific functionality (e.g., governance, token minting).
Advantages:
- Bypasses 24KB contract size limit.
- Enables granular upgrades per function group.
Drawbacks:
- Complex setup and tooling support.
- Steeper learning curve.
Key Constraints in Upgradeable Contracts
Avoid Constructors — Use Initializers
Constructors run only once at deployment and do not execute in upgradeable contexts. Instead, use an initializer function:
function initialize() public initializer {
owner = msg.sender;
}OpenZeppelin provides the initializer modifier to prevent re-initialization attacks.
No Initial Values in State Variables
Declaring:
uint256 public count = 100;is equivalent to setting it in a constructor — which won't run.
✅ Correct approach:
uint256 public count;
function initialize() public initializer {
count = 100;
}Note: constant and immutable variables are safe — they don’t occupy runtime storage.
Storage Layout Rules
When upgrading:
- ✅ Add new variables after existing ones.
- ❌ Do not change variable order.
- ❌ Do not delete or modify types of existing variables.
- ❌ Do not add state variables in base contracts unless carefully aligned.
Violations cause silent data corruption due to slot misalignment.
Frequently Asked Questions (FAQ)
Q: Why can’t I use constructors in upgradeable contracts?
A: Because constructors are executed only at deployment time and are not part of the runtime bytecode. Since upgradeable proxies reuse storage but swap logic, constructor code never runs in context.
Q: How do I safely upgrade a contract?
A: Use OpenZeppelin Upgrades plugins (upgradeProxy()), verify storage layout compatibility, test thoroughly in staging environments, and apply access controls like multi-sig approval for production upgrades.
Q: Can anyone call the implementation contract directly?
A: Technically yes — but it should have no state or access control. Best practice: lock logic contracts using modifiers like ifAdmin() or initialize guards.
Q: What happens if I rename a function in a new version?
A: As long as the function selector (first 4 bytes of keccak hash of signature) doesn’t clash with existing ones, renaming is safe. However, client-side integrations may break if ABI expectations change.
Q: Is UUPS safer than Transparent Proxy?
A: Not inherently — UUPS saves gas but places more responsibility on the implementation. A flawed _authorizeUpgrade() can permanently lock or expose upgrade paths.
Q: Can I make a contract non-upgradeable after some time?
A: Yes! In UUPS, remove or disable the upgrade function in a final version. This “freezes” the contract forever — ideal for mature protocols seeking immutability.
Final Thoughts
Mastering delegatecall and upgradeable patterns empowers developers to build resilient, long-term blockchain applications. Whether you're launching a DeFi protocol or managing thousands of user wallets, choosing the right proxy pattern — Transparent, UUPS, or Diamond — depends on your scalability, security, and maintenance needs.
Always follow best practices:
- Use standardized patterns (EIP-1967).
- Leverage audited libraries like OpenZeppelin.
- Test upgrades rigorously before going live.
👉 Explore developer tools that streamline upgradeable contract deployment.