ERC-1967 代理

ERC-1967 是一个标准化的代理模式,它为代理合约定义了特定的存储槽,以防止代理合约和实现合约之间的存储冲突。该标准确保代理合约可以安全地升级,而不会发生冲突。

OpenZeppelin Stylus Contracts 提供了 ERC-1967 标准的完整实现,包括用于管理标准化存储槽的 Erc1967Proxy 合约和 Erc1967Utils 库。

理解 ERC-1967

ERC-1967 为代理合约定义了特定的存储槽:

  • 实现槽 (Implementation Slot): 存储当前实现合约的地址。

  • 管理槽 (Admin Slot): 存储可以升级代理的管理账户的地址。

  • 信标槽 (Beacon Slot): 存储信标合约的地址(用于信标代理)。

这些槽使用特定的 keccak-256 哈希值计算,以确保它们与典型的存储布局不冲突。

存储槽计算

存储槽的计算方法如下:

// 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc_U256
const IMPLEMENTATION_SLOT: U256 = {
    const HASH: [u8; 32] = keccak_const::Keccak256::new()
        .update(b"eip1967.proxy.implementation")
        .finalize();
    U256::from_be_bytes(HASH).wrapping_sub(uint!(1_U256))
};

// 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103_U256
const ADMIN_SLOT: U256 = {
    const HASH: [u8; 32] = keccak_const::Keccak256::new()
        .update(b"eip1967.proxy.admin")
        .finalize();
    U256::from_be_bytes(HASH).wrapping_sub(uint!(1_U256))
};

// 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50_U256
const BEACON_SLOT: U256 = {
    const HASH: [u8; 32] = keccak_const::Keccak256::new()
        .update(b"eip1967.proxy.beacon")
        .finalize();
    U256::from_be_bytes(HASH).wrapping_sub(uint!(1_U256))
};

基本的 ERC-1967 代理实现

以下是如何实现基本的 ERC-1967 代理:

use openzeppelin_stylus::proxy::{
    erc1967::{self, Erc1967Proxy},
    IProxy,
};
use stylus_sdk::{
    abi::Bytes,
    alloy_primitives::Address,
    prelude::*,
    ArbResult,
};

#[entrypoint]
#[storage]
struct MyErc1967Proxy {
    erc1967: Erc1967Proxy,
}

#[public]
impl MyErc1967Proxy {
    #[constructor]
    fn constructor(
        &mut self,
        implementation: Address,
        data: Bytes,
    ) -> Result<(), erc1967::utils::Error> {
        self.erc1967.constructor(implementation, &data)
    }

    /// Get the current implementation address
    /// 获取当前的实现地址
    fn implementation(&self) -> Result<Address, Vec<u8>> {
        self.erc1967.implementation()
    }

    /// Fallback function that delegates all calls to the implementation
    /// 将所有调用委托给实现的 Fallback 函数
    #[fallback]
    fn fallback(&mut self, calldata: &[u8]) -> ArbResult {
        unsafe { self.erc1967.do_fallback(calldata) }
    }
}

ERC-1967 Utils 库

Erc1967Utils 库提供了用于管理 ERC-1967 存储槽的辅助函数:

获取值

use openzeppelin_stylus::proxy::erc1967::utils::Erc1967Utils;

// Get the current implementation address
// 获取当前的实现地址
let implementation = Erc1967Utils::get_implementation();

// Get the current admin address
// 获取当前的管理员地址
let admin = Erc1967Utils::get_admin();

// Get the current beacon address
// 获取当前的信标地址
let beacon = Erc1967Utils::get_beacon();

升级实现

use openzeppelin_stylus::proxy::erc1967::utils::Erc1967Utils;
use stylus_sdk::{abi::Bytes, prelude::*};

impl MyErc1967Proxy {
    /// Upgrade to a new implementation
    /// 升级到新的实现
    fn upgrade_implementation(
        &mut self,
        new_implementation: Address,
        data: Bytes,
    ) -> Result<(), erc1967::utils::Error> {
        Erc1967Utils::upgrade_to_and_call(self, new_implementation, &data)
    }
}

更改管理员

use openzeppelin_stylus::proxy::erc1967::utils::Erc1967Utils;

impl MyErc1967Proxy {
    /// Change the admin address
    /// 更改管理员地址
    fn change_admin(&mut self, new_admin: Address) -> Result<(), erc1967::utils::Error> {
        Erc1967Utils::change_admin(new_admin)
    }
}

升级信标

use openzeppelin_stylus::proxy::erc1967::utils::Erc1967Utils;
use stylus_sdk::{abi::Bytes, prelude::*};

impl MyErc1967Proxy {
    /// Upgrade to a new beacon
    /// 升级到新的信标
    fn upgrade_beacon(
        &mut self,
        new_beacon: Address,
        data: Bytes,
    ) -> Result<(), erc1967::utils::Error> {
        Erc1967Utils::upgrade_beacon_to_and_call(self, new_beacon, &data)
    }
}

构造函数数据

ERC-1967 代理构造函数接受可选的初始化数据:

impl MyErc1967Proxy {
    #[constructor]
    fn constructor(
        &mut self,
        implementation: Address,
        data: Bytes,
    ) -> Result<(), erc1967::utils::Error> {
        // If data is provided, it will be passed to the implementation
        // during construction via delegatecall
        // 如果提供了数据,它将在构造期间通过 delegatecall 传递给实现
        self.erc1967.constructor(implementation, &data)
    }
}

data 参数可用于:

  • 初始化存储: 传递编码的函数调用以设置初始状态。

  • Mint 初始代币: 在代币合约上调用 mint 函数。

  • 设置权限: 配置初始访问控制设置。

  • 空数据: 如果不需要初始化,则传递空字节。

示例: 使用数据初始化

use alloy_sol_macro::sol;
use alloy_sol_types::SolCall;

sol! {
    interface IERC20 {
        function mint(address to, uint256 amount) external;
    }
}

// In your deployment script or test
// 在你的部署脚本或测试中
let implementation = deploy_implementation();
let initial_owner = alice;
let initial_supply = U256::from(1000000);

// Encode the mint call
// 编码 mint 调用
let mint_data = IERC20::mintCall {
    to: initial_owner,
    amount: initial_supply,
}.abi_encode();

// Deploy proxy with initialization data
// 使用初始化数据部署代理
let proxy = MyErc1967Proxy::deploy(
    implementation,
    mint_data.into(),
).expect("Failed to deploy proxy"); // 部署代理失败

存储布局安全

ERC-1967 通过标准化槽提供存储布局安全:

优势

  • 无存储冲突: 实现存储不能与代理存储冲突。

  • 可预测的布局: 存储槽是标准化的且有详细的文档。

  • 升级安全: 新实现可以安全地使用任何存储布局。

  • Gas 效率: 无需复杂的存储间隙模式。

实现存储

你的实现合约可以使用任何存储布局,而无需担心冲突:

#[entrypoint]
#[storage]
struct MyToken {
    // These fields are safe to use - they won't conflict with ERC-1967 slots
    // 这些字段可以安全使用 - 它们不会与 ERC-1967 槽冲突
    balances: StorageMapping<Address, U256>,
    allowances: StorageMapping<(Address, Address), U256>,
    total_supply: StorageU256,
    name: StorageString,
    symbol: StorageString,
    decimals: StorageU8,
    // ... any other storage fields
    // ... 任何其他存储字段
}

最佳实践

  1. 始终验证地址: ERC-1967 自动验证实现地址和信标地址是否有代码。

  2. 使用适当的访问控制: 为升级函数实现管理控制。

  3. 彻底测试升级: 确保新的实现与现有的存储兼容。

  4. 发出事件: ERC-1967 事件会自动发出,提供透明度。

  5. 小心处理初始化数据: 仅在提供初始化数据时发送 value。

  6. 记录存储布局: 即使 ERC-1967 阻止冲突,也要记录实现的存储。

  7. 使用标准化的槽: 不要在实现中覆盖 ERC-1967 存储槽。

常见陷阱

  • 在没有数据的情况下发送 value: ERC-1967 阻止在没有初始化数据的情况下发送 value,以避免资金卡住。

  • 无效的实现地址: 始终确保在升级之前部署实现合约。

  • 缺少访问控制: 使用适当的访问控制来保护升级函数。

  • 存储布局更改: 在新的实现中更改存储布局时要小心。

  • 不正确的初始化数据: 确保初始化数据已正确编码。

相关模式

  • 基本代理: 使用 delegate_call 的基本代理模式,用于可升级合约。

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