Delegatecall Injection Exploit Generator
Sigvex exploit generator that validates unvalidated delegatecall targets by passing attacker-controlled contract addresses and checking for storage slot mutations.
Delegatecall Injection Exploit Generator
Overview
The delegatecall injection exploit generator validates findings where a contract executes DELEGATECALL (opcode 0xF4) to an address that the attacker controls. Because DELEGATECALL runs the target’s code in the context of the caller’s storage, an attacker-supplied malicious contract can overwrite any storage slot — including the owner address — or trigger selfdestruct.
This pattern directly caused the Parity Wallet hack #1 (2017, $30M stolen) and Parity Wallet hack #2 (2017, $150M permanently frozen), both of which exploited uninitialized or unguarded proxy library calls.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Three distinct sub-scenarios are validated:
Sub-scenario 1 — Arbitrary target injection:
- The victim proxy contract exposes
execute(address target, bytes calldata data)without whitelistingtarget. - The attacker deploys a
MaliciousLibrarythat writes to storage slot 0 (the owner slot) viaSSTORE. - The attacker calls
execute(maliciousLibrary, abi.encodeWithSignature("takeOwnership()")). - Because
DELEGATECALLexecutestakeOwnership()in the proxy’s storage context, slot 0 is overwritten with the attacker’s address. - The attacker is now the proxy’s owner and can drain funds, call
selfdestruct, or upgrade the implementation.
Sub-scenario 2 — Parity-style library self-destruct:
- A shared
WalletLibraryis deployed and used viaDELEGATECALLby 587 individual wallet contracts. - The library has an
initWallet(address)function that is not guarded against being called directly (outside the proxy context). - The attacker calls
initWallet(attackerAddress)on the library contract itself. - The attacker becomes the library’s
owner. - The attacker calls
kill()on the library, triggeringselfdestruct. - All 587 wallet contracts now
DELEGATECALLinto empty code — they are permanently broken.
Sub-scenario 3 — Forced ETH via storage slot collision:
- Malicious code:
PUSH3 attacker_addr; PUSH1 0x00; SSTOREoverwrites slot 0 of the calling contract.
Exploit Mechanics
The generator first checks whether the target bytecode contains the DELEGATECALL opcode (0xF4). If not present, the finding is immediately rejected as a false match.
The generator runs three scenarios:
- Legitimate library baseline: A simulated library that returns successfully without modifying storage. Used to confirm the delegatecall path is reachable.
- Malicious contract injection: A simulated malicious contract whose code writes the attacker’s address to storage slot 0. The post-execution world state is inspected for slot 0 mutations.
- Direct library initialization: The
initWalletselector (0xf0000320) is sent directly to the library address to test for Parity-style unguarded initialization.
Verdict:
- Malicious
DELEGATECALLsucceeds AND slot 0 is changed → confidence 0.95, critical finding. - Malicious
DELEGATECALLsucceeds but no slot change detected → confidence 0.80 (code execution still occurred). - Legitimate succeeds, malicious reverts → target validation is in place.
The generated PoC demonstrates ownership takeover and forced self-destruct:
contract MaliciousLibrary {
address public owner; // Slot 0 — matches victim layout
function takeOwnership() external {
owner = msg.sender; // Writes to VICTIM's storage slot 0
}
function drain() external {
selfdestruct(payable(msg.sender)); // Destroys VICTIM contract
}
function writeStorage(uint256 slot, uint256 value) external {
assembly { sstore(slot, value) } // Writes ANY slot in victim
}
}
contract DelegatecallAttacker {
IVulnerableProxy public victim;
MaliciousLibrary public malicious;
function exploit() external {
bytes memory call1 = abi.encodeWithSignature("takeOwnership()");
victim.execute(address(malicious), call1);
// victim.owner() == address(this) now
}
}
Remediation
- Detector: Delegatecall Detector
- Remediation Guide: Delegatecall Remediation
Whitelist delegatecall targets and require onlyOwner:
contract SafeProxy {
address public owner;
mapping(address => bool) public whitelistedLibraries;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function execute(address target, bytes calldata data)
external
payable
onlyOwner
returns (bytes memory)
{
require(whitelistedLibraries[target], "Target not whitelisted");
(bool success, bytes memory result) = target.delegatecall(data);
require(success, "Delegatecall failed");
return result;
}
}
For shared libraries, use the initializer modifier pattern from OpenZeppelin to prevent direct initialization on the library contract itself.