diff --git a/src/core/arbitrage/arbitrage.ts b/src/core/arbitrage/arbitrage.ts index ffa7b0f..d729261 100644 --- a/src/core/arbitrage/arbitrage.ts +++ b/src/core/arbitrage/arbitrage.ts @@ -1,4 +1,4 @@ -import { Asset, isNativeAsset } from "../types/base/asset"; +import { Asset } from "../types/base/asset"; import { BotConfig } from "../types/base/botConfig"; import { Path } from "../types/base/path"; import { getOptimalTrade } from "./optimizers/analyticalOptimizer"; @@ -12,34 +12,33 @@ export interface OptimalTrade { * */ export function trySomeArb(paths: Array, botConfig: BotConfig): OptimalTrade | undefined { - const [path, tradesize, profit] = getOptimalTrade(paths, botConfig.offerAssetInfo); + const optimalTrade: OptimalTrade | undefined = getOptimalTrade(paths, botConfig.offerAssetInfo); - if (path === undefined) { + if (!optimalTrade) { return undefined; } else { - const profitThreshold = - botConfig.profitThresholds.get(path.pools.length) ?? - Array.from(botConfig.profitThresholds.values())[botConfig.profitThresholds.size]; - if (profit < profitThreshold) { + if (!isAboveThreshold(botConfig, optimalTrade)) { return undefined; } else { - console.log("optimal tradesize: ", tradesize, " with profit: ", profit); - console.log("path: "), - path.pools.map((pool) => { - console.log( - pool.address, - isNativeAsset(pool.assets[0].info) - ? pool.assets[0].info.native_token.denom - : pool.assets[0].info.token.contract_addr, - pool.assets[0].amount, - isNativeAsset(pool.assets[1].info) - ? pool.assets[1].info.native_token.denom - : pool.assets[1].info.token.contract_addr, - pool.assets[1].amount, - ); - }); - const offerAsset: Asset = { amount: String(tradesize), info: botConfig.offerAssetInfo }; - return { path, offerAsset, profit }; + return optimalTrade; } } } + +/** + * + */ +function isAboveThreshold(botConfig: BotConfig, optimalTrade: OptimalTrade): boolean { + // We dont know the number of message required to execute the trade, so the profit threshold will be set to the most conservative value: nr_of_pools*2-1 + const profitThreshold = + botConfig.profitThresholds.get((optimalTrade.path.pools.length - 1) * 2 + 1) ?? + Array.from(botConfig.profitThresholds.values())[botConfig.profitThresholds.size - 1]; + if (botConfig.skipConfig) { + const skipBidRate = botConfig.skipConfig.skipBidRate; + return ( + (1 - skipBidRate) * optimalTrade.profit - (botConfig.flashloanFee / 100) * +optimalTrade.offerAsset.amount > + profitThreshold + ); //profit - skipbid*profit - flashloanfee*tradesize must be bigger than the set PROFIT_THRESHOLD + TX_FEE. The TX fees dont depend on tradesize nor profit so are set in config + } else + return optimalTrade.profit - (botConfig.flashloanFee / 100) * +optimalTrade.offerAsset.amount > profitThreshold; +} diff --git a/src/core/arbitrage/graph.ts b/src/core/arbitrage/graph.ts index 842f0e9..2777cf7 100644 --- a/src/core/arbitrage/graph.ts +++ b/src/core/arbitrage/graph.ts @@ -90,6 +90,7 @@ export function getPaths(graph: Graph, startingAsset: AssetInfo, depth: number): } paths.push({ pools: poolList, + cooldown: false, }); } return paths; diff --git a/src/core/arbitrage/optimizers/analyticalOptimizer.ts b/src/core/arbitrage/optimizers/analyticalOptimizer.ts index 64e641d..5fa71f0 100644 --- a/src/core/arbitrage/optimizers/analyticalOptimizer.ts +++ b/src/core/arbitrage/optimizers/analyticalOptimizer.ts @@ -1,6 +1,7 @@ import { AssetInfo } from "../../types/base/asset"; import { Path } from "../../types/base/path"; import { getAssetsOrder, outGivenIn } from "../../types/base/pool"; +import { OptimalTrade } from "../arbitrage"; // function to get the optimal tradsize and profit for a single path. // it assumes the token1 from pool1 is the same asset as token1 from pool2 and @@ -114,20 +115,26 @@ function getTradesizeAndProfitForPath(path: Path, offerAssetInfo: AssetInfo): [n * @param paths Type `Array` to check for arbitrage. * @param offerAssetInfo Type `AssetInfo` to start the arbitrage from. */ -export function getOptimalTrade(paths: Array, offerAssetInfo: AssetInfo): [Path | undefined, number, number] { +export function getOptimalTrade(paths: Array, offerAssetInfo: AssetInfo): OptimalTrade | undefined { let maxTradesize = 0; let maxProfit = 0; let maxPath; paths.map((path: Path) => { - const [tradesize, profit] = getOptimalTradeForPath(path, offerAssetInfo); - if (profit > maxProfit && tradesize > 0) { - maxProfit = profit; - maxTradesize = tradesize; - maxPath = path; + if (!path.cooldown) { + const [tradesize, profit] = getOptimalTradeForPath(path, offerAssetInfo); + if (profit > maxProfit && tradesize > 0) { + maxProfit = profit; + maxTradesize = tradesize; + maxPath = path; + } } }); - return [maxPath, maxTradesize, maxProfit]; + if (maxPath) { + return { path: maxPath, offerAsset: { amount: String(maxTradesize), info: offerAssetInfo }, profit: maxProfit }; + } else { + return undefined; + } } /** Given an ordered route, calculate the optimal amount into the first pool that maximizes the profit of swapping through the route diff --git a/src/core/types/arbitrageloops/mempoolLoop.ts b/src/core/types/arbitrageloops/mempoolLoop.ts index 5c51b64..0c12e40 100644 --- a/src/core/types/arbitrageloops/mempoolLoop.ts +++ b/src/core/types/arbitrageloops/mempoolLoop.ts @@ -119,6 +119,7 @@ export class MempoolLoop { if (arbTrade) { await this.trade(arbTrade); + arbTrade.path.cooldown = true; break; } } @@ -128,6 +129,10 @@ export class MempoolLoop { * */ public reset() { + // reset all paths that are on cooldown + this.paths.forEach((path) => { + path.cooldown = false; + }); this.totalBytes = 0; flushTxMemory(); } @@ -136,6 +141,9 @@ export class MempoolLoop { * */ private async trade(arbTrade: OptimalTrade) { + if (arbTrade.path.cooldown) { + return; + } const [msgs, nrOfMessages] = this.messageFunction( arbTrade, this.account.address, @@ -151,7 +159,7 @@ export class MempoolLoop { }; const TX_FEE = - this.botConfig.txFees.get(arbTrade.path.pools.length) ?? + this.botConfig.txFees.get(nrOfMessages) ?? Array.from(this.botConfig.txFees.values())[this.botConfig.txFees.size - 1]; // sign, encode and broadcast the transaction diff --git a/src/core/types/arbitrageloops/skipLoop.ts b/src/core/types/arbitrageloops/skipLoop.ts index 6315f7a..7e036f9 100644 --- a/src/core/types/arbitrageloops/skipLoop.ts +++ b/src/core/types/arbitrageloops/skipLoop.ts @@ -79,7 +79,7 @@ export class SkipLoop extends MempoolLoop { const arbTrade: OptimalTrade | undefined = this.arbitrageFunction(this.paths, this.botConfig); if (arbTrade) { await this.skipTrade(arbTrade, trade); - break; + arbTrade.path.cooldown = true; //set the cooldown of this path to true so we dont trade it again in next callbacks } } } @@ -90,6 +90,10 @@ export class SkipLoop extends MempoolLoop { * */ private async skipTrade(arbTrade: OptimalTrade, toArbTrade: MempoolTrade) { + if (arbTrade.path.cooldown) { + // dont execute if path is on cooldown + return; + } if ( !this.botConfig.skipConfig?.useSkip || this.botConfig.skipConfig?.skipRpcUrl === undefined || @@ -131,7 +135,7 @@ export class SkipLoop extends MempoolLoop { //if gas fee cannot be found in the botconfig based on pathlengths, pick highest available const TX_FEE = - this.botConfig.txFees.get(arbTrade.path.pools.length) ?? + this.botConfig.txFees.get(nrOfWasms) ?? Array.from(this.botConfig.txFees.values())[this.botConfig.txFees.size - 1]; const txRaw: TxRaw = await this.botClients.SigningCWClient.sign( diff --git a/src/core/types/base/botConfig.ts b/src/core/types/base/botConfig.ts index bde209c..e2dc061 100644 --- a/src/core/types/base/botConfig.ts +++ b/src/core/types/base/botConfig.ts @@ -1,4 +1,4 @@ -import { Coin, StdFee } from "@cosmjs/stargate"; +import { StdFee } from "@cosmjs/stargate"; import { assert } from "console"; import { NativeAssetInfo } from "./asset"; @@ -16,6 +16,7 @@ export interface BotConfig { maxPathPools: number; mappingFactoryRouter: Array<{ factory: string; router: string }>; flashloanRouterAddress: string; + flashloanFee: number; offerAssetInfo: NativeAssetInfo; mnemonic: string; useMempool: boolean; @@ -64,23 +65,15 @@ export function setBotConfig(envs: NodeJS.ProcessEnv): BotConfig { skipBidRate: envs.SKIP_BID_RATE === undefined ? 0 : +envs.SKIP_BID_RATE, }; } - const FLASHLOAN_FEE = +envs.FLASHLOAN_FEE; const PROFIT_THRESHOLD = +envs.PROFIT_THRESHOLD; //set all required fees for the depth of the hops set by user; - const GAS_FEES = new Map(); const TX_FEES = new Map(); const PROFIT_THRESHOLDS = new Map(); - for (let hops = 2; hops <= MAX_PATH_HOPS; hops++) { + for (let hops = 2; hops <= (MAX_PATH_HOPS - 1) * 2 + 1; hops++) { const gasFee = { denom: envs.BASE_DENOM, amount: String(GAS_USAGE_PER_HOP * hops * +GAS_UNIT_PRICE) }; - GAS_FEES.set(hops, gasFee); TX_FEES.set(hops, { amount: [gasFee], gas: String(GAS_USAGE_PER_HOP * hops) }); - const profitThreshold: number = - skipConfig === undefined - ? PROFIT_THRESHOLD / (1 - FLASHLOAN_FEE / 100) + +gasFee.amount //dont use skip bid on top of the threshold, include flashloan fee and gas fee - : PROFIT_THRESHOLD / (1 - FLASHLOAN_FEE / 100) + - +gasFee.amount + - skipConfig.skipBidRate * PROFIT_THRESHOLD; //need extra profit to provide the skip bid + const profitThreshold: number = PROFIT_THRESHOLD + +gasFee.amount; PROFIT_THRESHOLDS.set(hops, profitThreshold); } const botConfig: BotConfig = { @@ -90,6 +83,7 @@ export function setBotConfig(envs: NodeJS.ProcessEnv): BotConfig { maxPathPools: MAX_PATH_HOPS, mappingFactoryRouter: FACTORIES_TO_ROUTERS_MAPPING, flashloanRouterAddress: envs.FLASHLOAN_ROUTER_ADDRESS, + flashloanFee: +envs.FLASHLOAN_FEE, offerAssetInfo: OFFER_ASSET_INFO, mnemonic: envs.WALLET_MNEMONIC, useMempool: envs.USE_MEMPOOL == "1" ? true : false, diff --git a/src/core/types/base/path.ts b/src/core/types/base/path.ts index f9b1129..3b558ac 100644 --- a/src/core/types/base/path.ts +++ b/src/core/types/base/path.ts @@ -2,4 +2,5 @@ import { Pool } from "./pool"; export interface Path { pools: Array; + cooldown: boolean; } diff --git a/src/core/types/base/pool.ts b/src/core/types/base/pool.ts index 17ccfee..13a2577 100644 --- a/src/core/types/base/pool.ts +++ b/src/core/types/base/pool.ts @@ -99,108 +99,117 @@ export function applyMempoolTradesOnPools(pools: Array, mempoolTrades: Arr undefined, ); for (const trade of filteredTrades) { - const poolToUpdate = pools.find((pool) => trade.contract === pool.address); - const msg = trade.message; - if (poolToUpdate) { - // a direct swap or send to pool - if (isSwapMessage(msg) && trade.offer_asset !== undefined) { - applyTradeOnPool(poolToUpdate, trade.offer_asset); - } else if (isSendMessage(msg) && trade.offer_asset !== undefined) { - applyTradeOnPool(poolToUpdate, trade.offer_asset); - } else if (isJunoSwapMessage(msg) && trade.offer_asset === undefined) { - // For JunoSwap messages we dont have an offerAsset provided in the message - const offerAsset: Asset = { - amount: msg.swap.input_amount, - info: msg.swap.input_token === "Token1" ? poolToUpdate.assets[0].info : poolToUpdate.assets[1].info, - }; - applyTradeOnPool(poolToUpdate, offerAsset); - } else if (isJunoSwapOperationsMessage(msg) && trade.offer_asset === undefined) { - // JunoSwap operations router message - // For JunoSwap messages we dont have an offerAsset provided in the message - const offerAsset: Asset = { - amount: msg.pass_through_swap.input_token_amount, - info: - msg.pass_through_swap.input_token === "Token1" - ? poolToUpdate.assets[0].info - : poolToUpdate.assets[1].info, - }; - applyTradeOnPool(poolToUpdate, offerAsset); + try { + const poolToUpdate = pools.find((pool) => trade.contract === pool.address); + const msg = trade.message; + if (poolToUpdate) { + // a direct swap or send to pool + if (isSwapMessage(msg) && trade.offer_asset !== undefined) { + applyTradeOnPool(poolToUpdate, trade.offer_asset); + } else if (isSendMessage(msg) && trade.offer_asset !== undefined) { + applyTradeOnPool(poolToUpdate, trade.offer_asset); + } else if (isJunoSwapMessage(msg) && trade.offer_asset === undefined) { + // For JunoSwap messages we dont have an offerAsset provided in the message + const offerAsset: Asset = { + amount: msg.swap.input_amount, + info: + msg.swap.input_token === "Token1" + ? poolToUpdate.assets[0].info + : poolToUpdate.assets[1].info, + }; + applyTradeOnPool(poolToUpdate, offerAsset); + } else if (isJunoSwapOperationsMessage(msg) && trade.offer_asset === undefined) { + // JunoSwap operations router message + // For JunoSwap messages we dont have an offerAsset provided in the message + const offerAsset: Asset = { + amount: msg.pass_through_swap.input_token_amount, + info: + msg.pass_through_swap.input_token === "Token1" + ? poolToUpdate.assets[0].info + : poolToUpdate.assets[1].info, + }; + applyTradeOnPool(poolToUpdate, offerAsset); - // Second swap - const [outGivenIn0, nextOfferAssetInfo] = outGivenIn(poolToUpdate, offerAsset); - const secondPoolToUpdate = pools.find( - (pool) => pool.address === msg.pass_through_swap.output_amm_address, - ); + // Second swap + const [outGivenIn0, nextOfferAssetInfo] = outGivenIn(poolToUpdate, offerAsset); + const secondPoolToUpdate = pools.find( + (pool) => pool.address === msg.pass_through_swap.output_amm_address, + ); - if (secondPoolToUpdate !== undefined) { - applyTradeOnPool(secondPoolToUpdate, { amount: String(outGivenIn0), info: nextOfferAssetInfo }); - } - } else if (isTFMSwapOperationsMessage(msg) && trade.offer_asset !== undefined) { - let offerAsset: Asset = trade.offer_asset; - for (const operation of msg.execute_swap_operations.routes[0].operations) { - const currentPool = pools.find((pool) => pool.address === operation.t_f_m_swap.pair_contract); - if (currentPool) { - const [outGivenInNext, offerAssetInfoNext] = outGivenIn(currentPool, offerAsset); - applyTradeOnPool(currentPool, offerAsset); - offerAsset = { amount: String(outGivenInNext), info: offerAssetInfoNext }; + if (secondPoolToUpdate !== undefined) { + applyTradeOnPool(secondPoolToUpdate, { amount: String(outGivenIn0), info: nextOfferAssetInfo }); } - } - } - } - // not a direct swap or swaps on pools, but a routed message using a Router contract - else if (isSwapOperationsMessage(msg) && trade.offer_asset !== undefined) { - const poolsFromThisRouter = pools.filter((pool) => trade.contract === pool.routerAddress); - if (poolsFromThisRouter) { - let offerAsset: Asset = trade.offer_asset; - const operations = msg.execute_swap_operations.operations; - if (isWWSwapOperationsMessages(operations)) { - // terraswap router - for (const operation of operations) { - const currentPool = findPoolByInfos( - poolsFromThisRouter, - operation.terra_swap.offer_asset_info, - operation.terra_swap.ask_asset_info, - ); - - if (currentPool !== undefined) { - applyTradeOnPool(currentPool, offerAsset); + } else if (isTFMSwapOperationsMessage(msg) && trade.offer_asset !== undefined) { + let offerAsset: Asset = trade.offer_asset; + for (const operation of msg.execute_swap_operations.routes[0].operations) { + const currentPool = pools.find((pool) => pool.address === operation.t_f_m_swap.pair_contract); + if (currentPool) { const [outGivenInNext, offerAssetInfoNext] = outGivenIn(currentPool, offerAsset); + applyTradeOnPool(currentPool, offerAsset); offerAsset = { amount: String(outGivenInNext), info: offerAssetInfoNext }; } } } - if (isAstroSwapOperationsMessages(operations)) { - // astropoart router - for (const operation of operations) { - const currentPool = findPoolByInfos( - poolsFromThisRouter, - operation.astro_swap.offer_asset_info, - operation.astro_swap.ask_asset_info, - ); - if (currentPool !== undefined) { - applyTradeOnPool(currentPool, offerAsset); - const [outGivenInNext, offerAssetInfoNext] = outGivenIn(currentPool, offerAsset); - offerAsset = { amount: String(outGivenInNext), info: offerAssetInfoNext }; + } + // not a direct swap or swaps on pools, but a routed message using a Router contract + else if (isSwapOperationsMessage(msg) && trade.offer_asset !== undefined) { + const poolsFromThisRouter = pools.filter((pool) => trade.contract === pool.routerAddress); + if (poolsFromThisRouter) { + let offerAsset: Asset = trade.offer_asset; + const operations = msg.execute_swap_operations.operations; + if (isWWSwapOperationsMessages(operations)) { + // terraswap router + for (const operation of operations) { + const currentPool = findPoolByInfos( + poolsFromThisRouter, + operation.terra_swap.offer_asset_info, + operation.terra_swap.ask_asset_info, + ); + + if (currentPool !== undefined) { + applyTradeOnPool(currentPool, offerAsset); + const [outGivenInNext, offerAssetInfoNext] = outGivenIn(currentPool, offerAsset); + offerAsset = { amount: String(outGivenInNext), info: offerAssetInfoNext }; + } } } - } - if (isWyndDaoSwapOperationsMessages(operations)) { - for (const operation of operations) { - const offerAssetInfo = isWyndDaoNativeAsset(operation.wyndex_swap.offer_asset_info) - ? { native_token: { denom: operation.wyndex_swap.offer_asset_info.native } } - : { token: { contract_addr: operation.wyndex_swap.offer_asset_info.token } }; - const askAssetInfo = isWyndDaoNativeAsset(operation.wyndex_swap.ask_asset_info) - ? { native_token: { denom: operation.wyndex_swap.ask_asset_info.native } } - : { token: { contract_addr: operation.wyndex_swap.ask_asset_info.token } }; - const currentPool = findPoolByInfos(poolsFromThisRouter, offerAssetInfo, askAssetInfo); - if (currentPool !== undefined) { - applyTradeOnPool(currentPool, offerAsset); - const [outGivenInNext, offerAssetInfoNext] = outGivenIn(currentPool, offerAsset); - offerAsset = { amount: String(outGivenInNext), info: offerAssetInfoNext }; + if (isAstroSwapOperationsMessages(operations)) { + // astropoart router + for (const operation of operations) { + const currentPool = findPoolByInfos( + poolsFromThisRouter, + operation.astro_swap.offer_asset_info, + operation.astro_swap.ask_asset_info, + ); + if (currentPool !== undefined) { + applyTradeOnPool(currentPool, offerAsset); + const [outGivenInNext, offerAssetInfoNext] = outGivenIn(currentPool, offerAsset); + offerAsset = { amount: String(outGivenInNext), info: offerAssetInfoNext }; + } + } + } + if (isWyndDaoSwapOperationsMessages(operations)) { + for (const operation of operations) { + const offerAssetInfo = isWyndDaoNativeAsset(operation.wyndex_swap.offer_asset_info) + ? { native_token: { denom: operation.wyndex_swap.offer_asset_info.native } } + : { token: { contract_addr: operation.wyndex_swap.offer_asset_info.token } }; + const askAssetInfo = isWyndDaoNativeAsset(operation.wyndex_swap.ask_asset_info) + ? { native_token: { denom: operation.wyndex_swap.ask_asset_info.native } } + : { token: { contract_addr: operation.wyndex_swap.ask_asset_info.token } }; + const currentPool = findPoolByInfos(poolsFromThisRouter, offerAssetInfo, askAssetInfo); + if (currentPool !== undefined) { + applyTradeOnPool(currentPool, offerAsset); + const [outGivenInNext, offerAssetInfoNext] = outGivenIn(currentPool, offerAsset); + offerAsset = { amount: String(outGivenInNext), info: offerAssetInfoNext }; + } } } } } + } catch (e) { + console.log("cannot apply trade on pools:"); + console.log(trade); + console.log(e); } } } @@ -225,6 +234,8 @@ export function getAssetsOrder(pool: Pool, assetInfo: AssetInfo) { return [pool.assets[0], pool.assets[1]] as Array; } else if (isMatchingAssetInfos(pool.assets[1].info, assetInfo)) { return [pool.assets[1], pool.assets[0]] as Array; + } else { + return undefined; } }