# Sudoswap 中对于 EIP-1167 的应用

By [xyyme.eth](https://paragraph.com/@xyyme) · 2023-02-07

---

最近在读 Sudoswap 的合约代码，发现其中应用了 EIP-1167 的玩法，有些写法感觉很有意思，因此想特地写篇文章来记录分享一下。对于 EIP-1167 还不太了解的朋友可以看看我之前写的这篇[文章](https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8)。

工厂合约
----

在 Sudoswap 代码中，`LSSVMPairFactory` 合约是 Pair 的工厂合约，其中可以通过 `createPairETH` 创建 ETH 的交易对，通过 `createPairERC20` 创建 ERC20 的交易对。在创建交易对的过程中，调用 `LSSVMPairCloner` 库合约，其中应用了 EIP-1167 协议。

我们来看看库合约中创建 Pair 的方法：

    function cloneETHPair(
        address implementation,
        ILSSVMPairFactoryLike factory,
        ICurve bondingCurve,
        IERC721 nft,
        uint8 poolType
    ) internal returns (address instance) {
        assembly {
            let ptr := mload(0x40)
    
            mstore(
                ptr,
                hex"60_72_3d_81_60_09_3d_39_f3_3d_3d_3d_3d_36_3d_3d_37_60_3d_60_35_36_39_36_60_3d_01_3d_73_00_00_00"
            )
            mstore(add(ptr, 0x1d), shl(0x60, implementation))
    
            mstore(
                add(ptr, 0x31),
                hex"5a_f4_3d_3d_93_80_3e_60_33_57_fd_5b_f3_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00"
            )
    
            mstore(add(ptr, 0x3e), shl(0x60, factory))
            mstore(add(ptr, 0x52), shl(0x60, bondingCurve))
            mstore(add(ptr, 0x66), shl(0x60, nft))
            mstore8(add(ptr, 0x7a), poolType)
    
            instance := create(0, ptr, 0x7b)
        }
    }
    

其中前三个 `mstore` 与我们之前的文章介绍的 EIP-1167 的写法类似，但是后面又多了几行 `mstore` 以及 `mstore8`，这些都是什么意思呢？

字节码分析
-----

我们先来看看前三个 `mstore`，这里的具体字节码与之前我们介绍的 1167 写法有些出入，这三行组合出的字节码是（长度为 62 字节）：

分割一下，得到：

    1 -> 60723d8160093d39f3
    2 -> 3d3d3d3d363d3d37603d6035363936603d013d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e603357fd5bf3
    

其中第一部分的九个字节，反编译得到的结果是：

    contract Contract {
        function main() {
            memory[returndata.length:returndata.length + 0x72] = code[0x09:0x7b];
            return memory[returndata.length:returndata.length + 0x72];
        }
    }
    

可以看到，合约初始化时将字节码中从 9 字节到 123（0x7b） 字节的数据返回到 EVM 中，但是我们前面的字节码只有 62 字节，后面还有 61 字节都是什么呢？

此时我们再回看前面的 `cloneETHPair` 代码，没错，后面的 61 字节就是来自于其最后的几个 `mstore`，其中 `factory`、`bondingCurve` 以及 `nft` 都是地址类型，分别占据 20 字节，`poolType` 是 bool 类型，占据 1 个字节。

我们再将上面字节码的第二部分进行反编译：

    contract Contract {
        function main() {
            var temp0 = returndata.length;
            var var1 = returndata.length;
            var temp1 = msg.data.length;
            memory[returndata.length:returndata.length + temp1] = msg.data[returndata.length:returndata.length + temp1];
            memory[msg.data.length:msg.data.length + 0x3d] = code[0x35:0x72];
            var temp2;
            temp2, memory[returndata.length:returndata.length + returndata.length] = 
                address(0xbebebebebebebebebebebebebebebebebebebebe)
                .delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length + 0x3d]);
            var temp3 = returndata.length;
            var var0 = returndata.length;
            memory[temp0:temp0 + temp3] = returndata[temp0:temp0 + temp3];
        
            if (temp2) { return memory[var1:var1 + var0]; }
            else { revert(memory[var1:var1 + var0]); }
        }
    }
    

从上面代码中可以看到，代理合约在调用逻辑合约的时候，总会在最后加上 61 （0x3d）字节的 calldata，正是前面的 `factory`、`bondingCurve` 、`nft` 以及 `poolType`。那么也就是说，任何通过代理合约转发到逻辑合约的调用，最后都会加上这段 calldata，意味着在逻辑合约的方法中，总是可以获取到这段 calldata。

获取数据
----

我们来看看 Pair 合约中是如何获取这几个字段值的，以 `bondingCurve()` 为例：

    function bondingCurve() public pure returns (ICurve _bondingCurve) {
        // 这里 ETH Pair 是 61，ERC20 Pair 是 81
        uint256 paramsLength = _immutableParamsLength();
        assembly {
            _bondingCurve := shr(
                0x60,
                calldataload(add(sub(calldatasize(), paramsLength), 20))
            )
        }
    }
    

`_immutableParamsLength()` 返回常量，ETH Pair 返回 61，ERC20 Pair 返回 81。我们前面参考的方法是 `cloneETHPair`，因此我们这里默认其值为 61。

接下来的一段汇编代码，首先最里面的 `calldatasize()` 获取整个 calldata 的长度，其中是包含后面 61 字节的，然后用 `calldatasize()` 减去 `paramsLength`，得到的值就是调用合约方法的基础 calldata 长度，再加上 20（这里的 20 是前面 `factory` 的地址长度），获取到 `bondingCurve` 地址在整个 calldata 中的偏移量。然后调用 `calldataload`，将偏移量作为参数，可以获取到 `bondingCurve` 地址起始的 32 字节数据，其中前 20 字节是 `bondingCurve` 的地址，后面的 12 （0x60 / 8）字节通过 `shr` 操作移除，最终得到的就是 `bondingCurve` 的地址。

我们来看看图示，更加易懂：

![](https://storage.googleapis.com/papyrus_images/0be47ac03c7a620f79dbf0bd78f6033011b775583e4e338db04b18ecb5752667.png)

其余的 `factory` 等获取方法同理，只是偏移量不同，最后的 `add` 数值不同。

看懂这块，我们同理就可以分析 ERC20 Pair 的操作，唯一的区别是在 `cloneERC20Pair` 方法中，最后还要在 `mstore` token 地址，因此 calldata 会多 20，即 81。

那么我们思考一下，为什么 Sudoswap 要采用这么复杂的操作，而不是直接将这几个字段直接存储到 Pair 中呢？最重要的就是 gas 的原因，Solidity 中处理 calldata 的操作耗费的 gas 很少，而如果将这些字段存储到 Pair 中，首先 `sstore` 操作就要耗费大量 gas，其次每次读取 `sload` 也会耗费大量 gas，与在 calldata 中操作的 gas 消耗可以说不是一个数量级的。

总结
--

我们看到，Sudoswap 中采取了一个几乎极致的方法来节省 gas，这其中确实是有炫技的成分，不过里面的方法与思想还是值得我们学习的。随着合约开发逐渐卷起来，我觉得这种字节码层级操作的代码以后会越来越多，这块还是值得深入研究的。

关于我
---

欢迎[和我交流](https://linktr.ee/xyymeeth)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/sudoswap-eip-1167)*
