Skip to main content
Sigvex

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:

  1. The victim proxy contract exposes execute(address target, bytes calldata data) without whitelisting target.
  2. The attacker deploys a MaliciousLibrary that writes to storage slot 0 (the owner slot) via SSTORE.
  3. The attacker calls execute(maliciousLibrary, abi.encodeWithSignature("takeOwnership()")).
  4. Because DELEGATECALL executes takeOwnership() in the proxy’s storage context, slot 0 is overwritten with the attacker’s address.
  5. 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:

  1. A shared WalletLibrary is deployed and used via DELEGATECALL by 587 individual wallet contracts.
  2. The library has an initWallet(address) function that is not guarded against being called directly (outside the proxy context).
  3. The attacker calls initWallet(attackerAddress) on the library contract itself.
  4. The attacker becomes the library’s owner.
  5. The attacker calls kill() on the library, triggering selfdestruct.
  6. All 587 wallet contracts now DELEGATECALL into empty code — they are permanently broken.

Sub-scenario 3 — Forced ETH via storage slot collision:

  • Malicious code: PUSH3 attacker_addr; PUSH1 0x00; SSTORE overwrites 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:

  1. Legitimate library baseline: A simulated library that returns successfully without modifying storage. Used to confirm the delegatecall path is reachable.
  2. 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.
  3. Direct library initialization: The initWallet selector (0xf0000320) is sent directly to the library address to test for Parity-style unguarded initialization.

Verdict:

  • Malicious DELEGATECALL succeeds AND slot 0 is changed → confidence 0.95, critical finding.
  • Malicious DELEGATECALL succeeds 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

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.

References