The transaction fee of uniswap
is derived from the identity of x * y = K
. In a specific transaction scenario, such as a loopback transaction, our transaction cost can be far lower than the rated fee.
What is a loopback transaction
The loopback transaction is a transaction in a transaction pair tokenA/tokenB
, first exchange tokenA
to get tokenB
, and then immediately exchange the obtained tokenB
back to tokenA
.
The standard rate of uniswap v2
is 0.3%
, then the cost of loopback transaction is 0.6%
, this cost is quite high. If we are just to brush the transaction volume, we need an effective way to reduce Handling fees, loopback transactions are a very effective way.
Theoretical Derivation
The following is our detailed derivation process. Assumptions are as follows:
- dx: the number of tokenA entered
- r0: the reserve quantity of tokenA
- r1: The reserve quantity of tokenB
- dy: the amount of the first tokenA -> tokenB swap
- ex: the second swap tokenB -> the number of tokenA
According to the formula of uniswap, the calculation is as follows:
dy = dx*997*r1/(1000*r0 + dx*997)
R1 = r1-dy
R0 = r0 + dx
ex = dy*997*R0/(1000*R1 + dy*997)
Substituting dy, after simplification, we can get:
ex = 997*997*dx*(r0+dx) / (1000*1000*r0 + 997*997*dx)
Divide by dx at the same time:
ex/dx = 997*997*(r0+dx) / (1000*1000*r0 + 997*997*dx)
Since ex
is our tokenA quantity at the end of the final loopback transaction, dx
is our tokenA input quantity, and 1-ex/dx
is our handling fee ratio.
It can be seen from the above formula that the final loopback transaction ex
is only related to the input dx
and the reserve
of tokenA
, and the higher the ratio of the input number dx
to tokenA reserve
, the final result is The closer the ex/dx
is to 1, the less the fee is paid.
Code calculation
The test code is as following:
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
It can be seen from the results that when dx/reserve
exceeds 1, the handling fee rate drops rapidly, and when dx/reserve=10
, the handling fee is only 6/10000.
As a result, lightning loans can be used to lend a large amount of funds from the borrowing pool at a lower interest rate, and then perform loopback transactions. In some exchanges where transactions are mining, mining can be reduced in this way Handling fee, arbitrage profit.
Action
Now, We can write a contract to execute our loopback transaction.
It should be noted that we cannot directly set the path
parameter of uniswap router
to [tokenA, tokenB, tokenA]
for loopback transactions, but must be divided into two swaps, the first is [tokenA, tokenB]
, then [tokenB, tokenA]
.
The sample code is as following:
/// @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;
}