diff --git a/README.md b/README.md index 4e1045ac..94af52e0 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ node arb-bot -k 12ab... -r https://... --orderbook-address 0x1a2b... --arb-addre The app requires these arguments (all arguments can be set in env variables alternatively, more details below): - `-k` or `--key`, Private key of wallet that performs the transactions. Will override the 'BOT_WALLET_PRIVATEKEY' in env variables - `-r` or `--rpc`, RPC URL(s) that will be provider for interacting with evm, use different providers if more than 1 is specified to prevent banning. Will override the 'RPC_URL' in env variables -- `-m` or `--mode`, Running mode of the bot, must be one of: `0x` or `curve` or `router` or `crouter` or `srouter`, Will override the 'MODE' in env variables +- `-m` or `--mode`, Running mode of the bot, must be one of: `0x` or `curve` or `router` or `crouter` or `srouter` or `suniv2`, Will override the 'MODE' in env variables - `--orderbook-address`, Address of the deployed orderbook contract, Will override the 'ORDERBOOK_ADDRESS' in env variables - `--arb-address`, Address of the deployed arb contract, Will override the 'ARB_ADDRESS' in env variables - `--arb-contract-type`, Type of the Arb contract, can be either of `flash-loan-v2` or `flash-loan-v3` or `order-taker`, not availabe for `srouter` mode since it is a specialized mode, Will override the 'ARB_TYPE' in env variables @@ -61,6 +61,8 @@ Other optional arguments are: - `--flashbot-rpc`, Optional flashbot rpc url to submit transaction to, Will override the 'FLASHBOT_RPC' in env variables - `--interpreter-v2`, Flag for operating with interpreter V2, note that 'flash-loan-v2' is NOT compatible with interpreter v2. Will override the 'INTERPRETERV2' in env variables - `--no-bundle`, Flag for not bundling orders based on pairs and clear each order individually. Will override the 'NO_BUNDLE' in env variables +- `--hops`, Option to specify how many hops the binary search should do in srouter mode, default is 11 if left unspecified, Will override the 'HOPS' in env variables +- `--rp32`, Option to use sushi RouteProcessor v3.2, defaults to v3 if not passed, Will override the 'RP3_2' in env variables - `-V` or `--version`, output the version number - `-h` or `--help`, output usage information @@ -107,7 +109,7 @@ which will show: Options: -k, --key Private key of wallet that performs the transactions. Will override the 'BOT_WALLET_PRIVATEKEY' in env variables -r, --rpc RPC URL(s) that will be provider for interacting with evm, use different providers if more than 1 is specified to prevent banning. Will override the 'RPC_URL' in env variables - -m, --mode Running mode of the bot, must be one of: `0x` or `curve` or `router` or `crouter` or `srouter`, Will override the 'MODE' in env variables + -m, --mode Running mode of the bot, must be one of: `0x` or `curve` or `router` or `crouter` or `srouter` or `suniv2`, Will override the 'MODE' in env variables -o, --orders The path to a local json file containing the orders details, can be used in combination with --subgraph, Will override the 'ORDERS' in env variables -s, --subgraph Subgraph URL(s) to read orders details from, can be used in combination with --orders, Will override the 'SUBGRAPH' in env variables --orderbook-address
Address of the deployed orderbook contract, Will override the 'ORDERBOOK_ADDRESS' in env variables @@ -129,6 +131,8 @@ which will show: --use-public-rpcs Option to use public rpcs as fallback option for 'srouter' and 'router' mode, Will override the 'USE_PUBLIC_RPCS' in env variables --interpreter-v2 Flag for operating with interpreter V2, note that 'flash-loan-v2' is NOT compatible with interpreter v2. Will override the 'INTERPRETERV2' in env variables --no-bundle Flag for not bundling orders based on pairs and clear each order individually. Will override the 'NO_BUNDLE' in env variables + --hops Option to specify how many hops the binary search should do in srouter mode, default is 11 if left unspecified, Will override the 'HOPS' in env variables + --rp32 Option to use sushi RouteProcessor v3.2, defaults to v3 if not passed, Will override the 'RP3_2' in env variables -V, --version output the version number -h, --help display help for command
@@ -145,7 +149,7 @@ RPC_URL="https://polygon-mainnet.g.alchemy.com/v2/{API_KEY}, https://rpc.ankr.co # Option to submit transactions using the flashbot RPC. FLASHBOT_RPC="" -# bot running mode, one of "router", "0x", "curve", "crouter", "srouter" +# bot running mode, one of "router", "0x", "curve", "crouter", "srouter", "suniv2" MODE="router" # arb contract address @@ -213,6 +217,9 @@ NO_BUNDLE="false" # number of hops of binary search in srouter mode, if left unspecified will be 11 by default HOPS=11 + +# Option to use sushi RouteProcessorv3.2, default is v3 +RP3_2="true" ``` If both env variables and CLI argument are set, the CLI arguments will be prioritized and override the env variables. @@ -241,6 +248,8 @@ const configOptions = { timeout : 300, // seconds to wait for tx to mine before disregarding it interpreterv2 : true, // if interpreter v2 should be used, not compatible with flash-loan-v2 arb contract bundle : true, // if orders should be bundled based on token pair or be handled individually + hops : 6, // The amount of hops of binary search for sorouter mode + rp32 : true, // Option to use sushi RouteProcessorv3.2, default is v3 liquidityProviders : [ // list of liquidity providers for "router" mode to get quotes from (optional) "sushiswapv2", "uniswapv2" @@ -266,7 +275,7 @@ const sgFilters = { // fil const orderDetails = await RainArbBot.getOrderDetails(subgraphs, ordersJson, config.signer, sgFilters); // to run the clearing process and get the report object which holds the report of cleared orders -const mode = "srouter" // mode can be one of "router" or "0x" or "curve" or "crouter" or "srouter" +const mode = "srouter" // mode can be one of "router" or "0x" or "curve" or "crouter" or "srouter" or "suniv2" const reports = await RainArbBot.clear(mode, config, orderDetails, ...[clearOptions]) ```
diff --git a/arb-bot.js b/arb-bot.js index 59f6ceaa..f3035f99 100755 --- a/arb-bot.js +++ b/arb-bot.js @@ -34,6 +34,7 @@ const ENV_OPTIONS = { timeout : process?.env?.TIMEOUT, flashbotRpc : process?.env?.FLASHBOT_RPC, hops : process?.env?.HOPS, + rp32 : process?.env?.RP3_2?.toLowerCase() === "true" ? true : false, rpc : process?.env?.RPC_URL ? Array.from(process?.env?.RPC_URL.matchAll(/[^,\s]+/g)).map(v => v[0]) : undefined, @@ -46,7 +47,7 @@ const getOptions = async argv => { const cmdOptions = new Command("node arb-bot") .option("-k, --key ", "Private key of wallet that performs the transactions. Will override the 'BOT_WALLET_PRIVATEKEY' in env variables") .option("-r, --rpc ", "RPC URL(s) that will be provider for interacting with evm, use different providers if more than 1 is specified to prevent banning. Will override the 'RPC_URL' in env variables") - .option("-m, --mode ", "Running mode of the bot, must be one of: `0x` or `curve` or `router` or `crouter` or `srouter`, Will override the 'MODE' in env variables") + .option("-m, --mode ", "Running mode of the bot, must be one of: `0x` or `curve` or `router` or `crouter` or `srouter` or `suniv2`, Will override the 'MODE' in env variables") .option("-o, --orders ", "The path to a local json file containing the orders details, can be used in combination with --subgraph, Will override the 'ORDERS' in env variables") .option("-s, --subgraph ", "Subgraph URL(s) to read orders details from, can be used in combination with --orders, Will override the 'SUBGRAPH' in env variables") .option("--orderbook-address
", "Address of the deployed orderbook contract, Will override the 'ORDERBOOK_ADDRESS' in env variables") @@ -69,6 +70,7 @@ const getOptions = async argv => { .option("--interpreter-v2", "Flag for operating with interpreter V2, note that 'flash-loan-v2' is NOT compatible with interpreter v2. Will override the 'INTERPRETERV2' in env variables") .option("--no-bundle", "Flag for not bundling orders based on pairs and clear each order individually. Will override the 'NO_BUNDLE' in env variables") .option("--hops ", "Option to specify how many hops the binary search should do in srouter mode, default is 11 if left unspecified, Will override the 'HOPS' in env variables") + .option("--rp32", "Option to use sushi RouteProcessor v3.2, defaults to v3 if not passed, Will override the 'RP3_2' in env variables") .description([ "A NodeJS app to find and take arbitrage trades for Rain Orderbook orders against some DeFi liquidity providers, requires NodeJS v18 or higher.", "- Use \"node arb-bot [options]\" command alias for running the app from its repository workspace", @@ -104,6 +106,7 @@ const getOptions = async argv => { cmdOptions.timeout = cmdOptions.timeout || ENV_OPTIONS.timeout; cmdOptions.interpreterv2 = cmdOptions.interpreterv2 || ENV_OPTIONS.interpreterv2; cmdOptions.hops = cmdOptions.hops || ENV_OPTIONS.hops; + cmdOptions.rp32 = cmdOptions.rp32 || ENV_OPTIONS.rp32; cmdOptions.bundle = cmdOptions.bundle ? ENV_OPTIONS.bundle : false; return cmdOptions; @@ -136,6 +139,7 @@ const arbRound = async options => { interpreterv2 : options.interpreterv2, bundle : options.bundle, hops : options.hops, + rp32 : options.rp32, liquidityProviders : options.lps ? Array.from(options.lps.matchAll(/[^,\s]+/g)).map(v => v[0]) : undefined, diff --git a/config.json b/config.json index b4b52982..4476701e 100644 --- a/config.json +++ b/config.json @@ -1015,5 +1015,42 @@ "address": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", "decimals": 18 } + }, + { + "network": "flare mainnet", + "chainId": 14, + "explorer": "https://flarescan.com/", + "routeProcessor3Address": "0x9B3F1D56D9004e6C69d8247d402F38DE5F87A27c", + "uniV2Router02Address": "0x088EeCB467B3968Da36c71F05023A1d3133B2B83", + "zeroEx": { + }, + "curve": { + "pools": [] + }, + "enosys":{ + "pools": [ + { + "address": "0x7520005032F43229F606d3ACeae97045b9D6F7ea", + "token0": "0x1D80c49BbBCd1C0911346656B529DF9E5c2F783d", + "token1": "0x96B41289D90444B8adD57e6F265DB5aE8651DF29" + } + ] + }, + "nativeToken": { + "symbol": "FLR", + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "decimals": 18 + }, + "nativeWrappedToken": { + "symbol": "WFLR", + "address": "0x1D80c49BbBCd1C0911346656B529DF9E5c2F783d", + "decimals": 18 + }, + "liquidityProviders": [ + + ], + "stableTokens": { + + } } ] \ No newline at end of file diff --git a/example.env b/example.env index 53106487..6ea4f7eb 100644 --- a/example.env +++ b/example.env @@ -11,7 +11,7 @@ RPC_URL="https://polygon-mainnet.g.alchemy.com/v2/{API_KEY}, https://rpc.ankr.co # Option to submit transactions using the flashbot RPC. FLASHBOT_RPC="" -# bot running mode, one of "router", "0x", "curve", "crouter", "srouter" +# bot running mode, one of "router", "0x", "curve", "crouter", "srouter", "suniv2" MODE="router" # arb contract address @@ -78,4 +78,7 @@ INTERPRETERV2="true" NO_BUNDLE="false" # number of hops of binary search in srouter mode, if left unspecified will be 11 by default -HOPS=11 \ No newline at end of file +HOPS=11 + +# Option to use sushi RouteProcessorv3.2, default is v3 +RP3_2="true" \ No newline at end of file diff --git a/src/index.js b/src/index.js index 63ee5ef6..fa1fa939 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ const { zeroExClear } = require("./modes/zeroex"); const { routerClear } = require("./modes/router"); const { crouterClear } = require("./modes/crouter"); const { srouterClear } = require("./modes/srouter"); +const { suniv2Clear } = require("./modes/suniv2"); const { getOrderDetailsFromJson, appGlobalLogger } = require("./utils"); @@ -68,7 +69,11 @@ const configOptions = { /** * The amount of hops of binary search for sorouter mode */ - hops: 11 + hops: 11, + /** + * Option to use sushi RouteProcessorv3.2, default is v3 + */ + rp32: false }; /** @@ -236,6 +241,7 @@ const getConfig = async( config.usePublicRpcs = !!options?.usePublicRpcs; config.interpreterv2 = !!options?.interpreterv2; config.hops = hops; + config.rp32 = !!options?.rp32; return config; }; @@ -258,14 +264,11 @@ const clear = async( const _mode = mode.toLowerCase(); const version = versions.node; const majorVersion = Number(version.slice(0, version.indexOf("."))); - // const prioritization = options.prioritization !== undefined - // ? !!options.prioritization - // : clearOptions.prioritization; const gasCoveragePercentage = options.gasCoveragePercentage !== undefined ? options.gasCoveragePercentage : clearOptions.gasCoveragePercentage; - if (_mode !== "srouter") { + if (_mode !== "srouter" && _mode !== "suniv2") { if (!config.arbType) throw "undefined arb contract type"; if (!/^flash-loan-v[23]$|^order-taker$/.test(config.arbType)) { throw "invalid arb contract type, must be either of: 'flash-loan-v2' or 'flash-loan-v3' or 'order-taker'"; @@ -315,6 +318,15 @@ const clear = async( ); else throw `NodeJS v18 or higher is required for running the app in "router" mode, current version: ${version}`; } + else if (_mode === "suniv2") { + if (majorVersion >= 18) return await suniv2Clear( + config, + ordersDetails, + gasCoveragePercentage, + // prioritization + ); + else throw `NodeJS v18 or higher is required for running the app in "router" mode, current version: ${version}`; + } else throw "unknown mode, must be either of '0x' or 'curve' or 'router' or 'srouter'"; }; diff --git a/src/modes/crouter.js b/src/modes/crouter.js index cbf10176..8fcc708d 100644 --- a/src/modes/crouter.js +++ b/src/modes/crouter.js @@ -455,7 +455,9 @@ const crouterClear = async( fromToken, toToken, arb.address, - config.routeProcessor3_2Address, + config.rp32 + ? config.routeProcessor3_2Address + : config.routeProcessor3Address, // permits // "0.005" ); @@ -477,8 +479,12 @@ const crouterClear = async( exchangeData = ethers.utils.defaultAbiCoder.encode( ["address", "address", "bytes"], [ - config.routeProcessor3_2Address, - config.routeProcessor3_2Address, + config.rp32 + ? config.routeProcessor3_2Address + : config.routeProcessor3Address, + config.rp32 + ? config.routeProcessor3_2Address + : config.routeProcessor3Address, fnData ] ); diff --git a/src/modes/router.js b/src/modes/router.js index 8b668f98..09f2fd41 100644 --- a/src/modes/router.js +++ b/src/modes/router.js @@ -256,7 +256,9 @@ const routerClear = async( fromToken, toToken, arb.address, - config.routeProcessor3_2Address, + config.rp32 + ? config.routeProcessor3_2Address + : config.routeProcessor3Address, // permits // "0.005" ); @@ -297,8 +299,12 @@ const routerClear = async( const exchangeData = ethers.utils.defaultAbiCoder.encode( ["address", "address", "bytes"], [ - config.routeProcessor3_2Address, - config.routeProcessor3_2Address, + config.rp32 + ? config.routeProcessor3_2Address + : config.routeProcessor3Address, + config.rp32 + ? config.routeProcessor3_2Address + : config.routeProcessor3Address, fnData ] ); diff --git a/src/modes/srouter.js b/src/modes/srouter.js index 06db0f93..32027393 100644 --- a/src/modes/srouter.js +++ b/src/modes/srouter.js @@ -438,7 +438,7 @@ async function checkArb( fromToken, toToken, arb.address, - config.routeProcessor3_2Address, + config.rp32 ? config.routeProcessor3_2Address : config.routeProcessor3Address, // permits // "0.005" ); diff --git a/src/modes/suniv2.js b/src/modes/suniv2.js new file mode 100644 index 00000000..e306d874 --- /dev/null +++ b/src/modes/suniv2.js @@ -0,0 +1,531 @@ +const ethers = require("ethers"); +const { arbAbis, orderbookAbi } = require("../abis"); +const { Token } = require("sushiswap-router"); +const { + getIncome, + getActualPrice, + promiseTimeout, + bundleTakeOrders, + getActualClearAmount, + getAmountOutFlareSwap, + getUniV2Route +} = require("../utils"); + +/** + * Main function that gets order details from subgraph, bundles the ones that have balance and tries clearing them with specialized router contract specifically for flare mainnet + * + * @param {object} config - The configuration object + * @param {any[]} ordersDetails - The order details queried from subgraph + * @param {string} gasCoveragePercentage - (optional) The percentage of the gas cost to cover on each transaction + * for it to be considered profitable and get submitted + * @returns The report of details of cleared orders + */ +const suniv2Clear = async( + config, + ordersDetails, + gasCoveragePercentage = "100" +) => { + + if (!config.uniV2Router02Address) throw "no univ2Router contract address is specified for this network"; + if ( + gasCoveragePercentage < 0 || + !Number.isInteger(Number(gasCoveragePercentage)) + ) throw "invalid gas coverage percentage, must be an integer greater than equal 0"; + + const signer = config.signer; + const arbAddress = config.arbAddress; + const orderbookAddress = config.orderbookAddress; + const maxProfit = config.maxProfit; + const maxRatio = config.maxRatio; + const hops = config.hops; + const flashbotSigner = config.flashbotRpc + ? new ethers.Wallet( + signer.privateKey, + new ethers.providers.JsonRpcProvider(config.flashbotRpc) + ) + : undefined; + + // instantiating arb contract + const arb = new ethers.Contract(arbAddress, arbAbis["srouter"], signer); + + // instantiating orderbook contract + const orderbook = new ethers.Contract(orderbookAddress, orderbookAbi, signer); + + console.log( + "------------------------- Starting The", + "\x1b[32mS-ROUTER\x1b[0m", + "Mode -------------------------", + "\n" + ); + console.log("\x1b[33m%s\x1b[0m", Date()); + console.log("Arb Contract Address: " , arbAddress); + console.log("OrderBook Contract Address: " , orderbookAddress, "\n"); + + let bundledOrders = []; + + if (ordersDetails.length) { + console.log( + "------------------------- Bundling Orders -------------------------", "\n" + ); + bundledOrders = await bundleTakeOrders( + ordersDetails, + orderbook, + arb, + maxProfit, + config.rpc !== "test", + config.interpreterv2, + config.bundle + ); + } + else { + console.log("No orders found, exiting...", "\n"); + return; + } + + if (!bundledOrders.length) { + console.log("Could not find any order to clear for current market price, exiting...", "\n"); + return; + } + + const report = []; + for (let i = 0; i < bundledOrders.length; i++) { + try { + console.log( + `------------------------- Trying To Clear ${ + bundledOrders[i].buyTokenSymbol + }/${ + bundledOrders[i].sellTokenSymbol + } -------------------------`, + "\n" + ); + console.log(`Buy Token Address: ${bundledOrders[i].buyToken}`); + console.log(`Sell Token Address: ${bundledOrders[i].sellToken}`, "\n"); + + if (!bundledOrders[i].takeOrders.length) throw "All orders of this token pair have empty vault balance, skipping..."; + + const fromToken = new Token({ + chainId: config.chainId, + decimals: bundledOrders[i].sellTokenDecimals, + address: bundledOrders[i].sellToken, + symbol: bundledOrders[i].sellTokenSymbol + }); + const toToken = new Token({ + chainId: config.chainId, + decimals: bundledOrders[i].buyTokenDecimals, + address: bundledOrders[i].buyToken, + symbol: bundledOrders[i].buyTokenSymbol + }); + + const obSellTokenBalance = ethers.BigNumber.from(await signer.call({ + data: "0x70a08231000000000000000000000000" + orderbookAddress.slice(2), + to: bundledOrders[i].sellToken + })); + + if (obSellTokenBalance.isZero()) throw `Orderbook has no ${ + bundledOrders[i].sellTokenSymbol + } balance, skipping...`; + + let ethPrice; + const gasPrice = await signer.provider.getGasPrice(); + try { + if (gasCoveragePercentage !== "0") ethPrice = await getAmountOutFlareSwap( + signer, + config.uniV2Router02Address, + config.nativeWrappedToken.address, + "1" + "0".repeat(config.nativeWrappedToken.decimals), + bundledOrders[i].buyToken, + bundledOrders[i].buyTokenDecimals + ); + else ethPrice = "0"; + if (ethPrice === undefined) throw "could not find a route for ETH price, skipping..."; + } + catch { + throw "could not get ETH price, skipping..."; + } + + let rawtx, gasCostInToken, takeOrdersConfigStruct, price; + if (config.bundle) { + try { + ({ rawtx, gasCostInToken, takeOrdersConfigStruct, price } = await checkArb( + 0, + hops, + bundledOrders[i], + fromToken, + toToken, + signer, + obSellTokenBalance, + gasPrice, + gasCoveragePercentage, + maxProfit, + maxRatio, + arb, + ethPrice, + config, + )); + } catch { + rawtx = undefined; + } + } else { + const promises = []; + for (let j = 1; j < 4; j++) { + promises.push( + checkArb( + j, + hops, + bundledOrders[i], + fromToken, + toToken, + signer, + obSellTokenBalance, + gasPrice, + gasCoveragePercentage, + maxProfit, + maxRatio, + arb, + ethPrice, + config, + ) + ); + } + const allPromises = await Promise.allSettled(promises); + + let choice; + for (let j = 0; j < allPromises.length; j++) { + if (allPromises[j].status === "fulfilled") { + if (!choice || choice.maximumInput.lt(allPromises[j].value.maximumInput)) { + choice = allPromises[j].value; + } + } + } + if (choice) { + ({ rawtx, gasCostInToken, takeOrdersConfigStruct, price } = choice); + } + } + + if (!rawtx) { + console.log("\x1b[31m%s\x1b[0m", "found no match for this pair..."); + } + else { + // submit the tx only if dry runs with headroom is passed + try { + console.log(">>> Trying to submit the transaction...", "\n"); + rawtx.data = arb.interface.encodeFunctionData( + "arb", + [ + takeOrdersConfigStruct, + gasCostInToken.mul(gasCoveragePercentage).div("100") + ] + ); + console.log("Block Number: " + await signer.provider.getBlockNumber(), "\n"); + const tx = flashbotSigner !== undefined + ? await flashbotSigner.sendTransaction(rawtx) + : await signer.sendTransaction(rawtx); + + console.log("\x1b[33m%s\x1b[0m", config.explorer + "tx/" + tx.hash, "\n"); + console.log( + ">>> Transaction submitted successfully to the network, waiting for transaction to mine...", + "\n" + ); + const receipt = config.timeout + ? await promiseTimeout( + tx.wait(), + config.timeout, + `Transaction failed to mine after ${config.timeout}ms` + ) + : await tx.wait(); + if (receipt.status === 1) { + const clearActualAmount = getActualClearAmount( + arbAddress, + orderbookAddress, + receipt + ); + const income = getIncome(signer, receipt); + const clearActualPrice = getActualPrice( + receipt, + orderbookAddress, + arbAddress, + clearActualAmount.mul("1" + "0".repeat( + 18 - bundledOrders[i].sellTokenDecimals + )), + bundledOrders[i].buyTokenDecimals + ); + const actualGasCost = ethers.BigNumber.from( + receipt.effectiveGasPrice + ).mul(receipt.gasUsed); + const actualGasCostInToken = ethers.utils.parseUnits( + ethPrice + ).mul( + actualGasCost + ).div( + "1" + "0".repeat( + 36 - bundledOrders[i].buyTokenDecimals + ) + ); + const netProfit = income + ? income.sub(actualGasCostInToken) + : undefined; + + console.log( + "\x1b[36m%s\x1b[0m", + `Clear Initial Price: ${ethers.utils.formatEther(price)}` + ); + console.log("\x1b[36m%s\x1b[0m", `Clear Actual Price: ${clearActualPrice}`); + console.log("\x1b[36m%s\x1b[0m", `Clear Amount: ${ + ethers.utils.formatUnits( + clearActualAmount, + bundledOrders[i].sellTokenDecimals + ) + } ${bundledOrders[i].sellTokenSymbol}`); + console.log("\x1b[36m%s\x1b[0m", `Consumed Gas: ${ + ethers.utils.formatEther(actualGasCost) + } ${ + config.nativeToken.symbol + }`, "\n"); + if (income) { + console.log("\x1b[35m%s\x1b[0m", `Gross Income: ${ethers.utils.formatUnits( + income, + bundledOrders[i].buyTokenDecimals + )} ${bundledOrders[i].buyTokenSymbol}`); + console.log("\x1b[35m%s\x1b[0m", `Net Profit: ${ethers.utils.formatUnits( + netProfit, + bundledOrders[i].buyTokenDecimals + )} ${bundledOrders[i].buyTokenSymbol}`, "\n"); + } + + report.push({ + transactionHash: receipt.transactionHash, + tokenPair: + bundledOrders[i].buyTokenSymbol + + "/" + + bundledOrders[i].sellTokenSymbol, + buyToken: bundledOrders[i].buyToken, + buyTokenDecimals: bundledOrders[i].buyTokenDecimals, + sellToken: bundledOrders[i].sellToken, + sellTokenDecimals: bundledOrders[i].sellTokenDecimals, + clearedAmount: clearActualAmount.toString(), + clearPrice: ethers.utils.formatEther(price), + clearActualPrice, + gasUsed: receipt.gasUsed, + gasCost: actualGasCost, + income, + netProfit, + clearedOrders: takeOrdersConfigStruct.orders.map( + v => v.id + ), + }); + } + else { + console.log("could not arb this pair, tx receipt: "); + console.log(receipt); + } + } + catch (error) { + console.log("\x1b[31m%s\x1b[0m", ">>> Transaction execution failed due to:"); + console.log(error, "\n"); + } + } + } + catch (error) { + if (typeof error === "string") console.log("\x1b[31m%s\x1b[0m", error, "\n"); + else { + console.log("\x1b[31m%s\x1b[0m", ">>> Something went wrong, reason:", "\n"); + console.log(error); + } + } + } + return report; +}; + +async function checkArb( + mode, + hops, + bundledOrder, + fromToken, + toToken, + signer, + obSellTokenBalance, + gasPrice, + gasCoveragePercentage, + maxProfit, + maxRatio, + arb, + ethPrice, + config, +) { + let succesOrFailure = true; + let maximumInput = obSellTokenBalance; + const modeText = mode === 0 + ? "bundled orders" + : mode === 1 + ? "single order" + : mode === 2 + ? "double orders" + : "triple orders"; + for (let j = 1; j < hops + 1; j++) { + const maximumInputFixed = maximumInput.mul( + "1" + "0".repeat(18 - bundledOrder.sellTokenDecimals) + ); + + console.log(`>>> Trying to arb ${modeText} with ${ + ethers.utils.formatEther(maximumInputFixed) + } ${ + bundledOrder.sellTokenSymbol + } as maximum input`); + console.log(`>>> Getting best route ${modeText}`, "\n"); + + const amountOut = await getAmountOutFlareSwap( + signer, + config.uniV2Router02Address, + fromToken.address, + "1" + "0".repeat(config.nativeWrappedToken.decimals), + toToken.address, + toToken.decimals + ); + if (amountOut === undefined) { + succesOrFailure = false; + console.log( + "\x1b[31m%s\x1b[0m", + `could not find any route for ${modeText} for this token pair for ${ + ethers.utils.formatEther(maximumInputFixed) + } ${ + bundledOrder.sellTokenSymbol + }, trying with a lower amount...` + ); + } + else { + const amountOutBN = ethers.utils.parseUnits(amountOut,toToken.decimals); + const rateFixed = amountOutBN.mul( + "1" + "0".repeat(18 - bundledOrder.buyTokenDecimals) + ); + const price = rateFixed.mul("1" + "0".repeat(18)).div(maximumInputFixed); + + // filter out orders that are not price match or failed eval when --max-profit is enabled + // price check is at +2% as a headroom for current block vs tx block + if (!mode && maxProfit) bundledOrder.takeOrders = bundledOrder.takeOrders.filter( + v => v.ratio !== undefined ? price.mul("102").div("100").gte(v.ratio) : false + ); + + if (bundledOrder.takeOrders.length === 0) { + maximumInput = maximumInput.sub(obSellTokenBalance.div(2 ** j)); + continue; + } + + console.log( + `Current best route price for ${modeText} for this token pair:`, + `\x1b[33m${ethers.utils.formatEther(price)}\x1b[0m`, + "\n" + ); + console.log(""); + + const routeCode = getUniV2Route( + config, + fromToken.address, + toToken.address, + arb.address + ); + const orders = mode === 0 + ? bundledOrder.takeOrders.map(v => v.takeOrder) + : mode === 1 + ? [bundledOrder.takeOrders[0].takeOrder] + : mode === 2 + ? [ + bundledOrder.takeOrders[0].takeOrder, + bundledOrder.takeOrders[0].takeOrder + ] + : [ + bundledOrder.takeOrders[0].takeOrder, + bundledOrder.takeOrders[0].takeOrder, + bundledOrder.takeOrders[0].takeOrder + ]; + + const takeOrdersConfigStruct = { + minimumInput: ethers.constants.One, + maximumInput, + maximumIORatio: maxRatio ? ethers.constants.MaxUint256 : price, + orders, + data: ethers.utils.defaultAbiCoder.encode( + ["bytes"], + [routeCode] + ) + }; + + // building and submit the transaction + try { + const rawtx = { + data: arb.interface.encodeFunctionData("arb", [takeOrdersConfigStruct, "0"]), + to: arb.address, + gasPrice + }; + console.log("Block Number: " + await signer.provider.getBlockNumber(), "\n"); + let gasLimit; + try { + gasLimit = await signer.estimateGas(rawtx); + } + catch { + throw "nomatch"; + } + gasLimit = gasLimit.mul("103").div("100"); + rawtx.gasLimit = gasLimit; + const gasCost = gasLimit.mul(gasPrice); + const gasCostInToken = ethers.utils.parseUnits( + ethPrice + ).mul( + gasCost + ).div( + "1" + "0".repeat( + 36 - bundledOrder.buyTokenDecimals + ) + ); + if (gasCoveragePercentage !== "0") { + const headroom = ( + Number(gasCoveragePercentage) * 1.05 + ).toFixed(); + rawtx.data = arb.interface.encodeFunctionData( + "arb", + [ + takeOrdersConfigStruct, + gasCostInToken.mul(headroom).div("100") + ] + ); + try { + await signer.estimateGas(rawtx); + } + catch { + throw "dryrun"; + } + } + succesOrFailure = true; + if (j == 1 || j == hops) { + return {rawtx, maximumInput, gasCostInToken, takeOrdersConfigStruct, price}; + } + } + catch (error) { + succesOrFailure = false; + if (error !== "nomatch" && error !== "dryrun") { + console.log("\x1b[31m%s\x1b[0m", `>>> Transaction for ${modeText} failed due to:`); + console.log(error, "\n"); + // reason, code, method, transaction, error, stack, message + } + if (j < hops) console.log( + "\x1b[34m%s\x1b[0m", + `could not clear ${modeText} with ${ethers.utils.formatEther( + maximumInputFixed + )} ${ + bundledOrder.sellTokenSymbol + } as max input, trying with lower amount...`, "\n" + ); + else { + console.log("\x1b[34m%s\x1b[0m", `could not arb this pair for ${modeText}`, "\n"); + } + } + } + maximumInput = succesOrFailure + ? maximumInput.add(obSellTokenBalance.div(2 ** j)) + : maximumInput.sub(obSellTokenBalance.div(2 ** j)); + } + return Promise.reject(); +} + +module.exports = { + suniv2Clear +}; \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index e00172ee..3ec3c2b3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,6 @@ const { ethers, BigNumber } = require("ethers"); const { createPublicClient, http, fallback } = require("viem"); -const { erc20Abi, interpreterAbi, interpreterV2Abi } = require("./abis"); +const { erc20Abi, interpreterAbi, interpreterV2Abi, uniswapV2Route02Abi } = require("./abis"); const { DataFetcher, Router, LiquidityProviders, ChainId, Token, viemConfig } = require("sushiswap-router"); @@ -1582,6 +1582,61 @@ const shuffleArray = (array) => { return array; }; +// Get UniswapV2 pool amount out for token +const getAmountOutFlareSwap = async( + signer, + uniswapV2Router, + fromToken, + amountIn, + toToken, + toTokenDecimals +) => { + const swapRouter = new ethers.Contract(uniswapV2Router, uniswapV2Route02Abi, signer); + const amountOutBN = await swapRouter.getAmountsOut(amountIn, [fromToken,toToken]); + if (amountOutBN[1]) return ethers.utils.formatUnits(amountOutBN[1], toTokenDecimals); + return undefined; +}; + +// Get UniswapV2 route for tokens. +const getUniV2Route = (config,fromTokenAddress,toTokenAddress,toAddress) => { + const pool = config.enosys.pools.filter(e => { + if( + ( + e.token0.toLowerCase() == fromTokenAddress.toLowerCase() && + e.token1.toLowerCase() == toTokenAddress.toLowerCase() + ) + || + ( + e.token0.toLowerCase() == toTokenAddress.toLowerCase() && + e.token1.toLowerCase() == fromTokenAddress.toLowerCase() + ) + ) return true; + else return false; + }); + if(pool.length == 0) throw "UniswapV2 LP pool not found"; + + return getUniV2RouteData(pool[0],fromTokenAddress,toAddress); + +}; + +const getUniV2RouteData = (uniV2Pool, fromTokenAddress, toAddress) => { + + const offeringToken = ethers.BigNumber.from(fromTokenAddress); + const token0 = ethers.BigNumber.from(uniV2Pool.token0); + const token1 = ethers.BigNumber.from(uniV2Pool.token1); + + const poolDirection = token0.lt(token1) ? + (offeringToken.eq(token0) ? "01" : "00") : + (offeringToken.eq(token1) ? "00" : "01"); + + return "0x02"+ + `${fromTokenAddress.toString().split("x")[1]}` + + "01ffff00" + + `${uniV2Pool.address.split("x")[1]}` + + `${poolDirection}` + + `${toAddress.toString().split("x")[1]}`; +}; + module.exports = { fallbacks, bnFromFloat, @@ -1608,5 +1663,8 @@ module.exports = { build0xQueries, shuffleArray, createViemClient, - interpreterV2Eval + interpreterV2Eval, + getAmountOutFlareSwap, + getUniV2Route, + getUniV2RouteData, }; \ No newline at end of file