Astroport Arbitrage: A step-by-step guide to building a cross-chain arbitrage bot, Part 1

Astroport
10 min readAug 4, 2023

Introduction

Astroport, a cross-chain AMM in Cosmos, continues to innovate by leveraging the power of IBC and expanding its operations across multiple blockchain networks. With the platform already live on three chains — Terra, Neutron, and Injective — and with Sei testnet in operation, Astroport is revolutionizing the possibilities of decentralized trading in Cosmos. An exciting offshoot of these advancements is the emergence of cross-chain arbitrage opportunities, previously uncharted terrain that’s now accessible thanks to Astroport’s pioneering efforts.

Arbitrage is a trading strategy where an asset is purchased in one market at a lower price, only to be sold in another market where its price is higher. Such price disparities arise due to market inefficiencies and can potentially provide profit-making opportunities. While traditionally confined within single markets, the advent of blockchain technology and inter-blockchain communication (IBC) has given birth to cross-chain arbitrage, where price differences are exploited across different blockchain networks seamlessly. Astroport’s groundbreaking cross-chain model is a perfect playground for such arbitrage strategies.

The strategy we will follow in this tutorial includes the following steps:

  1. Connect to multiple Cosmos SDK-based networks via the CosmWasm JavaScript Client.
  2. Retrieve pool data (i.e., current asset prices) from each network.
  3. Identify potential arbitrage opportunities, i.e., instances where the price of an asset is lower on one network than on another.
  4. For each potential opportunity, simulate a series of transactions to calculate the net return from the arbitrage opportunity.

This guide, being the first part of two, will walk you through the code to achieve the steps mentioned above and explain each step along the way. The goal is to provide a solid understanding of how to interact with Cosmos SDK-based networks and how to manipulate the data returned from them.

In the second part of the tutorial, we will delve into the specifics of executing these arbitrage transactions and handling IBC transactions, which are crucial for cross-chain operations. We will examine how to initiate the actual transaction on the buy network and how to handle the IBC transaction on the sell network.

By the end of these two parts, you will have a comprehensive understanding of how to detect, simulate, and execute cross-chain arbitrage opportunities.

Tutorial

Prerequisites

  • Minimal coding/JavaScript experience
  • Node.js
  • Automation tools (Cron jobs or Task Scheduler)

Step 1: Importing Dependencies

The first thing we need to do is import the CosmWasmClient from the “@cosmjs/cosmwasm-stargate” package. CosmWasmClient is a helper object that provides a simple way to interact with Cosmos-SDK based blockchains.

import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";

Step 2: Creating the Client Object

We then establish connections to the different blockchains we’ll be interacting with. These connections are stored in a dictionary object called clients. For each blockchain, we create a CosmWasmClient instance and connect it to an RPC endpoint.

const clients = {
'terra': {
'client': await CosmWasmClient.connect('https://terra-rpc.polkachu.com/'),
'contract': 'terra1w579ysjvpx7xxhckxewk8sykxz70gm48wpcuruenl29rhe6p6raslhj0m6',
},
'neutron': {
'client': await CosmWasmClient.connect('https://neutron-rpc.polkachu.com/'),
'contract': 'neutron1jpm7j2cmj2mmk5pnxv20dxz869tw2vyy87qdl0xjasqnn23l04psudtf2y',
},
'injective': {
'client': await CosmWasmClient.connect('https://injective-rpc.polkachu.com/'),
'contract': 'inj1x2skrlvnq0cre76zrn0mm9rwefkexszatskj8p',
},
};

Thanks to Polkachu for providing rpc nodes and other resources to the community!

Here, the clients dictionary has three properties — ‘terra’, ‘neutron’, and ‘injective’. Each of these properties is an object that holds the blockchain’s client (a CosmWasmClient instance) and the contract address for a liquidity pool on the blockchain.

For this tutorial, we will focus on $ASTRO by looking at the ASTRO-axlUSDC pools on Terra and Neutron, and the ASTRO-USDT pool on Injective. Another example could be $ATOM, which has pairs in all 3 chains with liquidity.

Step 3: Initializing Price and Pair Storage

We initialize two empty dictionaries, prices and pairs, which will be used to store data related to the liquidity pools on each network.

const prices = {};
const pairs = {};

In this case, prices will hold the exchange rates of each asset pair on each network, while pairs will hold information about the asset pairs themselves.

Step 4: Gathering and Processing Price Data

We now move on to the main part of our bot — gathering and processing price data. We start by looping through all the networks in our clients dictionary and querying the price data from each network’s liquidity pool.

for (let network in clients) {
const { client, contract } = clients[network];

const poolResponse = await client.queryContractSmart(contract, { pool: {} });
const assets = poolResponse.pool.assets;

const asset1 = assets[0];
const asset2 = assets[1];

const asset1Amount = parseInt(asset1.amount);
const asset2Amount = parseInt(asset2.amount);

// Calculate the price ratio and store it
const price = asset1Amount / Math.pow(10, clients[network].decimalPlaces) /
(asset2Amount / Math.pow(10, clients[network].decimalPlaces));

prices[network] = price;
pairs[network] = [asset1.info.token.contract_addr, asset2.info.token.contract_addr];
}

Here, we use a for…in loop to iterate through all the networks. We then destructure the client and contract properties from each network’s client object and use them to query the liquidity pool’s data.

The response from the query is a poolResponse object which contains information about the liquidity pool, including the assets in the pool. We extract these assets into the assets variable.

We then extract the first and second assets from the assets array into the asset1 and asset2 variables respectively. We then parse the amounts of each asset from their amount properties and calculate the price ratio.

Finally, we store the calculated price and the pair of assets in the prices and pairs dictionaries respectively, using the network’s name as the key.

Step 5: Identifying Arbitrage Opportunities

After we’ve gathered and processed all the price data, we can now identify arbitrage opportunities. We can do this by iterating over the exchange rates of the assets on each network and comparing them to the rates on every other network.

let opportunities = {};

for (let net1 in prices) {
for (let net2 in prices) {
if (net1 === net2) continue;
if (prices[net1] > prices[net2]) {
if (!opportunities[net1]) opportunities[net1] = {};
opportunities[net1][net2] = prices[net1] - prices[net2];
}
}
}

In this code, prices is an object that contains the exchange rates for the assets on each network, which we collected in Step 2. For each pair of networks (net1 and net2), the code checks if the price on net1 is higher than the price on net2. If it is, then it indicates an arbitrage opportunity: buying the asset on net2 (where the price is lower) and selling it on net1 (where the price is higher) could yield profit.

This opportunity, along with the associated price difference, is then stored in the opportunities object. This object is structured such that each entry includes the network to buy from (net2), the network to sell at (net1), and the expected profit (the difference between prices on net1 and net2).

By the end of this step, the opportunities object contains a map of all potential arbitrage opportunities across the networks the bot is connected to. These identified opportunities will be further scrutinized in the next steps to confirm their profitability when transaction costs are taken into account.

Step 6: Setting the Investment Thresholds

In arbitrage trading, determining the volume or amount of investment is a critical aspect. It’s important not only to spot profitable opportunities but also to determine the appropriate investment size for each opportunity. Different opportunities might warrant different levels of investment depending on factors like expected profit, risk, market volatility, and liquidity.

In our script, we have defined three different investment levels: maximum, medium, and minimum. These are represented by the variables MAX_DEPOSIT, MID_DEPOSIT, and MIN_DEPOSIT, respectively. These variables set a framework for how much we’re willing to invest based on the potential arbitrage opportunity:

const MAX_DEPOSIT = 1000;
const MID_DEPOSIT = 500;
const MIN_DEPOSIT = 250;

After identifying a potential arbitrage opportunity, we then simulate the transaction with each of these investment levels. By doing this, we can estimate the potential return for each level and choose the one that fits our strategy and expectations the best.

Additionally, we set a profit threshold to filter out opportunities that might not yield a significant return. This is the minimum profit ratio we’re targeting for each trade. In our case, we’ve set this to 1.01, which represents a 1% profit. If the simulated transaction does not meet this threshold, it will be disregarded:

const PROFIT_THRESHOLD = 1.01;  // 1% profit

Keep in mind that this is a simplistic approach. More advanced strategies might use dynamic deposit levels based on real-time market conditions, include various profit thresholds for different assets, or incorporate risk assessment mechanisms to determine investment amounts. Ultimately, the approach should be adjusted to suit the trader’s strategy and market circumstances.

Step 7: Simulating Trades

In this step, we simulate trades to evaluate the profitability of the identified arbitrage opportunities. This is important as it takes into account various factors such as liquidity, slippage, etc., that could affect the actual return on a potential arbitrage opportunity.

for (let buyNetwork in opportunities) {
for (let sellNetwork in opportunities[buyNetwork]) {
let investments = [MIN_INVESTMENT, MID_INVESTMENT, MAX_INVESTMENT];

for (let investment of investments) {
// Simulation logic goes here
}
}
}

In this segment, opportunities is the object we populated in Step 3 with potential arbitrage opportunities. Each opportunity is characterized by a pair of networks: the one to buy from (buyNetwork) and the one to sell at (sellNetwork). For each such pair, the bot simulates trades with three different investment amounts — Minimum, Medium, and Maximum (MIN_INVESTMENT, MID_INVESTMENT, MAX_INVESTMENT).

For each of these investment amounts, the bot executes two simulations:

1) Buying simulation: It simulates buying the asset on the buyNetwork with the current investment amount. It uses the queryContractSmart function with a simulation query that includes the offer_asset (the amount of asset the bot is willing to offer) and ask_asset_info (the information of the asset the bot wants in return).

const sellSimulationQuery = {
"simulation": {
"offer_asset": {
"info": pairs[sellNetwork][0],
"amount": String(expectedTokenReturn * Math.pow(10, decimalPlaces))
},
"ask_asset_info": pairs[sellNetwork][1]
}
};

2) Selling simulation: After getting the expected return from the buying simulation, it simulates selling the returned asset on the sellNetwork. It uses the queryContractSmart function with a simulation query that includes the offer_asset (the expected return from the buy simulation) and ask_asset_info (the information of the asset the bot wants in return).

const sellSimulationQuery = {
"simulation": {
"offer_asset": {
"info": pairs[sellNetwork][0],
"amount": String(expectedTokenReturn * Math.pow(10, decimalPlaces))
},
"ask_asset_info": pairs[sellNetwork][1]
}
};

By the end of this step, the bot has simulated buying and selling of assets for each arbitrage opportunity and knows the expected net return for each opportunity and for each investment amount. This information is used in the subsequent steps to decide whether to execute the arbitrage trades in real-time.

Step 8: Calculating Net Profit and Evaluating the Trade

After simulating trades, the bot needs to calculate the net profit from the potential arbitrage opportunity and decide whether the profit is above a certain predefined threshold to execute the trade.

const finalTokenReturn = Number(BigInt(sellSimulationResult.return_amount) / BigInt(Math.pow(10, decimalPlaces)));
const initialInvestmentUSD = Number(investment);
const netReturnUSD = finalTokenReturn - initialInvestmentUSD;

console.log(`Net return after selling on ${sellNetwork}: $${netReturnUSD.toFixed(2)}`);

if (netReturnUSD < initialInvestmentUSD * INVESTMENT_THRESHOLD - initialInvestmentUSD) {
console.log(`Expected profit for $${investment} on ${buyNetwork} is too low. Skipping this opportunity.`);
continue;
}

console.log(`Invest $${investment} on ${buyNetwork} and sell on ${sellNetwork} for a net return of $${netReturnUSD.toFixed(2)}`);

The bot calculates the net return after selling the asset on the second network. Then, it compares the net return with the initial investment. If the net return is less than the initial investment times the predefined investment threshold minus the initial investment, the bot deems the potential profit as too low and skips the opportunity. Otherwise, it logs the amount to be invested and the expected net return.

However, it is important to note that at this point, the bot is only identifying the opportunities and evaluating them. It does not execute the trades. Executing trades involves signing and broadcasting transactions on the respective networks, which comes with its own set of complexities. This is covered in more depth in Part 2 of this guide, where we go over trade execution, IBC transfers, and more.

Step 9: Error Handling

The bot’s code is encapsulated within a try-catch block to handle any errors that might occur during its operation:

try {
// bot code…
} catch (err) {
console.error(err);
}

This block will catch and log any exceptions thrown by the bot, helping to identify and correct any issues.

Step 10: Running the Bot

To run the bot, the entire code logic is wrapped within an asynchronous start function. This is necessary since our code involves multiple asynchronous operations such as connecting to the networks and making smart contract queries.

const start = async () => {
// Bot logic goes here
};
start();

The start(); invocation at the end of the script is to call this asynchronous function and kick off our bot’s execution process.

To actually run the bot, you would typically have this code saved in a JavaScript file (for example, arbitrage-bot.js). To execute this script, you would use the Node.js runtime environment. If you have Node.js installed on your computer or server, you can run the bot with the following command in your terminal:

node arbitrage-bot.js

When you run this command, Node.js starts executing your script. It initiates the bot, and it begins looking for arbitrage opportunities across the specified networks. All the information such as identified opportunities, simulated trades, potential profits, etc., is logged to the console for review. To automate the bot and have it continuously look for arbitrage opportunities at regular intervals, you might consider using a Cron job if you’re on a Unix-like operating system, or Task Scheduler if you’re on Windows.

Conclusion

In this part of our guide, we’ve built a foundation for an arbitrage bot that can identify profitable opportunities across different appchains. We’ve gone through how to connect to different networks, query and understand their asset pairs, and calculate the exchange rates. We also delved into identifying arbitrage opportunities and simulating potential trades to estimate potential returns.

The logic used here is basic and can be expanded upon. For instance, transaction fees, network latency, liquidity, slippage, market fluctuations, and the time it takes to execute trades across networks are not considered but can significantly impact the profitability of trades.

This bot serves as a powerful tool to identify profitable arbitrage opportunities. However, keep in mind that the current state of the bot is more of a “read-only” mode. It identifies and evaluates potential trades but does not execute them. In the next installation of this guide, we’ll take our bot to the next level by implementing the ability to execute the trades it identifies. We will add functionalities for signing and broadcasting transactions on respective networks.

Stay tuned for Part 2!

Are you subscribed?

AstroIntern’s bonus relies upon adding new subscribers to Astroport’s email newsletter. Make him happy by subscribing here. And follow Astroport on Twitter to get the latest alerts from the mothership.

DISCLAIMER

Remember, Terra, Injective, Neutron and Astroport are experimental technologies. This article does not constitute investment advice and is subject to and limited by the Astroport disclaimers, which you should review before interacting with the protocol.

--

--

Astroport

An automated, decentralised exchange protocol on the Terra blockchain. http://astroport.fi