view函数的限制
刚开始的时候,我写过下面这个函数,用来获取两个代币间的所有路径,不包括三级路径。但实际上,这里面存在着一些问题。
WechatIMG66
首先,从业务逻辑上来说,tokenA > bases[i] > tokenB 路径,缺失了是否可配对的检查,应该对 tokenA-bases[i] 和 bases[i]-tokenB 这两对分别检查 pair 是否都存在,通过 factory.getPair() 函数得到币对的 pair,如果 pair 不为零地址就说明是匹配的,如果不匹配就说明该路径是无效的。示例代码如下:
if (factory.getPair(tokenA, bases[i]) != address(0) && factory.getPair(bases[i], tokenB) != address(0)) { paths.push([tokenA, bases[i], tokenB]); }
其次,从 solidity 层面来说,数组的 push() 函数是不能在 view 函数中使用的,因为调用 push 函数会有固定的 gas 成本,但 view 函数是不能产生 gas 的,所以就用不了。因此,在 view 函数中使用数组只能用下标的方式进行赋值,如以下代码:
address[] memory tempPath = new address[](3); tempPath[0] = tokenIn; tempPath[1] = baseTokens[i]; tempPath[2] = tokenOut;
最后,返回二维数组,在默认情况下是不支持的,要用 ABIEncoderV2 才能支持,需要对合约添加以下指令才能使用:
pragma experimental ABIEncoderV2;
experimental 说明这还是实验性的,所以建议尽量少用,因为可能存在未知的 bug。
最后,我就完全抛弃了该函数,将路径的遍历和价格对比的逻辑都放在了同一个函数去完成。
链式授权转账
而我遇到的第二个错误则是关于授权转账的,也是因为我对授权转账的原理没真正理解导致的。
在我的实现中,兑换函数存在着几层不同合约之间的链式调用。假设我编写了 A 和 B 两个合约,两个合约都分别定义了 swap() 函数,在 A 合约的 swap() 函数中会调用 B 合约的 swap() 函数,而 B 合约的 swap() 函数再去调用 Uniswap 的路由合约的 swap() 函数。
A.swap() -> B.swap() -> Router.swap()
而在 Router.swap() 中会调用代币的 transferFrom() 函数将调用者 msg.sender 的代币转入 Pair 合约。所以,在兑换之前,还需要调用者对合约进行授权。一开始我以为,只要对 A 合约进行授权就可以了,当然,结果就是兑换失败了。后来,我又添加了链式授权,即调用者授权给 A,A 再授权给 B,B 再授权给 Router,但结果依然还是失败。最后,真正理解了授权转账的原理之后,只需要调用者授权给A,B 授权给 Router,并在 A 合约增加一步操作,调用代币的 transferFrom() 函数将调用者 msg.sender 的代币转入 B 合约,整个链条的兑换就能成功了。
首先,先搞清楚在整个链条中,每一步的 msg.sender 是谁?像这种合约之间的直接调用,msg.sender 都是上一步的调用者,如下图:
而在 Router.swap() 中会调用代币的 transferFrom() 函数将 msg.sender 的代币转入 Pair 合约,即是说,Router 会从 B 合约中将代币转入 Pair 合约,所以 B 合约中必须有代币才能完成转账。那 B 合约的代币从哪来呢?自然是要从 Caller 中来。只要 Caller 授权给 A,A 再用 transferFrom 将 Caller 的代币转给 B,如此就解决问题了。
总结
虽然这个 DEX 交易聚合器功能很简单,只有查询和兑换功能,但扩展起来很简单,后续还会接入 UniswapV3、Bancor、DODO 等,功能上也还可以再加入添加流动性、移除流动性等功能。