前段时间接到一个面试电话,问道delegateCall
和代理合约的知识。当时对代理合约的了解不是很深入,就错失了一个很好的工作机会。
前段时间接到一个面试电话,问道delegateCall
和代理合约的知识。当时对代理合约的了解不是很深入,就错失了一个很好的工作机会。加上今天在做Paradigm的题时,也发现题目中涉及到代理合约这块的知识,所以索性专门写一篇文章,将最近我对于代理合约的理解记录一下,希望能得到经验丰富的大佬的指证。
目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 :fish: 。如果你觉得我写的还不错,可以加我的微信:woodward1993
本文的主要参考资料是: 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。 returndatasize
比dup
*少用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
的方式将外部对代理合约的调用全部转接到远程合约上,省去了部署一次合约的开销,但是它存在以下问题:
delegatecall
只能调用public 或者 external的方法,对于其internal 和 private 方法无法调用。所以代理合约相当于只拷贝了远程合约的公开的方法。在该实际应用中,有两个比较典型的特征:
guard.initialize(this);
代理合约需调用initialize函数来初始化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拷贝到内存中)然后返回。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!