文章首发于公众号:Keegan小钢
Swap 可分为两种场景:单池交易和跨池交易。在 PoolManager 合约里,要完成交易流程,会涉及到 lock()
、swap()
、settle()
、take()
四个函数。单池交易时只需要调一次 swap()
函数,而跨池交易时则需要多次调用 swap()
函数来完成。
我们先来聊聊单池交易如何实现,以下是流程图:
第一步,和其他操作一样,先执行 lock()
,锁定住接下来的系列操作。
第二步,就是在 lockAcquired()
回调函数里执行 swap()
函数。这一步执行完之后,记账系统中会记录用户欠池子的资产数量,即用户需要支付的代币;以及池子欠用户的资产数量,即用户此次交易可得的代币。
第三步,执行 settle()
函数,完成代币的支付。
第四步,执行 take()
函数,取回所得的代币。
最后,lock()
函数完成,返回结果。
而如果是跨池交易的话,则需要在 Router 层面确定好交易路径,然后根据路径执行多次 swap
。举个例子,现在要用 A 兑换成 C,但是 A 和 C 之间没有直接配对的池子,但是有中间代币 B,存在 A 和 B 配对的池子,也存在 B 和 C 配对的池子。那交易路径就可以先用 A 换成 B,再将 B 换成 C,最终实现了 A 换成 C。而不管中间经过了多少次 swap
,最后,只需要完成一次 settle
操作,即支付 A,也只需要执行一次 take
操作,即取回最后所得的 C。整个流程大致如下图所示:
下面,我们主要剖析讲解 swap()
函数的内部实现。
首先,看看其函数声明,如下:
function swap(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)
external
override
noDelegateCall
onlyByLocker
returns (BalanceDelta delta)
key
指定了要进行交易的池子,params
是具体的交易参数,hookData
即需要回传给 hooks 合约的数据。
来看看 params
具体有哪些参数:
struct SwapParams {
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96;
}
zeroForOne
指名了要用 currency0
兑换 currency1
,为 false
的话则反过来用 currency1
兑换 currency0
。amountSpecified
是指定的确定数额,正数表示输入,负数表示输出。sqrtPriceLimitX96
是滑点保护的限定价格。如果之前已经了解过 UniswapV3,那对这几个字段应该不陌生。
两个函数修饰器 noDelegateCall
和 onlyByLocker
,和之前文章介绍的一样,就不赘述了。
返回值 delta
,其组成里的两个数,正常情况下就是一个正数,一个负数。
接下来,看看函数体了。先看前面一段代码:
PoolId id = key.toId();
_checkPoolInitialized(id);
if (key.hooks.shouldCallBeforeSwap()) {
bytes4 selector = key.hooks.beforeSwap(msg.sender, key, params, hookData);
// Sentinel return value used to signify that a NoOp occurred.
if (key.hooks.isValidNoOpCall(selector)) retur