用JS实现与Swap合约的直接交互

1,798 阅读7分钟

前言

在前两篇已经基本了解Uniswap的工作原理,本篇将记录动手实践与Uniswap合约交互的过程。本文将使用ethers.js引用Uniswap sdk,实现与链上智能合约的直接交互,在测试链Goerli完成ETH到DAI的兑换,以及反向操作DAI兑换ETH 。

Uniswap SDK 的基本知识

  • ABI:

    • 为了允许其他人与智能合约进行交互,每个合约都会公开一个ABI(应用二进制接口)。由于这些ABI在区块链上定义,我们必须确保正确的定义被提供给我们的Javascript函数。ABI是从各种SDK中提供并根据需要进行导入。
  • Uniswap SDK

    • 合约代码分为core和Periphery两部分:

      • Core 实现某个交易的 Pair 的管理逻辑
      • Periphery 提供了与 Uniswap V2 进行交互的外围实现
  • Pair, Fetcher, Route, Trade等

    • 其概念和用法,可查阅Uniswap官方文档和开源代码,此处不再赘述。

环境准备

  1. node运行环境
  2. 安装uniswap sdk:npm install @uniswap/sdk
  3. 安装ethers.js: $ npm install ethers@5.7

交互操作一:查询代币的Swap价格

主要步骤

  1. 获取RPC节点,比如Alchemy,Infura等
  2. 获取查询的token在对应Chain上的合约地址
  3. 利用Fetcher获取链上数据信息

代码实现

const { ChainId, Fetcher, WETH, Route, Trade, TokenAmount, TradeType } = require ('@uniswap/sdk');
const ethers = require('ethers');  

//my goerli testnet RPC node
const url = '';

const customHttpProvider = new ethers.providers.JsonRpcProvider(url);
//goerli chain id
// const chainId = 5;
const chainId = ChainId.GÖRLI;

const tokenAddress = '0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60'; //DAI token address on goerli testnet

const init = async () => {
    const dai = await Fetcher.fetchTokenData(chainId, tokenAddress, customHttpProvider);
    const weth = WETH[chainId];
    const pair = await Fetcher.fetchPairData(dai, weth, customHttpProvider);
    const route = new Route([pair], weth);
    const trade = new Trade(route, new TokenAmount(weth, '100000000000000000'), TradeType.EXACT_INPUT);
    console.log("Execution Price WETH --> DAI:", trade.executionPrice.toSignificant(6));
    console.log("Mid Price after trade WETH --> DAI:", trade.nextMidPrice.toSignificant(6));
}

init();

运行结果

Execution Price WETH --> DAI: 374743000000000 Mid Price after trade WETH --> DAI: 280215000000000

由此查询结果可知,在Goerli测试链上已经创建过ETH和DAI的交易对Pair,并且已经添加过了流动性,接下来可以进行二者之间的swap操作。由于Uniswap路由合约做了ETH到WETH的包装,实际创建交易时用ETH和WETH应该都是可行的。如果想要swap的代币在链上没有被创建交易对,也没有添加过流动性,则不能直接发起swap操作。

交互操作二:用ETH兑换DAI

主要步骤

  1. 获取测试币:Goerli水龙头goerlifaucet.com/
  2. 配置私钥到secret文件
  3. 获取Router在对应Chain(Goerli)上的地址,可在这里查询到docs.uniswap.org/contracts/v…goerli.etherscan.io/address/0x7…
  4. 兑换的代币合约地址,DAI在Goerli上为:0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60

核心代码

async function swapETHForTokens(token1, token2, amount, slippage = "50") {

    try {
        //依次创建 pair,route和trade,
        //具体代码省略 ....
        
        //并通过trade获取能够swap的最小兑换金额amountOutMinHex
        const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw; // needs to be converted to e.g. hex
        const amountOutMinHex = ethers.BigNumber.from(amountOutMin.toString()).toHexString();
        
        //组装好参数,调用路由合约的swapExactETHForTokens方法
        const rawTxn = await UNISWAP_ROUTER_CONTRACT.populateTransaction.swapExactETHForTokens(amountOutMinHex, path, to, deadline, {
            value: valueHex
        })
    
        //发送交易
        let sendTxn = (await wallet).sendTransaction(rawTxn)

        //获得交易回执
        let reciept = (await sendTxn).wait()

    } catch(e) {
        console.log(e)
    }
}

//用0.002个ETH换尽可能多个DAI
swapETHForTokens(DAI, WETH[DAI.chainId], .002)

执行结果

根据交易哈希,可以在Goerli浏览器查询交易记录。

第一次执行结果

S1.png 0.002 WETH实际兑换到了DAI的数量:997772929302

思考:由于进行第一次swap后,这个交易对的流动资金池中流出了大量的DAI,导致DAI紧俏,相应的价格将会抬高,预测之后再进行WETH到DAI的兑换,等额WETH兑换到的DAI将减少。

为了验证猜想,马上进行第二次WETH到DAI的Swap操作。

S2.png

第二次执行结果

0.002 WETH换DAI:984352241472

第二次确实比第一次兑换的DAI数量减少了,结果符合预期。

说明:实际在主网上,WETH和DAI不会有这么大的价格差距,下图是通过Dapp查询的某时刻在以太主网上WETH兑换DAI的价格

A1.png

交互操作三:用DAI兑换ETH

依照葫芦画瓢,写了反向swap的代码

其他内容基本一致,最主要的是使用swapExactTokensForETH方法用token换取ETH

const rawTxn = await UNISWAP_ROUTER_CONTRACT.populateTransaction.swapExactTokensForETH(valueHex,amountOutMinHex,path,to,deadline);

但是反向过程没有那么顺利了,遇到一些问题:

问题一:遇到有歧义的错误提示

reason: 'execution reverted: UniswapV2Router: INVALID_PATH', code: 'UNPREDICTABLE_GAS_LIMIT', 这个错误提示有点歧义,不知道究竟是INVALID_PATH问题还是GAS_LIMIT问题。

先尝试了加GAS_LIMIT

即在下面这句补充gas限制:

const rawTxn = await UNISWAP_ROUTER_CONTRACT.populateTransaction.swapExactTokensForETH(valueHex,amountOutMinHex,path,to,deadline,{gasLimit: ethers.utils.parseUnits('200000', 'wei')});

问题二:添加Gas限制后再运行,仍然有关于INVALID_PATH的报错

添加Gas限制后再运行,仍然有关于INVALID_PATH的报错

reason: 'processing response error', code: 'SERVER_ERROR', body: '{"jsonrpc":"2.0","id":51,"error":{"code":3,"message":"execution reverted: UniswapV2Router: INVALID_PATH","data":"0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001d556e69737761705632526f757465723a20494e56414c49445f50415448000000"}}\n',

接下来排查关于INVALID_PATH的报错 查找官方文档对path的定义:

path: Token[] The full path from input token to output token.

也就是说path是有序的,这一点和定义Pair时不同,而我依葫芦画瓢写DAI swap WETH的时候忽略了path的顺序问题。调整path顺序后,再次执行。

问题三:Approve操作

经过前两次修改,发送的交易终于能在浏览器上查到了,但是却是执行失败的:

execution reverted: TransferHelper: TRANSFER_FROM_FAILED 经过搜索错误提示,发现很有可能是没有进行Approve操作。这导致swap交易提交上链了,但是在执行过程中会失败掉,白白消耗掉一些gas费用。

补充Approve操作代码:

async function apporve(amount) {
    try {
        const DAIABI = ['function approve(address spender, uint256 value) returns (bool)'];
        const DAIContract = new ethers.Contract('0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60', DAIABI, wallet);
        amount=amount.toString();
        const approveTx = await DAIContract.approve(UNISWAP_ROUTER_ADDRESS, ethers.utils.parseEther(amount));
        const approveReceipt = await approveTx.wait();
        console.log(approveReceipt);
    } catch(e) {
        console.log(e)
    }

}

执行Approve成功后,浏览器上也可以看到approve的记录。再执行DAI到WETH的swap操作,swap的DAI金额不能大于approve授予的金额,这样就可以成功了。

S3.png

反向兑换和之前用WETH兑换代币Token不同,在用swapExactETHForTokens方法前无需先授权(ETH原生币无需先授权),而在用DAI兑换ETH时,需要先对swap路由合约进行代币的approve再发起swap操作,这是因为DAI是一种ERC20代币,对ERC20代币进行金额转移之前,需要先对操作的合约地址进行授权。同理,在进行其他代币兑换操作,比如swapTokensForExactTokens操作时,也需要先进行授权,再执行swap。


参考资料

  1. How to Swap Tokens on Uniswap with Ethers.js www.quicknode.com/guides/defi…
  2. Uniswap v2 SDKdocs.uniswap.org/sdk/v2/over…
  3. Uniswap Githubgithub.com/Uniswap/v2-…
  4. Uniswap V2 SDK 学习笔记 kaai.dev/learn-unisw…