OneSwap Series 15 - Arbitrage between Contracts

This article mainly introduces how to use javascript to write a simple offline arbitrage robot to make a risk-free profit in various decentralized exchanges. This automated program does not have a production environment test. It is only for research. Readers assume sole responsibility for the potential risks.

Decentralized Exchange

Two decentralized exchanges are used in the project: UniSwapand OneSwap; with the function of flashSwap provided by UniSwap, risk-free arbitrage can be implemented in various decentralized exchanges.

Example of UniSwap and OneSwap arbitrage contracts: https://github.com/oneswap/uniswap_oneswap_arbitrage/blob/master/contracts/FlashSwap.sol

Since the algorithm of the trading engine varies among different decentralized exchanges, it is necessary to write specific arbitrage contracts for various exchanges.

Programming

The robot described in this article is a simple MVP (Minimum Viable Product) version, which mainly includes the following functions:

Configure the arbitrage fund pool

In a UniSwap-like decentralized exchange, each fund pool is composed of a trading pair (that is, containing two kinds of tokens), so the first step is to configure a fund pool for arbitrage. Considering the difference in the sorting algorithms of the two tokens that make up the trading pair contract on different exchanges, in this step, you need to manually set the order of the two trading tokens (to facilitate the price calculation in the next step)

The sample code is as follows:

function initPairs(){
pairs = new Map();
// note: The value here is the address of tokens, and it needs to be in the same order as the addresses of the pair in UniSwap and OneSwap
// eth/usdt; uniSwapPairAddr; oneSwapPairAddr
pairs.set("0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852;0xD5c97DaA0bfF751e4282BbC5AC8D008738881224;ETH/USDT",
["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xdAC17F958D2ee523a2206206994597C13D831ec7"]);
....
}

pair is a Map, key is a string, and value is an array

The ordering rules for tokens that make up the trading pair in UniSwap: tokens with a small address come first. The sorting rules for tokens that make up the trading pair in OneSwap: stock token first, followed by the money token;

For arbitrage of other trading pairs, just add more tokens to the configuration.

Query the deposited funds of the fund pool

In UniSwap-like decentralized exchanges, each fund pool has two corresponding token deposit amounts to provide traders with liquidity; for more details, see: https://uniswap.org/docs/v2/core-concepts/pools/;

Here, to query the deposited funds in the two trading pair contracts, the sample code is as follows:

async function queryUniSwapReserve(pairAddr){
console.log("uniswap pair : " + pairAddr);
let pairContract = await uniswapPairContract.at(pairAddr);
let reserves = await pairContract.getReserves();
return [reserves[0], reserves[1]]
}

async function queryOneSwapReserve(pairAddr){
console.log("oneswap pair : " + pairAddr);
let pairContract = await oneswapPairContract.at(pairAddr);
let reserves = await pairContract.getReserves();
return [reserves[0], reserves[1]]
}

Note: The deposit amount queried here is the same as the token sequence configured in the fund pool in the first step.

That is, the deposit amount for UniSwap: the deposited fund with index=0 of token0 and the deposited fund with index=1 of token1;

The deposit amount for OneSwap: the deposited fund with index=0 of stock and the deposited fund with index=1 of money;

Calculate the current prices of the two fund pools

Price calculation formula: price = money / stock;

Unify the order of the deposit amount

The first step in calculating the price is to unify the order of the deposit amount for the two fund pools; here it is based on the order of OneSwap;

The code example is as follows

functionresortUniReserves(tokens, uniReserves){
if(tokens[0] ===tokens[2]){ // uniToken0 == stock
returnuniReserves
}elseif(tokens[0] ===tokens[3]){ uniToken0==money
return[uniReserves[1], uniReserves[0]]
}
}

Adjust the order of the deposit amount of UniSwap to be consistent with that of OneSwap; at this time, the returned amount array is the amount with index=0 of stock tokenand the amount with index=1 of money token.

Calculate the price

According to the formula: price = moneyTokenAmount / stockTokenAmount

functioncalPrice(reserves){
returnreserves[1] /reserves[0]
}

Calculate the prices of trading pairs in the OneSwap and UniSwap fund pools respectively.

Calculate the amount of funds needed to fill the price spread

Based on the calculated price, calculate the amount of funds needed to fill the price spread between the two fund pools.

There are mainly three situations here:

Note: The order book function in the OneSwap market complicates the data query and calculation in calculating the arbitrage space; therefore, only the amount of the AMMdeposit is considered here;

But in this case, the order book data can be put into consideration as well; because for every arbitrage, if there happens to be an order book that can be traded in OneSwap, the price of the fund pool cannot be increased/decreased to the same level as in UniSwap. Under this circumstance, there is still room for arbitrage in the next query, and arbitrage can continue. The disadvantage is such that the arbitrage, which is supposed to be completed by only one transaction, may require multiple transactions; yet the advantage is that the offline calculation and data query become less complex.

The sample code is as follows

functiontillUniSwapPriceNeededAmount(uniReserves, oneReserves, tokens){
letuniPrice=calPrice(uniReserves)
letonePrice=calPrice(oneReserves)
letamount;
letborrowToken;

// Borrow money for stock arbitrage, and increase the price in the OneSwap market
if(uniPrice>onePrice){
if(uniPrice<onePrice*1.03){ return{amount: -2} }
uniPrice=onePrice*1.03
amount=Big(oneReserves[0]).times(Big(oneReserves[1])).times(Big(uniPrice)).sqrt().minus(Big(oneReserves[1]))
borrowToken=tokens[3] // Borrow money
}
......

console.log("calculate amount end: ", amount.toString())
return{
amount:amount,
borrowToken: borrowToken,
}
}

The return value of the above function is (amount(Big), borrowToken(string));

There is a hard-coded value ​​in the calculation: 1.03, which is arbitrarily set and has not been tested by actual data;

This value must be set because the amounts of deposited funds in the two decentralized exchanges differ greatly. For the same price range, the slippage varies a lot on the price curve of different pools; the smaller the deposit amount of the fund pool, the greater the slippage. As a result, costs will exceed returns when all the price spread is filled, which will lower the rate of return.

In the calculation, the derivation of two formulas is involved when the price range is filled:

Borrow the money token and buy the stock token to increase the price of the OneSwap fund pool.

$ (M + {\Delta m}) * (S — {\Delta s}) = K = M * S $

$ P_0 = M / S = K / S² = M² / K $;

$ P_1 = (M + {\Delta m}) / (S — {\Delta s}) = K / (S — {\Delta s})² = (M + {\ \Delta m})² / K $

So: $ (M + {\Delta m})² / P_1 = M² / P_0 $

Then: $ {\Delta m} = \sqrt{P_1 * S * M} — M $

Borrow the stock token and sell the stock token to lower the price of the OneSwap fund pool.

Similar to the above derivation, write the result directly here: $ {\Delta s} = \sqrt{S * M / P_1}-S $

Calculate the number of tokens refunded to UniSwap and the number of tokens obtained from OneSwap

Next, calculate the number of tokens that need to be refunded to UniSwap.

Note: Assuming that a user borrows Token A from UniSwap, then he needs to refund Token B in the equivalent value (price*amount);

The second step is to calculate the number of Token B obtained when inputting some Token A in OneSwap. The sample code is as follows:

unctioncalReceivedAndRequiredAmount(amountAndBorrowToken, uniReserves, oneReserves, tokens){
letuniRequired;
letoneSwapReceived;

// Step 1: Calculate the number of tokens that need to be refunded to UniSwap
if(amountAndBorrowToken.borrowToken===tokens[2]){ uniRequired=getAmountInUniSwap(amountAndInputToken.amount, uniReserves[1], uniReserves[0]) }
else{ uniRequired=getAmountInUniSwap(amountAndInputToken.amount, uniReserves[0], uniReserves[1]) }

// Step 2: Calculate the number of tokens obtained from OneSwap
if(amountAndInputToken.borrowToken===tokens[2]){ oneSwapReceived=getAmountOutOneSwap(amountAndInputToken.amount, oneReserves[1], oneReserves[0]) }
else{ oneSwapReceived=getAmountOutOneSwap(amountAndInputToken.amount, oneReserves[0], oneReserves[1]) }

console.log("calculate profit, oneSwapReceived: ", oneSwapReceived.toString(),"; uniRequired: ", uniRequired.toString())
return{uniRequired: uniRequired, oneSwapReceived: oneSwapReceived}
}

The return value of this function is the amount obtained from the two fund pools/the amount required;

This function involves two formulas, and the derivation process is as follows:

$ (M — {\Delta m}) * (S + {\Delta s}) = K = M * S $

So: $ (M — {\Delta m}) * (S + {\Delta s}) = M * S $

Then: $ {\Delta s} = S * {\Delta m} / (M — {\Delta m})$

We should add the 0.3% trade fee to the final formula: $ {\Delta s} = S * {\Delta m} * 1000 / ((M-{\Delta m}) * 997) $

2. The number of tokens you can get from OneSwap

$ (M + {\Delta m}) * (S — {\Delta s}) = K = M * S $

So: $ (M + {\Delta m}) * (S — {\Delta s}) = M * S $

Then: $ {\Delta s} = S * {\Delta m} / (M + {\Delta m})$

We should add the 0.5% trade fee to the final formula: $$ {\Delta s} = {\Delta s} * 50/1000 $$

Calculate whether there is arbitrage space

Based on the numbers of the two markets worked out in the previous step, calculate the profitable quantity; at the same time, two factors need to be considered here:

Therefore, it is necessary to send arbitrage transactions only when the profit is high enough (for example: 100 USDT).

The $100 here is derived from the comprehensive consideration of the transaction fee of Ethereum during this period + the time when the transaction gets on the chain (because there may be users on the chain during this period to reduce the price spread).

The sample code is as follows

// Exit when the profit is negligible
if(oneSwapReceived<uniRequired.times(slippage)){
console.log("oneSwapReceived: ", oneSwapReceived.toString(), "; uniRequired * slippage : ", uniRequired.times(slippage).toString())
return{input: -1}
}

Here, we did not introduce the profit amount but use only the profit slippage. We need to further optimize it by introducing the profit amount for control. That is because under normal circumstances, the unit price of each token will not change much in one day; therefore, you can write the price of the token of the day in the configuration to calculate profit.

Send an arbitrage transaction

After all the above calculations, if there is an arbitrage opportunity, you can send an arbitrage transaction and call UniSwap’s flash swap (flash loan) for profit.

The sample code is as follows:

asyncfunction sendTx(tokenAndAmount, uniSwapPairAddr, tokens){
let bytes = web3.eth.abi.encodeParameters(['bool','bool'],[tokenAndAmount.uniSwapToken0IsStock, false]);
let contract = await uniswapPairContract.at(uniSwapPairAddr);
if (tokenAndAmount.inputToken === tokens[0]) {
await contract.swap(tokenAndAmount.amount, 0, arbitrageAddr, bytes);
}else if ((tokenAndAmount.inputToken === tokens[1]) ){
await contract.swap(0, tokenAndAmount.amount, arbitrageAddr, bytes);
}
}

The above function has two sets of logic:

2. The amount to be borrowed when calling UniSwap

Assemble the robot

By assembling the above functions, we can get a running robot.

The sample code is as follows:

async function loop(){
initPairs()
console.log("enter loop")
while (true) {
console.log("enter ...")
await work()
}
}

async function work(){
console.log("work ...: ")

for (let pair of pairs) {
let pairAddrs = spiltPairs(pair[0])
console.log("\n\n\ncheck pair: ", pairAddrs[2])
let uniReserves = await queryUniSwapReserve(pairAddrs[0]); // toke1, token2
let oneReserves = await queryOneSwapReserve(pairAddrs[1]); // stock, money
let tokenAndAmount = hasChanceToArbitrage(pair[1], uniReserves, oneReserves)
console.log("calculate profit amount: ", tokenAndAmount.profit, "; input token amount: ", tokenAndAmount.amount)
if (tokenAndAmount.amount > 0) {
await sendTx(tokenAndAmount, pairAddrs[0], pair[1])
}
}
}

Summary

So far, the first version of the MVP of an arbitrage robot has been completed, and most of the core functions have already been made available. However, during arbitrage on decentralized exchanges of Ethereum, there is another essential element in addition to the arbitrage program: the Ethereum node the arbitrage program connects to, preferably is the mining pool. This can ensure that transactions are on the chain in time, thus guaranteeing the success rate of arbitrage transactions.

The author believes that the success of arbitrage depends on the above two factors: the performance of the arbitrage program and the connected Ethereum node; both are indispensable. The arbitrage program can ensure the arbitrage opportunities on decentralized exchanges; the connected nodes can guarantee the realization of such opportunities. The article below elaborates on the importance of nodes for arbitrage on Ethereum.

Ethereum is a Dark Forest

A fully decentralized exchange protocol on Smart Contract, with permission-free token listing and automated market making.