本文作者:bixia1994[1]
sudoswap 的一大特色就是非常节省 gas,在它的 twitter 上也和 seaport 消耗的 gas 进行了对比,所以这里就想学习下 sudoswap 是如何节省 gas 的。
通过 factory 创建 pair 合约的主要操作都是在 LSSVMPairCloner 这个 lib 里完成,可以看到这里有着非常多的 assembly 代码甚至是手写的 Opcode。比如其cloneETHPair
方法,里面就充斥了一堆手写的opcode[2]。
factory,bondingCurve,nft,poolType
这四个参数全部硬编码到了部署得到的 proxy 合约代码里面,如这个地址所示[4]:0x3d3d3d3d363d3d37603d6035363936603d013d73cd80c916b1194beb48abf007d0b79a7238436d565af43d3d93803e603357fd5bf3b16c1342e617a5b6e4b631eb114483fdb289c0a4432f962d8209781da23fb37b6b59ee15de7d984164ad353bc90a04361c4810ae7b3701f3beb48d7e02
其中,implement 合约地址是:0xCd80C916B1194beB48aBF007D0b79a7238436D56
。携带的 4 个参数分别是:
factory: 0xb16c1342e617a5b6e4b631eb114483fdb289c0a4
bondingCurve: 0x432f962d8209781da23fb37b6b59ee15de7d9841
nft:0x64ad353bc90a04361c4810ae7b3701f3beb48d7e
poolType: 0x02
factory,bondingCurve,nft,poolType
即,比如最简单的调用swapTokenForAnyNFTs
方法,其调用的 calldata 为:首先是用户直接调用proxy的swapTokenForAnyNFTs方法:
proxy.call(abi.encodeWithSelector(proxy.swapTokenForAnyNFTs.selector, numNFTs, maxExpectedTokenInput, nftRecipient, isRouter, routerCaller))
然后是proxy对该方法调用进行delegatecall转发:
impl.delegatecall(
abi.encodePacked(
abi.encodeWithSelector(
proxy.swapTokenForAnyNFTs.selector,
numNFTs,
maxExpectedTokenInput,
nftRecipient,
isRouter,
routerCaller),
abi.encodePacked(
factory,
bondingCurve,
nft,
poolType
)))
这样做有很多好处,一个好处就是可以极大的省 gas。
initstart:
push1 runtimeEnd-runtimeStart #runtimesize
returndatasize #0 runtimesize
dup2 #runtimesize 0 runtimesize
push1 runtimeStart-initstart #runtimeoffset runtimesize 0 runtimesize
returndatasize #0 runtimeoffset runtimesize 0 runtimesize
codecopy #0 runtimesize
return
initend:
runtimeStart:
returndatasize #0
returndatasize #0 0
returndatasize #0 0 0
returndatasize #0 0 0 0
calldatasize #calldatasize 0 0 0 0
returndatasize #0 calldatasize 0 0 0 0
returndatasize #0 0 calldatasize 0 0 0 0
calldatacopy #0 0 0 0
push1 extradataEnd-extradataStart #extradatasize 0 0 0 0
push1 extradataStart-runtimeStart #extradataoffset extradatasize 0 0 0 0
calldatasize #calldatasize extradataoffset extradatasize 0 0 0 0
codecopy #0 0 0 0
push1 extradataEnd-extradataStart #extradatasize 0 0 0 0
calldatasize #calldatasize extradatasize 0 0 0 0
add #inputSize 0 0 0 0
returndatasize #0 inputSize 0 0 0 0
push20 0x0000000000000000000000000000000000000000 #addr 0 inputSize 0 0 0 0
gas #gas addr 0 inputSize 0 0 0 0
delegatecall #success 0 0
returndatasize #returndatasize success 0 0
swap3 #0 success 0 returndatasize
dup4 #returndatasize 0 success 0 returndatasize
swap1 #0 returndatasize success 0 returndatasize
dup1 #0 0 returndatasize success 0 returndatasize
returndatacopy #success 0 returndatasize
push1 labelreturn #labelreturn success 0 returndatasize
jumpi #0 returndatasize
revert #
labelreturn:
jumpdest #0 returndatasize
return #
extradataStart:
%include_hex("extra.etk")
extradataEnd:
runtimeEnd:
然后是 extra.etk 文件,这里面放着的是需要硬编码到 proxy 合约里的字节码:
b16c1342e617a5b6e4b631eb114483fdb289c0a4432f962d8209781da23fb37b6b59ee15de7d984164ad353bc90a04361c4810ae7b3701f3beb48d7e02
通过 etk 工具对 op.etk 文件进行编码,得到的字节码段为:
60733d8160093d39f33d3d3d3d363d3d37603d60363639603d36013d7300000000000000000000000000000000000000005af43d928390803e603d57fd5bf3b16c1342e617a5b6e4b631eb114483fdb289c0a4432f962d8209781da23fb37b6b59ee15de7d984164ad353bc90a04361c4810ae7b3701f3beb48d7e02
那么我们为什么需要花费这么大的力气来手写这些编码呢?最大的一个好处是可以利用 etk 工具帮我们计算出对应的 offset 和 length 的数据,否则需要自己来手动计算,就很容易出错。并且我们可以在evm.codes
这个网站上在线调试一下,我们手写的这部分字节码是不是正确的。
当我们拿到需要的字节码之后,我们就可以开始编写相对应的 assembly 部分了。编写 assembly 的核心是把得到的字节码当成一串字符,然后存储到 memory 里,最后通过 create 方法创建出来。编写 assembly 之前,通过字节码,要算出对应的长度:
>>> s= "60733d8160093d39f33d3d3d3d363d3d37603d60363639603d36013d7300000000000000000000000000000000000000005af43d928390803e603d57fd5bf3b16c1342e617a5b6e4b631eb114483fdb289c0a4432f962d8209781da23fb37b6b59ee15de7d984164ad353bc90a04361c4810ae7b3701f3beb48d7e02"
>>> s[:0x1d*2]
'60733d8160093d39f33d3d3d3d363d3d37603d60363639603d36013d73'
>>> s[0x1d*2:0x1d*2+40]
'0000000000000000000000000000000000000000'
>>> s[0x31*2:0x3f*2]
'5af43d928390803e603d57fd5bf3'
>>> s[0x3f*2:0x53*2]
'b16c1342e617a5b6e4b631eb114483fdb289c0a4'
>>> s[0x53*2:0x67*2]
'432f962d8209781da23fb37b6b59ee15de7d9841'
>>> s[0x67*2:0x7b*2]
'64ad353bc90a04361c4810ae7b3701f3beb48d7e'
>>> s[0x7b*2:0x7c*2]
'02'
function cloneETHPair(
address implementation,
ILSSVMPairFactoryLike factory,
ICurve bondingCurve,
IERC721 nft,
uint8 poolType
) internal returns (address instance) {
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x60733d8160093d39f33d3d3d3d363d3d37603d60363639603d36013d73000000)
mstore(add(ptr,0x1d), shl(0x60,implementation))
mstore(add(ptr,0x31), 0x5af43d928390803e603d57fd5bf3000000000000000000000000000000000000)
mstore(add(ptr,0x3f), shl(0x60,factory))
mstore(add(ptr,0x53), shl(0x60,bondingCurve))
mstore(add(ptr,0x67), shl(0x60, nft))
mstore8(add(ptr,0x7b), poolType)
instance := create(0, ptr, 0x7c)
}
}
[1]
bixia1994: https://learnblockchain.cn/people/3295
[2]
opcode: https://github.com/sudoswap/lssvm/blob/c8b228b2a3b51664c433f487fb76db42d3509dd4/src/lib/LSSVMPairCloner.sol#L21-L101
[3]
这个链接: https://learnblockchain.cn/article/2663
[4]
地址所示: https://etherscan.io/address/0xb79f94e6f2460d3cb8b8970de4aa873f78589e34#code
[5]
etk: https://github.com/quilt/etk