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 |
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 proxy
Explanation:
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.
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.