深入浅出解读合约最小代理EIP-1167
EIP-1167是一种标准化的代理合约实现,旨在通过最小的字节码和委托调用的方式实现合约逻辑的复用。这种模式不仅节省部署和存储成本,还提供了开发者灵活的合约管理能力。
一、核心概念
EIP-1167定义了一种代理合约标准,代理合约的作用是将用户的调用委托到另一个逻辑合约(目标合约)。它通过 delegatecall 操作将调用转发给目标合约,从而实现代码复用,同时代理自身不存储逻辑,只存储状态。
代理合约的主要特点
轻量高效:代理合约的字节码极短,部署成本低。
逻辑复用:多个代理可以复用一个逻辑合约,节省存储。
工厂合约模型:支持创建交易中的克隆初始化(通过工厂合约模型)。
易于升级:通过调整代理指向的目标地址,可以实现逻辑的动态升级。
二、EIP-1167的标准字节码
代理合约的字节码模板如下:
0x363d3d373d3d3d363d73<logic_contract_address>5af43d82803e903d91602b57fd5bf3
1. 字节码结构详解
2. 生成代理字节码的Solidity代码
function createProxyBytecode(address logic) public pure returns (bytes memory) {
return abi.encodePacked(
hex"363d3d373d3d3d363d73",
logic,
hex"5af43d82803e903d91602b57fd5bf3"
);
}
三、EIP-1167的工作原理
●代理合约的部署
○ 使用上述标准字节码和目标逻辑合约地址,通过 create 部署代理合约
○ 部署后的代理只包含标准字节码,并指向逻辑合约的地址
●函数调用的转发
○ 用户调用代理合约时,代理合约通过 delegatecall 将调用转发到逻辑合约
○ delegatecall 的特点是使用调用者的上下文(代理合约的存储和余额),执行逻辑合约的代码
●逻辑合约的复用
○ 一个逻辑合约可以被多个代理合约复用,从而节约存储成本和开发成本
●可升级性
○ 如果代理合约使用了灵活的目标地址存储方式(如 EIP-1967 中的存储槽规范),可以动态改变逻辑合约地址,从而实现逻辑升级
四、EIP-1167使用场景
●多实例工厂模式:工厂合约可以快速部署多个代理合约实例,这些实例共享同一个逻辑合约
●可升级合约:可以通过设置存储中的逻辑合约地址,实现合约逻辑的动态升级
●模块化合约:通过代理合约实现模块化设计,各模块分离逻辑代码和状态存储,提高合约的可维护性
五、EIP-1167的优缺点
●优点:
○ 部署成本低:代理合约的字节码极短,部署时消耗的 Gas 显著减少
○逻辑代码复用: 多个代理合约共享一个逻辑合约,节省存储空间
○灵活性强:支持通过 delegatecall 动态转发调用,轻松实现复杂的功能
○ 升级便捷: 如果代理设计支持动态目标地址,可以实现逻辑的动态更新
●缺点
○ 调试复杂:由于状态存储在代理合约,而逻辑在目标合约中,调试时需要查看两者。
○ 无内置的升级机制:EIP-1167 本身并未提供升级机制,需要开发者自行设计目标地址的存储方案。
○ Gas 成本稍高:每次函数调用都需要额外的 delegatecall,略微增加了执行成本
六、EIP-1167实现案例
以下是一个完整的实现案例,包括逻辑合约、代理工厂合约以及交互流程。
● 逻辑合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract LogicContract {
uint256 public value;
function setValue(uint256 _value) external {
value = _value;
}
function getValue() external view returns (uint256) {
return value;
}
}
●代理工厂合约
○ 0x14: SHA3 操作码(也称为 KECCAK256)
○ 0x28: 将字节值 0x28(十进制 40)推入栈。PUSH1 是一字节操作码,用于将紧接的单字节值推入栈中
○ 0x37: CALLDATACOPY,将调用数据(calldata)从输入中复制到内存。通常用于在合约中处理调用参数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MinimalProxyFactory {
event ProxyCreated(address proxy);
function createProxy(address logic) external returns (address) {
bytes20 targetBytes = bytes20(logic);
address proxy;
assembly {
let ptr := mload(0x40) // Free memory pointer
mstore(ptr, 0x3d602d80600a3d3981f3) // Prefix
mstore(add(ptr, 0x14), targetBytes) // Logic address
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf3) // Suffix
proxy := create(0, ptr, 0x37) // Deploy proxy
}
require(proxy != address(0), "Proxy deployment failed");
emit ProxyCreated(proxy);
return proxy;
}
}
● 交互示例
○ 逻辑合约和工厂合约: 部署 LogicContract 和 MinimalProxyFactory
○ 使用工厂部署代理合约: 调用 createProxy 方法,传入逻辑合约地址
address proxy = factory.createProxy(logicAddress);
○ 与代理合约交互: 调用代理合约调用逻辑合约的方法
const proxyContract = new ethers.Contract(proxyAddress, LogicContract.abi, signer);
// 设置值
await proxyContract.setValue(42);
// 获取值
const value = await proxyContract.getValue();
console.log("Value:", value); // 42
EIP-1167 的重要性
优化资源使用:EIP-1167 提供了一种高效、标准化的代理合约实现,特别适用于需要部署大量实例的场景。
推动智能合约生态发展:EIP-1167 降低了开发者实现代理合约的门槛,是许多工厂模式和可升级合约实现的基础。
EIP-1167 是以太坊合约开发中不可或缺的工具,极大地提高了资源利用效率和开发灵活性。
七、OZ的最小代理实现源码解析
OZ 实现了EIP-1167 标准的一个库,提供了使用 create 和 create2 操作码部署最小代理合约(即 Clone)的功能,同时支持通过 create2 操作码预测代理合约地址。
1. 功能概述
clone:使用 create 操作码部署一个代理合约。
cloneDeterministic:使用 create2 操作码,结合 salt 部署代理合约,部署地址是可预测的。
predictDeterministicAddress:预测由 create2 部署的代理合约地址。
2. 合约函数解析
2.1 clone 函数
function clone(address implementation) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// 将 implementation 地址压缩存储在 0x00 和 0x20 内存位置,并拼接代理合约字节码。
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
// 使用 `create` 操作码部署代理合约。
instance := create(0, 0x09, 0x37)
}
// 如果部署失败,抛出错误。
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
作用
○ 使用 create 部署一个最小代理合约。
○ 代理合约将所有调用转发到指定的 implementation 地址。
部署的字节码
○ 使用 create 操作码动态部署代理合约。
○ 代理合约字节码包括:
◆前置字节码:0x3d602d80600a3d3981f3,用来设置代理合约环境。
◆ 指向 implementation 的逻辑转发代码。
2.2 cloneDeterministic 函数
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// 将 implementation 地址拼接代理合约字节码,存储到内存中。
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
// 使用 create2 操作码部署合约。
instance := create2(0, 0x09, 0x37, salt)
}
// 如果部署失败,抛出错误。
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
作用
○ 使用 create2 部署代理合约,结合 salt 确定地址。
○ 通过 salt,部署地址可以在部署前预测。
部署的字节码:create2 操作码的优点是可以通过 salt 确保每次部署的地址是确定的,适用于需求特定地址的场景。
2.3 predictDeterministicAddress 函数
function predictDeterministicAddress(
address implementation,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40)
// 部署者地址
mstore(add(ptr, 0x38), deployer)
// 后置字节码
mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
// 实现地址(implementation)
mstore(add(ptr, 0x14), implementation)
// 前置字节码
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
// salt
mstore(add(ptr, 0x58), salt)
// 计算合约地址
mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
predicted := keccak256(add(ptr, 0x43), 0x55)
}
}
作用
○ 用于预测通过 create2 部署的代理合约地址。
○ 两种实现:
指定部署者的预测版本。
使用当前合约地址的预测版本。
预测公式
○ keccak256(0xff ++ deployer ++ salt ++ keccak256(bytecode))
deployer:部署合约的地址。
salt:提供的随机数。
bytecode:代理合约的字节码。
八、总结
EIP-1167 提供了一种标准化的代理合约实现,广泛应用于工厂模式和模块化合约。通过代理技术,可以显著节约部署成本,提高合约逻辑复用性,同时提升开发灵活性。
它是以太坊生态中不可或缺的工具,促进了智能合约的高效实现和模块化设计。
声明:本网站所有相关资料如有侵权请联系站长删除,资料仅供用户学习及研究之用,不构成任何投资建议!