详解最小代理合约EIP7511
详解最小代理合约EIP7511
当人们需要反复部署同一个合约时,比如每个用户都需要部署一遍抽象账户合约,代理合约是最好的解决办法。在这个模式下,复杂的逻辑合约可以被重复利用,用户只需要部署一个简单的代理合约,从而降低gas成本。
回忆一下,代理的原理就是用户call代理,代理delegatecall
逻辑合约。这样以来,调用者还是用户,代理合约只负责转发调用请求和存储数据,方法用的都是目标逻辑合约的。这样一来用户只需要部署一个代理就相当于部署了任何一个别的合约。
那既然这玩意这么常用,肯定就要把它的gas消耗降低到一个令人发指的地步,而就像最高效的程序一定是汇编一样,要追求最低gas就必须手搓字节码。
EIP7511的字节码
这个合约长度仅有55
字节!
它的字节码是:
363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3
具体实现
最小代理合约的核心元素包括:
- 使用
CALLDATACOPY
复制交易的calldata。 - 使用
DELEGATECALL
将calldata转发到逻辑合约。 - 将
DELEGATECALL
返回的数据复制到内存。 - 根据
DELEGATECALL
是否成功来返回结果或回滚交易。
复制Calldata
CALLDATASIZE
PUSH0
PUSH0
CALLDATACOPY
CALLDATACOPY
执行前,栈里有[0, 0, cds]
,其中cds
代表calldata
的大小。它们三个将作为CALLDATACOPY
操作码的参数,代表从内存地址0开始存,从calldata
起始位置开始复制,复制整个calldata
的长度,整个复制到内存里,
进行delegatecall
DELEGATECALL
操作码所需的参数分别是[gas 0xbebe. 0 cds 0 0]
,其中gas
代表剩余的gas,0xbebe.
代表逻辑合约的地址(20字节,实际使用时需要替换成你的逻辑合约地址),下面注释里的suc
代表delegatecall
是否成功。
PUSH0
PUSH0
CALLDATASIZE
PUSH0
PUSH20 0xbebebebebebebebebebebebebebebebebebebebe
GAS
// 这里之前都在为DELEGATECALL准备参数
DELEGATECALL // 执行完栈里应该只剩一个表示成功与否的suc
复制返回值到内存
RETURNDATACOPY
所需的参数是[0, 0, rds]
,rds
是返回数据长度,
RETURNDATASIZE
PUSH0
PUSH0
RETURNDATACOPY
// 现在返回值已经在内存里了,而且是从0开始的位置
// 不用担心覆盖,到这里calldata没用了已经
返回数据或回滚交易
PUSH0 // 0 suc
RETURNDATASIZE // rds 0 suc
SWAP2 // suc 0 rds
PUSH1 0x2a // 0x2a suc 0 rds
JUMPI // 如果suc为真,交易成功,跳到2a
REVERT // 上一步没跳说明失败了,revert
JUMPDEST
RETURN // 此时栈里剩:0 rds,正好是RETURN的参数,返回交易的returndata
优化的部分
相比于原始代码,这部分用PUSH0
替换了RETURNDATASIZE
和DUP
操作,节省了gas并提高了代码的可读性。
其实写操作码或者汇编语言,核心思想就是给关键的指令准备参数,并且在需要时调整栈的顺序。这有点像打炉石,在拍启动组件之前你必须尽量把握战场,凑齐你需要的东西才能一波OTK带走对面。这么一想写字节码就简单多了,至少没有对手来破坏你的布局。