前言
目前,DeFi 赛道中,专门做 DEX 交易聚合的产品挺多的,以下是其中一些平台:
可以看到,这些平台都聚合了很多家 DEX,包括 AMM(自动做市商)模式的 DEX,也包括 Orderbook 模式的 DEX,主要功能都是为了将各个 DEX 的分散流动性整合到一起,提供最优的价格、最佳的深度和清晰简洁的界面。
这几天我也写了一个 DEX 交易聚合器,纯合约的。不过功能还比较简单,只聚合了 UniswapV2 和 SushiSwap,且只实现了从这两个平台中找出最优成交价来实现每笔交易。虽然只是个简单的交易聚合器,却也接连踩了好几个坑,这也暴露出了我的一些知识盲区。下面我就分享下在这过程中的一些经验和总结。
技术调研
既然接入的是 UniswapV2 和 SushiSwap,而且是从合约层面去接入的,所以第一步就是先要调研如何接入。
UniswapV2 的合约分为了两个项目:
- uniswap-v2-core
- uniswap-v2-periphery
uniswap-v2-core 的核心有三个合约:
- UniswapV2ERC20:UNI-V2 代币合约
- UniswapV2Factory:工厂合约
- UniswapV2Pair:配对合约
UNI-V2 代币合约即是 LP Token 合约,工厂合约则主要用来创建配对合约,配对合约则维护着每个币对的流动性池子,另外,配对合约还继承了 UniswapV2ERC20 合约,即是说,配对合约同时也是 LP Token 合约。
uniswap-v2-periphery 被称为外围,其实就是供外部接入使用的,其主要有三个合约:
- UniswapV2Migrator:迁移合约,用来将 v1 的流动性迁移到 v2 用的
- UniswapV2Router01:旧版的路由合约
- UniswapV2Router02:新版的路由合约,现在都是用这个
Uniswap 前端的兑换、添加流动性等操作其实都是通过和路由合约交互完成的,所以这个路由合约也是我们的聚合交易接入 Uniswap 的入口合约。以下页面是官方文档中对 Router02 的介绍:
至于 SushiSwap,则是完全复用了 UniswapV2Router02 合约作为接入 SushiSwap 的入口,只是和 Uniswap 的合约地址不一样而已。
不过,调研路由合约后发现,兑换的两个币种之间的路径,其实从是外部传入给到路由合约的。在 Uniswap 中,路径的选择算法实现是被封装在前端的 SDK 里的,但我所做的聚合器需在合约里自己完成最优路径的寻找,这成为了第一个难题。
最优路径
用户兑换时选定的两个币种 tokenA 和 tokenB,有时候并不存在直接配对的流动性池子。但是,只要存在另一个币种 tokenC,满足 tokenA 和 tokenC 存在流动性池子,tokenB 和 tokenC 也存在流动性池子,那么,只要先将 tokenA 换成 tokenC,再将 tokenC 换成 tokenB,tokenA 和 tokenB 就可以完成兑换,如此,tokenA > tokenC > tokenB 就组成了 tokenA 和 tokenB 兑换的一条路径。
这种路径可能存在不止一条,比如,也可能存在 tokenA > tokenD > tokenB,甚至 tokenA > tokenC > tokenD > tokenB。当然,如果 tokenA 和 tokenB 之间存在直接配对的流动性池子,那么 tokenA > tokenB 也是一条路径。
因为每个池子的流动性不一样,当指定币种数量后,比如指定 100 个 tokenA,那么每条路径最后兑换出来的 tokenB 数量其实也是不一样的。对用户来说,自然是希望能兑换回来的 token 数量越多越好,所以,这些路径中,那条兑换结果数量最多的就能成为最优路径。
有些人可能会陷入一个误区,觉得最优路径应该是最短路径,而实际上:最短路径不一定是最优路径。比如,ETH-WBTC 其实存在直接配对的流动性池子,所以最短路径就是 ETH > WBTC,但是,在界面查询时看到匹配的最优路径却是 ETH > USDC > WBTC,请看下图:
不过,币种那么多,要如何才能高效地找到一条最优路径呢?
寻找最优路径
寻找最优路径的第一步,要先找出所有潜在路径。但是,币种那么多,不可能将所有币种都进行路径组合,尤其在合约层面,效率太低了。其实,如果看看 Uniswap 前端页面,选择代币时,可以看到列出了几种常用代币,如下图:
可以看到,这些都是最主流的代币,所有代币都是与这些代币中的一种或多种配对组成流动性池子的。因此,只要用这些代币作为路径组合的中间币种即可,而无需考虑全部代币。
另外,路径也不能太长,最长的就如 tokenA > tokenC > tokenD > tokenB 就够了。
总而言之,tokenA 兑换 tokenB 可遍历的路径包括:
- tokenA > tokenB:只有两个代币存在直接配对的流动性池子时,该路径才有效
- tokenA > tokenC > tokenB:tokenC 就是常用代币中的一种,要求 tokenA-tokenC 和 tokenC-tokenB 分别都存在流动性池子
- tokenA > tokenC > tokenD > tokenB:tokenC 和 tokenD 是常用代币列表中的两种代币,要求 tokenA-tokenC、tokenC-tokenD、tokenD-tokenB 这三个配对的流动性池子是有效的
对每个有效路径读取出最后价格,对比后就知道最优路径是哪个了。
合约设计
设计上也很简单,核心的合约类图如下:
而实例关系图则如下:
在 Aggreswap 中保存一个 dexs 数组,用来存放所支持的 DEX,当调用 swap() 时,则遍历所有 Dex,查出具有最优价格的 Dex 并转给该 Dex 的 Handler 实例去完成兑换工作。
UniswapV2Handler 中则会保存常用代币列表 baseTokens。每次兑换时,则遍历 baseTokens,组装出每条有效路径并读取价格,从而查询出最优价格和路径,再去调用路由合约完成兑换工作。
Aggreswap 和每个 Handler 实现都统一用 ISwap 接口做交互,则具备了灵活性,而且调用方还可以根据需要绕开 Aggreswap 而与具体的 Handler 进行交互,且交互接口无需改动。
合约设计的整体思路就大致如此了,简单易理解。但具体实现时所犯过的错,我觉得很有必要分享下。




