diff --git a/.env.juno.example b/.env.juno.example index 474b766..26373a6 100644 --- a/.env.juno.example +++ b/.env.juno.example @@ -1,8 +1,8 @@ # The wallet mnemonic to send transactions from WALLET_MNEMONIC="" USE_MEMPOOL="1" -GAS_UNIT_USAGES="1400000,2750000" -PROFIT_THRESHOLD="4" +GAS_USAGE_PER_HOP="700000" #defines the gas usage per hop, 2 hop arb pays 1400000 gas, 3 hop will pay 2100000 etc +PROFIT_THRESHOLD="10000" # LOGGING ENVIRONMENT VARIABLES # SLACK_TOKEN = # SLACK_CHANNEL = @@ -20,6 +20,7 @@ CHAIN_PREFIX="juno" RPC_URL="https://juno-rpc.reece.sh" GAS_UNIT_PRICE="0.0025" FLASHLOAN_ROUTER_ADDRESS="juno1qa7vdlm6zgq3radal5sltyl4t4qd32feug9qs50kcxda46q230pqzny48s" +FLASHLOAN_FEE="0.3" #in % POOLS='{"pool": "juno1sg6chmktuhyj4lsrxrrdflem7gsnk4ejv6zkcc4d3vcqulzp55wsf4l4gl","inputfee": 0.301, "outputfee": 0}, {"pool": "juno1ctsmp54v79x7ea970zejlyws50cj9pkrmw49x46085fn80znjmpqz2n642","inputfee": 0.42,"outputfee": 0}, {"pool": "juno1e8n6ch7msks487ecznyeagmzd5ml2pq9tgedqt2u63vra0q0r9mqrjy6ys","inputfee": 0.301,"outputfee": 0}, diff --git a/.env.terra.example b/.env.terra.example index a710aba..b63e812 100644 --- a/.env.terra.example +++ b/.env.terra.example @@ -2,8 +2,9 @@ ##GENERAL SETTINGS WALLET_MNEMONIC="" ##change this USE_MEMPOOL="1" -GAS_UNIT_USAGES="1400000,1900000" -PROFIT_THRESHOLD="4" +GAS_USAGE_PER_HOP="700000" #defines the gas usage per hop, 2 hop arb pays 1400000 gas, 3 hop will pay 2100000 etc +PROFIT_THRESHOLD="5000" + ##LOGGING ENVIRONMENT VARIABLES, optional #SLACK_TOKEN = "" @@ -21,6 +22,7 @@ SKIP_URL= "http://phoenix-1-api.skip.money" SKIP_BID_WALLET= "terra1d5fzv2y8fpdax4u2nnzrn5uf9ghyu5sxr865uy" SKIP_BID_RATE="0.1" #e.g. 10% of the profit is used as a bid to win the auction FLASHLOAN_ROUTER_ADDRESS="terra1c8tpvta3umr4mpufvxmq39gyuw2knpyxyfgq8thpaeyw2r6a80qsg5wa00" +FLASHLOAN_FEE="0.3" #in % # # List of pool addresses that will be used to find arbitrage paths, add more paths and factories/routers POOLS='{"pool": "terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr","inputfee": 0,"outputfee": 0.3}, {"pool": "terra1zrs8p04zctj0a0f9azakwwennrqfrkh3l6zkttz9x89e7vehjzmqzg8v7n","inputfee": 0,"outputfee": 0.3}, diff --git a/src/chains/defaults/messages/getFlashArbMessages.ts b/src/chains/defaults/messages/getFlashArbMessages.ts index dccc36f..78d044b 100644 --- a/src/chains/defaults/messages/getFlashArbMessages.ts +++ b/src/chains/defaults/messages/getFlashArbMessages.ts @@ -18,72 +18,44 @@ export function getFlashArbMessages( walletAddress: string, flashloancontract: string, ): [Array, number] { - let flashLoanMessage: FlashLoanMessage; - if (arbTrade.path.pools.length == 3) { - flashLoanMessage = getFlashArbMessages3Hop(arbTrade.path, arbTrade.offerAsset); - } else { - flashLoanMessage = getFlashArbMessages2Hop(arbTrade.path, arbTrade.offerAsset); - } + const flashloanMessage = getFlashArbMessage(arbTrade.path, arbTrade.offerAsset); + const encodedMsgObject: EncodeObject = { typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", value: MsgExecuteContract.fromPartial({ sender: walletAddress, contract: flashloancontract, - msg: toUtf8(JSON.stringify(flashLoanMessage)), + msg: toUtf8(JSON.stringify(flashloanMessage)), funds: [], }), }; - return [[encodedMsgObject], flashLoanMessage.flash_loan.msgs.length]; + return [[encodedMsgObject], flashloanMessage.flash_loan.msgs.length]; } /** * */ -function getFlashArbMessages2Hop(path: Path, offerAsset0: Asset): FlashLoanMessage { - // double swap message as we trade only native assets - const [wasmMsgs0, offerAsset1] = getWasmMessages(path.pools[0], offerAsset0); - const [wasmMsgs1, offerAsset2] = getWasmMessages(path.pools[1], offerAsset1); - - const flashLoanMessage: FlashLoanMessage = { - flash_loan: { - assets: [offerAsset0], - msgs: [...wasmMsgs0, ...wasmMsgs1], - }, - }; - return flashLoanMessage; -} -/** - * - */ -function getFlashArbMessages3Hop(path: Path, offerAsset0: Asset): FlashLoanMessage { - // double swap message as we trade only native assets - const [wasmMsgs0, offerAsset1] = getWasmMessages(path.pools[0], offerAsset0); - const [wasmMsgs1, offerAsset2] = getWasmMessages(path.pools[1], offerAsset1); - const [wasmMsgs2, offerAsset3] = getWasmMessages(path.pools[2], offerAsset2); +function getFlashArbMessage(path: Path, offerAsset0: Asset): FlashLoanMessage { + const wasmMsgs = []; + let offerAsset = offerAsset0; + for (const pool of path.pools) { + const [wasmMsgsPool, offerAssetNext] = getWasmMessages(pool, offerAsset); + wasmMsgs.push(...wasmMsgsPool); + offerAsset = offerAssetNext; + } const flashLoanMessage: FlashLoanMessage = { flash_loan: { assets: [offerAsset0], - msgs: [...wasmMsgs0, ...wasmMsgs1, ...wasmMsgs2], + msgs: wasmMsgs, }, }; return flashLoanMessage; } - /** * */ function getWasmMessages(pool: Pool, offerAsset: Asset) { const [outGivenInTrade, returnAssetInfo] = outGivenIn(pool, offerAsset); - console.log( - pool.address, - ": ", - "in: ", - offerAsset.amount, - isNativeAsset(offerAsset.info) ? offerAsset.info.native_token.denom : offerAsset.info.token.contract_addr, - "out: ", - outGivenInTrade, - isNativeAsset(returnAssetInfo) ? returnAssetInfo.native_token.denom : returnAssetInfo.token.contract_addr, - ); const beliefPrice = Math.round((+offerAsset.amount / outGivenInTrade) * 1e6) / 1e6; //gives price per token bought const nextOfferAsset: Asset = { amount: String(outGivenInTrade), info: returnAssetInfo }; let msg: SwapMessage | JunoSwapMessage | SendMessage; @@ -91,7 +63,7 @@ function getWasmMessages(pool: Pool, offerAsset: Asset) { if (isNativeAsset(offerAsset.info)) { msg = { swap: { - max_spread: "0.05", + max_spread: "0.01", offer_asset: pool.dexname === AmmDexName.default ? offerAsset @@ -101,7 +73,7 @@ function getWasmMessages(pool: Pool, offerAsset: Asset) { }; } else { const innerSwapMsg: InnerSwapMessage = { - swap: { belief_price: String(beliefPrice), max_spread: "0.0005" }, + swap: { belief_price: String(beliefPrice), max_spread: "0.01" }, }; const objJsonStr = JSON.stringify(innerSwapMsg); const objJsonB64 = Buffer.from(objJsonStr).toString("base64"); diff --git a/src/core/arbitrage/arbitrage.ts b/src/core/arbitrage/arbitrage.ts index d7a8ffe..ffa7b0f 100644 --- a/src/core/arbitrage/arbitrage.ts +++ b/src/core/arbitrage/arbitrage.ts @@ -17,8 +17,10 @@ export function trySomeArb(paths: Array, botConfig: BotConfig): OptimalTra if (path === undefined) { return undefined; } else { - const minProfit = path.pools.length == 2 ? botConfig.profitThreshold2Hop : botConfig.profitThreshold3Hop; - if (profit * 0.997 < minProfit) { + const profitThreshold = + botConfig.profitThresholds.get(path.pools.length) ?? + Array.from(botConfig.profitThresholds.values())[botConfig.profitThresholds.size]; + if (profit < profitThreshold) { return undefined; } else { console.log("optimal tradesize: ", tradesize, " with profit: ", profit); diff --git a/src/core/types/arbitrageloops/mempoolLoop.ts b/src/core/types/arbitrageloops/mempoolLoop.ts index 04d3256..f989183 100644 --- a/src/core/types/arbitrageloops/mempoolLoop.ts +++ b/src/core/types/arbitrageloops/mempoolLoop.ts @@ -140,13 +140,15 @@ export class MempoolLoop { chainId: this.chainid, }; - const GAS_FEE = nrOfMessages === 2 ? this.botConfig.txFee2Hop : this.botConfig.txFee3Hop; + const TX_FEE = + this.botConfig.txFees.get(arbTrade.path.pools.length) ?? + Array.from(this.botConfig.txFees.values())[this.botConfig.gasFees.size]; // sign, encode and broadcast the transaction const txRaw = await this.botClients.SigningCWClient.sign( this.account.address, msgs, - GAS_FEE, + TX_FEE, "memo", signerData, ); diff --git a/src/core/types/arbitrageloops/skipLoop.ts b/src/core/types/arbitrageloops/skipLoop.ts index 2216b4b..b7ba621 100644 --- a/src/core/types/arbitrageloops/skipLoop.ts +++ b/src/core/types/arbitrageloops/skipLoop.ts @@ -8,10 +8,10 @@ import { WebClient } from "@slack/web-api"; import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx"; +import { OptimalTrade } from "../../arbitrage/arbitrage"; import { sendSlackMessage } from "../../logging/slacklogger"; import { BotClients } from "../../node/chainoperator"; import { SkipResult } from "../../node/skipclients"; -import { OptimalTrade } from "../../arbitrage/arbitrage"; import { BotConfig } from "../base/botConfig"; import { MempoolTrade, processMempool } from "../base/mempool"; import { Path } from "../base/path"; @@ -87,21 +87,21 @@ export class SkipLoop extends MempoolLoop { */ private async skipTrade(arbTrade: OptimalTrade, toArbTrade: MempoolTrade) { if ( - !this.botConfig.useSkip || - this.botConfig.skipRpcUrl === undefined || - this.botConfig.skipBidRate === undefined || - this.botConfig.skipBidWallet === undefined + !this.botConfig.skipConfig?.useSkip || + this.botConfig.skipConfig?.skipRpcUrl === undefined || + this.botConfig.skipConfig?.skipBidRate === undefined || + this.botConfig.skipConfig?.skipBidWallet === undefined ) { console.error("please setup skip variables in the config environment file", 1); return; } const bidMsg: MsgSend = MsgSend.fromJSON({ fromAddress: this.account.address, - toAddress: this.botConfig.skipBidWallet, + toAddress: this.botConfig.skipConfig.skipBidWallet, amount: [ { denom: this.botConfig.offerAssetInfo.native_token.denom, - amount: String(Math.max(Math.round(arbTrade.profit * this.botConfig.skipBidRate), 651)), + amount: String(Math.max(Math.round(arbTrade.profit * this.botConfig.skipConfig.skipBidRate), 651)), }, ], }); @@ -122,11 +122,14 @@ export class SkipLoop extends MempoolLoop { ); msgs.push(bidMsgEncodedObject); - const GAS_FEE = nrOfWasms === 2 ? this.botConfig.txFee2Hop : this.botConfig.txFee3Hop; + //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) ?? + Array.from(this.botConfig.txFees.values())[this.botConfig.gasFees.size]; const txRaw: TxRaw = await this.botClients.SigningCWClient.sign( this.account.address, msgs, - GAS_FEE, + TX_FEE, "", signerData, ); diff --git a/src/core/types/base/botConfig.ts b/src/core/types/base/botConfig.ts index 5bc8729..2e84f42 100644 --- a/src/core/types/base/botConfig.ts +++ b/src/core/types/base/botConfig.ts @@ -3,6 +3,12 @@ import { assert } from "console"; import { NativeAssetInfo } from "./asset"; +interface SkipConfig { + useSkip: boolean; + skipRpcUrl: string; + skipBidWallet: string; + skipBidRate: number; +} export interface BotConfig { chainPrefix: string; rpcUrl: string; @@ -15,10 +21,9 @@ export interface BotConfig { baseDenom: string; gasPrice: string; - profitThreshold3Hop: number; - profitThreshold2Hop: number; - txFee3Hop: StdFee; - txFee2Hop: StdFee; + gasFees: Map; + txFees: Map; + profitThresholds: Map; // logging specific (optionally) // Slack OAuth2 token for the specific SlackApp @@ -27,10 +32,7 @@ export interface BotConfig { slackChannel?: string | undefined; // Skip specific (optionally) - useSkip?: boolean; - skipRpcUrl?: string | undefined; - skipBidWallet?: string | undefined; - skipBidRate?: number | undefined; + skipConfig: SkipConfig | undefined; } /** @@ -44,22 +46,40 @@ export function setBotConfig(envs: NodeJS.ProcessEnv): BotConfig { JSON.parse(mapping), ); const OFFER_ASSET_INFO: NativeAssetInfo = { native_token: { denom: envs.BASE_DENOM } }; - - const GAS_UNIT_USAGES = envs.GAS_UNIT_USAGES.split(","); const GAS_UNIT_PRICE = envs.GAS_UNIT_PRICE; //price per gas unit in BASE_DENOM - const GAS_FEE_2HOP: Coin = { denom: envs.BASE_DENOM, amount: String(+GAS_UNIT_USAGES[0] * +GAS_UNIT_PRICE) }; - const GAS_FEE_3HOP: Coin = { denom: envs.BASE_DENOM, amount: String(+GAS_UNIT_USAGES[1] * +GAS_UNIT_PRICE) }; - const TX_FEE_2HOP: StdFee = { amount: [GAS_FEE_2HOP], gas: GAS_UNIT_USAGES[0] }; - const TX_FEE_3HOP: StdFee = { amount: [GAS_FEE_3HOP], gas: GAS_UNIT_USAGES[1] }; - //make sure amount is GAS_FEE.gas * GAS_PRICE at minimum - //make sure gas units used is adjusted based on amount of msgs in the arb - //make sure amount is GAS_FEE.gas * GAS_PRICE at minimum - //make sure gas units used is adjusted based on amount of msgs in the arb - const MIN_PROFIT_THRESHOLD3Hop = +envs.PROFIT_THRESHOLD * +GAS_FEE_3HOP.amount; //minimal profit threshold as multiplier of paid GAS_COIN.amount - const MIN_PROFIT_THRESHOLD2Hop = +envs.PROFIT_THRESHOLD * +GAS_FEE_2HOP.amount; //minimal profit threshold as multiplier of paid GAS_COIN.amount + const GAS_USAGE_PER_HOP = +envs.GAS_USAGE_PER_HOP; + const MAX_PATH_HOPS = +envs.MAX_PATH_HOPS; //required gas units per trade (hop) + + // setup skipconfig if present + let skipConfig; + if (envs.USE_SKIP == "1") { + validateSkipEnvs(envs); + skipConfig = { + useSkip: true, + skipRpcUrl: envs.SKIP_URL ?? "", + skipBidWallet: envs.SKIP_BID_WALLET ?? "", + 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++) { + 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) + +gasFee.amount //dont use skip bid on top of the threshold, include flashloan fee and gas fee + : PROFIT_THRESHOLD / (1 - FLASHLOAN_FEE) + +gasFee.amount + skipConfig.skipBidRate * PROFIT_THRESHOLD; //need extra profit to provide the skip bid + PROFIT_THRESHOLDS.set(hops, profitThreshold); + } - const SKIP_BID_RATE = envs.SKIP_BID_RATE !== undefined ? +envs.SKIP_BID_RATE : undefined; const botConfig: BotConfig = { chainPrefix: envs.CHAIN_PREFIX, rpcUrl: envs.RPC_URL, @@ -71,16 +91,12 @@ export function setBotConfig(envs: NodeJS.ProcessEnv): BotConfig { useMempool: envs.USE_MEMPOOL == "1" ? true : false, baseDenom: envs.BASE_DENOM, gasPrice: envs.GAS_UNIT_PRICE, - profitThreshold3Hop: MIN_PROFIT_THRESHOLD3Hop, - profitThreshold2Hop: MIN_PROFIT_THRESHOLD2Hop, - txFee3Hop: TX_FEE_3HOP, - txFee2Hop: TX_FEE_2HOP, + profitThresholds: PROFIT_THRESHOLDS, + gasFees: GAS_FEES, + txFees: TX_FEES, slackToken: envs.SLACK_TOKEN, slackChannel: envs.SLACK_CHANNEL, - useSkip: envs.USE_SKIP == "1" ? true : false, - skipRpcUrl: envs.SKIP_URL, - skipBidWallet: envs.SKIP_BID_WALLET, - skipBidRate: SKIP_BID_RATE, + skipConfig: skipConfig, }; return botConfig; } @@ -100,5 +116,10 @@ function validateEnvs(envs: NodeJS.ProcessEnv) { } /** - * Runs the main program. + * */ +function validateSkipEnvs(envs: NodeJS.ProcessEnv) { + assert(envs.SKIP_URL, `Please set SKIP_URL in env or ".env" file`); + assert(envs.SKIP_BID_WALLET, `Please set SKIP_BID_WALLET in env or ".env" file`); + assert(envs.SKIP_BID_RATE, `Please set SKIP_BID_RATE in env or ".env" file`); +} diff --git a/src/core/types/modules.d.ts b/src/core/types/modules.d.ts index 49ef6f7..c26ccfe 100644 --- a/src/core/types/modules.d.ts +++ b/src/core/types/modules.d.ts @@ -29,6 +29,10 @@ declare namespace NodeJS { * Flashloan router contract that handles the flashloan and execute the operations/messages. */ FLASHLOAN_ROUTER_ADDRESS: string; + /** + * Fee used for taking a flashloan. + */ + FLASHLOAN_FEE: string; /** * A list of all the pools to map, separated by ", \n". * @@ -55,7 +59,8 @@ declare namespace NodeJS { */ PROFIT_THRESHOLD: string; - GAS_UNIT_USAGES: string; + GAS_USAGE_PER_HOP: string; + MAX_PATH_HOPS: string; SLACK_TOKEN: string | undefined; SLACK_CHANNEL: string | undefined; diff --git a/src/index.ts b/src/index.ts index 07f54ed..b816968 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,9 +22,10 @@ console.log("OFFER DENOM: ", botConfig.offerAssetInfo); // console.log("POOLS: ", botConfig.poolEnvs); console.log("FACTORIES_TO_ROUTERS_MAPPING", botConfig.mappingFactoryRouter); console.log("USE MEMPOOL: ", botConfig.useMempool); -console.log("USE SKIP: ", botConfig.useSkip); -if (botConfig.useSkip) { - console.log("SKIP URL: ", botConfig.skipRpcUrl); + +if (botConfig.skipConfig) { + console.log("USE SKIP: ", botConfig.skipConfig.useSkip); + console.log("SKIP URL: ", botConfig.skipConfig.skipRpcUrl); } console.log("---".repeat(30)); @@ -50,7 +51,7 @@ async function main() { if (botConfig.slackToken) { slackClient = getSlackClient(botConfig.slackToken); } - + console.log(botConfig); const { accountNumber, sequence } = await botClients.SigningCWClient.getSequence(account.address); const chainId = await ( await botClients.HttpClient.execute(createJsonRpcRequest("block")) @@ -72,15 +73,10 @@ async function main() { console.log("Removed ", allPools.length - filteredPools.length, " unused pools"); let loop; - if ( - botConfig.useSkip && - botConfig.skipRpcUrl !== undefined && - botConfig.skipBidRate !== undefined && - botConfig.skipBidWallet !== undefined - ) { + if (botConfig.skipConfig) { console.log("Initializing skip loop"); const [skipClient, skipSigner] = await getSkipClient( - botConfig.skipRpcUrl, + botConfig.skipConfig.skipRpcUrl, botConfig.mnemonic, botConfig.chainPrefix, );