Skip to main content
Sigvex

Unsafe Delegatecall Remediation

How to prevent contract takeover via delegatecall by fixing the target address to a trusted, immutable implementation instead of accepting it from callers or unguarded storage.

Unsafe Delegatecall Remediation

Overview

delegatecall runs the target’s code in the caller’s storage context while preserving msg.sender and msg.value. If the target address comes from calldata, or from a storage slot that any caller can change, an attacker supplies their own contract and gains write access to every storage slot — including the owner slot — and can drain funds or self-destruct the contract. The fix is to remove attacker control over the target: the implementation a contract delegates to must be fixed at construction or changeable only by an authorized admin.

Related Detector: Unsafe Delegatecall

Before (Vulnerable)

function execute(address target, bytes calldata data) external {
    // Caller chooses the code that runs against this contract's storage
    (bool ok, ) = target.delegatecall(data);
    require(ok);
}

After (Fixed)

// The implementation is fixed at construction and cannot be redirected
address private immutable _implementation;

constructor(address impl) {
    require(impl.code.length > 0, "Implementation has no code");
    _implementation = impl;
}

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()) }
    }
}

An immutable target cannot be changed after deployment, so there is no attacker-reachable path to substitute the code.

Alternative Mitigations

Upgradeable proxies legitimately need a mutable implementation. Store it in the standard EIP-1967 slot and gate every change behind an admin role, so only the authorized account can call upgradeTo. Use an audited proxy implementation (for example, OpenZeppelin’s ERC1967Proxy / UUPS) rather than hand-rolling the upgrade path.

Allowlist — if a contract must delegate to one of several known modules, store the permitted targets in an admin-controlled mapping and check membership before the delegatecall. Never delegate to an address taken directly from calldata.

Common Mistakes

Validating the target with extcodesize only — a non-empty code check does not make an attacker-supplied target safe; the attacker’s contract has code too. The target must be trusted, not merely deployed.

Authorizing the caller but not the targetonlyOwner on a delegatecall(target, ...) where target is still caller-supplied does not help once the owner key is the attacker, and it does nothing for a compromised privileged role. Constrain the target itself.

References