From b7d385277392c1bbaf3911c9addeb0fd3a1fe7dd Mon Sep 17 00:00:00 2001 From: 0xBossanova <0xBossanova@proton.me> Date: Fri, 17 Feb 2023 11:46:31 +0100 Subject: [PATCH] Bug/profit calculation and path cooldown (#50) * feat: add cooldown feature to paths, calculate fees based on msgs We use a cooldown feature on paths to prevent the bot for overtrading a path whiles the arb is already gone. if it did one attempt cooldown is set to true, on next block arrival the bot sets all paths cooldowns back to false. Also, this commit changes the way fees are attached to TXs, because fees do not only scale with amount of pools, but also amount of messages used for a pooltrade, therefore the amount of msgs is used as an indexer for the amount of txfees to attach to a tx * bug: fix profit calculation based on tradesize and skipbid * chore: more comments on profit calc and try-catch on mempool tx --- src/core/arbitrage/arbitrage.ts | 47 +++-- src/core/arbitrage/graph.ts | 1 + .../optimizers/analyticalOptimizer.ts | 21 +- src/core/types/arbitrageloops/mempoolLoop.ts | 10 +- src/core/types/arbitrageloops/skipLoop.ts | 8 +- src/core/types/base/botConfig.ts | 16 +- src/core/types/base/path.ts | 1 + src/core/types/base/pool.ts | 187 +++++++++--------- 8 files changed, 158 insertions(+), 133 deletions(-) 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; } }