uniswap
的交易费用,是通过 x * y = K
的恒等式中推导而来, 在特定的交易场景时,例如环回交易中,我们的交易成本可以做到远远低于额定手续费。
什么是环回交易
环回交易是在一个交易对 tokenA/tokenB
交易,先从 tokenA
兑换得到 tokenB
, 然后立刻将得到的 tokenB
换回 tokenA
的交易。
uniswap v2
的标准费率是千分之三, 那么环回交易的成本就是千分之六, 这个成本相当之高. 如果我们仅仅是为了刷交易量, 我们需要一种有效的途径来降低手续费, 环回交易就是一种非常有效的途径。
理论推导
下面是我们的详细推导过程. 假设如下:
- dx: 输入的 tokenA 数量
- r0: tokenA 的 reserve 数量
- r1: tokenB 的 reserve 数量
- dy: 第一次 tokenA -> tokenB swap 的数量
- ex: 第二次 swap tokenB -> tokenA 的数量
根据uniswap的公式, 计算如下:
dy = dx*997*r1/(1000*r0 + dx*997)
R1 = r1 - dy
R0 = r0 + dx
ex = dy*997*R0/(1000*R1 + dy*997)
代入dy,化简后,可得:
ex = 997*997*dx*(r0+dx) / (1000*1000*r0 + 997*997*dx)
同时除以dx:
ex/dx = 997*997*(r0+dx) / (1000*1000*r0 + 997*997*dx)
由于ex
是最终环回交易结束时我们的 tokenA 数量, dx
是我们 tokenA 的输入数量, 1-ex/dx
就是我们的手续费比例。
从上面的公式可知,最终环回交易的ex
只跟输入dx
和tokenA
的reserve
有关,而且,输入数量dx
与tokenA reserve
比值越高,最终得到的ex/dx
越接近1,也就是付出的手续费越少。
代码演算
测试代码如下:
Filename: loopbackSwap.js
const BigNumber = require('ethers').BigNumber
const e18 = BigNumber.from('1000000000000000000')
const getAmountOut = (amountIn, r0, r1) => {
const amountInWithFee = amountIn.mul(997)
, numerator = amountInWithFee.mul(r1)
, denominator = r0.mul(1000).add(amountInWithFee);
return numerator.div(denominator)
}
const getAmountBack = (amtIn, r0, r1, printable = false) => {
const out = getAmountOut(amtIn, r0, r1)
r0 = r0.add(amtIn)
r1 = r1.sub(out)
const dx = getAmountOut(out, r1, r0)
if (printable) {
console.info('swap x->y: out=%s r0=%s r1=%s backx=%s',
out.toString(), r0.toString(), r1.toString(), dx.toString())
}
return { amtInter: out, amtOut: dx }
}
function loopbackSwap() {
const reserveA = BigNumber.from(1000000).mul(e18)
, reserveB = BigNumber.from(2000000).mul(e18)
, ratio = (multor) => reserveA.mul(multor).div(1000)
let amts = [
1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 3000, 5000, 10000 ]
for (let amt of amts) {
let amtIn = ratio(amt)
let { amtOut } = getAmountBack(amtIn, reserveA, reserveB)
console.info('dx/reserve=%s amtOut/amtIn=%s', amt/1000, amtOut.mul(10000).div(amtIn).toNumber()/10000)
}
}
loopbackSwap()
run the scripts:
npx hardhat run loopbackSwap.js
dx/reserve=0.001 amtOut/amtIn=0.994
dx/reserve=0.002 amtOut/amtIn=0.994
dx/reserve=0.005 amtOut/amtIn=0.994
dx/reserve=0.01 amtOut/amtIn=0.994
dx/reserve=0.02 amtOut/amtIn=0.9941
dx/reserve=0.05 amtOut/amtIn=0.9942
dx/reserve=0.1 amtOut/amtIn=0.9945
dx/reserve=0.2 amtOut/amtIn=0.995
dx/reserve=0.5 amtOut/amtIn=0.9959
dx/reserve=1 amtOut/amtIn=0.9969
dx/reserve=2 amtOut/amtIn=0.9979
dx/reserve=3 amtOut/amtIn=0.9984
dx/reserve=5 amtOut/amtIn=0.9989
dx/reserve=10 amtOut/amtIn=0.9994
从结果可以看出,当 dx/reserve 超过1后,手续费率极速降低,当dx/reserve=10,手续费仅为万分之6
由此,可以使用闪电贷,在较低的利率下,将大量资金从借贷池中借出,然后进行环回交易,在一些交易即挖矿的交易所中,可以通过这种方式降低挖矿手续费,套利获利。
实战
我们可以编写一个合约来执行我们的环回交易。
需要注意的是,我们不能直接将 uniswap router
的 path
参数设置为 [tokenA, tokenB, tokenA]
来进行环回交易,而必须分成两次swap, 首先是 [tokenA, tokenB]
, 然后是 [tokenB, tokenA]
.
示例代码如下:
/// @dev swapLoopback swap tokenA to tokenB, then swap tokenB to tokenA
/// @param _router uniswap-like router
/// @param reward the reward token
/// @param amountIn amount in
/// @param amountOutMin just set to 0
/// @param path [tokenA, tokenB]
function swapLoopback(
address _router, // router
address reward, // reward token
uint amountIn,
uint amountOutMin,
address[] memory path
)
public
onlyOwner {
address tokenIn = path[0];
uint tokenInitial = IERC20(tokenIn).balanceOf(address(this));
_approve(IERC20(tokenIn), address(_router));
// solhint-disable-next-line
uint256 ts = block.timestamp + 60;
uint[] memory amounts = IUniswapRouter(_router).swapExactTokensForTokens(amountIn, amountOutMin, path, address(this), ts);
path[0] = path[1];
path[1] = tokenIn;
// console.log("amounts:", amounts[1]);
_approve(IERC20(path[0]), address(_router));
amounts = IUniswapRouter(_router).swapExactTokensForTokens(amounts[1], amountOutMin, path, address(this), ts);
// other arbitrage code with reward..
reward;
}