Solidity

UUPS Proxy

A UUPS proxy is an upgradeable proxy pattern where upgrade logic lives in the implementation contract instead of the proxy contract.

The proxy stores state and delegates calls, while the implementation decides how upgrades are authorized.

UUPS Proxy Explained in Detail

A UUPS proxy is an upgradeable proxy pattern where the proxy delegates calls to an implementation, but the implementation contains the upgrade function and authorization logic.

This differs from a transparent proxy, where admin and upgrade logic live in the proxy or proxy admin contract. In UUPS, a bad implementation can break or expose upgrades because the implementation controls the upgrade path.

Smart contract example

The critical function is usually _authorizeUpgrade:

contract VaultV2 is UUPSUpgradeable, OwnableUpgradeable {
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

If onlyOwner is missing, the proxy owner was never initialized, or the wrong account controls ownership, an attacker can upgrade the proxy.

UUPS Proxy in Auditing

UUPS combines delegatecall, proxy storage, initialization, and access control. A single mistake can produce permanent takeover, bricked upgrades, or storage collision.

Auditors focus on who can upgrade, whether the implementation is initialized or locked correctly, and whether each new implementation preserves storage layout and UUPS compatibility.

Red flags in code

  • _authorizeUpgrade is empty, weak, or tied to uninitialized ownership.

  • Implementation contract can be initialized by anyone.

  • Upgrade functions remain callable directly on the implementation or through a proxy path that was not intended to expose upgrades.

  • New implementation removes UUPS compatibility or changes storage layout unsafely.

  • UUPS implementation is used behind a transparent proxy without reviewing mixed-pattern behavior.

How to test or review it

  • Attempt upgrades as owner, non-owner, proxy admin, and random contract callers.

  • Verify the implementation contract calls _disableInitializers() when appropriate.

  • Compare storage layouts before and after every upgrade.

  • Verify _authorizeUpgrade is tested through the proxy, not only by calling the implementation contract.

  • Review upgrade authority alongside multisig and timelock controls.

Sources