首页>>资讯>>产业

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

2024-12-11 12:02:56 2

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


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


一、核心概念


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


代理合约的主要特点


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

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

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

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

4.png

二、EIP-1167的标准字节码


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


0x363d3d373d3d3d363d73<logic_contract_address>5af43d82803e903d91602b57fd5bf3


1. 字节码结构详解

4.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 提供了一种标准化的代理合约实现,广泛应用于工厂模式和模块化合约。通过代理技术,可以显著节约部署成本,提高合约逻辑复用性,同时提升开发灵活性。

它是以太坊生态中不可或缺的工具,促进了智能合约的高效实现和模块化设计。

声明:本网站所有相关资料如有侵权请联系站长删除,资料仅供用户学习及研究之用,不构成任何投资建议!