Stylus Upgrades
Contents
Stylus Upgrade Model
Stylus contracts run on Arbitrum as WebAssembly (WASM) programs alongside the EVM. They share the same state trie, storage model, and account system as Solidity contracts. Because of this, EVM proxy patterns work identically for Stylus — a Solidity proxy can delegate to a Stylus implementation and vice versa.
| Stylus | Solidity |
|---|
| Proxy mechanism | Same — delegatecall to implementation contract | INLINECODE1 to implementation contract |
| Storage layout |
#[storage] fields map to the same EVM slots as equivalent Solidity structs | Sequential slot allocation per Solidity rules |
|
EIP standards | ERC-1967 storage slots, ERC-1822 proxiable UUID | Same |
|
Context detection |
logic_flag boolean in a unique storage slot (no
immutable support) |
address(this) stored as
immutable |
|
Initialization | Two-step: constructor sets
logic_flag, then
set_version() via proxy | Constructor + initializer via proxy |
|
Reactivation | WASM contracts must be reactivated every 365 days or after a Stylus protocol upgrade | Not applicable |
Existing Solidity contracts can upgrade to a Stylus (Rust) implementation via proxy patterns. The #[storage] macro lays out fields in the EVM state trie identically to Solidity, so storage slots line up when type definitions match.
Proxy Patterns
OpenZeppelin Contracts for Stylus provides three proxy patterns:
| Pattern | Key types | Best for |
|---|
| UUPS | INLINECODE10 , IErc1822Proxiable, INLINECODE12 | Most projects — upgrade logic in the implementation, lighter proxy |
| Beacon |
BeaconProxy,
UpgradeableBeacon | Multiple proxies sharing one implementation — updating the beacon upgrades all proxies atomically |
|
Basic Proxy |
Erc1967Proxy,
Erc1967Utils | Low-level building block for custom proxy patterns |
Note: The Transparent proxy pattern is not currently provided by OpenZeppelin Contracts for Stylus. Use UUPS instead (recommended for most projects).
UUPS
The implementation contract composes UUPSUpgradeable in its #[storage] struct alongside access control (e.g., Ownable). Integration requires:
- 1. Add
UUPSUpgradeable (and access control) as fields in the #[storage] struct - Call
self.uups.constructor() and initialize access control in the constructor - Expose
initialize calling self.uups.set_version() — invoked via proxy after deployment - Implement
IUUPSUpgradeable — upgrade_to_and_call guarded by access control, upgrade_interface_version delegating to INLINECODE28 - Implement
IErc1822Proxiable — proxiable_uuid delegating to INLINECODE31
The proxy contract is a thin Erc1967Proxy with a constructor that takes the implementation address and initialization data, and a #[fallback] handler that delegates all calls.
Deploy the proxy with set_version as the initialization call data. Use cargo stylus deploy or a deployer contract. The initialization data is the ABI-encoded setVersion call:
CODEBLOCK0
Beacon
Multiple BeaconProxy contracts point to a single UpgradeableBeacon that stores the current implementation address. Updating the beacon upgrades all proxies in one transaction.
Context detection (Stylus-specific)
Stylus does not support the immutable keyword. Instead of storing __self = address(this), UUPSUpgradeable uses a logic_flag boolean in a unique storage slot:
- - The implementation's constructor sets
logic_flag = true in its own storage. - When code runs via a proxy (
delegatecall), the proxy's storage does not contain this flag, so it reads as false. - INLINECODE46 checks this flag to ensure upgrade functions can only be called through the proxy, not directly on the implementation.
INLINECODE47 also verifies that the ERC-1967 implementation slot is non-zero and that the proxy-stored version matches the implementation's VERSION_NUMBER.
Examples: See the examples/ directory of the rust-contracts-stylus repository for full working integration examples of UUPS, Beacon, and related patterns.
Access Control
Upgrade functions must be guarded with access control. OpenZeppelin's Stylus contracts do not embed access control into the upgrade logic itself — you must add it in upgrade_to_and_call:
CODEBLOCK1
Common options:
- - Ownable — single owner, simplest pattern
- AccessControl / RBAC — role-based, finer granularity
- Multisig or governance — for production contracts managing significant value
Upgrade Safety
Storage compatibility
Stylus #[storage] fields are laid out in the EVM state trie identically to Solidity. The same storage layout rules apply when upgrading:
- - Never reorder, remove, or change the type of existing storage fields
- Never insert new fields before existing ones
- Only append new fields at the end of the struct
- ERC-1967 proxy storage slots are in high, standardized locations — they will not collide with implementation storage
One difference from Solidity: nested structs in Stylus #[storage] (e.g., composing Erc20, Ownable, UUPSUpgradeable as fields) are laid out with each nested struct starting at its own deterministic slot. This is consistent with regular struct nesting in Solidity, but not with Solidity's inheritance-based flat layout where all inherited variables share a single sequential slot range.
Initialization safety
- - The implementation constructor sets
logic_flag and any implementation-only state. It runs once at implementation deployment. - INLINECODE57 must be called via the proxy (during deployment or via
upgrade_to_and_call) to write the VERSION_NUMBER into the proxy's storage. - If additional initialization is needed (ownership, token supply), expose a protected initialization function and include
set_version() in it. - Failing to initialize properly can result in orphaned contracts with no owner, uninitialized state, or denied future upgrades.
Front-running warning: Always pass initialization calldata as part of the proxy constructor to ensure deployment and initialization are atomic (single transaction). Never deploy a proxy and initialize in a separate transaction — an attacker can front-run the initialization call, potentially setting themselves as owner or corrupting initial state. The initialization function should include a guard to prevent re-initialization:
> // Re-initialization guard pattern
> fn initialize(&mut self, owner: Address) -> Result<(), Vec<u8>> {
> if self.initialized.get() {
> return Err(b"already initialized".to_vec());
> }
> self.initialized.set(true);
> self.uups.set_version();
> self.ownable.init(owner)?;
> Ok(())
> }
>
Without such a guard, the initialization function can be called multiple times, allowing an attacker to re-initialize the contract and seize ownership.
UUPS upgrade checks
The UUPS implementation enforces three safety checks:
- 1. Access control — restrict
upgrade_to_and_call (e.g., self.ownable.only_owner()) - Proxy context enforcement —
only_proxy() reverts if the call is not via INLINECODE64 - Proxiable UUID validation —
proxiable_uuid() must return the ERC-1967 implementation slot, confirming UUPS compatibility
Reactivation
Stylus WASM contracts must be reactivated once per year (365 days) or after any Stylus protocol upgrade. Reactivation can be done using cargo-stylus or the ArbWasm precompile. If a contract is not reactivated, it becomes uncallable. This is orthogonal to proxy upgrades but must be factored into maintenance planning.
Testing upgrade paths
Before upgrading a production contract:
- - [ ] Deploy V1 implementation and proxy on a local Arbitrum devnet
- [ ] Write state with V1, upgrade to V2 via
upgrade_to_and_call, and verify that all existing state reads correctly - [ ] Verify new functionality works as expected after the upgrade
- [ ] Confirm access control — only authorized callers can invoke INLINECODE69
- [ ] Check storage layout — ensure no reordering, removal, or type changes to existing fields
- [ ] Verify
VERSION_NUMBER is incremented in the new implementation - [ ] Test reactivation — ensure the upgraded contract can be reactivated
- [ ] Manual review — there is no automated storage layout validation for Stylus Rust contracts; rely on struct comparison and devnet testing
Stylus 升级
目录
Stylus 升级模型
Stylus 合约在 Arbitrum 上作为 WebAssembly (WASM) 程序与 EVM 并行运行。它们与 Solidity 合约共享相同的状态树、存储模型和账户系统。因此,EVM 代理模式对 Stylus 完全适用——Solidity 代理可以委托给 Stylus 实现,反之亦然。
| Stylus | Solidity |
|---|
| 代理机制 | 相同——通过 delegatecall 调用实现合约 | 通过 delegatecall 调用实现合约 |
| 存储布局 |
#[storage] 字段映射到与等效 Solidity 结构体相同的 EVM 存储槽 | 按 Solidity 规则顺序分配存储槽 |
|
EIP 标准 | ERC-1967 存储槽、ERC-1822 可代理 UUID | 相同 |
|
上下文检测 | 在唯一存储槽中使用 logic_flag 布尔值(不支持 immutable) | 将 address(this) 存储为 immutable |
|
初始化 | 两步:构造函数设置 logic
flag,然后通过代理调用 setversion() | 通过代理执行构造函数 + 初始化函数 |
|
重新激活 | WASM 合约必须每 365 天或在 Stylus 协议升级后重新激活 | 不适用 |
现有的 Solidity 合约可以通过代理模式升级到 Stylus (Rust) 实现。#[storage] 宏在 EVM 状态树中布局字段的方式与 Solidity 完全相同,因此当类型定义匹配时,存储槽会保持一致。
代理模式
OpenZeppelin Stylus 合约提供三种代理模式:
| 模式 | 关键类型 | 最佳用途 |
|---|
| UUPS | UUPSUpgradeable、IErc1822Proxiable、Erc1967Proxy | 大多数项目——升级逻辑在实现中,代理更轻量 |
| Beacon |
BeaconProxy、UpgradeableBeacon | 多个代理共享一个实现——更新 Beacon 可原子化升级所有代理 |
|
基础代理 | Erc1967Proxy、Erc1967Utils | 自定义代理模式的底层构建块 |
注意: OpenZeppelin Stylus 合约目前不提供透明代理模式。请使用 UUPS(推荐用于大多数项目)。
UUPS
实现合约在其 #[storage] 结构体中组合 UUPSUpgradeable 和访问控制(例如 Ownable)。集成需要:
- 1. 在 #[storage] 结构体中添加 UUPSUpgradeable(和访问控制)作为字段
- 在构造函数中调用 self.uups.constructor() 并初始化访问控制
- 暴露 initialize 函数,调用 self.uups.setversion()——部署后通过代理调用
- 实现 IUUPSUpgradeable——upgradetoandcall 受访问控制保护,upgradeinterfaceversion 委托给 self.uups
- 实现 IErc1822Proxiable——proxiable_uuid 委托给 self.uups
代理合约是一个轻量的 Erc1967Proxy,其构造函数接受实现地址和初始化数据,并包含一个将所有调用委托出去的 #[fallback] 处理器。
使用 set_version 作为初始化调用数据部署代理。使用 cargo stylus deploy 或部署合约。初始化数据是 ABI 编码的 setVersion 调用:
rust
let data = MyContractAbi::setVersionCall {}.abi_encode();
// 在部署时将 data 作为代理构造函数的第二个参数传入。
Beacon
多个 BeaconProxy 合约指向一个存储当前实现地址的 UpgradeableBeacon。更新 Beacon 可在一次交易中升级所有代理。
上下文检测(Stylus 特有)
Stylus 不支持 immutable 关键字。UUPSUpgradeable 不使用 self = address(this),而是在唯一存储槽中使用 logic_flag 布尔值:
- - 实现的构造函数在其自身存储中设置 logicflag = true。
- 当代码通过代理运行时(delegatecall),代理的存储中不包含此标志,因此读取为 false。
- onlyproxy() 检查此标志,确保升级函数只能通过代理调用,而不能直接在实现上调用。
onlyproxy() 还会验证 ERC-1967 实现槽非零,并且代理存储的版本与实现的 VERSIONNUMBER 匹配。
示例: 请参阅 rust-contracts-stylus 仓库 的 examples/ 目录,获取 UUPS、Beacon 及相关模式的完整工作集成示例。
访问控制
升级函数必须受访问控制保护。OpenZeppelin 的 Stylus 合约未将访问控制嵌入升级逻辑本身——您必须在 upgradetoand_call 中添加:
rust
fn upgradetoandcall(&mut self, newimplementation: Address, data: Bytes) -> Result<(), Vec> {
self.ownable.only_owner()?; // 或任何访问控制检查
self.uups.upgradetoandcall(newimplementation, data)?;
Ok(())
}
常见选项:
- - Ownable——单一所有者,最简单的模式
- AccessControl / RBAC——基于角色,更细粒度
- 多签或治理——用于管理重要价值的生产合约
升级安全
存储兼容性
Stylus #[storage] 字段在 EVM 状态树中的布局方式与 Solidity 完全相同。升级时适用相同的存储布局规则:
- - 切勿重新排序、删除或更改现有存储字段的类型
- 切勿在现有字段之前插入新字段
- 只能在结构体末尾追加新字段
- ERC-1967 代理存储槽位于高位、标准化的位置——不会与实现存储冲突
与 Solidity 的一个区别:Stylus #[storage] 中的嵌套结构体(例如,将 Erc20、Ownable、UUPSUpgradeable 组合为字段)的布局中,每个嵌套结构体从其自身的确定性存储槽开始。这与 Solidity 中常规结构体嵌套一致,但与 Solidity 基于继承的扁平布局不同,后者所有继承变量共享一个连续的存储槽范围。
初始化安全
- - 实现构造函数设置 logicflag 和任何仅实现状态。它在实现部署时运行一次。
- setversion() 必须通过代理调用(在部署期间或通过 upgradetoandcall),以将 VERSIONNUMBER 写入代理的存储。
- 如果需要额外初始化(所有权、代币供应),请暴露受保护的初始化函数并将 set_version() 包含在其中。
- 初始化不当可能导致合约成为孤儿,没有所有者、状态未初始化或无法进行未来升级。
抢跑警告: 始终将初始化调用数据作为代理构造函数的一部分传递,以确保部署和初始化是原子化的(单笔交易)。切勿在单独的交易中部署代理和初始化——攻击者可以抢跑初始化调用,可能将自己设置为所有者或破坏初始状态。初始化函数应包含防止重新初始化的防护:
rust
// 重新初始化防护模式
fn initialize(&mut self, owner: Address) -> Result<(), Vec> {
if self.initialized.get() {
return Err(balready initialized.to_vec());
}
self.initialized.set(true);
self.uups.set_version();
self.ownable.init(owner)?;
Ok(())
}
如果没有这样的防护,初始化函数可以被多次调用,允许攻击者重新初始化合约并夺取所有权。
UUPS 升级检查
UUPS 实现强制执行三项安全检查:
- 1. 访问控制——限制 upgradetoandcall(例如 self.ownable.onlyowner())
- 代理上下文强制——如果调用不是通过 delegatecall 进行的,onlyproxy() 会回退
- 可代理 UUID 验证——proxiableuuid() 必须返回 ERC-1967 实现槽,确认 UUPS 兼容性
重新激活
Stylus WASM 合约必须每年重新激活一次(365 天)或在任何 Stylus 协议升级后重新激活。可以使用 cargo-stylus 或 ArbWasm 预编译进行重新激活。如果合约未重新激活,它将变得不可调用。这与代理升级正交,但必须纳入维护计划。
测试升级路径
在升级生产合约