如何在合约中集成 Uniswap v3

如何在合约中集成 Uniswap v3

Uniswap v3的新内容及集成

如果你还不熟悉Uniswap,它是一个去中心化的交易所(DEX),依靠外部流动性提供者将代币添加到流动池配对中,用户可以直接交易这些代币。

由于它在以太坊上运行,可以交易的是以太坊ERC-20代币。每种代币都有自己的智能合约和流动资金池。Uniswap--作为完全的去中心化--对哪些代币可以添加没有限制。如果一个代币对还没有流动池合约存在,任何人都可以Uniswap的工厂创建一个,任何人都可以向池子提供流动性。每笔交易有0.3%的费用给流动性提供者作为奖励。

代币的价格是由池中的流动性决定的。例如,如果一个用户用TOKEN2购买TOKEN1,池中TOKEN1的供应将减少,而TOKEN2的供应将增加,TOKEN1的价格将增加。同样地,如果一个用户正在出售TOKEN1TOKEN1的价格将下降。因此,代币价格总是反映了供需关系。

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

Uniswap UI

UniSwap v3 中有什么新内容?

之前有一篇文章讨论了Uniswap v2的新内容,现在让我们看看Uniswap v3的新内容。

  • 为流动性提供者提供的一个新功能,允许设置有效的价格范围。每当资金池价格在该范围之外时,他们的流动性就会被忽略。这不仅减少了流动性提供者的无常损失的风险,提高了资本效率...
  • 提供了不同的收费等级,由资金池的风险水平决定收费等级。有三个不同的级别:

    • 稳定币交易对:费率0.05%,针对像USDT/DAI这样波动风险低的货币对。由于两者都是稳定币,这些潜在的无常损失是非常低的。这对交易者来说特别有趣,因为它将允许在稳定币之间进行非常便宜的兑换。
    • 中度风险对:费率0.30%, 中等风险被认为是任何具有高交易量/主流的非相关货币对,主流货币对往往在波动性方面具有稍低的风险。
    • 高风险货币对:费率1.00%,其他独特的货币对可以被视为流动性提供者的高风险,并产生最高的交易费用1%。
  • 改进了Uniswap v2 TWAP预言机机制,一个链上调用就可以检索到过去9天的TWAP价格。为了实现这一点,不是只存储一个累积价格总和,而是将所有相关的价格存储在一个固定大小的数组中。这可以说稍微增加了Gas成本,但总的来说,对于大型预言机的增强是值得的。

进一步的Uniswap v3资源

整合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)为任何主网或测试网实例化它。接口代码可以在这里找到。
  • WETH: 你可能注意到,我们在这里使用ETH。在Uniswap中,不再有直接的ETH对,所有的ETH必须首先转换为WETH(这是ETH包裹的ERC-20)。在我们的案例中,这是由SwapRouter完成的。
  • exactOutputSingle: 该函数可用于使用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函数来精确计算:

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() 功能来读取前端的结果。

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

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

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

在这种情况下,你可以使用exactInputexactOutput函数,它以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),了解如何在前端计算这个路径

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,它还包括exactOutputSingle的替代方案,即exactInputSingle,允许你用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都会退还给我们。

以准确的DAI购买

而下面,我们正在使用exactInput进行交易。我们提供1个ETH,并希望得到多少DAI,而这恰好是196个DAI。

用精确的ETH购买

请注意,如果你困惑为什么价格会如此不同,这是测试网的一个小池子,第一个交易严重影响了池子里的价格。没有多少人在测试网中进行套利交易 :)


本翻译由 Cell Network 赞助支持。

  • 发表于 2021-06-03 10:36
  • 阅读 ( 1805 )
  • 学分 ( 66 )
  • 分类:Uniswap

0 条评论

请先 登录 后评论
翻译小组
翻译小组

首席翻译官

75 篇文章, 11946 学分