Use MemPool Utilities to Build Your Arbitrage Bot

BlockVision
8 min readDec 15, 2022

--

In this article, we will provide an overview of arbitrage trading and explain how you can use BlockVision MemPool utilities to build your own arbitrage bot. By integrating our services, you can significantly reduce the technical costs associated with this type of trading. We will also discuss the benefits of using an arbitrage bot and how it can help you take advantage of market inefficiencies and maximize your profits.

What is Arbitrage Trading?

Arbitrage trading is a strategy that involves taking advantage of price differences in different markets or exchanges for the same asset. By buying an asset where the price is relatively low and selling it in a market where the price is higher, traders can profit from the difference in prices. This type of trading is often used to exploit inefficiencies in financial markets and can be performed using various financial instruments, such as stocks, bonds, currencies, and derivatives. While it can be a complex and risky strategy, it also provides opportunities for traders to make quick profits. In the world of cryptocurrency, competition is also fierce, making it important for traders to use tools like arbitrage bots to gain an edge in the market.

Practice — Integrating BlockVision MemPool tool collection into your arbitrage bot

Why — To monitor MemPool and get arbitrage opportunities one step ahead

For those engaged in on-chain arbitrage trading, the speed at which they can search for arbitrage opportunities is a crucial factor in their success. In the world of cryptocurrency, all transactions must be sent to the MemPool and packaged by miners before they are added to the blockchain. By monitoring transactions that are not yet on the chain in the MemPool, we can find arbitrage opportunities faster than other arbitrage traders who only search for opportunities based on transactions that are already on the blockchain. This allows us to complete the search for arbitrage opportunities one step ahead of the competition and potentially make more profitable trades.

For example, suppose our arbitrage goal is to take advantage of price differences between token pairs on the CFAMM DEX. If we are listening to the mempool and we see a transaction for the token pair we are interested in, and it causes a significant price fluctuation, we can use the simple trading formula x * y = k to calculate the latest price of the token based on the number of transactions and the reserves of the corresponding pool. If we discover a profitable spread after our calculation, we have successfully found an arbitrage opportunity through the MemPool before the transaction is added to the blockchain. This allows us to act quickly and potentially profit from the price difference before other traders have a chance to do so.

Monitor MemPool

To integrate MemPool into arbitrage bots, firstly we need to integrate the BlockVision MemPool listening service into our software. Suppose that the pools we need to listen to are ETH-BTC trading pairs on Uniswap and Sushiswap. Then we can write the following code to monitor the trading pair on two different exchanges.

The on-chain addresses are:

UniswapV2 WETH-WBTC: 0xbb2b8038a1640196fbe3e38816f3e67cba72d940

Sushiswap WETH-WBTC: 0xCEfF51756c56CeFFCA006cD410B03FFC46dd3a58

The MemPool may contain transactions that are not relevant to your arbitrage strategy, such as transactions for different token pairs or transactions that use a different method (methodsId: 0x022c0d9f). In the past, arbitrage traders had to write their own programs to filter out these unnecessary transactions, but with the BlockVision MemPool monitoring service, you can easily filter transactions by address and one or more method IDs in one place. This makes it easier and more efficient to find the arbitrage opportunities you are looking for without having to sift through irrelevant transactions.

#!/usr/bin/env node
var WebSocketClient = require('websocket').client;

var client = new WebSocketClient();

client.on('connectFailed', function(error) {
console.log('Connect Error: ' + error.toString());
});

client.on('connect', function(connection) {
console.log('WebSocket Client Connected');
connection.on('error', function(error) {
console.log("Connection Error: " + error.toString());
});
connection.on('close', function() {
console.log('echo-protocol Connection Closed');
});
connection.on('message', function(message) {
if (message.type === 'utf8') {
console.log("Received: '" + message.utf8Data + "'");
}
});

function bvPendingTransaction() {
if (connection.connected){
// subscribe transactions which related to token paris
// and the swap method.
var payload = '{"jsonrpc":"2.0","id": 2, "method": "eth_subscribe", "params": ["bvPendingTransactions", {"toAddress": ["0xbb2b8038a1640196fbe3e38816f3e67cba72d940", "0xCEfF51756c56CeFFCA006cD410B03FFC46dd3a58"], "methodsId": "0x022c0d9f"}]}';
connection.sendUTF(payload.toString());
}
}

bvPendingTransaction()
});

client.connect('wss://eth-mainnet.blockvision.org/v1/<api-key>');

After you have successfully established a subscription, you should be able to see transactions like the following in your terminal. By parsing the input data, you can determine the actions that the transaction will perform. For example, if you are looking for transactions that involve a specific token pair on the CFAMM DEX, you can use the input data to filter out transactions that are not relevant to your arbitrage strategy. This allows you to focus on the transactions that have the potential to provide profitable arbitrage opportunities.

{
"blockHash": "",
"blockNumber": "",
"from": "0x1d57184a354d58d8a0809b8a692e7a246b475c59",
"gas": "0x6ab56",
"gasPrice": "0x15426df2e",
"maxPriorityFeePerGas": "0x4d5ae996",
"maxFeePerGas": "0x161cd80e0",
"hash": "0x3faa1d4ce54732d78b18c06769fa08ed0d546354499fbe88a673ae961b1fa9c0",
"input": "0x022c0d9f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001bfc44ca3d497100000000000000000000000000a88157429f300dc5e796151ba91cd53b8e64e913000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c302810079003ffedcddd268511acc44ae67845bdd56c8417fa4018100600075bff91af9878f5ec3fede9b52d51159afc2430a01810047005777d92f208679db4b9778590fa3cab3ac9e21680181002e0088e6a0c2ddd26feeb64f039a2c41296fcb3f56400100c02aaa39b223fe8d0a0e5c4f27ead9083c756cc201c02aaa39b223fe8d0a0e5c4f27ead9083c756cc29b19f7f92e60c1fe20d6080ec390c7aa6bb72811000000000000000000000000000000000000000000000000000815d5ce5445e70000000000000000000000000000000000000000000000000000000000",
"nonce": "0x63",
"to": "0xbb2b8038a1640196fbe3e38816f3e67cba72d940",
"transactionIndex": "",
"value": "0x0",
"type": "0x2",
"chainId": "0x1",
"v": "0x1",
"r": "0xf15fcfa63e8cf08ff2211231e87db4f89f28a1000ebd6060d8d064de31ff5c14",
"s": "0x37d38f7b93ae5f7a31363be559c94872cbfcd16840aeb5f972fef974653c4d6c"
}

The parsing code is as follows:

if (txHash['params']['result'] != undefined) {        
result = txHash['params']['result'];
input = result['input'];

const iface = new ethers.utils.Interface(['function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external'])
decodedData = iface.decodeFunctionData('swap', input);
}

// Parsing result
[
BigNumber { _hex: '0x00', _isBigNumber: true },
BigNumber { _hex: '0x1bfc44ca3d497100', _isBigNumber: true },
'0xA88157429f300dc5E796151BA91CD53B8e64e913',
'0x02810079003ffedcddd268511acc44ae67845bdd56c8417fa4018100600075bff91af9878f5ec3fede9b52d51159afc2430a01810047005777d92f208679db4b9778590fa3cab3ac9e21680181002e0088e6a0c2ddd26feeb64f039a2c41296fcb3f56400100c02aaa39b223fe8d0a0e5c4f27ead9083c756cc201c02aaa39b223fe8d0a0e5c4f27ead9083c756cc29b19f7f92e60c1fe20d6080ec390c7aa6bb72811000000000000000000000000000000000000000000000000000815d5ce5445e7',
amount0Out: BigNumber { _hex: '0x00', _isBigNumber: true },
amount1Out: BigNumber { _hex: '0x1bfc44ca3d497100', _isBigNumber: true },
to: '0xA88157429f300dc5E796151BA91CD53B8e64e913',
data: '0x02810079003ffedcddd268511acc44ae67845bdd56c8417fa4018100600075bff91af9878f5ec3fede9b52d51159afc2430a01810047005777d92f208679db4b9778590fa3cab3ac9e21680181002e0088e6a0c2ddd26feeb64f039a2c41296fcb3f56400100c02aaa39b223fe8d0a0e5c4f27ead9083c756cc201c02aaa39b223fe8d0a0e5c4f27ead9083c756cc29b19f7f92e60c1fe20d6080ec390c7aa6bb72811000000000000000000000000000000000000000000000000000815d5ce5445e7'
]

Through analysis, we can obtain the amount0Out and amount1Out of this transaction, and then calculate the price of the latest transaction pair if the transaction is successful through the following formula:

If a trade causes a large price swing in one of the pools, we say we have some confidence that we have identified an arbitrage opportunity.

Write the basic arbitrage contract

The purpose of this article is to explain how the use of the BlockVision toolset can reduce the technical cost of arbitrage trading, rather than to provide a complete guide to implementing arbitrage contracts. Please note that the contract implementation provided in this article is for illustration purposes only and may not have real-world production value. It is up to individual arbitrage traders to explore and develop their own arbitrage strategies. By using the tools and services provided by BlockVision, traders can focus on finding profitable opportunities and executing trades, rather than spending time and resources on technical implementation.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
pragma abicoder v2;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "hardhat/console.sol";

import "./interfaces/IUniswapV2Pair.sol";
import "./libraries/Decimal.sol";

contract arbitrageDemo {
struct arbitrageParams {
address pool1;
address pool2;
address inputToken;
uint256 amountIn1;
uint256 amountIn2;
}

function arbitrageWithinDex(params arbitrageParams) external {
// Ensure that the arbitrage path goes one way
require(amountIn1 == 0 || amountIn2 == 0, "amountInx both not equal to zero");

// Make sure arbitrage target pool addresses are inconsistent
require(params.pool0 != params.pool1, "same pair tokens");

// Acquisition of pools reserves
(uint256 pool0Reserve0, uint256 pool0Reserve1, ) = IUniswapV2Pair(params.pool0).getReserves();
(uint256 pool1Reserve1, uint256 pool1Reserve1, ) = IUniswapV2Pair(params.pool1).getReserves();

// Arbitrage from pool1 to pool2
if (params.amountIn1 != 0) {
IUniswapV2Pair pair = IUniswapV2Pair(params.pool0);
{
(uint256 reserve0, uint256 reserve1, ) = pair.getReserves();
(uint256 reserveInput, uint256 reserveOutput) = params.inputToken == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
amountInput = IERC20(params.inputToken).balanceOf(address(pair)).sub(reserveInput);
amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
}
(uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOutput) : (amountOutput, uint256(0));
pair.swap(amount0Out, amount1Out, msg.sender, new bytes(0));
}

// Arbitrage from pool2 to pool1
if (params.amountIn2 != 0) {
IUniswapV2Pair pair = IUniswapV2Pair(params.pool1);
{
(uint256 reserve0, uint256 reserve1, ) = pair.getReserves();
(uint256 reserveInput, uint256 reserveOutput) = params.inputToken == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
amountInput = IERC20(params.inputToken).balanceOf(address(pair)).sub(reserveInput);
amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
}
(uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOutput) : (amountOutput, uint256(0));
pair.swap(amount0Out, amount1Out, msg.sender, new bytes(0));
}
}
}

Deploy the contract to Goerli to test on-chain and get the contract address:

0xba849911c809f6c922bc4767328a01db30c331e5

Now that everything is set up, we can use the arbitrage contracts deployed in this chapter to simulate a flashbots bundle and initiate an arbitrage trade using the BlockVision toolset. In the next chapter, we will go through the steps of using the toolset to send a bundle and execute the trade, so that you can see how easy it is to integrate arbitrage trading into your own bot using BlockVision’s services.

Initiate an arbitrage trade

Once you have found an arbitrage opportunity, you can use the arbitrage contract that you have deployed on-chain to initiate the trade. In this chapter, we will show you how to build arbitrage transactions using the tools provided by BlockVision, and how easy it is to send transactions using the BlockVision-Flashbots tool collection. With these tools, you no longer need to incur the technical costs associated with using flashbots, making it a convenient and cost-effective way to execute arbitrage trades.

In the previous chapter, we briefly wrote an arbitrage contract demo and got its contract address 0xba849911c809f6c922bc4767328a01db30c331e5. Before packaging the transaction and sending it to BlockVision-Flashbots, we need to build the arbitrage transaction first. Well, let's build this transaction first. Please see the following code:

const ethers = require('ethers');
const fetch = require('node-fetch');
const dotenv = require('dot-env');
dotenv.config();

async function main() {
let abi = [
"struct arbitrageParams {address pool1;address pool2;address inputToken;uint256 amountIn1;uint256 amountIn2;}",
"function arbitrageWithinDex(params arbitrageParams)",
];

const contract = new ethers.Contract("CONTRACT_ADDRESS", abi, Wallet);
const params = [
"0xbb2b8038a1640196fbe3e38816f3e67cba72d940",
"0xCEfF51756c56CeFFCA006cD410B03FFC46dd3a58",
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
1000000000000000000,
0,
];
const action = 'arbitrageWithinDex';
const unsignedTx = await contract.populateTransaction[action](...params);

let transaction = {
to: '<contract-address>',
value: ethers.utils.parseEther('1'),
gasLimit: '21000',
maxPriorityFeePerGas: ethers.utils.parseUnits('5', 'gwei'),
maxFeePerGas: ethers.utils.parseUnits('20', 'gwei'),
nonce: 1,
type: 2,
chainId: 3
};
let rawTransaction = await wallet.signTransaction(transaction).then(ethers.utils.serializeTransaction(transaction));
console.log('Raw txhash string ' + rawTransaction);

// post to bv eth_sendBundle api
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

var raw = JSON.stringify({
"jsonrpc": "2.0",
"method": "eth_sendBundle",
"params": {
"txsHex": [unsignedTx],
"blockNumber": 0
},
"id": 1
});

var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};

fetch("https://api.blockvision.org/v1/<api-key>", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
}

main();

If the sending is successful, the interface will return the bundle hash allocated by the Flashbots relay. Through the hash value, combined with the eth_getBundleStat API, the state of the sent bundle can be continuously tracked. The return result should be as follows:

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"isSimulated": true,
"isSentToMiners": false,
"isHighPriority": false,
"simulatedAt": "2022-10-27T00:05:01.436Z",
"submittedAt": "2022-10-27T00:05:31.436Z",
"sentToMinersAt": "0001-01-01T00:00:00Z"
}
}

In this article, we have shown you how to use the BlockVision MemPool tool to integrate arbitrage trading into your bot. By following the steps outlined in this article, you can easily access the MemPool and find arbitrage opportunities in real time, allowing you to take advantage of market inefficiencies and maximize your profits. We hope that this article has been helpful and thank you for reading.

--

--

BlockVision

Next Generation of Web 3.0 Data Infrastructure | Cloud & API Services