代理模式

代理合约是智能合约开发中的一个基本模式,它允许你分离合约的存储和逻辑。这实现了强大的功能,如可升级性、Gas 优化和代码重用。

OpenZeppelin Stylus Contracts 提供了 IProxy trait,它使用 Stylus delegate_call 函数实现了一个低级别的代理模式。这允许你将所有调用委托给另一个合约,同时保持相同的存储上下文。

理解代理模式

一个代理合约充当实现合约的“包装器”。当用户与代理交互时:

  1. 代理接收调用。

  2. 它使用 delegate_call 将调用委托给实现合约。

  3. 实现执行代理存储上下文中的逻辑。

  4. 结果返回给用户。

这个模式提供了几个好处:

  • 可升级性:你可以在保持相同代理地址的同时更改实现。

  • 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 示例中,代理结构包含诸如 implementationadmin 之类的字段用于代理管理,但它还需要为实现合约将使用的所有状态变量(例如 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
    // ... 实现使用的任何其他状态
}

关于存储初始化的重要说明

代理中的实现存储字段不需要显式设置 - 它们在部署代理合约时会自动初始化为其默认值。

通过以这种方式构建代理的存储,你可以确保代理和实现合约始终在每个数据存储位置方面保持同步,从而防止存储冲突和升级问题。

最佳实践

  1. 始终验证实现地址:检查实现是否不是零地址并且是有效的合约。

  2. 使用适当的访问控制:实现管理功能以控制谁可以升级实现。

  3. 彻底测试:代理模式可能很复杂,因此全面的测试至关重要。

  4. 考虑升级安全性:确保实现之间的存储布局兼容。

  5. 记录存储布局:清楚地记录存储布局以防止将来发生冲突。

  6. 使用事件:在升级实现时发出事件以提高透明度。

常见陷阱

  • 存储冲突:确保代理和实现存储不冲突。

  • 缺少验证:始终验证实现地址。

  • 不正确的 delegatecall 用法:代理必须使用 delegate_call,而不是 call

  • 忘记实现 IProxy:必须实现该 trait 才能使回退工作。

工作示例

一个基本代理模式的完整工作示例可以在 examples/proxy/ 存储库中找到。此示例演示了:

  • 使用 IProxy 的最小代理实现。

  • 与 ERC-20 Token 合约集成。

  • 全面的测试覆盖率。

  • 正确的错误处理。

相关模式

  • ERC-1967 代理:一种具有特定存储槽的标准化代理模式。

  • 信标代理:多个代理指向单个信标合约,以大规模升级实现合约地址。