mcpField
U
upgrade-stylus-contracts

upgrade-stylus-contracts

github.com/openzeppelin开发219211 次安装2 天前更新
代码仓库

关于 upgrade-stylus-contracts

面向 Arbitrum Stylus 合约开发者的升级指引,讲解代理模式、访问控制、存储兼容性及重新激活要求。

主要功能

  1. 01介绍 Stylus 与 Solidity 的代理模式
  2. 02涵盖 UUPS、Beacon 和基础代理
  3. 03要求升级函数实施访问控制
  4. 04检查存储布局与初始化安全
  5. 05测试升级路径和合约重新激活

适用场景

  1. 01为 Stylus 合约选择代理模式
  2. 02通过代理升级 Solidity 或 Stylus 实现
  3. 03审查升级前后的存储布局
  4. 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 实现,反之亦然。

StylusSolidity
代理机制相同——对实现合约执行 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 提供三种代理模式:

模式关键类型适用场景
UUPSUUPSUpgradeable、IErc1822Proxiable、Erc1967Proxy大多数项目——将升级逻辑放在实现中,代理更轻量
BeaconBeaconProxy、UpgradeableBeacon多个代理共享同一实现——更新 Beacon 即可通过一次原子操作升级所有代理
基础代理Erc1967Proxy、Erc1967Utils用于自定义代理模式的底层构件
UUPS

实现合约会在其 #[storage] 结构体中,与访问控制(例如 Ownable)一同组合使用 UUPSUpgradeable。集成步骤如下:

  1. 在 #[storage] 结构体中将 UUPSUpgradeable(以及访问控制)添加为字段
  2. 在构造函数中调用 self.uups.constructor() 并初始化访问控制
  3. 暴露 initialize,在其中调用 self.uups.set_version()——部署后通过代理调用
  4. 实现 IUUPSUpgradeable——由访问控制保护的 upgrade_to_and_call,以及委托给 self.uups 的 upgrade_interface_version
  5. 实现 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 实现会执行三项安全检查:

  1. 访问控制 — 限制对 upgrade_to_and_call 的调用(例如,self.ownable.only_owner())
  2. 代理上下文强制检查 — 如果调用并非通过 delegatecall 进行,only_proxy() 就会回退
  3. 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 测试

评论

支持 Markdown