Unsafe Delegatecall
Detects delegatecall operations where the target address is user-controlled or loaded from storage without authorization, enabling complete contract takeover.
Unsafe Delegatecall
Overview
Remediation Guide: How to Fix Unsafe Delegatecall
The unsafe delegatecall detector identifies DELEGATECALL instructions where the target address originates from user input (calldata) or from storage without a preceding authorization check, in contracts that have mutable state. A delegatecall executes the target’s code in the caller’s storage context — if an attacker controls the target, they can run arbitrary code that reads and writes every storage slot in the calling contract.
Sigvex classifies the target address source into four categories: user input (from CALLDATALOAD), storage (from SLOAD), hardcoded constant, or unknown. Findings are generated when the source is user input without authorization, or storage without an access control check.
Why This Is an Issue
The Parity wallet hack (July 2017, $31M) exploited an unprotected delegatecall — the wallet contract allowed anyone to call a library function that invoked delegatecall with a user-supplied address, enabling the attacker to execute initWallet and claim ownership of 596 wallets. The subsequent Parity freeze (November 2017, $150M) was also rooted in delegatecall-related library misuse.
Unlike a regular call, delegatecall preserves msg.sender and msg.value while executing in the caller’s storage. An attacker who controls the target can:
- Overwrite the
ownervariable to claim ownership - Drain all ETH and tokens
- Self-destruct the contract
- Set any storage slot to any value
How to Resolve
// Before: Vulnerable — delegatecall to user-provided address
function execute(address target, bytes calldata data) external {
(bool success, ) = target.delegatecall(data);
require(success);
}
// After: Fixed — target is immutable and set at construction
address public immutable implementation;
constructor(address _impl) {
implementation = _impl;
}
function execute(bytes calldata data) external {
(bool success, ) = implementation.delegatecall(data);
require(success);
}
Examples
Vulnerable Code
contract VulnerableProxy {
address public owner;
uint256 public balance;
// CRITICAL: anyone can delegatecall any contract
function forward(address target, bytes calldata data) public {
(bool success, ) = target.delegatecall(data);
require(success, "Delegatecall failed");
}
}
Fixed Code
contract SafeProxy {
address public owner;
address private immutable _implementation;
constructor(address impl) {
_implementation = impl;
owner = msg.sender;
}
fallback() external payable {
address impl = _implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
Sample Sigvex Output
{
"detector_id": "unsafe-delegatecall",
"severity": "critical",
"confidence": 0.92,
"description": "DELEGATECALL at offset 0x6e targets an address from calldata without authorization. The contract has mutable state — an attacker can overwrite any storage slot.",
"location": { "function": "forward(address,bytes)", "offset": 110 }
}
Detection Methodology
- Locate DELEGATECALL instructions: Identifies all
DELEGATECALLopcodes in the contract bytecode. - Classify target source: Traces the target address operand to determine its origin — calldata (user input), storage, hardcoded constant, or unknown.
- Check mutable state: Verifies whether the contract contains any
SSTOREinstructions. Contracts without mutable state are at lower risk from delegatecall. - Authorization check detection: For storage-sourced targets, checks whether a
CALLER/EQcomparison guards the execution path. For calldata-sourced targets, checks for any access control before the delegatecall. - Confidence scoring: User-input targets without authorization receive the highest confidence. Storage targets without authorization receive medium-high. Hardcoded targets are not flagged.
Limitations
False positives:
- Proxy patterns (EIP-1967, UUPS) that load the implementation from a well-known storage slot may be flagged if the slot is not recognized as admin-controlled.
- Diamond proxy (EIP-2535) facet routing via delegatecall may be flagged even though the facet registry is access-controlled.
False negatives:
- Targets computed through complex logic (e.g., loaded from a mapping, computed via CREATE2 address derivation) may not be classified correctly.
- Delegatecall via inline assembly with non-standard stack manipulation may be missed.
Related Detectors
- Delegatecall — broader delegatecall pattern detection
- Arbitrary External Calls — detects arbitrary call/delegatecall targets
- Storage Collision — detects proxy storage layout conflicts