代理模式
代理合约是智能合约开发中的一个基本模式,它允许你分离合约的存储和逻辑。这实现了强大的功能,如可升级性、Gas 优化和代码重用。
OpenZeppelin Stylus Contracts 提供了 IProxy
trait,它使用 Stylus delegate_call
函数实现了一个低级别的代理模式。这允许你将所有调用委托给另一个合约,同时保持相同的存储上下文。
理解代理模式
一个代理合约充当实现合约的“包装器”。当用户与代理交互时:
-
代理接收调用。
-
它使用
delegate_call
将调用委托给实现合约。 -
实现执行代理存储上下文中的逻辑。
-
结果返回给用户。
这个模式提供了几个好处:
-
可升级性:你可以在保持相同代理地址的同时更改实现。
-
Gas 效率:多个代理可以共享相同的实现代码。
-
存储分离:逻辑和存储被清晰地分离。
IProxy Trait
IProxy
trait 提供了实现代理模式的核心功能:
use openzeppelin_stylus::proxy::IProxy;
use stylus_sdk::prelude::*;
pub unsafe trait IProxy: TopLevelStorage + Sized {
/// Delegates the current call to a specific implementation
/// 将当前调用委托给特定的实现
fn delegate(
&mut self,
implementation: Address,
calldata: &[u8],
) -> Result<Vec<u8>, Error>;
/// Returns the address of the implementation contract
/// 返回实现合约的地址
fn implementation(&self) -> Result<Address, Vec<u8>>;
/// Fallback function that delegates calls to the implementation
/// 将调用委托给实现的 Fallback 函数
fn do_fallback(&mut self, calldata: &[u8]) -> Result<Vec<u8>, Vec<u8>>;
}
基本代理实现
这是一个如何实现基本代理合约的最小示例:
use openzeppelin_stylus::proxy::IProxy;
use stylus_sdk::{
alloy_primitives::Address,
prelude::*,
storage::StorageAddress,
ArbResult,
};
#[entrypoint]
#[storage]
struct MyProxy {
implementation: StorageAddress,
}
#[public]
impl MyProxy {
#[constructor]
fn constructor(&mut self, implementation: Address) {
self.implementation.set(implementation);
}
/// Fallback function that delegates all calls to the implementation
/// 将所有调用委托给实现的 Fallback 函数
#[fallback]
fn fallback(&mut self, calldata: &[u8]) -> ArbResult {
unsafe { self.do_fallback(calldata) }
}
}
impl IProxy for MyProxy {
fn implementation(&self) -> Result<Address, Vec<u8>> {
Ok(self.implementation.get())
}
}
这是工作代理所需的最小实现。IProxy
trait 提供了处理委托逻辑的 do_fallback
方法。
具有管理控制的增强代理
对于生产用途,你通常需要添加管理控制来升级实现:
use openzeppelin_stylus::proxy::IProxy;
use stylus_sdk::{
alloy_primitives::Address,
prelude::*,
storage::{StorageAddress, StorageBool},
ArbResult,
};
#[entrypoint]
#[storage]
struct MyUpgradeableProxy {
implementation: StorageAddress,
admin: StorageAddress,
}
#[public]
impl MyUpgradeableProxy {
#[constructor]
fn constructor(&mut self, implementation: Address, admin: Address) {
self.implementation.set(implementation);
self.admin.set(admin);
}
/// Admin function to update the implementation
/// 用于更新实现的 Admin 函数
fn upgrade_implementation(&mut self, new_implementation: Address) -> Result<(), Vec<u8>> {
// Only admin can upgrade
// 只有 admin 可以升级
if self.admin.get() != msg::sender() {
return Err("Only admin can upgrade".abi_encode());
}
self.implementation.set(new_implementation);
Ok(())
}
/// Admin function to transfer admin rights
/// 用于转移 admin 权限的 Admin 函数
fn transfer_admin(&mut self, new_admin: Address) -> Result<(), Vec<u8>> {
if self.admin.get() != msg::sender() {
return Err("Only admin can transfer admin".abi_encode());
}
self.admin.set(new_admin);
Ok(())
}
/// Fallback function that delegates all calls to the implementation
/// 将所有调用委托给实现的 Fallback 函数
#[fallback]
fn fallback(&mut self, calldata: &[u8]) -> ArbResult {
self.do_fallback(calldata)
}
}
impl IProxy for MyUpgradeableProxy {
fn implementation(&self) -> Result<Address, Vec<u8>> {
let impl_addr = self.implementation.get();
if impl_addr == Address::ZERO {
return Err("Implementation not set".abi_encode());
}
Ok(impl_addr)
}
}
实现合约
实现合约包含实际的业务逻辑。这是一个 ERC-20 实现示例:
#[entrypoint]
#[storage]
struct MyToken {
// ⚠️ The storage layout here must match the proxy's storage layout exactly.
// ⚠️ 这里的存储布局必须与代理的存储布局完全匹配。
// For example, if the proxy defines implementation and admin addresses,
// 例如,如果代理定义了实现和 admin 地址,
// the implementation must define them in the same order and type.
// 则实现必须以相同的顺序和类型定义它们。
// This prevents storage collisions when using delegatecall.
// 这可以防止在使用 delegatecall 时发生存储冲突。
implementation: StorageAddress,
admin: StorageAddress,
// Now you can set the actual implementation-specific state fields.
// 现在你可以设置实际的特定于实现的状态字段。
erc20: Erc20,
}
#[public]
#[implements(IErc20<Error = erc20::Error>)]
impl MyToken {
#[constructor]
fn constructor(&mut self, name: String, symbol: String) {
// Initialize the ERC-20 with metadata
// 使用元数据初始化 ERC-20
self.erc20.constructor(name, symbol);
}
/// Mint tokens to a specific address (only for demonstration)
/// 将 Token 铸造到特定地址(仅用于演示)
fn mint(&mut self, to: Address, amount: U256) -> Result<(), erc20::Error> {
self.erc20._mint(to, amount)
}
}
#[public]
impl IErc20 for MyToken {
// ...
}
高级代理功能
直接委托
你还可以使用 IProxy
trait 中的 delegate
方法将调用直接委托给不同的实现:
impl MyProxy {
/// Delegate to a specific implementation (useful for testing or special cases)
/// 委托给特定的实现(对于测试或特殊情况很有用)
fn delegate_to_implementation(
&mut self,
target_implementation: Address,
calldata: &[u8],
) -> Result<Vec<u8>, Vec<u8>> {
Ok(IProxy::delegate(self, target_implementation, calldata)?)
}
}
存储布局注意事项
当使用像上面的 MyUpgradeableProxy
示例这样的代理模式时,理解存储在底层是如何实际结构化的至关重要。即使实现合约包含业务逻辑,所有状态都存储在代理合约本身中。这意味着必须仔细设计代理的存储布局,以匹配实现所期望的内容。
例如,在 MyUpgradeableProxy
示例中,代理结构包含诸如 implementation
和 admin
之类的字段用于代理管理,但它还需要为实现合约将使用的所有状态变量(例如 Token 余额、授权等)保留空间。这确保了当通过 delegate_call
执行实现逻辑时,它与正确的存储槽进行交互。
以下是代理的存储结构在实践中可能在底层是什么样的:
#[storage]
struct MyUpgradeableProxy {
// Proxy-specific storage
// 代理特定的存储
implementation: StorageAddress,
admin: StorageAddress,
// Implementation storage (shared with the implementation contract)
// 实现存储(与实现合约共享)
// These fields must exactly match the implementation contract's storage layout
// 这些字段必须与实现合约的存储布局完全匹配
// They are automatically initialized to default values (0, empty mappings, etc.)
// 它们会自动初始化为默认值(0、空映射等)
balances: StorageMapping<Address, U256>,
allowances: StorageMapping<(Address, Address), U256>,
total_supply: StorageU256,
// ... any additional state used by the implementation
// ... 实现使用的任何其他状态
}
最佳实践
-
始终验证实现地址:检查实现是否不是零地址并且是有效的合约。
-
使用适当的访问控制:实现管理功能以控制谁可以升级实现。
-
彻底测试:代理模式可能很复杂,因此全面的测试至关重要。
-
考虑升级安全性:确保实现之间的存储布局兼容。
-
记录存储布局:清楚地记录存储布局以防止将来发生冲突。
-
使用事件:在升级实现时发出事件以提高透明度。
常见陷阱
-
存储冲突:确保代理和实现存储不冲突。
-
缺少验证:始终验证实现地址。
-
不正确的 delegatecall 用法:代理必须使用
delegate_call
,而不是call
。 -
忘记实现 IProxy:必须实现该 trait 才能使回退工作。
工作示例
一个基本代理模式的完整工作示例可以在 examples/proxy/
存储库中找到。此示例演示了:
-
使用
IProxy
的最小代理实现。 -
与 ERC-20 Token 合约集成。
-
全面的测试覆盖率。
-
正确的错误处理。
相关模式
-
ERC-1967 代理:一种具有特定存储槽的标准化代理模式。
-
信标代理:多个代理指向单个信标合约,以大规模升级实现合约地址。