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 通过标准化槽提供存储布局安全:
实现存储
你的实现合约可以使用任何存储布局,而无需担心冲突:
#[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
// ... 任何其他存储字段
}
最佳实践
-
始终验证地址: ERC-1967 自动验证实现地址和信标地址是否有代码。
-
使用适当的访问控制: 为升级函数实现管理控制。
-
彻底测试升级: 确保新的实现与现有的存储兼容。
-
发出事件: ERC-1967 事件会自动发出,提供透明度。
-
小心处理初始化数据: 仅在提供初始化数据时发送 value。
-
记录存储布局: 即使 ERC-1967 阻止冲突,也要记录实现的存储。
-
使用标准化的槽: 不要在实现中覆盖 ERC-1967 存储槽。