前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >EIP-1167: 代理合约

EIP-1167: 代理合约

作者头像
Tiny熊
发布2021-07-14 15:13:20
2.5K0
发布2021-07-14 15:13:20
举报

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

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 拷贝到内存中)然后返回。

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • EIP-1167
    • 要解决的问题:
      • 字节码分析
        • 缺点
          • 实际的应用
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档