深入浅出解读合约最小代理 EIP-1167

  • Dapplink
  • 发布于 2024-12-10 23:51
  • 阅读 16

EIP-1167是一种标准化的代理合约实现,旨在通过最小的字节码和委托调用的方式实现合约逻辑的复用。这种模式不仅节省部署和存储成本,还提供了开发者灵活的合约管理能力。

一、核心概念

EIP-1167定义了一种代理合约标准,代理合约的作用是将用户的调用委托到另一个逻辑合约(目标合约)。它通过 delegatecall 操作将调用转发给目标合约,从而实现代码复用,同时代理自身不存储逻辑,只存储状态。

代理合约的主要特点

  • 轻量高效 :代理合约的字节码极短,部署成本低。

  • 逻辑复用 :多个代理可以复用一个逻辑合约,节省存储。

  • 工厂合约模型 :支持创建交易中的克隆初始化(通过工厂合约模型)。

  • 易于升级 :通过调整代理指向的目标地址,可以实现逻辑的动态升级。

微信图片_20250224173625.png

二、EIP-1167的标准字节码

代理合约的字节码模板如下:

0x363d3d373d3d3d363d73<logic_contract_address>5af43d82803e903d91602b57fd5bf3

1. 字节码结构详解

微信图片_20250224173630.png

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 提供了一种标准化的代理合约实现,广泛应用于工厂模式和模块化合约。通过代理技术,可以显著节约部署成本,提高合约逻辑复用性,同时提升开发灵活性。它是以太坊生态中不可或缺的工具,促进了智能合约的高效实现和模块化设计。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Dapplink
Dapplink
0xBdcb...f214
首个模块化、可组合的Layer3协议。