Skip to content

Delegatecall storage risk

Delegatecall runs another contract's code against the caller's storage. Learn how storage collisions, upgrade authority, and untrusted targets can compromise a proxy or smart wallet.

Updated

For educational purposes only; not investment advice. Investing may result in loss.

Direct answer

delegatecall executes code from a target contract in the caller contract’s context. The caller keeps its own storage, balance, and address(this), while msg.sender and msg.value retain the values from the original call.

This behavior makes proxies, libraries, and smart-wallet modules possible, but it also gives the delegated code the caller’s effective authority. A storage write, asset transfer, approval, or external call is performed as the caller, not as the target that supplied the code.

Treat every reachable delegatecall target as privileged code. Safety depends on the target-selection rules, compatible storage layouts, initialization state, upgrade controls, and the exact implementation active for the transaction.

How it works

With an ordinary external call, the callee reads and writes its own storage. With delegatecall, the target’s bytecode runs against the caller’s storage: an SSTORE instruction changes a slot belonging to the caller. The target’s variable names do not matter at runtime; only computed slot positions do.

That creates four boundaries to audit:

  • Target control: determine whether the destination is fixed, selected by a user, resolved through a registry, or changeable by an administrator.
  • Storage compatibility: compare variable order, types, inheritance, storage gaps, and namespaced or standardized slots across every implementation version.
  • Initialization and authorization: confirm initializers cannot be replayed and that upgrade or module-management functions enforce the intended caller and governance delay.
  • Return handling: verify failures are propagated and returned data is decoded as the expected type; low-level calls do not supply Solidity’s usual contract-type checks.

ERC-1967 reduces proxy collisions by placing implementation, beacon, and admin addresses in standardized slots outside normal compiler allocation. It does not prove that an implementation is safe or that an authorized upgrade is benign.

Example

Suppose a wallet stores owner in slot 0. A plugin compiled with counter in slot 0 increments the counter when the wallet reaches it through delegatecall.

The write changes the wallet’s owner value because storage belongs to the wallet. If the resulting word encodes an attacker-controlled address, later authorization checks may recognize the attacker as owner even though the plugin never held the wallet’s assets.

A successful receipt does not distinguish intended state changes from harmful ones. Transaction simulation should therefore inspect storage diffs, asset and approval changes, emitted events, and downstream calls against the exact proxy and implementation addresses.

Risks

  • Arbitrary-target execution: user-controlled or weakly validated targets can run malicious code with the caller’s permissions.
  • Storage collision: an implementation can overwrite ownership, balances, pause state, or even the slot that selects the next implementation.
  • Unsafe upgrade: a compromised administrator or governance process can replace previously reviewed code after users have deposited assets or granted approvals.
  • Initialization failure: an uninitialized proxy or implementation may let another account claim privileged roles or configure dangerous dependencies.
  • Misleading inspection: verifying only the proxy source, current implementation, or interface can miss a beacon, pending upgrade, module registry, or alternate execution path.

Before signing, resolve the implementation at a recent block, verify who can change it and under what delay, inspect the target’s verified bytecode and storage layout, simulate the full calldata, and compare sensitive storage and token approvals before and after execution. For a smart wallet, also review how modules are enabled, disabled, and allowed to choose targets.

Common misconceptions

  • “The target cannot touch the caller’s assets.” Delegated code runs as the caller and can invoke external contracts, transfer assets, or create approvals if the caller has those capabilities.
  • “Matching variable names prevent collisions.” The EVM uses storage slots, not source-code names. Layout order, inheritance, and types must remain compatible.
  • “Verified proxy code means the system is verified.” The active implementation, beacon, upgrade administrator, initialization state, and module permissions are separate parts of the trust boundary.

Sources

Navigation

Search the wiki...