EIP-1167: 代理合约

前段时间接到一个面试电话,问道delegateCall和代理合约的知识。当时对代理合约的了解不是很深入,就错失了一个很好的工作机会。

# EIP-1167: 代理合约 前段时间接到一个面试电话,问道`delegateCall`和代理合约的知识。当时对代理合约的了解不是很深入,就错失了一个很好的工作机会。加上今天在做Paradigm的题时,也发现题目中涉及到代理合约这块的知识,所以索性专门写一篇文章,将最近我对于代理合约的理解记录一下,希望能得到经验丰富的大佬的指证。 > 目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 :fish: 。如果你觉得我写的还不错,可以加我的微信:woodward1993 ## EIP-1167 本文的主要参考资料是: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1167.md 以及 https://learnblockchain.cn/article/721 学习以太坊的合约设计,最好是翻看有没有官方的介绍。比如关于代理合约,就存在EIP-1167的一个专门介绍代理合约知识点的EIP。 下面我们将主要基于该EIP-1167分析: ### 要解决的问题: 避免重复部署同样的合约代码,取而代之的是只部署一次合约代码,当需要一份拷贝的时候,就只需要部署一个简单的代理合约。代理合约使用`delegatecall`来调用合约代码,代理合约有自己的地址、存储插槽和以太余额等。主要目的是为了节约Gas。 EIP-1167标准是为了以不可改变的方式简单而廉价地克隆目标合约的功能,它规定了一个最小的字节码实现,它将所有调用委托给一个已知的固定地址。 ### 字节码分析 EIP-1167标准的字节码如下: ```assembly 363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 ``` 其中`bebebebebebebebebebebebebebebebebebebebe`是目标合约的地址。 ```assembly 0000 36 CALLDATASIZE cSize 0001 3D RETURNDATASIZE cSize 0 0002 3D RETURNDATASIZE cSize 0 0 0003 37 CALLDATACOPY 0004 3D RETURNDATASIZE 0 0005 3D RETURNDATASIZE 0 0 0006 3D RETURNDATASIZE 0 0 0 0007 36 CALLDATASIZE 0 0 0 cSize 0008 3D RETURNDATASIZE 0 0 0 cSize 0 0009 73 PUSH20 0xbebebebebebebebebebebebebebebebebebebebe 0 0 0 cSize 0 addr 001E 5A GAS 0 0 0 cSize 0 addr gas 001F F4 DELEGATECALL 0 success 0020 3D RETURNDATASIZE 0 success rSize 0021 82 DUP3 0 success rSize 0 0022 80 DUP1 0 success rSize 0 0 0023 3E RETURNDATACOPY 0 success 0024 90 SWAP1 success 0 0025 3D RETURNDATASIZE success 0 rSize 0026 91 SWAP2 rSize 0 success 0027 60 PUSH1 0x2b rSize 0 success 0x2b 0029 57 *JUMPI 002A FD *REVERT 002B 5B JUMPDEST rSize 0 002C F3 *RETURN => function proxy(address addr) { assembly{ let cSize := calldatasize() calldatacopy(0,0,cSize) // 此时MEM[0:0+cSize] = input data,即把函数选择器连同参数一起存放在内存0x00位置处 let gas := gas() let success := delegatecall(gas, addr, 0, cSize, 0, 0) //此时调用了addr地址处的代码,方法参数为我方合约内存MEM[0:0+cSize] returndatacopy(0,0,returndatasize()) //拷贝返回值到内存中MEM[0:rSize] if (success) { return(0, rSize) //将存放在内存中的返回值返回回去 } revert(0, rSize) } } ``` 注意:为了尽可能减少gas成本,上述字节码依赖于EIP-211规范,即`returndatasize`在调用帧内的任何调用之前返回0。 `returndatasize`比`dup`*少用1 gas。 > 可以将`returndatasize`换成更好理解的`push1 0x00`,如下字节码实现相同功能,但更好理解 ```assembly calldatasize cSize push1 0x00 cSize 0x00 push1 0x00 cSize 0x00 0x00 calldatacopy push1 0x00 0x00 push1 0x00 0x00 0x00 calldatasize 0x00 0x00 cSize push1 0x00 0x00 0x00 cSize 0x00 push20 addr 0x00 0x00 cSize 0x00 addr gas 0x00 0x00 cSize 0x00 addr gas delegatecall success returndatasize success rSize push1 0x00 success rSize 0x00 push1 0x00 success rSize 0x00 0x00 returndatacopy success dup1 success success push1 0xxx success success 0xxx jump1 success push1 0x00 success 0x00 push1 0x00 success 0x00 0x00 revert jumpdest success returndatasize success rSize push1 0x00 success rSize 0x00 return ``` ### 缺点 虽然通过`delegatecall`的方式将外部对代理合约的调用全部转接到远程合约上,省去了部署一次合约的开销,但是它存在以下问题: - 代理合约只拷贝了远程合约的runtime code,由于涉及初始化部分的代码在init code中,故代理合约无法拷贝远程合约的构造函数内的内容,需要一个额外的initialize 函数来初始化代理合约的状态值。 - `delegatecall`只能调用public 或者 external的方法,对于其internal 和 private 方法无法调用。所以代理合约相当于只拷贝了远程合约的公开的方法。 ### 实际的应用 在该实际应用中,有两个比较典型的特征: - `guard.initialize(this);`代理合约需调用initialize函数来初始化 - create函数中,存放在内存的代码段包含了初始化代码 ```js function createGuard(bytes32 implementation) private returns (Guard) { address impl = registry.implementations(implementation); require(impl != address(0x00)); if (address(guard) != address(0x00)) { guard.cleanup(); } guard = Guard(createClone(impl)); guard.initialize(this); return guard; } function createClone(address target) internal returns (address result) { bytes20 targetBytes = bytes20(target); assembly { let clone := mload(0x40) mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)//32 bytes mstore(add(clone, 0x14), targetBytes) //20bytes mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)//32 bytes result := create(0, clone, 0x37) } } //内存MEM[0x40:0x40+0x37]存放的值: //3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 ``` ```assembly mstore(clone, 0x3d602d80600a3d3981f3) =>初始化代码,作用是把runtime code拷贝到内存中 0000 3D RETURNDATASIZE 0 0001 60 PUSH1 0x2d 0 0x2d 0003 80 DUP1 0 0x2d 0x2d 0004 60 PUSH1 0x0a 0 0x2d 0x2d 0x0a 0006 3D RETURNDATASIZE 0 0x2d 0x2d 0x0a 0 0007 39 CODECOPY 0 0x2d //把从第0x0a个byte到0x2d个byte值拷贝到内存0x00 0008 81 DUP2 0 0x2d 0 0009 F3 *RETURN 0 =>逻辑代码(runtimecode) 与上文一致 ``` 问题是:为什么该代码段需要一个初始化代码,实际问题是create opcode 到底是如何工作的? $$ (\boldsymbol{\sigma}', \boldsymbol{\mu}'_{\mathrm{g}}, A^+, \mathbf{o}) \equiv \begin{cases}{lambda}{\Lambda}(\boldsymbol{\sigma}^*, I_{\mathrm{a}}, I_{\mathrm{o}}, L(\boldsymbol{\mu}_{\mathrm{g}}), I_{\mathrm{p}}, \boldsymbol{\mu}_{\mathbf{s}}[0], \mathbf{i}, I_{\mathrm{e}} + 1, \zeta, I_{\mathrm{w}}) & \text{if} \quad \boldsymbol{\mu}_{\mathbf{s}}[0] \leqslant \boldsymbol{\sigma}[I_{\mathrm{a}}]_{\mathrm{b}} \; \\ \quad &\wedge\; I_{\mathrm{e}} < 1024\\ \big(\boldsymbol{\sigma}, \boldsymbol{\mu}_{\mathrm{g}}, \varnothing\big) & \text{otherwise} \end{cases} $$ create简单说是先计算出新合约的地址,然后执行init code逻辑(init code 需要将runtime code拷贝到内存中)然后返回。

EIP-1167: 代理合约

前段时间接到一个面试电话,问道delegateCall和代理合约的知识。当时对代理合约的了解不是很深入,就错失了一个很好的工作机会。加上今天在做Paradigm的题时,也发现题目中涉及到代理合约这块的知识,所以索性专门写一篇文章,将最近我对于代理合约的理解记录一下,希望能得到经验丰富的大佬的指证。

目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 :fish: 。如果你觉得我写的还不错,可以加我的微信:woodward1993

EIP-1167

本文的主要参考资料是: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1167.md 以及 https://learnblockchain.cn/article/721

学习以太坊的合约设计,最好是翻看有没有官方的介绍。比如关于代理合约,就存在EIP-1167的一个专门介绍代理合约知识点的EIP。

下面我们将主要基于该EIP-1167分析:

要解决的问题:

避免重复部署同样的合约代码,取而代之的是只部署一次合约代码,当需要一份拷贝的时候,就只需要部署一个简单的代理合约。代理合约使用delegatecall来调用合约代码,代理合约有自己的地址、存储插槽和以太余额等。主要目的是为了节约Gas。

EIP-1167标准是为了以不可改变的方式简单而廉价地克隆目标合约的功能,它规定了一个最小的字节码实现,它将所有调用委托给一个已知的固定地址。

字节码分析

EIP-1167标准的字节码如下:

363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3

其中bebebebebebebebebebebebebebebebebebebebe是目标合约的地址。

0000    36  CALLDATASIZE        cSize
0001    3D  RETURNDATASIZE      cSize 0
0002    3D  RETURNDATASIZE      cSize 0 0
0003    37  CALLDATACOPY        
0004    3D  RETURNDATASIZE      0
0005    3D  RETURNDATASIZE      0 0
0006    3D  RETURNDATASIZE      0 0 0
0007    36  CALLDATASIZE        0 0 0 cSize
0008    3D  RETURNDATASIZE      0 0 0 cSize 0
0009    73  PUSH20 0xbebebebebebebebebebebebebebebebebebebebe   0 0 0 cSize 0 addr
001E    5A  GAS                 0 0 0 cSize 0 addr gas
001F    F4  DELEGATECALL        0 success
0020    3D  RETURNDATASIZE      0 success rSize
0021    82  DUP3                0 success rSize 0
0022    80  DUP1                0 success rSize 0 0
0023    3E  RETURNDATACOPY      0 success
0024    90  SWAP1               success 0
0025    3D  RETURNDATASIZE      success 0 rSize
0026    91  SWAP2               rSize 0 success
0027    60  PUSH1 0x2b          rSize 0 success 0x2b
0029    57  *JUMPI              
002A    FD  *REVERT
002B    5B  JUMPDEST            rSize 0
002C    F3  *RETURN
=>
function proxy(address addr) {
    assembly{
        let cSize := calldatasize()
        calldatacopy(0,0,cSize) // 此时MEM[0:0+cSize] = input data,即把函数选择器连同参数一起存放在内存0x00位置处
        let gas := gas()
        let success := delegatecall(gas, addr, 0, cSize, 0, 0) //此时调用了addr地址处的代码,方法参数为我方合约内存MEM[0:0+cSize]
        returndatacopy(0,0,returndatasize()) //拷贝返回值到内存中MEM[0:rSize]
        if (success) {
            return(0, rSize) //将存放在内存中的返回值返回回去
        }
        revert(0, rSize)
    }
}

注意:为了尽可能减少gas成本,上述字节码依赖于EIP-211规范,即returndatasize在调用帧内的任何调用之前返回0。 returndatasizedup*少用1 gas。

可以将returndatasize换成更好理解的push1 0x00,如下字节码实现相同功能,但更好理解

calldatasize     cSize
push1 0x00      cSize 0x00
push1 0x00      cSize 0x00 0x00
calldatacopy    
push1 0x00      0x00 
push1 0x00      0x00 0x00
calldatasize    0x00 0x00 cSize
push1 0x00      0x00 0x00 cSize 0x00
push20 addr     0x00 0x00 cSize 0x00 addr
gas             0x00 0x00 cSize 0x00 addr gas
delegatecall    success
returndatasize  success rSize
push1 0x00      success rSize 0x00
push1 0x00      success rSize 0x00 0x00
returndatacopy  success 
dup1            success success
push1 0xxx      success success 0xxx
jump1           success
push1 0x00      success 0x00
push1 0x00      success 0x00 0x00
revert
jumpdest        success 
returndatasize  success rSize
push1 0x00      success rSize 0x00
return

缺点

虽然通过delegatecall的方式将外部对代理合约的调用全部转接到远程合约上,省去了部署一次合约的开销,但是它存在以下问题:

  • 代理合约只拷贝了远程合约的runtime code,由于涉及初始化部分的代码在init code中,故代理合约无法拷贝远程合约的构造函数内的内容,需要一个额外的initialize 函数来初始化代理合约的状态值。
  • delegatecall只能调用public 或者 external的方法,对于其internal 和 private 方法无法调用。所以代理合约相当于只拷贝了远程合约的公开的方法。

实际的应用

在该实际应用中,有两个比较典型的特征:

  • guard.initialize(this);代理合约需调用initialize函数来初始化
  • create函数中,存放在内存的代码段包含了初始化代码
function createGuard(bytes32 implementation) private returns (Guard) {
    address impl = registry.implementations(implementation);
    require(impl != address(0x00));

    if (address(guard) != address(0x00)) {
        guard.cleanup();
    }

    guard = Guard(createClone(impl));
    guard.initialize(this);
    return guard;
}
function createClone(address target) internal returns (address result) {
    bytes20 targetBytes = bytes20(target);
    assembly {
        let clone := mload(0x40)
        mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)//32 bytes
        mstore(add(clone, 0x14), targetBytes) //20bytes
        mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)//32 bytes
        result := create(0, clone, 0x37)
    }
}
//内存MEM[0x40:0x40+0x37]存放的值:
//3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3
mstore(clone, 0x3d602d80600a3d3981f3)
=>初始化代码,作用是把runtime code拷贝到内存中 
0000    3D  RETURNDATASIZE  0
0001    60  PUSH1 0x2d      0 0x2d
0003    80  DUP1            0 0x2d 0x2d
0004    60  PUSH1 0x0a      0 0x2d 0x2d 0x0a
0006    3D  RETURNDATASIZE  0 0x2d 0x2d 0x0a 0
0007    39  CODECOPY        0 0x2d //把从第0x0a个byte到0x2d个byte值拷贝到内存0x00
0008    81  DUP2            0 0x2d 0
0009    F3  *RETURN         0
=>逻辑代码(runtimecode)
与上文一致

问题是:为什么该代码段需要一个初始化代码,实际问题是create opcode 到底是如何工作的?

$$ (\boldsymbol{\sigma}', \boldsymbol{\mu}'{\mathrm{g}}, A^+, \mathbf{o}) \equiv \begin{cases}{lambda}{\Lambda}(\boldsymbol{\sigma}^*, I{\mathrm{a}}, I{\mathrm{o}}, L(\boldsymbol{\mu}{\mathrm{g}}), I{\mathrm{p}}, \boldsymbol{\mu}{\mathbf{s}}[0], \mathbf{i}, I{\mathrm{e}} + 1, \zeta, I{\mathrm{w}}) & \text{if} \quad \boldsymbol{\mu}{\mathbf{s}}[0] \leqslant \boldsymbol{\sigma}[I{\mathrm{a}}]{\mathrm{b}} \; \ \quad &\wedge\; I{\mathrm{e}} < 1024\ \big(\boldsymbol{\sigma}, \boldsymbol{\mu}_{\mathrm{g}}, \varnothing\big) & \text{otherwise} \end{cases} $$

create简单说是先计算出新合约的地址,然后执行init code逻辑(init code 需要将runtime code拷贝到内存中)然后返回。

区块链技术网。

  • 发表于 2021-06-27 13:46
  • 阅读 ( 554 )
  • 学分 ( 54 )
  • 分类:智能合约

评论