Skip to content

Commit

Permalink
Merge pull request #40 from White-Whale-Defi-Platform/feat/multi-hop-…
Browse files Browse the repository at this point in the history
…support

feat: multihop messaging, config update, profit threshold absolute
  • Loading branch information
SirTLB authored Feb 13, 2023
2 parents 170e917 + 5c92965 commit 8130313
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 102 deletions.
5 changes: 3 additions & 2 deletions .env.juno.example
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -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},
Expand Down
6 changes: 4 additions & 2 deletions .env.terra.example
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand All @@ -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},
Expand Down
58 changes: 15 additions & 43 deletions src/chains/defaults/messages/getFlashArbMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,80 +18,52 @@ export function getFlashArbMessages(
walletAddress: string,
flashloancontract: string,
): [Array<EncodeObject>, 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;
if (pool.dexname === AmmDexName.default || pool.dexname === AmmDexName.wyndex) {
if (isNativeAsset(offerAsset.info)) {
msg = <SwapMessage>{
swap: {
max_spread: "0.05",
max_spread: "0.01",
offer_asset:
pool.dexname === AmmDexName.default
? offerAsset
Expand All @@ -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");
Expand Down
6 changes: 4 additions & 2 deletions src/core/arbitrage/arbitrage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ export function trySomeArb(paths: Array<Path>, 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);
Expand Down
6 changes: 4 additions & 2 deletions src/core/types/arbitrageloops/mempoolLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down
21 changes: 12 additions & 9 deletions src/core/types/arbitrageloops/skipLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)),
},
],
});
Expand All @@ -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,
);
Expand Down
81 changes: 51 additions & 30 deletions src/core/types/base/botConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,10 +21,9 @@ export interface BotConfig {
baseDenom: string;

gasPrice: string;
profitThreshold3Hop: number;
profitThreshold2Hop: number;
txFee3Hop: StdFee;
txFee2Hop: StdFee;
gasFees: Map<number, Coin>;
txFees: Map<number, StdFee>;
profitThresholds: Map<number, number>;

// logging specific (optionally)
// Slack OAuth2 token for the specific SlackApp
Expand All @@ -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;
}

/**
Expand All @@ -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<number, Coin>();
const TX_FEES = new Map<number, StdFee>();
const PROFIT_THRESHOLDS = new Map<number, number>();
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,
Expand All @@ -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;
}
Expand All @@ -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`);
}
7 changes: 6 additions & 1 deletion src/core/types/modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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".
*
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 8130313

Please sign in to comment.