Uniswap V2 SDK 学习笔记
date
May 26, 2022
slug
learn-uniswap-v2-sdk
status
Published
tags
DeFi
ethers.js
summary
type
Post
前言合约功能简介用 SDK 做什么环境配置合约函数调用:SwapswapExactETHForTokens参数列表创建 Provider 实例创建合约对象提供 Token 信息获取 Pair 信息创建路径 route创建交易 trade计算参数发送交易完整代码swapExactTokensForETH参数列表照葫芦画瓢授权完整代码补充:查询额度合约函数调用:LP参考资料
前言
Uniswap V3 Release FAQ 里提供的在 URL 后加 “?use=V2” 的方法已经行不通。
现在想要访问 Uniswap V2 UI 可以使用这个链接:
合约功能简介
Uniswap V2 的合约代码分两部分:Core 实现某个交易的 Pair 的管理逻辑,Periphery 提供了与 Uniswap V2 进行交互的外围合约实现路由,即一个或者多个交易对的兑换逻辑。
Core 主要有以下合约
- UniswapV2ERC20:UNI-V2 代币合约,即 LP Token 合约。
- UniswapV2Factory:工厂合约,用来部署配对合约。
- UniswapV2Pair:配对合约,管理着流动性资金池,不同币对有着不同的配对合约实例,比如 USDT-WETH 这一个币对,就对应一个配对合约实例,DAI-WETH 又对应另一个配对合约实例。配对合约继承了 UniswapV2ERC20 合约,即配对合约同时也是 LP Token 合约。
Periphery 主要有以下合约
- UniswapV2Migrator:迁移合约,用于将 V1 的流动性迁移到 V2
- UniswapV2Router01:旧版路由合约,已弃用
- UniswapV2Router02:新版路由合约
路由合约是与用户进行交互的入口,主要提供了添加流动性、移除流动性和兑换的系列接口,并提供了几个查询接口。

用 SDK 做什么
文档 SDK Quick start 章节:
Uniswap SDK 的存在是为了帮助开发者在 Uniswap 的基础上进行开发。它被设计成可以在任何可以执行 JavaScript 的环境中运行(如网站、Node脚本等)…
所以 Uniswap SDK 是一个同构 (Isomorphic) 的库,既可以在客户端使用也可以在服务端使用。
文档 Trade 章节:
SDK不能代表您执行或发送交易。相反,它提供了实用的类和函数,使您可以轻松计算出安全地与 Uniswap 交互所需要的数据。几乎所有您需要与 Uniswap 进行安全交易的数据都由 Trade 类 提供。然而,使用这些数据来发送交易是你的责任,在任何对你的应用程序有意义的情况下。
这篇笔记主要记录在服务端上使用 Uniswap SDK,通过 ethers.js 调用路由合约函数实现与 Uniswap 交互。
环境配置
$ mkdir univ2 && cd univ2
$ npm init -y
$ npm i @uniswap/sdk ethers dotenv
# 在 .env 文件里填好节点信息和私钥
合约函数调用:Swap
实现 swap 功能的接口有 9 个:
- swapExactTokensForTokens:用 ERC20 兑换 ERC20,但支付的数量是指定的,而兑换回的数量则是未确定的
- swapTokensForExactTokens:也是用 ERC20 兑换 ERC20,与上一个函数不同,指定的是兑换回的数量
- swapExactETHForTokens:指定 ETH 数量兑换 ERC20
- swapTokensForExactETH:用 ERC20 兑换成指定数量的 ETH
- swapExactTokensForETH:用指定数量的 ERC20 兑换 ETH
- swapETHForExactTokens:用 ETH 兑换指定数量的 ERC20
- swapExactTokensForTokensSupportingFeeOnTransferTokens:指定数量的 ERC20 兑换 ERC20,支持转账时扣费
- swapExactETHForTokensSupportingFeeOnTransferTokens:指定数量的 ETH 兑换 ERC20,支持转账时扣费
- swapExactTokensForETHSupportingFeeOnTransferTokens:指定数量的 ERC20 兑换 ETH,支持转账时扣费
下面从最简单的不需 Approve 操作的 swapExactETHForTokens 讲起:
swapExactETHForTokens
实际上,Uniswap V2 的 pair 在内部都使用 WETH,但路由合约可以帮我们解决 ETH → WETH 包装的问题,所以我们调用函数时可以直接发送 ETH。
目标:在 Rinkeby 测试网上将 0.003 ETH 兑换为尽可能多的 LINK.
参数列表
function swapExactETHForTokens(
uint amountOutMin, // 交易获得代币最小值
address[] calldata path, // 交易路径列表
address to, // 交易获得的 token 发送到的地址
uint deadline // 过期时间
) external virtual override payable ensure(deadline) returns (
uint[] memory amounts // 交易期望数量列表
){
...
}
path, to, deadline 都很好解决,问题就是如何求出 amountOutMin.
SDK 提供了方便的类和函数来帮助我们计算。
创建 Provider 实例
// import { ethers } from 'ethers'
// import 'dotenv/config'
const rpcurl = `https://rinkeby.infura.io/v3/${process.env.INFURA_PROJECT_ID}`;
const provider = new ethers.providers.JsonRpcProvider(rpcurl);
const signer = new ethers.Wallet(process.env.PRIVATE_KEY);
const account = signer.connect(provider);
创建合约对象
Router02 在主网和测试网上的合约地址都是一样的。
// import { ethers } from 'ethers'
const uniV2ABI = ['function swapExactETHForTokens(uint amountOutMin, address[] calldata path, \
address to, uint deadline) external payable returns (uint[] memory amounts)'];
const uniswapContract = new ethers.Contract('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', uniV2ABI, account);
提供 Token 信息
// import { ChainId, Token } from '@uniswap/sdk'
const WETH = new Token(ChainId.RINKEBY, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18);
const LINK = new Token(ChainId.RINKEBY, '0x01BE23585060835E02B77ef475b0Cc51aA1e0709', 18);
获取 Pair 信息
Fetcher.fetchPairData(tokenA: Token, tokenB: Token, provider?: ethers.providers.BaseProvider): Promise<Pair>
// import { Fetcher } from '@uniswap/sdk'
const pair = await Fetcher.fetchPairData(LINK, WETH, provider);
创建路径 route
new Route(pairs: Pair[], input: Currency, output?: Currency): Route
本例的路径(Pair 数组)就是最简单的一个单独的 WETH → LINK Pair.
// import { Route } from '@uniswap/sdk'
const route = new Route([pair], WETH);
创建交易 trade
new Trade(route: Route, amount: CurrencyAmount, tradeType: TradeType): Trade
new TokenAmount(token: Token, amount: BigintIsh): TokenAmount
TokenAmount 继承了 CurrencyAmount 类。
因为我们要将 Exact ETH 换成其他 Token, 所以
tradeType
使用 TradeType.EXACT_INPUT
// import { TokenAmount, Trade, TradeType } from '@uniswap/sdk'
// import { ethers } from 'ethers'
const trade = new Trade(route, new TokenAmount(WETH, ethers.utils.parseEther('0.003')), TradeType.EXACT_INPUT);
计算参数
就像点击 Uniswap 前端界面“齿轮”图标后看到的那样,要为交易设置滑点容差。
// import { Percent } from '@uniswap/sdk'
const slippageTolerance = new Percent('50', '10000'); // 50 / 10000 = 0.50%
然后调用
trade.minimumAmountOut()
方法就得到了amountOutMin
.const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw;
// 官方文档注释:needs to be converted to e.g. hex
发送交易
参考 Uniswap Tutorial for Developers (Solidity & Javascript),但视频里的代码有点问题。
如文档所说,raw 是一个 JSBI 对象,要转换后才可以塞进调用swap函数时填的参数里,实测并不一定必须转成16进制字符串表示,普通字符串即可。
console.log(amountOutMin)
console.log("String", String(amountOutMin))
console.log("toString", amountOutMin.toString())
console.log("toHexString", ethers.BigNumber.from(amountOutMin.toString()).toHexString())
/*
JSBI(3) [ 672895231, 1055274567, 18, sign: false ]
String 21885679521987531007
toString 21885679521987531007
toHexString 0x012fb98d91e81b90ff
*/
const path = [WETH.address, LINK.address];
const to = '0x...' // PRIVATE_KEY's Address
const deadline = Math.floor(Date.now() / 1000) + 60 * 20 // 20 minutes from the current Unix time
const value = trade.inputAmount.raw; // 随交易同时发送的 ETH
const tx = await uniswapContract.swapExactETHForTokens(amountOutMin.toString(), path, to, deadline, {
value: value.toString()
});
console.log(`Transaction hash: ${tx.hash}`);
const receipt = await tx.wait();
console.log(receipt);
完整代码
包含了一些调试语句。
// swapExactETHForTokens
import { ChainId, Token, Fetcher, Pair, TokenAmount, Route, Trade, TradeType, Percent } from '@uniswap/sdk'
import { ethers } from 'ethers'
import 'dotenv/config'
const rpcurl = `https://rinkeby.infura.io/v3/${process.env.INFURA_PROJECT_ID}`;
const provider = new ethers.providers.JsonRpcProvider(rpcurl);
const signer = new ethers.Wallet(process.env.PRIVATE_KEY);
const account = signer.connect(provider);
const WETH = new Token(ChainId.RINKEBY, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18);
const LINK = new Token(ChainId.RINKEBY, '0x01BE23585060835E02B77ef475b0Cc51aA1e0709', 18);
const uniV2ABI = ['function swapExactETHForTokens(uint amountOutMin, address[] calldata path, \
address to, uint deadline) external payable returns (uint[] memory amounts)'];
const uniswapContract = new ethers.Contract('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', uniV2ABI, account);
const run = async () => {
const pair = await Fetcher.fetchPairData(LINK, WETH, provider);
// input: WETH
const route = new Route([pair], WETH);
// 1 WETH = ??? LINK
// toSignificant(6) 保留6位有效数字
console.log(route.midPrice.numerator.toString());
console.log(route.midPrice.denominator.toString());
console.log('WETH-LINK', route.midPrice.toSignificant(6));
// 1 LINK = ??? WETH
console.log(route.midPrice.invert().numerator.toString());
console.log(route.midPrice.invert().denominator.toString());
console.log('LINK-WETH', route.midPrice.invert().toSignificant(6));
const trade = new Trade(route, new TokenAmount(WETH, ethers.utils.parseEther('0.003')), TradeType.EXACT_INPUT);
console.log(trade.executionPrice.toSignificant(6));
const slippageTolerance = new Percent('50', '10000');
const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw;
// console.log(amountOutMin.toString())
const path = [WETH.address, LINK.address];
const to = '0x...' // PRIVATE_KEY's Address, 或者随便一个地址用来接收
const deadline = Math.floor(Date.now() / 1000) + 60 * 20 // 20 minutes from the current Unix time
const value = trade.inputAmount.raw;
console.log(value.toString())
const tx = await uniswapContract.swapExactETHForTokens(amountOutMin.toString(), path, to, deadline, {
value: value.toString(),
// maxFeePerGas: ethers.utils.parseUnits('2','gwei'),
// maxPriorityFeePerGas: ethers.utils.parseUnits('2','gwei'),
});
console.log(`Transaction hash: ${tx.hash}`);
const receipt = await tx.wait();
console.log(receipt);
}
run();
swapExactTokensForETH
接下来看如何将 Exact 数量的 Token 换成 ETH.
目标:将 10 LINK 兑换为尽可能多的 ETH.
参数列表
function swapExactTokensForETH(
uint amountIn,// 交易支付代币数量
uint amountOutMin, // 交易获得代币最小值
address[] calldata path, // 交易路径列表
address to, // 交易获得的 token 发送到的地址
uint deadline // 过期时间
) external virtual override ensure(deadline) returns (
uint[] memory amounts // 交易期望数量列表
){
...
}
照葫芦画瓢
const uniV2ABI = ['function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, \
address to, uint deadline) external returns (uint[] memory amounts)'];
const uniswapContract = new ethers.Contract('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', uniV2ABI, account);
...
const pair = await Fetcher.fetchPairData(LINK, WETH, provider);
const route = new Route([pair], LINK);
const trade = new Trade(route, new TokenAmount(LINK, ethers.utils.parseEther('10')), TradeType.EXACT_INPUT);
const slippageTolerance = new Percent('50', '10000');
const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw;
const path = [LINK.address, WETH.address];
const to = '0x...';
const deadline = Math.floor(Date.now() / 1000) + 60 * 20 // 20 minutes from the current Unix time
const amountIn = trade.inputAmount.raw;
const swapTx = await uniswapContract.swapExactTokensForETH(amountIn.toString(), amountOutMin.toString(), path, to, deadline);
根据经验,应该先授权 (approve)
允许 Uniswap 调用您的 LINK
,再兑换,授权成功前兑换按钮是灰的。但是在命令行这么自由的地方,如果直接发送交易会怎么样?
(node:11444) UnhandledPromiseRejectionWarning: Error: cannot estimate gas; transaction may fail or may require manual gas limit [ See: https://links.ethers.org/v5-errors-UNPREDICTABLE_GAS_LIMIT ] (reason="execution reverted: TransferHelper: TRANSFER_FROM_FAILED", method="estimateGas", transaction=…)
连 TxHash 都没有,提前就给毙掉了。如果我还不死心,手动指定 gas limit 呢?
const swapTx = await uniswapContract.swapExactTokensForETH(amountIn.toString(), amountOutMin.toString(), path, to, deadline, {
gasLimit: ethers.utils.parseUnits('200000', 'wei'),
});
结果:
(node:15476) Error: transaction failed [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (transactionHash=…)
提交上链了,但显然还是寄。
似乎会一直用到提供的上限然后才失败。我的记录:
Gas Limit & Usage by Txn: 200,000 | 197,643 (98.82%)
闹完了来学习正确做法。
授权
回想在小狐狸钱包 MetaMask 的操作过程:

根据 ERC20 规范:
function approve(address _spender, uint256 _value) public returns (bool success)
授权
_spender
可以从我们账户最多转移代币的数量 _value
,可以多次转移,总量不超过 _value
。这个函数可以再次调用,以覆盖授权额度 _value
。所以需要做的就是创建 LINK 合约对象,调用 approve 方法,把路由合约地址填进去。
const linkABI = ['function approve(address spender, uint256 value) returns (bool)'];
const linkContract = new ethers.Contract('0x01BE23585060835E02B77ef475b0Cc51aA1e0709', linkABI, account);
...
// 最多允许使用 11 LINK
const approveTx = await linkContract.approve(uniswapContract.address, ethers.utils.parseEther('11'));
console.log(`Transaction hash: ${approveTx.hash}`);
const approveReceipt = await approveTx.wait();
console.log(approveReceipt);
如果需要取消授权,只需将第二个参数改为0.
完整代码
// swapExactTokensForETH
import { ChainId, Token, Fetcher, TokenAmount, Route, Trade, TradeType, Percent} from '@uniswap/sdk'
import { ethers } from 'ethers'
import 'dotenv/config'
const rpcurl = `https://rinkeby.infura.io/v3/${process.env.INFURA_PROJECT_ID}`;
const provider = new ethers.providers.JsonRpcProvider(rpcurl);
const signer = new ethers.Wallet(process.env.PRIVATE_KEY);
const account = signer.connect(provider);
const WETH = new Token(ChainId.RINKEBY, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18);
const LINK = new Token(ChainId.RINKEBY, '0x01BE23585060835E02B77ef475b0Cc51aA1e0709', 18);
const uniV2ABI = ['function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, \
address to, uint deadline) external returns (uint[] memory amounts)'];
const linkABI = ['function approve(address spender, uint256 value) returns (bool)'];
const uniswapContract = new ethers.Contract('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', uniV2ABI, account);
const linkContract = new ethers.Contract('0x01BE23585060835E02B77ef475b0Cc51aA1e0709', linkABI, account);
const run = async () => {
const pair = await Fetcher.fetchPairData(LINK, WETH, provider);
const route = new Route([pair], LINK);
const trade = new Trade(route, new TokenAmount(LINK, ethers.utils.parseEther('10')), TradeType.EXACT_INPUT);
const slippageTolerance = new Percent('50', '10000');
const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw;
const path = [LINK.address, WETH.address];
const to = '0x...' // PRIVATE_KEY_1's Address
const deadline = Math.floor(Date.now() / 1000) + 60 * 20 // 20 minutes from the current Unix time
const amountIn = trade.inputAmount.raw;
const approveTx = await linkContract.approve(uniswapContract.address, ethers.utils.parseEther('11'));
console.log(`Transaction hash: ${approveTx.hash}`);
const approveReceipt = await approveTx.wait();
console.log(approveReceipt);
const swapTx = await uniswapContract.swapExactTokensForETH(amountIn.toString(),
amountOutMin.toString(), path, to, deadline);
console.log(`Transaction hash: ${swapTx.hash}`);
const swapReceipt = await swapTx.wait();
console.log(swapReceipt);
}
run();
补充:查询额度
模仿授权操作的写法。
// check allowance
const linkABI = ['function approve(address spender, uint256 value) returns (bool)',
// 添加这一段
'function allowance(address _owner, address _spender) public view returns (uint256 remaining)'];
const run = async () => {
const remaining = await linkContract.allowance(account.address, uniswapContract.address);
console.log(ethers.utils.formatUnits(remaining, 'ether'));
}
run();
合约函数调用:LP
参考资料
- Uniswap-v2 Router合约分析(上) LP 功能
- Uniswap-v2 Router合约分析(下) swap 功能