UUPS Proxy
The Universal Upgradeable Proxy Standard (UUPS) is a minimal and gas-efficient pattern for upgradeable contracts. Defined in the ERC-1822 specification, UUPS delegates upgrade logic to the implementation contract itself — reducing proxy complexity and deployment costs.
The OpenZeppelin Stylus Contracts provide a full implementation of the UUPS pattern via UUPSUpgradeable and Erc1967Proxy.
Overview
UUPS uses the ERC-1967 proxy architecture to separate upgrade logic from proxy behavior. Instead of maintaining upgradeability in the proxy, all upgrade control is implemented within the logic contract.
Key components:
- Proxy Contract (
Erc1967Proxy) — delegates calls viadelegate_call. - Implementation Contract — contains application logic and upgrade control.
 - Upgrade Functions — reside in the implementation, not the proxy.
 
In Solidity, upgrade safety often relies on an immutable self-address and context checks. Stylus, however, uses a small adaptation that is covered below.
Context Detection in Stylus
Stylus does not currently support the immutable keyword. Instead of storing __self = address(this), the implementation uses a dedicated boolean flag in a unique storage slot to distinguish direct vs delegated (proxy) execution contexts.
// boolean flag stored in a unique slot
logic_flag: bool;The implementation’s constructor sets logic_flag = true in the implementation’s own storage. When code runs via a proxy (delegatecall), the proxy’s storage does not contain this flag, so reads as false. This enables only_proxy() to check for delegated execution without relying on an address sentinel.
Initialization
Stylus requires explicit initialization for both the implementation and the proxy:
- Constructor – called exactly once on deployment of the logic (implementation) contract. This sets the implementation-only 
logic_flagused for context checks. - Version setup (
set_version) – called via the proxy (delegatecall) to write the logic’sVERSION_NUMBERinto the proxy’s storage. This aligns the proxy’s version with the logic and enables upgrade paths guarded byonly_proxy(). 
Call set_version() is triggered automatically via upgrade_to_and_call().
Devs are only required to manually call set_version() when deploying the proxy.
Trade-offs:
- Storage Cost: Requires one additional storage slot.
 - Runtime Safety: Maintains the same guarantees as the Solidity version.
 - Gas Impact: One-time cost during initialization; negligible runtime overhead.
 
Why UUPS?
- Gas Efficient — Upgrades are handled within the logic contract.
 - Secure — Authorization and validation are managed in one place.
 - Standardized — Conforms to ERC-1822 and ERC-1967.
 - Flexible — Upgrade logic can include custom access control and validation.
 - Safe by Design — Uses dedicated ERC-1967 slots to prevent storage collisions.
 
How It Works
- Deploy 
Erc1967Proxywith an initial implementation and encodedset_versiondata. - Proxy delegates all calls to the implementation contract via 
delegate_call. - Implementation exposes 
upgrade_to_and_call, guarded by access control (e.g.Ownable). - Upgrades validate the new implementation using 
proxiable_uuid(). - Stylus uses a two-step pattern: 
constructoron logic deployment, thenset_versionduring proxy setup. 
Implementing a UUPS Contract
Minimal example with Ownable, UUPSUpgradeable, and Erc20 logic:
#[entrypoint]
#[storage]
struct MyUUPSContract {
    erc20: Erc20,
    ownable: Ownable,
    uups: UUPSUpgradeable,
}
#[public]
#[implements(IErc20<Error = erc20::Error>, IUUPSUpgradeable, IErc1822Proxiable, IOwnable)]
impl MyUUPSContract {
    // Accepting owner here only to enable invoking functions directly on the
    // UUPS
    #[constructor]
    fn constructor(&mut self, owner: Address) -> Result<(), Error> {
        self.uups.constructor();
        self.ownable.constructor(owner)?;
        Ok(())
    }
    fn mint(&mut self, to: Address, value: U256) -> Result<(), erc20::Error> {
        self.erc20._mint(to, value)
    }
    /// Initializes the contract.
    fn initialize(&mut self, owner: Address) -> Result<(), Error> {
        self.uups.set_version()?;
        self.ownable.constructor(owner)?;
        Ok(())
    }
    fn set_version(&mut self) -> Result<(), Error> {
        Ok(self.uups.set_version()?)
    }
    fn get_version(&self) -> U32 {
        self.uups.get_version()
    }
}
#[public]
impl IUUPSUpgradeable for MyUUPSContract {
    #[selector(name = "UPGRADE_INTERFACE_VERSION")]
    fn upgrade_interface_version(&self) -> String {
        self.uups.upgrade_interface_version()
    }
    #[payable]
    fn upgrade_to_and_call(
        &mut self,
        new_implementation: Address,
        data: Bytes,
    ) -> Result<(), Vec<u8>> {
        // Make sure to provide upgrade authorization in your implementation
        // contract.
        self.ownable.only_owner()?;
        self.uups.upgrade_to_and_call(new_implementation, data)?;
        Ok(())
    }
}
#[public]
impl IErc1822Proxiable for MyUUPSContract {
    #[selector(name = "proxiableUUID")]
    fn proxiable_uuid(&self) -> Result<B256, Vec<u8>> {
        self.uups.proxiable_uuid()
    }
}Implementing the Proxy
A simple UUPS-compatible proxy using ERC-1967:
#[entrypoint]
#[storage]
struct MyUUPSProxy {
    proxy: Erc1967Proxy,
}
#[public]
impl MyUUPSProxy {
    #[constructor]
    fn constructor(&mut self, implementation: Address, data: Bytes) -> Result<(), erc1967::utils::Error> {
        self.proxy.constructor(implementation, &data)
    }
    fn implementation(&self) -> Result<Address, Vec<u8>> {
        self.proxy.implementation()
    }
    #[fallback]
    fn fallback(&mut self, calldata: &[u8]) -> ArbResult {
        unsafe { self.proxy.do_fallback(calldata) }
    }
}
unsafe impl IProxy for MyUUPSProxy {
    fn implementation(&self) -> Result<Address, Vec<u8>> {
        self.proxy.implementation()
    }
}Upgrade Safety
1. Access Control
Upgrades must be restricted to trusted accounts, e.g. via only_owner:
self.ownable.only_owner()?;2. Proxy Context Enforcement
Ensures upgrade calls come from a delegate call:
self.uups.only_proxy()?; // Reverts if not called via proxyExplanation:
only_proxy() checks that execution is delegated (not direct), the caller is an ERC-1967 proxy (implementation slot is non-zero), and the proxy-stored version equals the logic’s VERSION_NUMBER.
3. Proxiable UUID Validation
Guarantees compatibility with UUPS:
self.uups.proxiable_uuid()? == IMPLEMENTATION_SLOT;Initialization
The UUPS proxy supports initialization data that is delegated to the implementation on deployment. This is typically used to invoke set_version first, and optionally invoke your own initialization routines (e.g., ownership or token supply setup) if needed.
let data = IMyContract::setVersionCall {}.abi_encode();
MyUUPSProxy::deploy(implementation_addr, data.into());⚠️ Initialization Must Be Explicit (Your Contract State)
If your contract needs additional initialization beyond set_version() (e.g., ownership, token supply), expose a properly designed initialization function and protect it appropriately (e.g., single-use guard or access control). Failing to do so can lead to:
- Orphaned contracts with no owner.
 - Uninitialized token supply or core state.
 - Denial of future upgrades if your own guards are misused.
 
/// Optional contract initialization (example).
fn init_contract_state(&mut self, owner: Address) -> Result<(), Vec<u8>> {
    self.ownable.constructor(owner)?;
    /// other initialization logic.
    self.uups.set_version()?;
    Ok(())
}If you expose additional initialization functions, ensure they are protected from re-execution after the proxy is live.
Initializing the Proxy
Initialization data is typically a call to the implementation’s set_version function:
let data = IMyContract::setVersionCall {}.abi_encode();
MyUUPSProxy::deploy(implementation_addr, data.into());This setup call is run via delegate_call during proxy deployment.
Security Best Practices
- Restrict upgrade access (e.g. 
only_owner). - Validate all upgrade targets.
 - Test upgrades across versions.
 - Monitor upgrade events (
Upgraded). - Use empty data unless initialization is needed.
 - Ensure new implementations return the correct 
proxiable UUID. - Enforce proxy context checks — 
only_proxy()ensures upgrades cannot be called directly on the implementation. 
Common Pitfalls
- Forgetting access control.
 - Direct calls to upgrade logic (not via proxy).
 - Missing 
proxiable UUIDvalidation. - Changing storage layout without planning.
 - Sending ETH to constructor without data (will revert).
 - The 
VERSION_NUMBERis not increased to the higher value. 
Use Cases
- Upgradeable tokens standards (e.g. ERC-20, ERC-721, ERC-1155).
 - Modular DeFi protocols.
 - DAO frameworks.
 - NFT marketplaces.
 - Access control registries.
 - Cross-chain bridges.