前言
在前两篇已经基本了解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官方文档和开源代码,此处不再赘述。
环境准备
- node运行环境
- 安装uniswap sdk:npm install @uniswap/sdk
- 安装ethers.js:
$ npm install ethers@5.7
交互操作一:查询代币的Swap价格
主要步骤
- 获取RPC节点,比如Alchemy,Infura等
- 获取查询的token在对应Chain上的合约地址
- 利用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
主要步骤
- 获取测试币:Goerli水龙头goerlifaucet.com/
- 配置私钥到secret文件
- 获取Router在对应Chain(Goerli)上的地址,可在这里查询到docs.uniswap.org/contracts/v…goerli.etherscan.io/address/0x7…
- 兑换的代币合约地址,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浏览器查询交易记录。
第一次执行结果
0.002 WETH实际兑换到了DAI的数量:997772929302
思考:由于进行第一次swap后,这个交易对的流动资金池中流出了大量的DAI,导致DAI紧俏,相应的价格将会抬高,预测之后再进行WETH到DAI的兑换,等额WETH兑换到的DAI将减少。
为了验证猜想,马上进行第二次WETH到DAI的Swap操作。
第二次执行结果
0.002 WETH换DAI:984352241472
第二次确实比第一次兑换的DAI数量减少了,结果符合预期。
说明:实际在主网上,WETH和DAI不会有这么大的价格差距,下图是通过Dapp查询的某时刻在以太主网上WETH兑换DAI的价格
交互操作三:用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授予的金额,这样就可以成功了。
反向兑换和之前用WETH兑换代币Token不同,在用swapExactETHForTokens方法前无需先授权(ETH原生币无需先授权),而在用DAI兑换ETH时,需要先对swap路由合约进行代币的approve再发起swap操作,这是因为DAI是一种ERC20代币,对ERC20代币进行金额转移之前,需要先对操作的合约地址进行授权。同理,在进行其他代币兑换操作,比如swapTokensForExactTokens操作时,也需要先进行授权,再执行swap。
参考资料
- How to Swap Tokens on Uniswap with Ethers.js www.quicknode.com/guides/defi…
- Uniswap v2 SDK:docs.uniswap.org/sdk/v2/over…
- Uniswap Github :github.com/Uniswap/v2-…
- Uniswap V2 SDK 学习笔记 kaai.dev/learn-unisw…