← Back to Blog

delegatecall Vulnerabilities in Solidity: Beyond Proxy Patterns

2026-04-17 solidity security delegatecall proxy reentrancy wallet context 2026

delegatecall is one of the most powerful and most dangerous opcodes in Solidity. Most developers know about proxy storage collisions — but delegatecall vulnerabilities extend well beyond that. Arbitrary delegatecall, context confusion attacks, and reentrancy through delegatecall callbacks have each been responsible for major exploits.

This post covers the delegatecall attack surface that static analysis tools often miss.


How delegatecall Works

Normal call:  Caller → Callee
              (executes in Callee's storage context)

delegatecall: Caller → Library/Implementation
              (executes in Caller's storage context)
              msg.sender and msg.value are PRESERVED from caller

The critical property: code runs in the calling contract's storage, but with the called contract's logic. This means:
- A delegatecall to a malicious contract lets that contract read and write your storage
- A delegatecall that calls back into your contract can execute arbitrary functions in your context


Attack Vector 1: Arbitrary delegatecall

The most catastrophic delegatecall exploit: a contract that lets callers specify where to delegatecall.

// VULNERABLE: arbitrary delegatecall — wallet that delegates any call
contract VulnerableWallet {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function execute(
        address target,
        bytes calldata data
    ) external onlyOwner {
        // Delegates to caller-controlled target — ARBITRARY DELEGATECALL
        (bool ok,) = target.delegatecall(data);
        require(ok);
    }
}

An attacker who gains owner control (via a phishing signature, key compromise, or a separate access control bug) can delegatecall to any contract with any calldata. They can:
1. Delegatecall to their own malicious contract to overwrite owner in this wallet's storage
2. Delegatecall to a selfdestruct operation to destroy the wallet
3. Delegatecall to drain all ETH and tokens

Real-world incident: The Parity Multisig Wallet hack (2017, $31M) used an arbitrary delegatecall through a library function. A public initWallet function allowed anyone to become the owner of the library contract, which then called kill() via delegatecall across 500+ wallets.

Fix: Whitelist allowed targets

contract SafeWallet {
    address public owner;
    mapping(address => bool) public allowedTargets;

    function addTarget(address target) external onlyOwner {
        allowedTargets[target] = true;
    }

    function execute(address target, bytes calldata data) external onlyOwner {
        require(allowedTargets[target], "Target not allowed");
        (bool ok,) = target.delegatecall(data);
        require(ok);
    }
}

Or use call instead of delegatecall for external execution (unless you specifically need the delegatecall semantics).


Attack Vector 2: Context Confusion (msg.sender preserved)

delegatecall preserves msg.sender. This breaks authentication logic that assumes msg.sender is the direct caller.

// Library contract
contract MathLib {
    function multiply(uint256 a, uint256 b) external pure returns (uint256) {
        return a * b;
    }
}

// Target contract with admin check
contract TokenVault {
    address public admin;
    mapping(address => uint256) public balances;

    // Admin function — but who is msg.sender when called via delegatecall?
    function adminWithdraw(address to, uint256 amount) external {
        require(msg.sender == admin, "Not admin");
        payable(to).transfer(amount);
    }
}

// Attacker contract
contract Attacker {
    function attack(address vault) external {
        // delegatecall to vault.adminWithdraw
        // msg.sender in vault's execution context = Attacker address
        // But if Attacker == admin... this becomes an admin call
        vault.delegatecall(
            abi.encodeWithSignature("adminWithdraw(address,uint256)", msg.sender, 1 ether)
        );
    }
}

More commonly, context confusion appears when a contract delegatecalls into a library that has msg.sender-based checks, and the semantics are different than intended.

Fix: Be explicit about whether you need call or delegatecall. In general:
- Use call to execute code in the target's context (normal external call)
- Use delegatecall only when you need the called code to operate in your storage (proxy pattern, library with state)


Attack Vector 3: Reentrancy via delegatecall

delegatecall doesn't reset reentrancy guards unless the guard is stored in the same storage slot you're protecting:

// VULNERABLE: reentrancy guard doesn't protect against delegatecall reentrancy
contract VaultWithDelegatecall {
    bool private _locked;
    uint256 public balance;

    modifier nonReentrant() {
        require(!_locked);
        _locked = true;
        _;
        _locked = false;
    }

    function withdraw(uint256 amount) external nonReentrant {
        require(balance >= amount);
        balance -= amount;
        payable(msg.sender).transfer(amount);
    }

    // Allows delegatecall to "trusted" library
    function useLibrary(address lib, bytes calldata data) external nonReentrant {
        (bool ok,) = lib.delegatecall(data);
        require(ok);
    }
}

If lib is a malicious contract that calls back into withdraw(), the _locked state is in the vault's storage — but useLibrary already set _locked = true before the delegatecall. However, if the library itself modifies _locked (by writing to slot 0 in the calling context), it can clear the lock, enabling reentrancy into withdraw().

More subtle: A delegatecall that triggers an external call (to return ETH, etc.) creates a new execution context where reentrancy is possible via the normal callback path.


Attack Vector 4: Storage Slot Collision via Malicious Library

// Library that appears safe but overwrites critical storage
contract MaliciousLib {
    // This function writes to slot 0 in the calling context
    function initialize(uint256 value) external {
        assembly {
            sstore(0, value)  // Slot 0 in the CALLER's storage
        }
    }
}

contract Victim {
    address public owner;  // slot 0

    function callLib(address lib) external {
        lib.delegatecall(
            abi.encodeWithSignature("initialize(uint256)", uint256(uint160(msg.sender)))
        );
        // This just overwrote 'owner' with msg.sender's address
    }
}

Any delegatecall to a library that writes to storage writes to the calling contract's storage. If the library's storage layout doesn't match the calling contract's expectations, arbitrary state is corrupted.

Real-world relevance: Early Ethereum multisig wallets used delegatecall to libraries for gas efficiency, and malicious library upgrades (or library address manipulation) could corrupt wallet state.

Fix: Audit every library you delegatecall to. Never delegatecall to contracts at addresses you can't verify (e.g., addresses passed as function arguments).


Attack Vector 5: Selfdestruct via delegatecall

// Attacker deploys this contract
contract Bomb {
    function detonate() external {
        selfdestruct(payable(msg.sender));
    }
}

// Victim allows arbitrary delegatecall
contract Victim {
    function execute(address target, bytes calldata data) external onlyOwner {
        target.delegatecall(data);
    }
}

When Bomb.detonate() runs via delegatecall from Victim, the selfdestruct destroys Victim — not Bomb. The selfdestruct opcode operates in the execution context of the caller (Victim's storage and code).

This is how the Parity Multisig Library was destroyed in the second 2017 hack: someone called initWallet on the library contract (not a wallet), became the owner, then called kill() — which destructed the library contract. All wallets that delegatecalled to that library address were then broken (no code at the target).

Fix: Never allow delegatecall to untrusted or user-provided addresses. Validate all library addresses at deploy time or via governance.


Spotting Dangerous delegatecall Patterns

// DANGEROUS: any of these patterns merit scrutiny

// 1. User-controlled target
target.delegatecall(data);  // where target is from msg.sender or function param

// 2. Unchecked delegatecall result
address(lib).delegatecall(data);  // no success check

// 3. delegatecall before state update (reentrancy window)
(bool ok,) = lib.delegatecall(data);
state = newState;  // should be BEFORE delegatecall

// 4. delegatecall in a loop
for (uint i; i < targets.length; i++) {
    targets[i].delegatecall(calldata[i]);  // each iteration is an attack surface
}

Safe delegatecall Checklist


Detection Coverage

Vulnerability Slither Mythril Semgrep AI
Arbitrary delegatecall (user-controlled target)
Missing return value check ⚠️
delegatecall to mutable address ⚠️
selfdestruct via delegatecall ⚠️
Storage layout collision in library
Reentrancy window in delegatecall ⚠️

Arbitrary delegatecall is well-covered by static analysis — it's a known dangerous pattern with clear syntactic markers. The harder cases (storage layout collision via library, reentrancy in combination with delegatecall, context confusion) require reasoning about semantics, which AI analysis handles by understanding the intended behavior vs the actual behavior.


Scan your contract with ContractScan — arbitrary delegatecall detection is covered by all three static engines, with AI analysis covering the context-dependent variants.


Related: Proxy Pattern Vulnerabilities: UUPS, Transparent, and Diamond — proxy-specific storage collision and initialization attacks.

Related: Reentrancy: From The DAO to Euler Finance — delegatecall reentrancy in context of the broader reentrancy attack class.

Important Notes

This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.

Scan your contract now
Slither + AI analysis — Unlimited quick scans. No signup required.
Try Free Scan →