A Few Strokes of Genius: dexArb, A Free, Open Source Triangular Ether Dex Arbitrage Bot

Posted by

View this article in it’s original form on jare.cloud!

TL;DR: pre-alpha, almost-working aggregated dex arbitrage bot is FOSS and lives here: https://github.com/DunnCreativeSS/dexArb

Spanning 12+ exchanges:

https://api.totle.com/exchanges

This arb bot was the proceeds of me getting hired by the dex aggregator Totle a long time ago. While the engagement didn’t work out, they funded creating a dex aggregated arbitrage bot with 2+ ether as part of the interview process.

Want it?

Here it is!

const io = require('socket.io-client');
const request = require('request');
var socket = io.connect("https://socket.etherdelta.com", {
    transports: ['websocket']
});

//starting bal 0.329945342678081181 Ether






socket.on("connect", function() {
    socket.emit("getMarket", {
        user: "0x8ebA329784974b96EC6293DD83bf462651BB75E6"
    })
});
const xpath = require('xpath');
const parse5 = require('parse5');
const xmlser = require('xmlserializer');
const dom = require('xmldom').DOMParser;

var Web3 = require('web3');
var web3 = new Web3();
web3.setProvider(new web3.providers.HttpProvider("http://localhost:8545"));
var totleabi = JSON.parse('[{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"handler","type":"address"},{"name":"allowed","type":"bool"}],"name":"setHandler","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tokenAddresses","type":"address[]"},{"name":"buyOrSell","type":"bool[]"},{"name":"amountToObtain","type":"uint256[]"},{"name":"amountToGive","type":"uint256[]"},{"name":"tokenForOrder","type":"address[]"},{"name":"exchanges","type":"address[]"},{"name":"orderAddresses","type":"address[8][]"},{"name":"orderValues","type":"uint256[6][]"},{"name":"exchangeFees","type":"uint256[]"},{"name":"v","type":"uint8[]"},{"name":"r","type":"bytes32[]"},{"name":"s","type":"bytes32[]"}],"name":"executeOrders","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"handlerWhitelist","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MAX_EXCHANGE_FEE_PERCENTAGE","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previousOwner","type":"address"},{"indexed":true,"name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"}]')

var totle = web3.eth.contract(totleabi).at( "0xd94c60e2793ad587400d86e4d6fd9c874f0f79ef");
web3.eth.defaultAccount = web3.eth.accounts[5];

socket.on("market", function(data) {
   
    setInterval(function(){
    console.log('lala');
        for (var d in data['returnTicker']) {
            fun(data['returnTicker'][d], data['returnTicker'].length)
        }    }, 60000); 
    setInterval(function(){
    console.log('lala arbbing');
       
        arbbing = false;
    }, 500000); 
    console.log('lala');
    for (var d in data['returnTicker']) {
        fun(data['returnTicker'][d], data['returnTicker'].length)
    }
});
var arbbing = false;
var aTotal;
var bTotal;
var total = [];
var total2;
async function fun(d, t) {
    setTimeout(function() {
        var req = {
            "operationName": null,
            "variables": {
                "address": d.tokenAddr
            },
            "query": "query ($address: String) {\n  token(address: $address) {\n    address\n    name\n    price\n    priceChange {\n      change1h\n      change24h\n      change7d\n    }\n    bestPrice {\n      bid\n      ask\n    }\n    orders {\n      asks {\n        price\n        volume\n        exchangeId\n      }\n      bids {\n        price\n        volume\n        exchangeId\n      }\n    }\n    exchanges\n  }\n}\n"
        }

        request.post({
            url: 'https://services.totlesystem.com/graph',
            body: req,
            json: true
        }, function(error, response, body) {
            if (!error && response.statusCode == 200) {
                if (body.data.token != null) {
                    //console.log(body.data.token.bestPrice);
                    if (body.data.token.bestPrice.bid > (1.03* body.data.token.bestPrice.ask)) {
                        //console.log(body.data.token.name + ' arb!');
                        //console.log(body.data.token.bestPrice.ask / body.data.token.bestPrice.bid);
                        aTotal = 0;
                        var aPrice;
                        var acount = 0;
                        for (var o in body.data.token.orders.asks) {

                            if (body.data.token.orders.asks[o].price <= body.data.token.bestPrice.bid) {
                                aTotal += parseFloat(body.data.token.orders.asks[o].volume)
                                aPrice = parseFloat(body.data.token.orders.asks[o].price)
                            }
                        }
                        bTotal = 0;
                        var bPrice;
                        var bcount = 0;
                        for (var o in body.data.token.orders.bids) {
                        	
                            	   if (body.data.token.orders.bids[o].price >= body.data.token.bestPrice.ask) {
                                bTotal += parseFloat(body.data.token.orders.bids[o].volume)
                                bPrice = parseFloat(body.data.token.orders.bids[o].price)
                         }
                       
                        }
                        total2 = 0;
                        if (aTotal > bTotal) {
                            total2 = aTotal
                        } else {
                            total2 = bTotal
                        }
                            var token = body.data.token.address;
                            var name = body.data.token.name
                        	total[token] = totaled(total2, bPrice);
                        if ((total[token] * bPrice) > (0.01 * Math.pow(10,18))) { //100000000000000000
                        	if (arbbing == false){
                        		                        	console.log('arb!')

                        		arbbing = true;
                        		//console.log(body.data);;
                            console.log(token);
                            console.log(name);
                            request("https://etherscan.io/address/" + token + "#code", function(err, response, body) {
                                if (err) {
                                    console.log('err');
                                    console.log(err);
                                } else {
                                    // do stuff with body
                                    const html = body;
                                    //console.log(html);
                                    const document = parse5.parse(html.toString());
                                    const xhtml = xmlser.serializeToString(document);
                                    const doc = new dom().parseFromString(xhtml);
                                    const select = xpath.useNamespaces({
                                        "x": "http://www.w3.org/1999/xhtml"
                                    });
                                    const nodes = select('//*[@id="js-copytextarea2"]/text()', doc);
                                    //console.log(nodes);
                                    var contractABI = JSON.parse(nodes);
                                    var contract =  web3.eth.contract(contractABI).at( token);
                                    if (contract.stopped){
                                    var stopped = contract.stopped.call();
                                    if (stopped != true){
                                    console.log(body.data.token.name + ' arb! ');

                                    buy(token, total[token], contract);
                                }
                                else {
                                	arbbing = false
                                	console.log('stopped! eos?')
                                }
                            }else {
                            	console.log(name + ' arb! ');

                            	buy(token, total[token], contract);
                                }
                            }
                            });

                        	}
                        }
                    }
                }
            }
        })


    }, Math.random() * t * 200);
}
async function buy(token, total, contract) {

    var reqbuy = {
            "buys": [{
                "token": token,
                "amount": total
            }],
            "address": "0x8ebA329784974b96EC6293DD83bf462651BB75E6"
        }
        request.post({
            url: 'https://services.totlesystem.com/suggester',
            body: reqbuy,
            json: true
        }, function(error, response, body) {
            if (!error && response.statusCode == 200) {
                console.log(body);
               var tx = totle.executeOrders(
                    body.response.orders[0],
                    body.response.orders[1],
                    body.response.orders[2],
                    body.response.orders[3],
                    body.response.orders[4],
                    body.response.orders[5],
                    body.response.orders[6],
                    body.response.orders[7],
                    body.response.orders[8],
                    body.response.orders[9],
                    body.response.orders[10],
                    body.response.orders[11], {
                    value: (body.response.ethValue),
                    from: "0x8ebA329784974b96EC6293DD83bf462651BB75E6",
                    gas: 400000,
                    gasPrice: "6000000000"
                })
               console.log(tx);
               approve(tx, token, total, contract);
            }
        })

    
}
function totaled(total, price){
 if ((parseFloat(total) * parseFloat(price)) >(1.05 * parseFloat(web3.eth.getBalance("0x8ebA329784974b96EC6293DD83bf462651BB75E6")))){
	var atotal = total / 1.3
	totaled(parseFloat(atotal),parseFloat(price))
} 
	return parseFloat(total)

}
async function approve(tx, token, total, contract) {
	web3.eth.getTransaction(tx, function (err, receipt) {
    if (err) {
      console.log(err);
    }
    console.log(receipt.blockNumber);
    if (receipt.blockNumber == null){
    	setTimeout(function(){
    		approve(tx,token,total, contract);
    	}, 30000);
    } else {
	var approve2 = contract.approve("0xd94c60e2793ad587400d86e4d6fd9c874f0f79ef", (total), {
                                        from: "0x8ebA329784974b96EC6293DD83bf462651BB75E6",
                                        gas: 250000,
                                        gasPrice: "6000000000"
                                    })
	sell(approve2,token,total,contract);
    }
    })
}
async function sell(tx, token, total, contract){
	console.log(tx);
web3.eth.getTransaction(tx, function (err, receipt) {
    if (err) {
      console.log(err);
    }
    console.log(receipt.blockNumber);
    if (receipt.blockNumber == null){
    	setTimeout(function(){
    		sell(tx,token,total, contract);
    	}, 30000);
    } else {
    	arbbing = false;
        var reqbuy2 = {
            "sells": [{
                "token": token,
                "amount": total
            }],
            "address": "0x8ebA329784974b96EC6293DD83bf462651BB75E6"
        }
        request.post({
            url: 'https://services.totlesystem.com/suggester',
            body: reqbuy2,
            json: true
        }, function(error, response, body) {
            if (!error && response.statusCode == 200) {

                console.log(body);
                totle.executeOrders(
                    body.response.orders[0],
                    body.response.orders[1],
                    body.response.orders[2],
                    body.response.orders[3],
                    body.response.orders[4],
                    body.response.orders[5],
                    body.response.orders[6],
                    body.response.orders[7],
                    body.response.orders[8],
                    body.response.orders[9],
                    body.response.orders[10],
                    body.response.orders[11], {
                    value: (body.response.ethValue),
                    from: "0x8ebA329784974b96EC6293DD83bf462651BB75E6",
                    gas: 4000000,
                    gasPrice: "6000000000"
                })
            }
        });
    }
})
}

Note: this doesn’t work. Neither does backwards engineering it to work, work. The Totle graphql endpoint returns bid: null ask: null for bestPrice. Woe!

Now, this bot didn’t work in it’s original form. The transactions were all reversed or otherwise died in a fire. Don’t believe me? https://etherscan.io/address/0x8ebA329784974b96EC6293DD83bf462651BB75E6

This is because these orders on various dexes ALREADY EXISTED at runtime. Because there’s a delay updating order books, these orders would remain long after some other arbitrager armed with a higher gasPrice and in a quicker fashion datetime wise acted on it.

Check out the earning potential of arbitraging dexes: https://stat.bloxy.info/superset/dashboard/arbitrage/?standalone=true

Check out arbitrage action over the last 90 days. . . there’s money to be had, here.

Now, the first stroke of genius? Index those trades that exist at runtime, ignore them, and re-consider those tokens only after they exit arbitrage-able state and re-enter. This way, we act on only brand-new opportunities.

if (arbWins[token['symbol']] < 0.05 && sym[syms] > 0) {
                                                try {
                                                    ignore.splice(ignore.indexOf(sym), 1)

                                                    console.log('length ignore ' + (ignore.length))
                                                } catch (err) {

                                                }
                                            }
                                            if (arbWins[token['symbol']] > 0.05 && syms[token['symbol']] == 0) {
                                                ignore.push(sym)
                                                console.log('length ignore ' + ((ignore.length)))
                                            }
                                            if ((arbWins[token['symbol']] > 0.05 && syms[token['symbol']] != 0 && !ignore.includes(sym))){// || first && arbWins[token['symbol']] > -1.5) {
                                                first = false
                                                console.log('arb! ' + sym)

Check it out…. genius.

Now, I built a bot surrounding this that trades ether -> token -> ether, on the Totle API. The benefit of Totle API is that it’s connected to all those exchanges, and I can code in a % of the fees (equal to Totle’s %, at 0.25%) for myself should anyone use my bot.

Then.. .all of a sudden.. .as I was publishing the newest version. . .another stroke of genius! Let’s load from a list of tokens I don’t mind holding, see which one I have the highest $USD value in on my wallet (including ETH) and trade with that. Instead of eth -> any token -> eth, I now have:

[“ETH”, “DAI”, “wBTC”, “SAI”, “cSAI”, “MKR”, “USDC”, “UBT”, “KNC”, “USDT”, “LINK”] -> any tradeable token -> [“ETH”, “DAI”, “wBTC”, “SAI”, “cSAI”, “MKR”, “USDC”, “UBT”, “KNC”, “USDT”, “LINK”]

This maximizes the chances I find a profitable arbitrage opportunity. If I edit my strategy to only consider cTokens, Aave tokens, etc. DeFi tokens, then when my bot isn’t trading it’s earning DeFi interest. Genius!

Now, the bot is in an unusable state.

Hi again,

Sorry for all the notes!

I rewrote everything in Node and now tx are going through.

I query /swap for x amount ether -> token, and get an approve and swap tx back.

I then query /swap for token -> back to ether, and get a tx back.

Here’s a sample of three:

1. approve https://etherscan.io/tx/
0xc572a3dcff2826afdc315a32e98d
a4cd9f8203c5a650ffc5fffc18326679ec8f


2. swap token -> ether (Failed!) https://etherscan.io/tx/
0x222229cee345f5dc493aaa15299b
48ca384a9ef0847615317e92354c502bfefa


3. swap ether -> token, with an Ether value (Failed!) https://etherscan.io/tx/
0xb508a9f7e7f5610b2d5a6184740e
4064a3d9cf0447014950fe90de53bca70450


Firstly, am I doing these in the right order? I assume I’d want to trade my ether for token, then approve, then swap back.

Secondly, why is the ether -> token for 0.05 Ether failing?

Thanks,

Email to Totle reps

The ‘rewrote to javascript’ bit has a bit of a backstory. Read previous email:

Hi folks,

running into some issues with python web3. 


after version 4.x, it requires proper capitalization for eth addresses.


https://ethereum.
stackexchange.com/questions/
51148/how-can-i-simply-sign-an-ethereum-transaction
 


this was apparent at first when the ‘from’ address was wrong.


from field must match key’s 0x24E7be68c63B6707f567b933Bdae
546c7C94Ff37, but it was 0x24e7be68c63b6707f567b933bdae546c7c94ff37


I fixed this by manually entering ‘from.’


Now, Transaction had invalid fields: {‘to’: ‘
0x3b21d6d25e7f036c8277efe9a7ebf17ca12fe4f6′, ‘value’: ‘50000000000000000’, ‘gas’: ‘2250000’}


Note the proper capitalization: https://
etherscan.io/address/
0x3b21d6d25e7f036c8277efe9a7ebf17ca12fe4f6



I tried rolling web3 back to 3.16.5, but got errors about the rest of my standardized code.


Is there a way for the totle API to return the proper eth addresses?


Thanks, 

I wanted multithreading 🙁

So, we now wait for 12ish hours from now when folks’ll be in the office on Monday to continue writing the bot – for fun and profit.

Want to run it now? You might lose some $ gas when it does find arb transactions it acts on, but you have all the opportunity to run it and see the pending git commits fixing the Tx:

https://github.com/DunnCreativeSS/dexArb

Like it? Fork it! Star it! Better yet, follow the ‘Sponsor’ button and sign up for some monthly tokens of appreciation for myself, wouldya?

What’s it look like, in production?