深入剖析 ERC1167

  • BY_DLIFE
  • 更新于 2024-05-03 14:05
  • 阅读 183

EIP-1167,又称Minimal Proxy Contract,提供了一种低成本复制合约的方法,也可以叫作是克隆合约的方法。

1. ERC1167 简介

EIP-1167,又称Minimal Proxy Contract,提供了一种低成本复制合约的方法,也可以叫作是克隆合约的方法。如何理解克隆呢?克隆就是类似复制的意思,这里的合约克隆是指:克隆合约和原合约具有相同的逻辑功能。而且创建克隆合约的成本比直接部署原合约低,部署克隆合约的前提是得有一个原件。

2. 原理复现

2.1 工作原理

一说到代理,首先就会想到代理合约,合约升级。但是ERC1167不是合约升级,它只是负责合约的调用转发。

可升级合约的代理合约架构:

image.png

整个架构中存在一个代理合约和多个逻辑合约,只有一套数据(即代理合约的数据),需要升级时则替换掉代理合约中的逻辑合约,而且同一时间只能存在一个逻辑合约。

Minimal Proxy Contract合约架构:

image.png

整个架构中存在多个代理合约和一个逻辑合约,有多套数据分别存储在不同的代理合约中,所有代理合约共享逻辑合约的执行逻辑,同一时间存在多个代理合约。Minimal Proxy Contract的原理就是将代理合约作为逻辑合约的复制品,各个代理合约存储各自的数据,需要多少份复制品就创建多少个代理合约。而代理合约本身只负责请求转发,因此其内容很少,从而耗费的gas就更少。

2.2 解析字节码

从官方文档上可以看到这串字节码: 363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3,经过反编译之后:

    0x0: CALLDATASIZE
    0x1: RETURNDATASIZE
    0x2: RETURNDATASIZE
    0x3: CALLDATACOPY
    0x4: RETURNDATASIZE
    0x5: RETURNDATASIZE
    0x6: RETURNDATASIZE
    0x7: CALLDATASIZE
    0x8: RETURNDATASIZE
    0x9: PUSH20    0xbebebebebebebebebebebebebebebebebebebebe
   0x1e: GAS       
   0x1f: DELEGATECALL
   0x20: RETURNDATASIZE
   0x21: DUP3      
   0x22: DUP1      
   0x23: RETURNDATACOPY
   0x24: SWAP1     
   0x25: RETURNDATASIZE
   0x26: SWAP2     
   0x27: PUSH1     0x2b
   0x29: JUMPI     
   0x2a: REVERT    
   0x2b: JUMPDEST  
   0x2c: RETURN 

这串字节码的执行的逻辑就是对 0xbebebebebebebebebebebebebebebebebebebebe地址执行delegatecall,如果调用失败则revert,如果调用成功则返回代理调用返回的结果。

可以自己用汇编语言写出来,returndata的部分可能不太对,但是大体逻辑是这样的。

        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            // delegatecall returns 0 on error.
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }

2.3 实现复制功能

要如何实现克隆功能,可以参考openzeppelin官方的代码。

    function clone(address implementation, uint256 value) internal returns (address instance) {
        if (address(this).balance < value) {
            revert Errors.InsufficientBalance(address(this).balance, value);
        }
        /// @solidity memory-safe-assembly
        assembly {
            // Stores the bytecode after address
            mstore(0x20, 0x5af43d82803e903d91602b57fd5bf3)
            // implementation address
            mstore(0x11, implementation)
            // Packs the first 3 bytes of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            instance := create(value, 0x09, 0x37)
        }
        if (instance == address(0)) {
            revert Errors.FailedDeployment();
        }
    }

直接看到汇编部分,三个mstore操作码的作用是拼接克隆合约的createionCode,拼接的结果:

0x3d602d80600a3d3981f3363d3d373d3d3d363d73 + implementation + 5af43d82803e903d91602b57fd5bf3

然后通过create操作码部署克隆合约。

演示:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.22;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Clones.sol";

contract Demo {

    uint256 public num;

    fallback() external payable {
        ++num;
    }
}

contract Test {

    uint256 public num;
    event result(uint256);

    function call(address cloner) public  {
        cloner.call("aaa"); // call fallback() 
        (, bytes memory _result) = cloner.call(abi.encodeWithSignature("num()"));
        emit result(abi.decode(_result, (uint256)));
    }
}

contract CloneLib {

    using Clones for address;

    function clone(address implementation) public returns (address cloner){
        cloner = implementation.clone();
    }
}
  1. 先部署 Demo合约
  2. 部署CloneLib合约,并调用clone函数,并传入Demo合约地址
  3. 最后部署Test合约,并调用call函数,传入克隆地址

结果:

image.png

可以看到结果返回1,说明克隆成功。

同理,知道了克隆的逻辑,可以用另一种方式复现:

contract Clone {

    uint256 public num;

    event a(bytes);

    constructor(address implementation) {
        bytes memory head = hex"363d3d373d3d3d363d73";
        bytes memory tail = hex"5af43d82803e903d91602b57fd5bf3";
        bytes memory runtimeCode = abi.encodePacked(head, implementation, tail);
        emit a(runtimeCode);

        assembly {
            return(add(runtimeCode, 0x20), mload(runtimeCode))
        }
    }
}

solidity的智能合约执行的逻辑都是通过runtimeCode,而只要将合约runtimeCode部分的内容按克隆合约的逻辑编写,即照样也可以完成相同的要求。

执行操作相同,执行结果为:

image.png

3. 节省gas费用

通过部署原合约和部署克隆合约所需的gas费的多少来判断

部署Demo所需的gas费用为:"122325"

image.png

部署克隆合约所需的gas费用为:"63334"

image.png

可以看到几乎是节省了一倍的花销。

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

0 条评论

请先 登录 后评论
BY_DLIFE
BY_DLIFE
0x994d...4240
立志成为一名智能合约安全审计师。文章都是我的个人理解,如果有不对的地方欢迎在评论区指出来。