Delegatecall
EVM opcode that executes another contract's code in the calling contract's storage context, enabling proxy patterns and code reuse.
Delegatecall is a low-level EVM opcode that allows a contract to execute code from another contract while preserving the original contract's storage, msg.sender, and msg.value. This powerful feature enables proxy patterns, upgradeable contracts, and code libraries—but also introduces significant security risks if misused. Understanding delegatecall is essential for auditing any upgradeable protocol or contract that uses external code execution.
How Delegatecall Works
When Contract A uses delegatecall to Contract B:
1Regular call:2┌─────────────┐ call ┌─────────────┐3│ Contract A │ ────────────► │ Contract B │4│ Storage: A │ │ Storage: B │ ← Uses B's storage5│ msg.sender │ │ msg.sender=A│6└─────────────┘ └─────────────┘78Delegatecall:9┌─────────────┐ delegatecall ┌─────────────┐10│ Contract A │ ────────────► │ Contract B │11│ Storage: A │ │ Storage: A │ ← Uses A's storage!12│ msg.sender │ │ msg.sender │ ← Preserved!13└─────────────┘ └─────────────┘
The key difference: delegatecall runs B's code but reads/writes A's storage.
Solidity Usage
1// Low-level delegatecall2(bool success, bytes memory data) = target.delegatecall(3 abi.encodeWithSignature("someFunction(uint256)", 123)4);5require(success, "Delegatecall failed");67// In assembly (commonly used in proxies)8assembly {9 calldatacopy(0, 0, calldatasize())10 let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)11 returndatacopy(0, 0, returndatasize())12 switch result13 case 0 { revert(0, returndatasize()) }14 default { return(0, returndatasize()) }15}
Use Cases
Proxy Patterns
The primary use case—separating storage from logic:
1contract Proxy {2 address implementation;34 fallback() external payable {5 (bool success,) = implementation.delegatecall(msg.data);6 require(success);7 }8}
All calls to the proxy execute the implementation's code but modify the proxy's storage.
Library Contracts
Reusable code that operates on the caller's storage:
1library MathLib {2 function add(uint256 a, uint256 b) internal pure returns (uint256) {3 return a + b;4 }5}67// When using `using MathLib for uint256`, Solidity uses delegatecall internally
Diamond Standard (EIP-2535)
Routes function calls to different implementation contracts (facets):
1function _fallback() internal {2 address facet = selectorToFacet[msg.sig];3 assembly {4 calldatacopy(0, 0, calldatasize())5 let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)6 // ...7 }8}
Security Vulnerabilities
Storage Layout Collisions
The most critical delegatecall vulnerability:
1contract Proxy {2 address public implementation; // Slot 03 address public owner; // Slot 14}56contract Implementation {7 uint256 public value; // Slot 0 - COLLISION!89 function setValue(uint256 _v) external {10 value = _v; // Actually overwrites Proxy's implementation address!11 }12}
An attacker calling setValue() could overwrite the implementation address with arbitrary data.
Delegatecall to Untrusted Contracts
Never delegatecall to user-supplied addresses:
1// CRITICAL VULNERABILITY2function execute(address target, bytes calldata data) external {3 target.delegatecall(data); // Attacker controls the code!4}56// Attacker deploys malicious contract:7contract Malicious {8 function attack() external {9 // Runs in victim's context - can modify any storage10 assembly { sstore(0, 0xATTACKER_ADDRESS) }11 }12}
Context Preservation Attacks
msg.sender is preserved, which can bypass access controls:
1contract Vault {2 address public owner;34 function withdraw() external {5 require(msg.sender == owner); // Check passes if delegatecalled!6 // ...7 }8}910// If Vault is delegatecalled from a proxy where attacker is owner,11// the check passes even though attacker shouldn't have access
Initialization Hijacking
Implementation contracts can be initialized directly:
1contract Implementation {2 address public owner;3 bool public initialized;45 function initialize(address _owner) external {6 require(!initialized);7 owner = _owner;8 initialized = true;9 }10}1112// Attacker calls initialize() directly on implementation13// Then uses this to attack systems that trust the implementation
Delegatecall vs Call vs Staticcall
| Feature | call | delegatecall | staticcall |
|---|---|---|---|
| Executes code | Target | Target | Target |
| Uses storage | Target | Caller | Target |
| msg.sender | Caller | Preserved | Caller |
| msg.value | Sent value | Preserved | Sent value |
| Can modify state | Yes | Yes (caller's) | No |
Audit Checklist
When auditing delegatecall usage:
- Target address is trusted/immutable or properly validated
- Storage layouts are compatible between caller and target
- No user-controlled delegatecall targets
- Implementation contracts are initialized or use initializer guards
- No
selfdestructin delegatecall targets - Access control considers delegatecall context
- Return data handled correctly
Best Practices
- Never delegatecall to untrusted addresses
- Use established proxy patterns (OpenZeppelin, EIP-1967)
- Match storage layouts exactly between proxy and implementation
- Initialize implementations with replay protection
- Avoid
selfdestructin any delegatecall target - Use storage gaps for upgradeable base contracts
Delegatecall is one of the most powerful and dangerous features in the EVM. It enables elegant patterns like upgradeable contracts but requires careful security analysis to use safely.
Related Terms
Proxy Pattern
Smart contract design separating storage and logic, enabling upgrades by changing implementation while preserving state.
Diamond Standard
EIP-2535 proxy pattern allowing a single contract to delegate calls to multiple implementation contracts (facets).
Reentrancy Attack
A vulnerability where external calls allow malicious contracts to recursively call back before state updates complete.
Need expert guidance on Delegatecall?
Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.
Get a Quote
