Skip to content

Commit

Permalink
split up gas cost estimating from profit calculation, optimize the fe…
Browse files Browse the repository at this point in the history
…tching of data from vaults using multicall
  • Loading branch information
chuckbergeron committed Aug 16, 2023
1 parent 9198fec commit 4fac1c1
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 97 deletions.
183 changes: 100 additions & 83 deletions packages/library/src/liquidatorArbitrageSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Relayer } from 'defender-relay-client';
import { ContractsBlob, getContract, getSubgraphVaults } from '@generationsoftware/pt-v5-utils-js';
import chalk from 'chalk';

import { ArbLiquidatorConfigParams, ArbLiquidatorContext, VaultPopulated } from './types';
import { ArbLiquidatorConfigParams, ArbLiquidatorContext, VaultWithContext } from './types';
import {
logTable,
logStringValue,
Expand All @@ -21,6 +21,7 @@ import {
} from './utils';
import { ERC20Abi } from './abis/ERC20Abi';
import { canUseIsPrivate, NETWORK_NATIVE_TOKEN_INFO } from './utils/network';
import { getVaultsWithContextMulticall } from './utils/getVaultsWithContextMulticall';

interface SwapExactAmountOutParams {
liquidationPairAddress: string;
Expand Down Expand Up @@ -61,10 +62,16 @@ export async function liquidatorArbitrageSwap(

// #1. Get contracts
//
printSpacer();
console.log('Starting ...');

const { liquidationRouterContract, liquidationPairContracts, marketRateContract } =
await getLiquidationContracts(contracts, params);

const vaultsPopulated: VaultPopulated[] = await getVaultsPopulated(
printSpacer();
console.log('Collecting information about vaults ...');

const vaultsWithContext: VaultWithContext[] = await getVaultsContext(
chainId,
readProvider,
contracts,
Expand Down Expand Up @@ -94,10 +101,10 @@ export async function liquidatorArbitrageSwap(
);

// Check if we have the corresponding vault to get the underlying asset's value for determining profitability
const vaultPopulated = vaultsPopulated.find(
(vaultPopulated) => vaultPopulated.liquidationPair === liquidationPairContract.address,
const vaultWithContext = vaultsWithContext.find(
(vaultWithContext) => vaultWithContext.liquidationPair === liquidationPairContract.address,
);
const vaultUnderlyingAssetAddress = vaultPopulated?.asset;
const vaultUnderlyingAssetAddress = vaultWithContext?.asset;
if (!vaultUnderlyingAssetAddress) {
console.log(chalk.yellow('Could not find matching Vault for LiquidationPair'));
logNextPair(liquidationPair, liquidationPairContracts);
Expand Down Expand Up @@ -178,17 +185,8 @@ export async function liquidatorArbitrageSwap(

// #5. Test tx to get estimated return of tokenOut
//
printAsterisks();
console.log(chalk.blue.bold(`3. Getting amount to receive ...`));
console.log('liquidationPair.address');
console.log(liquidationPair.address);
const swapExactAmountOutParams: SwapExactAmountOutParams = {
liquidationPairAddress: liquidationPair.address,
swapRecipient,
amountOut,
amountInMin,
};

// printAsterisks();
// console.log(chalk.blue.bold(`3. Getting amount to receive ...`));
// let amountOutEstimate;
// try {
// amountOutEstimate = await liquidationRouter.callStatic.swapExactAmountIn(
Expand All @@ -212,16 +210,45 @@ export async function liquidatorArbitrageSwap(
// context.tokenOut.symbol,
// );

// #6. Decide if profitable or not
//
// #6. Find an estimated amount of gas cost
const swapExactAmountOutParams: SwapExactAmountOutParams = {
liquidationPairAddress: liquidationPair.address,
swapRecipient,
amountOut,
amountInMin,
};

let maxFeeUsd;
try {
maxFeeUsd = await getGasCost(
chainId,
liquidationRouterContract,
swapExactAmountOutParams,
readProvider,
);
} catch (e) {
console.error(chalk.red(e));

console.log(chalk.yellow('---'));
console.log(chalk.yellow('Could not estimate gas costs!'));
console.log(chalk.yellow('---'));

stats.push({
pair,
estimatedProfitUsd: 0,
error: `Could not get gas cost`,
});
logNextPair(liquidationPair, liquidationPairContracts);
continue;
}

// #7. Decide if profitable or not
const { estimatedProfitUsd, profitable } = await calculateProfit(
chainId,
liquidationRouterContract,
swapExactAmountOutParams,
readProvider,
context,
minProfitThresholdUsd,
amountIn,
maxFeeUsd,
);
if (!profitable) {
console.log(
Expand All @@ -238,7 +265,7 @@ export async function liquidatorArbitrageSwap(
continue;
}

// #7. Finally, populate tx when profitable
// #8. Finally, populate tx when profitable
try {
let transactionPopulated: PopulatedTransaction | undefined;
console.log(chalk.blue('6. Populating swap transaction ...'));
Expand Down Expand Up @@ -431,55 +458,14 @@ const checkBalance = async (
* @returns {Promise} Promise boolean of profitability
*/
const calculateProfit = async (
chainId: number,
liquidationRouter: Contract,
swapExactAmountOutParams: SwapExactAmountOutParams,
readProvider: Provider,
context: ArbLiquidatorContext,
minProfitThresholdUsd: number,
amountIn: BigNumber,
maxFeeUsd: number,
): Promise<{ estimatedProfitUsd: number; profitable: boolean }> => {
const { amountOut, amountInMin } = swapExactAmountOutParams;

const nativeTokenMarketRateUsd = await getNativeTokenMarketRateUsd(chainId);

printAsterisks();
console.log(chalk.blue('4. Current gas costs for transaction:'));

let estimatedGasLimit;
try {
estimatedGasLimit = await liquidationRouter.estimateGas.swapExactAmountOut(
...Object.values(swapExactAmountOutParams),
);
} catch (e) {
console.error(chalk.red(e));

console.log(chalk.yellow('---'));
console.log(chalk.yellow('Could not estimate gas costs!'));
console.log(chalk.yellow('---'));
return { estimatedProfitUsd: 0, profitable: false };
}
const { baseFeeUsd, maxFeeUsd, avgFeeUsd } = await getFeesUsd(
chainId,
estimatedGasLimit,
nativeTokenMarketRateUsd,
readProvider,
);
logStringValue(
`Native (Gas) Token ${NETWORK_NATIVE_TOKEN_INFO[chainId].symbol} Market Rate (USD):`,
`$${nativeTokenMarketRateUsd}`,
);

printSpacer();
logBigNumber(
'Estimated gas limit:',
estimatedGasLimit,
18,
NETWORK_NATIVE_TOKEN_INFO[chainId].symbol,
);

logTable({ baseFeeUsd, maxFeeUsd, avgFeeUsd });

printAsterisks();
console.log(chalk.blue('5. Profit/Loss (USD):'));
printSpacer();
Expand Down Expand Up @@ -525,6 +511,51 @@ const calculateProfit = async (
return { estimatedProfitUsd: roundTwoDecimalPlaces(netProfitUsd), profitable };
};

/**
* Get the gas cost for the tx
* @returns {Promise} Promise with the maximum gas fee in USD
*/
const getGasCost = async (
chainId: number,
liquidationRouter: Contract,
swapExactAmountOutParams: SwapExactAmountOutParams,
readProvider: Provider,
): Promise<number> => {
const { amountOut } = swapExactAmountOutParams;

const nativeTokenMarketRateUsd = await getNativeTokenMarketRateUsd(chainId);

printAsterisks();
console.log(chalk.blue('4. Current gas costs for transaction:'));

const estimatedGasLimit = await liquidationRouter.estimateGas.swapExactAmountOut(
...Object.values(swapExactAmountOutParams),
);

const { baseFeeUsd, maxFeeUsd, avgFeeUsd } = await getFeesUsd(
chainId,
estimatedGasLimit,
nativeTokenMarketRateUsd,
readProvider,
);
logStringValue(
`Native (Gas) Token ${NETWORK_NATIVE_TOKEN_INFO[chainId].symbol} Market Rate (USD):`,
`$${nativeTokenMarketRateUsd}`,
);

printSpacer();
logBigNumber(
'Estimated gas limit:',
estimatedGasLimit,
18,
NETWORK_NATIVE_TOKEN_INFO[chainId].symbol,
);

logTable({ baseFeeUsd, maxFeeUsd, avgFeeUsd });

return maxFeeUsd;
};

/**
* Calculates necessary input parameters for the swap call based on current state of the contracts
* @returns {Promise} Promise object with the input parameters exactAmountIn and amountOutMin
Expand Down Expand Up @@ -557,6 +588,7 @@ const calculateAmounts = async (
amountInMin: BigNumber.from(0),
};
}

// Needs to be based on how much the bot owner has of tokenIn
// as well as how big of a trade they're willing to do
// TODO: Should this be calculated automatically or a config param?
Expand All @@ -578,7 +610,7 @@ const calculateAmounts = async (
const amountInMin = ethers.constants.MaxInt256;

return {
amountOut,
amountOut: wantedAmountOut,
amountIn,
amountInMin,
};
Expand All @@ -590,31 +622,16 @@ const logNextPair = (liquidationPair, liquidationPairContracts) => {
}
};

const getVaultsPopulated = async (
const getVaultsContext = async (
chainId: number,
readProvider: Provider,
contracts: ContractsBlob,
): Promise<VaultPopulated[]> => {
): Promise<VaultWithContext[]> => {
const vaults = await getSubgraphVaults(chainId);

if (vaults.length === 0) {
throw new Error('No vaults found in subgraph');
}

// Get Vault ABI
const vaultContractData = contracts.contracts.find((contract) => contract.type === 'Vault');

const populatedVaults: VaultPopulated[] = [];
for (let vault of vaults) {
const vaultContract = new ethers.Contract(vault.id, vaultContractData.abi, readProvider);

const asset = await vaultContract.asset();
const liquidationPair = await vaultContract.liquidationPair();

const vaultPopulated: VaultPopulated = { vaultContract, liquidationPair, asset };

populatedVaults.push(vaultPopulated);
}

return populatedVaults;
return await getVaultsWithContextMulticall(vaults, readProvider, contracts);
};
7 changes: 4 additions & 3 deletions packages/library/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ export interface AuctionContracts {
rngRelayAuctionContract: Contract;
}

export interface VaultPopulated {
export interface VaultWithContext {
id: string;
vaultContract: Contract;
liquidationPair: string;
asset: string;
liquidationPair?: string;
asset?: string;
}
11 changes: 2 additions & 9 deletions packages/library/src/utils/getArbLiquidatorContextMulticall.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { Contract, BigNumber } from 'ethers';
import { Provider } from '@ethersproject/providers';
import { getEthersMulticallProviderResults } from '@generationsoftware/pt-v5-utils-js';
import chalk from 'chalk';

import {
ArbLiquidatorContext,
ArbLiquidatorRelayerContext,
VaultPopulated,
Token,
TokenWithRate,
} from '../types';

import { ArbLiquidatorContext, ArbLiquidatorRelayerContext, Token, TokenWithRate } from '../types';
import { parseBigNumberAsFloat, MARKET_RATE_CONTRACT_DECIMALS, printSpacer } from '../utils';
import { ERC20Abi } from '../abis/ERC20Abi';

Expand Down
3 changes: 1 addition & 2 deletions packages/library/src/utils/getLiquidationPairsMulticall.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ethers } from 'ethers';
import { Contract } from 'ethers';
import { Provider } from '@ethersproject/providers';
import { getEthersMulticallProviderResults } from '@generationsoftware/pt-v5-utils-js';

import { LiquidationPairAbi } from '../abis/LiquidationPairAbi';
import { LiquidationPairFactoryAbi } from '../abis/LiquidationPairFactoryAbi';

import { ethers } from 'ethers';

import ethersMulticallProviderPkg from 'ethers-multicall-provider';
const { MulticallWrapper } = ethersMulticallProviderPkg;

Expand Down
53 changes: 53 additions & 0 deletions packages/library/src/utils/getVaultsWithContextMulticall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ethers } from 'ethers';
import { Provider } from '@ethersproject/providers';
import { getEthersMulticallProviderResults } from '@generationsoftware/pt-v5-utils-js';
import { ContractsBlob, Vault } from '@generationsoftware/pt-v5-utils-js';

import { VaultWithContext } from '../../src/types';

import ethersMulticallProviderPkg from 'ethers-multicall-provider';
const { MulticallWrapper } = ethersMulticallProviderPkg;

/**
* Uses multicall to gather information about each vault
*
* @param populatedVaults vaults with a vaultContract ethers Contract initialized
* @param readProvider a read-capable provider for the chain that should be queried
* @returns
*/
export const getVaultsWithContextMulticall = async (
vaults: Vault[],
readProvider: Provider,
contracts: ContractsBlob,
): Promise<VaultWithContext[]> => {
// @ts-ignore Provider == BaseProvider
const multicallProvider = MulticallWrapper.wrap(readProvider);

let queries: Record<string, any> = {};

// Get Vault ABI
const vaultContractData = contracts.contracts.find((contract) => contract.type === 'Vault');

let populatedVaults: VaultWithContext[] = [];
for (let vault of vaults) {
const vaultContract = new ethers.Contract(vault.id, vaultContractData.abi, multicallProvider);

const vaultPopulated: VaultWithContext = { id: vault.id, vaultContract };

// Queries:
queries[`vault-${vault.id}-asset`] = vaultContract.asset();
queries[`vault-${vault.id}-liquidationPair`] = vaultContract.liquidationPair();

populatedVaults.push(vaultPopulated);
}

// Get and process results:
const results = await getEthersMulticallProviderResults(multicallProvider, queries);

for (let populatedVault of populatedVaults) {
populatedVault.asset = results[`vault-${populatedVault.id}-asset`];
populatedVault.liquidationPair = results[`vault-${populatedVault.id}-liquidationPair`];
}

return populatedVaults;
};

0 comments on commit 4fac1c1

Please sign in to comment.