该文章详细介绍了如何在Arbitrum上使用OpenZeppelin代理模式(如UUPS和Beacon)升级Stylus智能合约。它涵盖了Stylus特有的升级机制,包括logic_flag上下文检测、WASM合约的重新激活、存储兼容性、访问控制集成以及确保升级安全性的最佳实践。
Stylus 合约在 Arbitrum 上作为 WebAssembly (WASM) 程序与 EVM 并行运行。它们与 Solidity 合约共享相同的 state 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 |
| 初始化 | 两步:constructor 设置 logic_flag,然后通过代理 set_version() |
constructor + 通过代理 initializer |
| 重新激活 | WASM 合约必须每 365 天或 Stylus 协议升级后重新激活 | 不适用 |
现有的 Solidity 合约可以通过代理模式升级到 Stylus (Rust) 实现。#[storage] 宏以与 Solidity 相同的方式在 EVM state trie 中布局字段,因此当类型定义匹配时,存储槽会对齐。
OpenZeppelin Contracts for Stylus 提供了三种代理模式:
| 模式 | 关键类型 | 最适合 |
|---|---|---|
| UUPS | UUPSUpgradeable, IErc1822Proxiable, Erc1967Proxy |
大多数项目 — 升级逻辑在实现中,代理更轻量 |
| Beacon | BeaconProxy, UpgradeableBeacon |
多个代理共享一个实现 — 更新 beacon 会原子性地升级所有代理 |
| Basic Proxy | Erc1967Proxy, Erc1967Utils |
用于自定义代理模式的低级构建块 |
实现合约在其 #[storage] 结构中与访问控制(例如,Ownable)一起组合 UUPSUpgradeable。集成需要:
#[storage] 结构中添加 UUPSUpgradeable(和访问控制)作为字段self.uups.constructor() 并初始化访问控制initialize 调用 self.uups.set_version() — 在部署后通过代理调用IUUPSUpgradeable — upgrade_to_and_call 受访问控制保护,upgrade_interface_version 委托给 self.uupsIErc1822Proxiable — proxiable_uuid 委托给 self.uups代理合约是一个轻量级的 Erc1967Proxy,带有一个接收实现地址和初始化数据的 constructor,以及一个委托所有调用的 #[fallback] 处理程序。
使用 set_version 作为初始化调用数据来部署代理。使用 cargo stylus deploy 或部署者合约。初始化数据是 ABI-encoded 的 setVersion 调用:
let data = MyContractAbi::setVersionCall {}.abi_encode();
// 在部署时将 `data` 作为代理 constructor 的第二个参数传入。
多个 BeaconProxy 合约指向一个存储当前实现地址的 UpgradeableBeacon。更新 beacon 会在一次交易中升级所有代理。
Stylus 不支持 immutable 关键字。UUPSUpgradeable 不存储 __self = address(this),而是使用一个 logic_flag 布尔值在一个唯一的存储槽中:
logic_flag = true。delegatecall) 运行时,代理的存储不包含此标志,因此它读取为 false。only_proxy() 检查此标志,以确保升级函数只能通过代理调用,而不能直接在实现上调用。only_proxy() 还验证 ERC-1967 实现槽是非零的,并且代理存储的版本与实现的 VERSION_NUMBER 匹配。
示例: 请参阅 rust-contracts-stylus repository 的
examples/目录,了解 UUPS、Beacon 和相关模式的完整工作集成示例。
升级函数必须受到访问控制的保护。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()?; // 或任何访问控制检查
self.uups.upgrade_to_and_call(new_implementation, data)?;
Ok(())
}
常见选项:
Stylus #[storage] 字段在 EVM state trie 中的布局与 Solidity 完全相同。升级时适用相同的存储布局规则:
与 Solidity 的一个区别是:Stylus #[storage] 中的嵌套结构(例如,将 Erc20、Ownable、UUPSUpgradeable 作为字段组合)的布局是每个嵌套结构从其自己的确定性槽开始。这与 Solidity 中常规的结构体嵌套一致,但与 Solidity 基于继承的扁平布局(其中所有继承的变量共享一个单一的连续槽范围)不一致。
logic_flag 和任何仅限实现的 state。它在实现部署时运行一次。set_version() 必须通过代理(在部署期间或通过 upgrade_to_and_call)调用,才能将 VERSION_NUMBER 写入代理的存储中。set_version()。UUPS 实现强制执行三项安全检查:
upgrade_to_and_call(例如,self.ownable.only_owner())delegatecall,only_proxy() 会回滚proxiable_uuid() 必须返回 ERC-1967 实现槽,确认 UUPS 兼容性Stylus WASM 合约必须每年重新激活一次(365 天)或在任何 Stylus 协议升级后重新激活。重新激活可以使用 cargo-stylus 或 ArbWasm precompile 完成。如果合约未重新激活,它将变得不可调用。这与代理升级无关,但必须纳入维护计划。
在升级生产合约之前:
upgrade_to_and_call 升级到 V2,并验证所有现有 state 都正确读取upgrade_to_and_callVERSION_NUMBER 在新实现中已递增
- 原文链接: github.com/OpenZeppelin/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!