ROOTVIELROOTVIEL
← Knowledge Base

Web3 & smart contracts

Partial13 min

Proxy & Upgrade (Code Authority)

An upgradeable contract delegates its logic to an implementation whose address can be changed; if the authority to change that address is weak, or if an upgrade shifts the storage layout, an attacker can make the proxy run arbitrary code or silently corrupt every stored value — the question is literally "who controls the code this contract executes".

Classification

Domain
Web3 & smart contracts
ROOTVIEL support
Partial — Core is built; parts are not yet wired end-to-end.
Analysis family
RootRaven
OWASP
SC01

Definition

Proxy and upgrade defects are the class of bugs rooted in the separation between a contract’s persistent storage (held by the proxy) and its executable logic (held by a separate implementation the proxy delegatecalls into). This separation is what makes upgrades possible, and it concentrates an unusual power: whoever can set the implementation address controls the code that runs against the proxy’s storage and funds.

Three recurring failure shapes: (1) an upgrade/admin function whose authority is missing, weak, or seizable, letting an attacker point the proxy at malicious code; (2) an admin/ownership transition that hands control to the wrong party (including an implementation whose own initializer is unlocked); and (3) a storage-layout mismatch introduced by an upgrade, where the new implementation interprets the old storage slots differently and corrupts balances, owners, or flags without any explicit write.

Context

This family applies to every upgradeable deployment pattern: transparent proxies, UUPS proxies (where the upgrade logic lives in the implementation itself), beacon proxies, and diamonds (EIP-2535). Because the implementation is initialized rather than constructed, the implementation contract itself — not just the proxy — can be a target if left uninitialized. The power to upgrade is usually the single most valuable capability in a protocol, so it is also the most attacked.

  • ▸UUPS proxies where the upgrade function lives in the implementation and must be guarded there.
  • ▸Implementation contracts left uninitialized, allowing an attacker to initialize and (in UUPS) upgrade or self-destruct them.
  • ▸Upgrades that add, remove, or reorder state variables without preserving slot order (storage collision).
  • ▸delegatecall-based modules/diamonds where a called facet can write the proxy’s storage.
  • ▸Admin transitions (changeAdmin, transferOwnership) that can be front-run or sent to the wrong address.

Technical fundamentals

A proxy forwards calls with delegatecall, which executes the implementation’s bytecode in the proxy’s storage and msg.sender/value context. Storage is addressed by slot number, and the implementation’s variable declarations are just names the compiler maps to slots — the proxy does not enforce that two implementations agree on that mapping. So the safety of an upgrade rests on an off-chain discipline (preserve slot order, use storage gaps or unstructured/ERC-7201 namespaced storage) that the chain cannot check for you.

The upgrade authority is a privileged pointer. In a transparent proxy the admin lives in a dedicated slot and admin calls are routed away from the implementation; in UUPS the upgrade entrypoint is in the implementation, so forgetting to guard it there (or leaving the implementation initializable) is catastrophic. The ERC-1967 standard fixes well-known slots for the implementation and admin addresses specifically so tools and proxies do not collide with application storage.

delegatecall
Runs another contract’s code against the caller’s storage and context — the mechanism that lets a proxy "become" its implementation.
Storage collision
Two implementations (or a facet and the proxy) mapping different meanings to the same slot, so a write or read corrupts unrelated state.
Storage gap / namespaced storage
Reserved slots (uint256[__gap]) or ERC-7201 namespaced layouts that let future upgrades add variables without shifting existing slots.
UUPS
An upgrade pattern where the upgrade function is in the implementation; it must be access-controlled there, and the implementation must be non-initializable on its own.

Root cause

The defect is that the authority to change executable code (or to initialize/own the contract that holds it) is not correctly bound, or that an upgrade violates the unwritten contract of storage-layout stability. In the authority case it is an access-control failure applied to the most powerful capability in the system; in the layout case it is a correctness failure where the chain’s slot-based storage model is not honored across versions.

The SWC Registry covers the delegatecall-to-untrusted-callee hazard that underlies malicious-implementation attacks; the broader upgrade-authority and storage-layout issues are design concerns best referenced against the registry generally and OWASP Smart Contract Top 10 SC01 (access control over code authority). This is why the article is marked partial: the authority sub-case is testable like access control, while full storage-layout corruption proof is a frontier capability.

How the attack works, step by step

  1. 1The attacker finds that the proxy’s upgrade path is reachable without proper authority — e.g. a UUPS upgradeTo without an onlyOwner guard, or an implementation contract whose initialize() was never called and is still open.
  2. 2For the uninitialized-implementation case: the attacker calls initialize() on the implementation directly, becoming its owner.
  3. 3The attacker calls the upgrade function to set the implementation to a contract they control (or, in some UUPS cases, triggers a self-destruct of the logic, bricking the proxy).
  4. 4The proxy now delegatecalls attacker code, which runs against the proxy’s real storage and balance — the attacker can transfer funds, rewrite owner, or mint.
  5. 5Storage-layout variant: a legitimate-looking upgrade reorders variables; afterward a slot that held a balance is read as an owner flag (or vice versa), silently corrupting state with no explicit malicious write.

Vulnerable example

A synthetic UUPS-style implementation with an unguarded upgrade function and an unlocked initializer — anyone can take ownership and then point the proxy at arbitrary code.

synthetic Solidity — do not deploy; illustrates the flaw
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // UNSAFE: upgrade entrypoint is unguarded and the initializer is re-callable. contract LogicV1 { address public owner; bool private _initialized; // declared, but not enforced as single-use uint256 public totalFunds; // slot 2 — remember this for the layout note // BUG 1: no single-use guard — anyone can initialize and become owner. function initialize(address _owner) external { owner = _owner; } // BUG 2: no authority check — anyone can repoint the implementation. // In a UUPS proxy this sets the ERC-1967 implementation slot to attacker code. function upgradeTo(address newImplementation) external { assembly { sstore( 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, // ERC-1967 impl slot newImplementation ) } } } // A later "LogicV2" that REORDERS state would also corrupt storage: // contract LogicV2 { uint256 public totalFunds; address public owner; } // now slot 0 (was owner) is read as totalFunds and slot 1 (was _initialized) // as owner — a silent storage collision, no explicit malicious write needed.

Corrected example

Guard the upgrade entrypoint with authority, lock the implementation so it cannot be initialized on its own, keep storage layout stable with an explicit gap (or namespaced storage), and route upgrades through a timelock/multisig. This synthetic version mirrors the OpenZeppelin UUPS discipline.

synthetic Solidity — guarded upgrade, locked initializer, storage gap
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // SAFER: authority-gated upgrade + one-shot initializer + reserved gap. contract LogicV1 { // --- storage layout: never reorder or remove; only append before the gap --- address public owner; // slot 0 uint256 public totalFunds; // slot 1 uint8 private _initStage; // slot 2: 0 = none, 1 = done, 255 = locked // Reserve slots so a future upgrade can add variables without collision. uint256[47] private __gap; modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; } // Call on the implementation at deploy time to make it non-initializable. function lockImplementation() external { require(_initStage == 0, "already set"); _initStage = 255; // implementation can never be initialized/owned } // One-shot initializer for the proxy context. function initialize(address _owner) external { require(_initStage == 0, "already initialized"); require(_owner != address(0), "zero owner"); _initStage = 1; owner = _owner; } // Authority-gated upgrade. In production, owner is a timelock + multisig. function upgradeTo(address newImplementation) external onlyOwner { require(newImplementation.code.length > 0, "not a contract"); // ... emit event; perform ERC-1967 implementation-slot update via a // reviewed helper that also runs an authorizeUpgrade hook ... } }

Why it works: the upgrade function now requires owner authority bound to msg.sender; the implementation is locked so it cannot be independently initialized and hijacked; the explicit __gap reserves slots so appending state in a future version does not shift existing slots; and routing owner through a timelock/multisig means no single compromised key can push a malicious implementation instantly.

Note

Limits of the fix: these controls bind who may upgrade and keep layout stable, but they do not make the upgrade contents correct — a fully authorized upgrade to buggy or malicious code still ships. A storage gap prevents collisions only if every future version honors the append-only discipline; the chain cannot enforce it. And a compromised owner key (or captured governance) defeats the whole scheme. "Partial" support reflects exactly this: authority is testable, but proving a given upgrade is layout-safe and semantically correct is a harder, less mature problem — treat a clean check as UNKNOWN about the next upgrade, not as proof of safety.

Impact

  • ▸Total takeover: controlling the implementation means controlling all logic executed against the proxy’s storage and funds.
  • ▸Instant drain or mint: malicious implementation code can move every asset the proxy holds.
  • ▸Bricking: an attacker (or a buggy upgrade) can self-destruct the logic or set a non-contract implementation, freezing the proxy permanently.
  • ▸Silent corruption: a storage-layout collision mis-assigns balances, owners, or flags with no obvious failing transaction, surfacing later as inconsistent state.

Detection

Static analysis can flag unguarded upgrade entrypoints, implementations whose initializer is not locked, delegatecall to addresses derived from attacker-influenceable input, and diffs between two implementation versions that reorder or remove storage variables (a layout-collision signal). Authority gaps are confirmable dynamically: on a local chain, have an unauthorized account attempt the upgrade and observe whether the implementation slot changes. Layout corruption is harder to confirm — it requires simulating the upgrade against real storage and checking that each slot still means what it should. A proven unauthorized implementation change is a Finding; a layout diff is a signal that needs a simulated-upgrade experiment to become one.

How RootRaven analyzes it

Support level
Partial — a frontier family. The upgrade-authority sub-case is testable like access control; full storage-layout corruption proof is declared but not yet operational end-to-end.
The truth it decides
Who controls the implementation code the proxy delegates to — covering unauthorized or unsafe upgrade, an admin/ownership transition to the wrong party, and storage-layout corruption introduced by an upgrade.
Evidence required
For authority: an executed transaction from an unauthorized account that changes the implementation (or initializes/owns the logic). For layout: a simulated upgrade on a local fork whose deterministic verifier shows a storage slot’s meaning changed against a known-good mapping.

RootRaven treats an unguarded upgradeTo or an unlocked implementation initializer as an Observation, forms the Hypothesis "an unauthorized party can change the executed code", and has the AI compose a candidate takeover transaction — the AI proposes, it never rules. The candidate executes on a disposable local blockchain or a local fork of real on-chain state (never a live public network), and an AI-free verifier checks whether the ERC-1967 implementation/admin slot changed, or whether the logic was initialized/owned, by an account with no authority. That authority path can reach a confirmed Finding today.

Partial

Maturity caveat, kept visible: storage-layout corruption is in the family’s declared scope but is not yet proven end-to-end — RootRaven can surface a layout-diff signal but does not yet always close the loop with an executed, verifier-confirmed corruption. Those cases are reported as signals/UNKNOWN, never as Findings and never as "the proxy is safe". A clean authority run means "no unauthorized code-authority change was proven within the tested scope", and says nothing about whether the next legitimate upgrade is correct.

Mitigation

Bind upgrade authority tightly, lock the implementation, keep storage layout stable, and review every upgrade as the most dangerous change in the system.

  • ▸Preventive: guard the upgrade entrypoint with strict authority (UUPS _authorizeUpgrade / onlyOwner), and in production put that authority behind a timelock + multisig, not a single key.
  • ▸Preventive: call _disableInitializers() (or equivalent) on implementation contracts so they can never be initialized/owned on their own.
  • ▸Preventive: preserve storage layout across versions — append-only, with uint256[__gap] reservations or ERC-7201 namespaced storage; validate layout with upgrade tooling (e.g. OpenZeppelin Upgrades plugins).
  • ▸Preventive: avoid delegatecall to addresses derived from untrusted input; in diamonds, control who can add/replace facets.
  • ▸Detective/Corrective: monitor the ERC-1967 implementation/admin slots and emit/track Upgraded events; keep a rollback plan and a tested emergency pause.

Verifying the mitigation

Verify by attempting the takeover and by simulating the upgrade. On a local chain, call the upgrade function from an unauthorized account and confirm it reverts; call initialize() on the implementation directly and confirm it reverts; then, for the proposed new implementation, run an automated storage-layout comparison against the current one and simulate the upgrade on a local fork, asserting that each pre-existing slot still holds its original meaning and value. Report "unauthorized upgrade attempts failed and the simulated upgrade preserved layout within the tested scope", never "the upgrade is safe".

Note

Layout tooling catches declared-variable reorderings; it cannot judge whether new code is semantically correct or whether governance can be captured. Those remain UNKNOWN after a clean layout check.

Common mistakes

  • ▸Guarding the proxy admin but forgetting that a UUPS upgrade function lives in the implementation and must be guarded there too.
  • ▸Leaving the implementation contract uninitialized, so an attacker initializes it and (in UUPS) upgrades or destroys it.
  • ▸Reordering or removing storage variables between versions, causing a silent slot collision.
  • ▸Adding new variables without a reserved gap, shifting the slots of inherited contracts.
  • ▸Treating a single owner key as sufficient authority for the most powerful capability in the system.
  • ▸Concluding the proxy is safe from a clean authority check, when upgrade correctness and governance capture were never proven.

References

  1. [1]OWASP Smart Contract Top 10 — SC01 Access Control Vulnerabilities
  2. [2]SWC Registry (overview)
  3. [3]OpenZeppelin — Proxies, UUPS and Upgrade safety
  4. [4]EIP-1967 — Standard Proxy Storage Slots
  5. [5]EIP-7201 — Namespaced Storage Layout

References point to public, well-known technical sources for further study.

Related articles