1. 首页
  2. 技术

如何在合约中集成 Uniswap v3 ?

Uniswap v3 的新内容及集成
如果你还不熟悉Uniswap[4],它是一个去中心化的交易所(DEX),依靠外部流动性提供者将代币添加到流动池配对中,用户可以直接交易这些代币。
由于它在以太坊上运行,可以交易的是以太坊 ERC-20 代币。每种代币都有自己的智能合约和流动资金池。Uniswap–作为完全的去中心化–对哪些代币可以添加没有限制。如果一个代币对还没有流动池合约存在,任何人都可以 Uniswap 的工厂创建一个,任何人都可以向池子提供流动性。每笔交易有 0.3%的费用给流动性提供者作为奖励。
代币的价格是由池中的流动性决定的。例如,如果一个用户用TOKEN2购买TOKEN1,池中TOKEN1的供应将减少,而TOKEN2的供应将增加,TOKEN1的价格将增加。同样地,如果一个用户正在出售TOKEN1TOKEN1的价格将下降。因此,代币价格总是反映了供需关系。
当然,用户不一定是人,也可以是一个智能合约。这使得可以将 Uniswap 添加到我们自己的合约中,为我们合约的用户增加额外的支付选项。Uniswap 使这个过程非常方便,请看下面的整合方法。
如何在合约中集成 Uniswap v3 ?
Uniswap UI
UniSwap v3 中有什么新内容?
之前有一篇文章[5]讨论了 Uniswap v2 的新内容,现在让我们看看 Uniswap v3 的新内容。
  • 为流动性提供者提供的一个新功能,允许设置有效的价格范围。每当资金池价格在该范围之外时,他们的流动性就会被忽略。这不仅减少了流动性提供者的无常损失的风险,提高了资本效率…
  • 提供了不同的收费等级,由资金池的风险水平决定收费等级。有三个不同的级别:
  • 稳定币交易对:费率 0.05%,针对像 USDT/DAI 这样波动风险低的货币对。由于两者都是稳定币,这些潜在的无常损失是非常低的。这对交易者来说特别有趣,因为它将允许在稳定币之间进行非常便宜的兑换。
  • 中度风险对:费率 0.30%, 中等风险被认为是任何具有高交易量/主流的非相关货币对,主流货币对往往在波动性方面具有稍低的风险。
  • 高风险货币对:费率 1.00%,其他独特的货币对可以被视为流动性提供者的高风险,并产生最高的交易费用 1%。
  • 改进了 Uniswap v2 TWAP 预言机机制,一个链上调用就可以检索到过去 9 天的 TWAP 价格。为了实现这一点,不是只存储一个累积价格总和,而是将所有相关的价格存储在一个固定大小的数组中。这可以说稍微增加了 Gas 成本,但总的来说,对于大型预言机的增强是值得的。

进一步的 Uniswap v3 资源

  • v3 自 2021 年 5 月 5 日起在主网上运行[6]
  • 文件[7]
  • 白皮书[8]

整合 UniSwap v3

Uniswap 如此受欢迎的原因之一可能是将它们整合到自己的智能合约中的非常简单。比方说,你有一个系统,用户用 DAI 支付。有了 Uniswap,只需几行代码,你就可以增加他们也可以用 ETH 支付的选项。ETH 可以在实际逻辑之前自动转换为 DAI。它看起来像这样:

function pay(uint paymentAmountInDai) public payable {
if (msg.value > 0) {
convertEthToExactDai(paymentAmountInDai);
} else {
require(daiToken.transferFrom(msg.sender, address(this), paymentAmountInDai);
}
// do something with that DAI
...
}

在你的函数的开头做一个简单的检查就足够了。现在,对于convertEthToExactDai函数,它将看起来像这样的东西。

function convertEthToExactDai(uint256 daiAmount) external payable {
    require(daiAmount > 0, "Must pass non 0 DAI amount");
    require(msg.value > 0, "Must pass non 0 ETH amount");

    uint256 deadline = block.timestamp + 15; // using 'now' for convenience, for mainnet pass deadline from frontend!
    address tokenIn = WETH9;
    address tokenOut = multiDaiKovan;
    uint24 fee = 3000;
    address recipient = msg.sender;
    uint256 amountOut = daiAmount;
    uint256 amountInMaximum = msg.value;
    uint160 sqrtPriceLimitX96 = 0;

    ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams(
        tokenIn,
        tokenOut,
        fee,
        recipient,
        deadline,
        amountOut,
        amountInMaximum,
        sqrtPriceLimitX96
    );

    uniswapRouter.exactOutputSingle{ value: msg.value }(params);
    uniswapRouter.refundETH();

    // refund leftover ETH to user
    (bool success,) = msg.sender.call{ value: address(this).balance }("");
    require(success, "refund failed");
}

这里有几件事情需要解读。

  • Swap Router:SwapRouter 将是一个由 Uniswap 提供的包装合约,它有几个安全机制和便利功能。你可以使用ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564)为任何主网或测试网实例化它。接口代码可以在这里[9]找到。
  • WETH: 你可能注意到,我们在这里使用 ETH。在 Uniswap 中,不再有直接的 ETH 对,所有的 ETH 必须首先转换为 WETH(这是 ETH 包裹的 ERC-20)。在我们的案例中,这是由 SwapRouter 完成的。
  • exactOutputSingle[10]: 该函数可用于使用 ETH 并接收准确的代币数量。任何剩余的 ETH 将被退还,但不是自动! 我自己没有第一时间意识到这一点,ETH 最后在路由器合约中。所以不要忘记在兑换后调用uniswapRouter.refundETH()! 并确保你的合约中有一个回退函数来接收 ETH:receive() payable external {}deadline参数控制交易有效期。确保从你的前端传递这个 UNIX 时间戳,不要在合约内使用now
  • Refund(退款):一旦交易完成,我们可以将任何剩余的 ETH 返还给用户。这里将发送合约中的所有 ETH,所以如果你的合约可能因为其他原因有 ETH 余额,请确保改变这一点。
  • Fee(费用):这是一个不稳定的,但很受欢迎的货币对,所以我们在这里使用的费用是 0.3%(见上面的费用部分)。
  • sqrtPriceLimitX96。可用于确定互换不能超过的池子价格的限制。如果你把它设置为 0,它就被忽略了。

在前台使用 V3

我们现在遇到的一个问题是,当用户调用支付函数并想用 ETH 支付时,不知道他需要多少 ETH。我们可以使用quoteExactOutputSingle[11]函数来精确计算:

function getEstimatedETHforDAI(uint daiAmount) external payable returns (uint256) {
   address tokenIn = WETH9;
   address tokenOut = multiDaiKovan;
   uint24 fee = 500;
   uint160 sqrtPriceLimitX96 = 0;
   return quoter.quoteExactOutputSingle(
   tokenIn,
   tokenOut,
   fee,
   daiAmount,
   sqrtPriceLimitX96
   );

}

但是请注意,我们没有把它声明为视图函数,但是不要在链上调用这个函数。尽管它可以作为一个视图函数来调用的,但它会采用非视图方式(底层)来获得计算结果。由于 Solidity 的特性,所以这里也不可能将它本身声明为一个视图函数,仅能使用场景如 Web3 的 call()[12]功能来读取前端的结果。

现在我们可以在前端调用getEstimatedETHforDAI。为了确保我们发送了足够的 ETH,并且交易不会被退回,我们可以将估计的 ETH 数量增加一点。

const requiredEth = (await myContract.getEstimatedETHforDAI(daiAmount).call())[0];
const sendEth = requiredEth * 1.1;

如果没有直接兑换流动池怎么办?

在这种情况下,你可以使用exactInput[13]和exactOutput[14]函数,它以path为参数。这个路径是代币地址的字节编码数据(为了 Gas 效率而编码)。

任何兑换都需要有一个开始和结束的路径。虽然在 Uniswap 中,你可以有代币 1 到代币 2 的兑换,但不一定能保证这样一个池子真的存在。但是,只要你能找到一条路径,你仍然可以交易它们,例如,Token1 → Token2 → WETH → Token3。在这种情况下,你仍然可以用 Token1 换 Token3,只是比直接兑换要多花一点 gas。

在下边你可以看到[Uniswap 示例代码](https://soliditydeveloper.com/path frontend: https://github.com/Uniswap/uniswap-v3-periphery/blob/9ca9575d09b0b8d985cc4d9a0f689f7a4470ecb7/test/shared/path.ts “Uniswap 示例代码”),了解如何在前端计算这个路径:

function encodePath(tokenAddresses, fees) {
  const FEE_SIZE = 3

  if (path.length != fees.length + 1) {
    throw new Error('path/fee lengths do not match')
  }

  let encoded = '0x'
  for (let i = 0; i < fees.length; i++) {
    // 20 byte encoding of the address
    encoded += path[i].slice(2)
    // 3 byte encoding of the fee
    encoded += fees[i].toString(16).padStart(2 * FEE_SIZE, '0')
  }
  // encode the final token
  encoded += path[path.length - 1].slice(2)

  return encoded.toLowerCase()
}

为 Remix 提供完整的工作实例

这里有一个完全可用的例子,你可以直接在 Remix 上使用。它允许你用 ETH 交易Multi-collaterized Kovan DAI[15],它还包括exactOutputSingle[16]的替代方案,即exactInputSingle[17],允许你用 ETH 换取多少 DAI,你就能得到多少。

// SPDX-License-Identifier: MIT
pragma solidity =0.7.6;
pragma abicoder v2;

import "https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/interfaces/ISwapRouter.sol";
import "https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/interfaces/IQuoter.sol";

interface IUniswapRouter is ISwapRouter {
    function refundETH() external payable;
}

contract Uniswap3 {
  IUniswapRouter public constant uniswapRouter = IUniswapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
  IQuoter public constant quoter = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6);
  address private constant multiDaiKovan = 0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa;
  address private constant WETH9 = 0xd0A1E359811322d97991E03f863a0C30C2cF029C;

  function convertExactEthToDai() external payable {
    require(msg.value > 0, "Must pass non 0 ETH amount");

    uint256 deadline = block.timestamp + 15; // using 'now' for convenience, for mainnet pass deadline from frontend!
    address tokenIn = WETH9;
    address tokenOut = multiDaiKovan;
    uint24 fee = 3000;
    address recipient = msg.sender;
    uint256 amountIn = msg.value;
    uint256 amountOutMinimum = 1;
    uint160 sqrtPriceLimitX96 = 0;

    ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams(
        tokenIn,
        tokenOut,
        fee,
        recipient,
        deadline,
        amountIn,
        amountOutMinimum,
        sqrtPriceLimitX96
    );

    uniswapRouter.exactInputSingle{ value: msg.value }(params);
    uniswapRouter.refundETH();

    // refund leftover ETH to user
    (bool success,) = msg.sender.call{ value: address(this).balance }("");
    require(success, "refund failed");
  }

  function convertEthToExactDai(uint256 daiAmount) external payable {
    require(daiAmount > 0, "Must pass non 0 DAI amount");
    require(msg.value > 0, "Must pass non 0 ETH amount");

    uint256 deadline = block.timestamp + 15; // using 'now' for convenience, for mainnet pass deadline from frontend!
    address tokenIn = WETH9;
    address tokenOut = multiDaiKovan;
    uint24 fee = 3000;
    address recipient = msg.sender;
    uint256 amountOut = daiAmount;
    uint256 amountInMaximum = msg.value;
    uint160 sqrtPriceLimitX96 = 0;

    ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams(
        tokenIn,
        tokenOut,
        fee,
        recipient,
        deadline,
        amountOut,
        amountInMaximum,
        sqrtPriceLimitX96
    );

    uniswapRouter.exactOutputSingle{ value: msg.value }(params);
    uniswapRouter.refundETH();

    // refund leftover ETH to user
    (bool success,) = msg.sender.call{ value: address(this).balance }("");
    require(success, "refund failed");
  }

  // do not used on-chain, gas inefficient!
  function getEstimatedETHforDAI(uint daiAmount) external payable returns (uint256) {
    address tokenIn = WETH9;
    address tokenOut = multiDaiKovan;
    uint24 fee = 3000;
    uint160 sqrtPriceLimitX96 = 0;

    return quoter.quoteExactOutputSingle(
        tokenIn,
        tokenOut,
        fee,
        daiAmount,
        sqrtPriceLimitX96
    );
  }

  // important to receive ETH
  receive() payable external {}
}

ExactInput 和 ExactOutput 的区别

一旦你执行这些函数并在 Etherscan 中查看它们,区别就会立即变得很明显。这里我们是用 exactOutput 进行交易。我们提供 1 个 ETH,希望收到 100 个 DAI 作为回报。任何多余的 ETH 都会退还给我们。

如何在合约中集成 Uniswap v3 ?
以准确的DAI购买
而下面,我们正在使用 exactInput 进行交易。我们提供 1 个 ETH,并希望得到多少 DAI,而这恰好是 196 个 DAI。
如何在合约中集成 Uniswap v3 ?
用精确的ETH购买
请注意,如果你困惑为什么价格会如此不同,这是测试网的一个小池子,第一个交易严重影响了池子里的价格。没有多少人在测试网中进行套利交易 🙂

该内容来自于互联网公开内容,非区块链原创内容,如若转载,请注明出处:https://htzkw.com/archives/33978

联系我们

aliyinhang@gmail.com