关于 upgrade-stylus-contracts
面向 Arbitrum Stylus 合约开发者的升级指引,讲解代理模式、访问控制、存储兼容性及重新激活要求。
主要功能
- 01介绍 Stylus 与 Solidity 的代理模式
- 02涵盖 UUPS、Beacon 和基础代理
- 03要求升级函数实施访问控制
- 04检查存储布局与初始化安全
- 05测试升级路径和合约重新激活
适用场景
- 01为 Stylus 合约选择代理模式
- 02通过代理升级 Solidity 或 Stylus 实现
- 03审查升级前后的存储布局
- 04规划合约升级测试与重新激活
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ openzeppelin/ openzeppelin-skills --skill upgrade-stylus-contracts安装后在 Claude Code 中输入/upgrade-stylus-contracts确认已加载。
详细介绍
Stylus 升级
目录
Stylus 升级模型
Stylus 合约作为 WebAssembly(WASM)程序与 EVM 一同运行在 Arbitrum 上。它们与 Solidity 合约共享相同的状态 Trie、存储模型和账户系统。因此,EVM 代理模式在 Stylus 中的工作方式完全相同——Solidity 代理可以委托调用 Stylus 实现,反之亦然。
| Stylus | Solidity | |
|---|---|---|
| 代理机制 | 相同——对实现合约执行 delegatecall | 对实现合约执行 delegatecall |
| 存储布局 | #[storage] 字段映射到与等效 Solidity 结构体相同的 EVM 槽位 | 按 Solidity 规则顺序分配槽位 |
| EIP 标准 | ERC-1967 存储槽位、ERC-1822 proxiable UUID | 相同 |
| 上下文检测 | 唯一存储槽位中的 logic_flag 布尔值(不支持 immutable) | 将 address(this) 存储为 immutable |
| 初始化 | 两步:构造函数设置 logic_flag,然后通过代理调用 set_version() | 构造函数 + 通过代理调用初始化函数 |
| 重新激活 | WASM 合约必须每 365 天或在 Stylus 协议升级后重新激活 | 不适用 |
现有 Solidity 合约可以通过代理模式升级为 Stylus(Rust)实现。#[storage] 宏会以与 Solidity 相同的方式在 EVM 状态 Trie 中布局字段,因此当类型定义匹配时,存储槽位也会对应一致。
代理模式
OpenZeppelin Contracts for Stylus 提供三种代理模式:
| 模式 | 关键类型 | 适用场景 |
|---|---|---|
| UUPS | UUPSUpgradeable、IErc1822Proxiable、Erc1967Proxy | 大多数项目——将升级逻辑放在实现中,代理更轻量 |
| Beacon | BeaconProxy、UpgradeableBeacon | 多个代理共享同一实现——更新 Beacon 即可通过一次原子操作升级所有代理 |
| 基础代理 | Erc1967Proxy、Erc1967Utils | 用于自定义代理模式的底层构件 |
UUPS
实现合约会在其 #[storage] 结构体中,与访问控制(例如 Ownable)一同组合使用 UUPSUpgradeable。集成步骤如下:
- 在
#[storage]结构体中将UUPSUpgradeable(以及访问控制)添加为字段 - 在构造函数中调用
self.uups.constructor()并初始化访问控制 - 暴露
initialize,在其中调用self.uups.set_version()——部署后通过代理调用 - 实现
IUUPSUpgradeable——由访问控制保护的upgrade_to_and_call,以及委托给self.uups的upgrade_interface_version - 实现
IErc1822Proxiable——委托给self.uups的proxiable_uuid
代理合约是一个精简的 Erc1967Proxy,包含一个接收实现地址和初始化数据的构造函数,以及一个将所有调用委托出去的 #[fallback] 处理程序。
使用 set_version 作为初始化调用数据部署代理。使用 cargo stylus deploy 或部署合约。初始化数据是 ABI 编码后的 setVersion 调用:
let data = MyContractAbi::setVersionCall {}.abi_encode();
// Pass `data` as the proxy constructor's second argument at deployment time.
Beacon
多个 BeaconProxy 合约指向一个存储当前实现地址的 UpgradeableBeacon。更新 Beacon 即可在一笔交易中升级所有代理。
上下文检测(Stylus 专用)
Stylus 不支持 immutable 关键字。UUPSUpgradeable 不会存储 __self = address(this),而是在唯一的存储槽中使用一个 logic_flag 布尔值:
- 实现合约的构造函数会在其自身存储中将
logic_flag设为true。 - 通过代理运行代码(
delegatecall)时,代理的存储中没有此标志,因此读取结果为false。 only_proxy()会检查此标志,以确保升级函数只能通过代理调用,而不能直接在实现合约上调用。
only_proxy() 还会验证 ERC-1967 实现槽非零,并确认代理中存储的版本与实现合约的 VERSION_NUMBER 相符。
示例: 如需查看 UUPS、Beacon 及相关模式的完整集成示例,请参阅 rust-contracts-stylus 仓库中的
examples/目录。
访问控制
升级函数必须受到访问控制保护。OpenZeppelin 的 Stylus 合约不会在升级逻辑本身中嵌入访问控制——你必须在 upgrade_to_and_call 中添加访问控制:
fn upgrade_to_and_call(&mut self, new_implementation: Address, data: Bytes) -> Result<(), Vec<u8>> {
self.ownable.only_owner()?; // or any access control check
self.uups.upgrade_to_and_call(new_implementation, data)?;
Ok(())
}
常见选项:
- Ownable — 单一所有者,是最简单的模式
- AccessControl / RBAC — 基于角色,可实现更细粒度的控制
- 多签或治理 — 适用于管理大量价值的生产合约
升级安全性
存储兼容性
Stylus #[storage] 字段在 EVM 状态 Trie 中的布局与 Solidity 完全相同。升级时也适用相同的存储布局规则:
- 绝不要重新排序、删除或更改现有存储字段的类型
- 绝不要在现有字段之前插入新字段
- 只在结构体末尾追加新字段
- ERC-1967 代理存储槽位于较高的标准化位置,不会与实现合约的存储发生冲突
与 Solidity 不同的一点是:Stylus #[storage] 中的嵌套结构体(例如,将 Erc20、Ownable、UUPSUpgradeable 组合作为字段)会从各自确定的槽位开始布局。这与 Solidity 中常规的结构体嵌套方式一致,但不同于 Solidity 基于继承的扁平布局;在后者中,所有继承的变量都共享一个连续的槽位范围。
初始化安全性
- 实现合约的构造函数会设置
logic_flag和仅限实现合约使用的状态。它会在实现合约部署时运行一次。 - 必须通过代理调用
set_version()(在部署期间或通过upgrade_to_and_call),以将VERSION_NUMBER写入代理的存储。 - 如果还需要其他初始化操作(所有权、代币供应量),请公开一个受保护的初始化函数,并在其中调用
set_version()。 - 初始化不当可能导致合约无主、状态未初始化,或未来升级被拒绝。
UUPS 升级检查
UUPS 实现会执行三项安全检查:
- 访问控制 — 限制对
upgrade_to_and_call的调用(例如,self.ownable.only_owner()) - 代理上下文强制检查 — 如果调用并非通过
delegatecall进行,only_proxy()就会回退 - Proxiable UUID 验证 —
proxiable_uuid()必须返回 ERC-1967 实现槽,以确认与 UUPS 兼容
重新激活
Stylus WASM 合约必须每年重新激活一次(365 天),或在 Stylus 协议升级后重新激活。可以使用 cargo-stylus 或 ArbWasm 预编译合约进行重新激活。如果合约未重新激活,就会变得无法调用。这与代理升级无关,但必须纳入维护计划。
测试升级路径
升级生产合约之前:
- 在本地 Arbitrum devnet 上部署 V1 实现和代理
- 使用 V1 写入状态,通过
upgrade_to_and_call升级到 V2,并验证所有现有状态都能正确读取 - 验证升级后新功能能否按预期运行
- 确认访问控制——只有经过授权的调用方才能调用
upgrade_to_and_call - 检查存储布局——确保现有字段未被重新排序、移除或更改类型
- 验证新实现中的
VERSION_NUMBER已递增 - 测试重新激活——确保升级后的合约可以重新激活
- 人工审查——Stylus Rust 合约没有自动化存储布局验证;请依靠结构体比较和 devnet 测试