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 storage
5│ msg.sender │ │ msg.sender=A│
6└─────────────┘ └─────────────┘
7
8Delegatecall:
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 delegatecall
2(bool success, bytes memory data) = target.delegatecall(
3 abi.encodeWithSignature("someFunction(uint256)", 123)
4);
5require(success, "Delegatecall failed");
6
7// 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 result
13 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;
3
4 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}
6
7// 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 0
3 address public owner; // Slot 1
4}
5
6contract Implementation {
7 uint256 public value; // Slot 0 - COLLISION!
8
9 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 VULNERABILITY
2function execute(address target, bytes calldata data) external {
3 target.delegatecall(data); // Attacker controls the code!
4}
5
6// Attacker deploys malicious contract:
7contract Malicious {
8 function attack() external {
9 // Runs in victim's context - can modify any storage
10 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;
3
4 function withdraw() external {
5 require(msg.sender == owner); // Check passes if delegatecalled!
6 // ...
7 }
8}
9
10// 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;
4
5 function initialize(address _owner) external {
6 require(!initialized);
7 owner = _owner;
8 initialized = true;
9 }
10}
11
12// Attacker calls initialize() directly on implementation
13// Then uses this to attack systems that trust the implementation

Delegatecall vs Call vs Staticcall

Featurecalldelegatecallstaticcall
Executes codeTargetTargetTarget
Uses storageTargetCallerTarget
msg.senderCallerPreservedCaller
msg.valueSent valuePreservedSent value
Can modify stateYesYes (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 selfdestruct in delegatecall targets
  • Access control considers delegatecall context
  • Return data handled correctly

Best Practices

  1. Never delegatecall to untrusted addresses
  2. Use established proxy patterns (OpenZeppelin, EIP-1967)
  3. Match storage layouts exactly between proxy and implementation
  4. Initialize implementations with replay protection
  5. Avoid selfdestruct in any delegatecall target
  6. 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.

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

oog
zealynx

Subscribe to Our Newsletter

Stay updated with our latest security insights and blog posts

© 2024 Zealynx