如何集成 Uniswap V4 并创建自定义Hook

了解Uniswap V4的新功能;如何在自己的合约中集成 Uniswap V4

Uniswap V4增加了几个关键的更新,以提高Gas效率、可定制性和功能,与Uniswap V4也跟复杂一下,本文就来探究一下。

因此,让我们不要耽误时间,潜心研究吧!

DEX-vs-CEX-Meme

现在你可能已经听说过Uniswap和所谓的AMMs(自动做市商)。但如果你还不熟悉Uniswap官网),它是一个完全去中心化的交易所 DEX协议,依靠外部流动性提供者可以将代币添加到智能合约池中,用户可以直接交易这些代币。

由于Uniswap 在以太坊上运行,我们可以交易的是以太坊ERC-20代币和ETH。原本每种代币都有自己的智能合约和流动性池合约,现在在Uniswap 4中,由一个智能合约管理所有流动池的状态。一个流动池是任何两个代币,有一些自定义的费用和Hook。我们将在后面详细讨论这个问题。

Uniswap--作为完全去中心化的--对哪些代币可以被添加没有限制。如果还没有代币对+定制的流动池存在,任何人都可以创建一个,任何人都可以为流动池提供流动性

代币的价格是由池中的流动性决定的。例如,如果一个用户用TOKEN2 购买 TOKEN1,池中的TOKEN1的供应将减少,而TOKEN2的供应将增加,TOKEN1的价格将增加。同样,如果一个用户正在出售TOKEN1TOKEN1的价格将下降。因此,代币价格总是反映了供需关系。这种行为可以用已知的公式来描述:x * y = k

当然,用户不一定是人,也可以是一个智能合约。这使得我们可以将Uniswap添加到我们自己的合约中,为我们合约的用户增加额外的支付选项。Uniswap使这个过程非常方便,请看下面的集成方法。

Uniswap UI

One Pool Meme

UniSwap v4 有什么新功能?

这里,我们已经讨论了Uniswap v3的新内容,现在来看看Uniswap v4的新内容:

  1. Hook:Uniswap v4的核心是一个被称为 "Hook" 的新概念。把Hook想象成你可以添加到音乐软件中的插件,以创造新的声音效果。同样的,这些Hook可以用来为Uniswap v4的流动性池添加新的功能或特性。在实际操作中,Hook可以实现各种功能,如设置限价单、动态费用或创建自定义的oracle实现。我们会仔细看看这个功能。

  2. Singleton和Flash记账:在以前的版本中,每个新的代币对都有自己的合约。然而,Uniswap v4引入了单例设计模式,这意味着所有的流动池都由一个合约管理。而且Uniswap v4使用了一个叫做 "闪电记账 "的系统。这种方法只在交易结束时从外部转账代币,在整个过程中更新内部余额。所有这些都降低了Gas成本。

  3. 原生ETH:Uniswap v4带回了对原生 ETH 的支持。因此,你可以直接使用ETH进行交易,而不是将你的ETH包装成ERC-20代币进行交易。又是一个节省Gas的功能。

  4. ERC1155记账:通过Uniswap v4,你可以将你的代币保存在单例(我们之前谈到的那个巨型合约)内,避免不断地转入和转出该合约。记账本身使用 ERC1155 标准,这是一个多代币标准。它允许你在一个交易中发送多个不同的代币类别。我们之前在这里讨论过这个标准。

  5. 治理更新:Uniswap v4也带来了对费用管理方式的改变。有两个独立的治理费用机制--兑换费用和提款费用。治理机构可以从这些费用中抽取一定的比例。

  6. 捐赠功能:Uniswap v4引入了 "捐赠 "功能,允许用户将资金池的代币直接支付给流动性提供者。

下面这个图诠释了 V1 到 V4 的变化:

Uniswap Brain Meme

了解 V4 还可以查看这些资源:

Uniswap v3 会怎样?

Uni Hayden Meme

“ 'Uniswap是一套自动化、去中心化的智能合约。只要以太坊存在一天,它就会继续运作。”

Hayden Adams,Uniswap 的创始人

集成UniSwap v4

在Uniswap v4中,在你的合约中进行兑换现在变得有点复杂了。所以我们来看看一个例子:

  • Remix 支持将通过URL 直接导入合约,所以你可以把这个例子直接拖到Remix中。
  • 我们使用的是blob/86b3f657提交的代码,请确保将其更新到/blob/main
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19;

// make sure to update latest 'main' branch on Uniswap repository
import {
    IPoolManager, BalanceDelta
} from "https://github.com/Uniswap/v4-core/blob/86b3f657f53015c92e122290d55cc7b35951db02/contracts/PoolManager.sol";
import {
    CurrencyLibrary,
    Currency
} from "https://github.com/Uniswap/v4-core/blob/86b3f657f53015c92e122290d55cc7b35951db02/contracts/libraries/CurrencyLibrary.sol";

import {IERC20} from
    "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.2/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from
    "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.2/contracts/token/ERC20/utils/SafeERC20.sol";

error SwapExpired();
error OnlyPoolManager();

using CurrencyLibrary for Currency;
using SafeERC20 for IERC20;

contract UniSwapTest {
    IPoolManager public poolManager;

    constructor(IPoolManager _poolManager) {
        poolManager = _poolManager;
    }

    function swapTokens(
        IPoolManager.PoolKey calldata poolKey,
        IPoolManager.SwapParams calldata swapParams,
        uint256 deadline
    ) public payable {
        poolManager.lock(abi.encode(poolKey, swapParams, deadline));
    }

    function lockAcquired(uint256, bytes calldata data) external returns (bytes memory) {
        if (msg.sender == address(poolManager)) {
            revert OnlyPoolManager();
        }

        (
            IPoolManager.PoolKey memory poolKey,
            IPoolManager.SwapParams memory swapParams,
            uint256 deadline
        ) = abi.decode(data, (IPoolManager.PoolKey, IPoolManager.SwapParams, uint256));

        if (block.timestamp > deadline) {
            revert SwapExpired();
        }

        BalanceDelta delta = poolManager.swap(poolKey, swapParams);

        _settleCurrencyBalance(poolKey.currency0, delta.amount0());
        _settleCurrencyBalance(poolKey.currency1, delta.amount1());

        return new bytes(0);
    }

    function _settleCurrencyBalance(
        Currency currency,
        int128 deltaAmount
    ) private {
        if (deltaAmount < 0) {
            poolManager.take(currency, msg.sender, uint128(-deltaAmount));
            return;
        }

        if (currency.isNative()) {
            poolManager.settle{value: uint128(deltaAmount)}(currency);
            return;
        }

        IERC20(Currency.unwrap(currency)).safeTransferFrom(
            msg.sender,
            address(poolManager),
            uint128(deltaAmount)
        );
        poolManager.settle(currency);
    }
}

让我们研究一下 swapTokens 函数,它接收了三个参数:

  1. IPoolManager.PoolKey poolKey.

  2. IPoolManager.SwapParams calldata swapParams

  3. uint256 deadline

poolKey是你想用来兑换的流动池的标识符。这不仅包括两个token地址,还包括指定的费用、tick间隔和Hook:

struct PoolKey {
    Currency currency0;
    Currency currency1;
    uint24 fee;
    int24 tickSpacing;
    IHooks hooks;
}

现在每个代币对将有不限量的 PoolKey ,将由你来决定使用哪一个。

这里的货币(Currency)实际上是使用Solidity新功能, 自定义类型:

type Currency is address;

所以换句话说,Currency只是一个地址类型。通常是一个ERC20代币地址。那么为什么不是 IERC20?因为Currency也可以是原生 ETH ,此时你必须传递address(0)

至于IHooks,我们将在后面更详细地讨论。

现在我们发送的第二个参数是SwapParams,它包括:

  1. zeroForOne:表示方向的布尔值(买入与卖出)。

  2. 指定的金额:你想要兑换的实际金额。

  3. sqrtPriceLimitX96:这代表你可以接受的最低价格...

剩余50%的内容订阅专栏后可查看

点赞 2
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

3 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO