第一笔交易
\[ \]

第一笔交易 #

现在我们已经有了流动性,我们可以进行我们的第一笔交易了!

计算交易数量 #

首先,我们需要知道如何计算交易出入的数量。同样,我们在这小节中也会硬编码我们希望交易的 USDC 数额,这里我们选择 42,也即花费 42 USDC 购买 ETH。

在决定了我们希望投入的资金量之后,我们需要计算我们会获得多少 token。在 Uniswap V2 中,我们会使用当前池子的资产数量来计算,但是在 V3 中我们有 $L$ 和 $\sqrt{P}$,并且我们知道在交易过程中,$L$ 保持不变而只有 $\sqrt{P}$ 发生变化(当在同一区间内进行交易时,V3 的表现和 V2 一致)。我们还知道如下公式:

$$L = \frac{\Delta y}{\Delta \sqrt{P}}$$

并且,在这里我们知道了$\Delta y$。它正是我们希望投入的 42 USDC。因此,我们可以根据公式得出投入的 42 USDC 会对价格造成多少影响:

$$\Delta \sqrt{P} = \frac{\Delta y}{L}$$

在 V3 中,我们得到了我们交易导致的价格变动(回忆一下,交易使得现价沿着曲线移动)。知道了目标价格(target price),合约可以计算出投入 token 的数量和输出 token 的数量。

我们将数字代入上述公式:

$$\Delta \sqrt{P} = \frac{42 \enspace USDC}{1517882343751509868544} = 2192253463713690532467206957$$

把差价加到现在的 $\sqrt{P}$,我们就能得到目标价格:

$$\sqrt{P_{target}} = \sqrt{P_{current}} + \Delta \sqrt{P}$$

$$\sqrt{P_{target}} = 5604469350942327889444743441197$$

在 Python 中进行相应计算:

amount_in = 42 * eth
price_diff = (amount_in * q96) // liq
price_next = sqrtp_cur + price_diff
print("New price:", (price_next / q96) ** 2)
print("New sqrtP:", price_next)
print("New tick:", price_to_tick((price_next / q96) ** 2))
# New price: 5003.913912782393
# New sqrtP: 5604469350942327889444743441197
# New tick: 85184

知道了目标价格,我们就能计算出投入 token 的数量和获得 token 的数量:

$$ x = \frac{L(\sqrt{p_b}-\sqrt{p_a})}{\sqrt{p_b}\sqrt{p_a}}$$ $$ y = L(\sqrt{p_b}-\sqrt{p_a}) $$

用Python:

amount_in = calc_amount1(liq, price_next, sqrtp_cur)
amount_out = calc_amount0(liq, price_next, sqrtp_cur)

print("USDC in:", amount_in / eth)
print("ETH out:", amount_out / eth)
# USDC in: 42.0
# ETH out: 0.008396714242162444

我们使用另一个公式验证一下:

$$\Delta x = \Delta \frac{1}{\sqrt{P}} L$$

使用上述公式,在知道价格变动和流动性数量的情况下,我们能求出我们购买了多少 ETH,也即 $\Delta x$。一个需要注意的点是: $\Delta \frac{1}{\sqrt{P}}$ 不等于 $\frac{1}{\Delta \sqrt{P}}$!前者才是 ETH 价格的变动,并且能够用如下公式计算:

$$\Delta \frac{1}{\sqrt{P}} = \frac{1}{\sqrt{P_{target}}} - \frac{1}{\sqrt{P_{current}}}$$

我们知道了公式里面的所有数值,接下来将其带入即可(可能会在显示上有些问题):

$$\Delta \frac{1}{\sqrt{P}} = \frac{1}{5604469350942327889444743441197} - \frac{1}{5602277097478614198912276234240}$$

$$\Delta \frac{1}{\sqrt{P}} = -0.00000553186106731426$$

接下来计算 $\Delta x$:

$$\Delta x = -0.00000553186106731426 * 1517882343751509868544 = -8396714242162698 $$

即 0.008396714242162698 ETH,这与我们第一次算出来的数量非常接近!注意到这个结果是负数,因为我们是从池子中提出 ETH。

实现swap #

交易在 swap 函数中实现:

function swap(address recipient)
    public
    returns (int256 amount0, int256 amount1)
{
    ...

此时,它仅仅接受一个 recipient 参数,即提出 token 的接收者。

首先,我们需要计算出目标价格和对应 tick,以及 token 的数量。同样,我们将会在这里硬编码我们之前计算出来的值:

...
int24 nextTick = 85184;
uint160 nextPrice = 5604469350942327889444743441197;

amount0 = -0.008396714242162444 ether;
amount1 = 42 ether;
...

接下来,我们需要更新现在的 tick 和对应的 sqrtP

...
(slot0.tick, slot0.sqrtPriceX96) = (nextTick, nextPrice);
...

然后,合约把对应的 token 发送给 recipient 并且让调用者将需要的 token 转移到本合约:

...
IERC20(token0).transfer(recipient, uint256(-amount0));

uint256 balance1Before = balance1();
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(
    amount0,
    amount1
);
if (balance1Before + uint256(amount1) < balance1())
    revert InsufficientInputAmount();
...

我们使用 callback 函数来将控制流转移到调用者,让它转入 token,之后我们需要通过检查确认 caller 转入了正确的数额。

最后,合约释放出一个 swap 事件,使得该笔交易能够被监听到。这个事件包含了所有有关这笔交易的信息:

...
emit Swap(
    msg.sender,
    recipient,
    amount0,
    amount1,
    slot0.sqrtPriceX96,
    liquidity,
    slot0.tick
);

这样就完成了。这个函数的功能仅仅是将一些 token 发送到了指定的接收地址,并且从调用者处接受一定数量的另一种 token。随着本书的进展,这个函数会变得越来越复杂。

测试交易 #

现在,我们来测试 swap 函数。在相同的测试文件中(即 UniswapV3Pool.t.sol),创建 testSwapBuyEth 函数并进行初始化设置。准备阶段的参数与 testMintSuccess 一致:

function testSwapBuyEth() public {
    TestCaseParams memory params = TestCaseParams({
        wethBalance: 1 ether,
        usdcBalance: 5000 ether,
        currentTick: 85176,
        lowerTick: 84222,
        upperTick: 86129,
        liquidity: 1517882343751509868544,
        currentSqrtP: 5602277097478614198912276234240,
        shouldTransferInCallback: true,
        mintLiqudity: true
    });
    (uint256 poolBalance0, uint256 poolBalance1) = setupTestCase(params);

    ...

我们不会测试流动性是否正确添加到了池子里,因为之前已经有过针对此功能的测试样例了。

在测试中,我们需要 42 USDC:

token1.mint(address(this), 42 ether);

交易之前,我们还需要实现 callback 函数,来确保能够将钱转给池子合约:

function uniswapV3SwapCallback(int256 amount0, int256 amount1) public {
    if (amount0 > 0) {
        token0.transfer(msg.sender, uint256(amount0));
    }

    if (amount1 > 0) {
        token1.transfer(msg.sender, uint256(amount1));
    }
}

由于交易中的数额可以为正或负(从池子中拿走的数量),在 callback 中我们只发出数额为正的对应 token,也即我们希望卖出的 token。

现在我们可以调用 swap 了:

(int256 amount0Delta, int256 amount1Delta) = pool.swap(address(this));

函数返回了在本次交易中涉及到的两种 token 数量,我们需要验证一下它们是否正确:

assertEq(amount0Delta, -0.008396714242162444 ether, "invalid ETH out");
assertEq(amount1Delta, 42 ether, "invalid USDC in");

接下来,我们需要验证 token 的确从调用者(即本测试合约)处转出:

assertEq(
    token0.balanceOf(address(this)),
    uint256(userBalance0Before - amount0Delta),
    "invalid user ETH balance"
);
assertEq(
    token1.balanceOf(address(this)),
    0,
    "invalid user USDC balance"
);

并且被发送到了池子合约中:

assertEq(
    token0.balanceOf(address(pool)),
    uint256(int256(poolBalance0) + amount0Delta),
    "invalid pool ETH balance"
);
assertEq(
    token1.balanceOf(address(pool)),
    uint256(int256(poolBalance1) + amount1Delta),
    "invalid pool USDC balance"
);

最后,我们验证池子的状态是否正确更新:

(uint160 sqrtPriceX96, int24 tick) = pool.slot0();
assertEq(
    sqrtPriceX96,
    5604469350942327889444743441197,
    "invalid current sqrtP"
);
assertEq(tick, 85184, "invalid current tick");
assertEq(
    pool.liquidity(),
    1517882343751509868544,
    "invalid current liquidity"
);

注意到,在这里交易并没有改变池子流动性——在后面的某个章节,我们会看到它将如何改变

练习 #

写一个测试样例,失败并报错 InsufficientInputAmount。要记得,这里还有一个隐藏的bug🙂