Skip to content

Commit

Permalink
Simple market-price auction driver implementation using exactIn swaps (
Browse files Browse the repository at this point in the history
…#14)

* Simple real-auction driver implementation without exactOut

* Add driver conf

* Add order volume limit to driverConf

* implement ignoreReferrers from init api

---------

Co-authored-by: mrlotfi <mrlotfi@MacBook-Pro.local>
  • Loading branch information
mrlotfi and mrlotfi authored Oct 7, 2024
1 parent 4648e46 commit 93b3822
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 57 deletions.
283 changes: 253 additions & 30 deletions src/auction.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,263 @@
import axios from 'axios';
import { CHAIN_ID_SOLANA, WhChainIdToEvm } from './config/chains';
import { RpcConfig } from './config/rpc';
import { Token } from './config/tokens';
import { driverConfig } from './driver.conf';
import { get1InchQuote } from './driver/routers';
import { Swap } from './swap.dto';
import { SwiftCosts } from './utils/fees';
import logger from './utils/logger';

export class AuctionFulfillerConfig {
/**
* Specfiy if the driver will participate in the auction for this `swap` or not
* @param swap User's order details
* @param effectiveAmountIn Total available after considering gas costs which is equivalent to `swap.fromAmount - totalCosts`
* @param costDetails Gas cost details to consider more factors
**/
async shouldParticipate(swap: Swap, effectiveAmountIn: number, costDetails: SwiftCosts): Promise<boolean> {
return true;
private readonly bidAggressionPercent = driverConfig.bidAggressionPercent;
private readonly fulfillAggressionPercent = driverConfig.fulfillAggressionPercent;
private readonly forceBid = true;

constructor(private readonly rpcConfig: RpcConfig) {}

async normalizedBidAmount(
driverToken: Token,
effectiveAmountIn: number,
swap: Swap,
costs: SwiftCosts,
): Promise<bigint> {
if (swap.fromAmount.toNumber() * costs.fromTokenPrice > driverConfig.volumeLimitUsd) {
throw new Error(`Volume limit exceeded for ${swap.sourceTxHash} and dropping bid`);
}

const normalizedMinAmountOut = BigInt(swap.minAmountOut64);

let output64: bigint;
if (swap.destChain === CHAIN_ID_SOLANA) {
output64 = await this.getSolanaEquivalentOutput(driverToken, effectiveAmountIn, swap.toToken);
} else {
output64 = await this.getEvmEquivalentOutput(
swap.destChain,
driverToken,
effectiveAmountIn,
normalizedMinAmountOut,
swap.toToken,
);
}
let output = Number(output64) / 10 ** swap.toToken.decimals;

const bpsFees = await this.calcProtocolAndRefBps(
swap.fromAmount64,
swap.fromToken,
swap.toToken,
swap.destChain,
swap.referrerBps,
);
const realMinAmountOut =
swap.toToken.decimals > 8
? normalizedMinAmountOut * BigInt(10 ** (swap.toToken.decimals - 8))
: normalizedMinAmountOut;
const minAmountNeededForFulfill64 = realMinAmountOut + (realMinAmountOut * bpsFees) / 10000n;
const minAmountNeededForFulfill = Number(minAmountNeededForFulfill64) / 10 ** swap.toToken.decimals;

const mappedMinAmountIn = minAmountNeededForFulfill * (effectiveAmountIn / output);
const mappedBpsAmountIn = (swap.fromAmount.toNumber() * Number(bpsFees)) / 10000; // upper estimate

if (mappedMinAmountIn > effectiveAmountIn - mappedBpsAmountIn) {
logger.warn(
`AuctionFulfillerConfig.normalizedBidAmount: mappedMinAmountIn > effectiveAmountIn ${mappedMinAmountIn} > ${effectiveAmountIn}`,
);
throw new Error(`mappedMinAmountIn > effectiveAmountIn for ${swap.sourceTxHash}`);
}

const bidAggressionPercent = this.bidAggressionPercent; // 0 - 100

const profitMargin = effectiveAmountIn - mappedMinAmountIn - mappedBpsAmountIn;

const finalAmountIn = mappedMinAmountIn + (profitMargin * bidAggressionPercent) / 100;

const mappedAmountOut = (finalAmountIn * Number(output)) / effectiveAmountIn;
let normalizedAmountOut;
if (swap.toToken.decimals > 8) {
normalizedAmountOut = BigInt(Math.floor(mappedAmountOut * 10 ** 8));
} else {
normalizedAmountOut = BigInt(Math.floor(mappedAmountOut * 10 ** swap.toToken.decimals));
}

if (normalizedAmountOut < normalizedMinAmountOut && this.forceBid) {
logger.warn(`normalizedBidAmount is less than minAmountOut`);
normalizedAmountOut = normalizedMinAmountOut;
}

return normalizedAmountOut;
}

async getEvmEquivalentOutput(
destChain: number,
driverToken: Token,
effectiveAmountInDriverToken: number,
normalizedMinAmountOut: bigint,
toToken: Token,
): Promise<bigint> {
let output: bigint;
if (driverToken.contract === toToken.contract) {
output = BigInt(Math.floor(effectiveAmountInDriverToken * 10 ** driverToken.decimals));
} else {
const quoteRes = await get1InchQuote(
{
realChainId: WhChainIdToEvm[destChain],
srcToken: driverToken.contract,
destToken: toToken.contract,
amountIn: BigInt(Math.floor(effectiveAmountInDriverToken * 10 ** driverToken.decimals)).toString(),
timeout: 2000,
},
this.rpcConfig.oneInchApiKey,
true,
3,
);

if (!quoteRes) {
throw new Error('1inch quote for bid in swift failed');
}

output = BigInt(Math.floor(Number(quoteRes.toAmount)));
}

return output;
}

/**
* Calculate the bid amount for the auction. The bid should be in the same token as the fromToken used in the swap on the destination chain.
* For example, if the swap is from `Ethereum (ETH)` to the `Arbitrum (any token)` network, the bid should be in ETH.
* The bid amount will then be converted to the destination chain token via a swap using the Jupiter or 1inch aggregator.
* We are bidding effectiveAmountIn by default because we assume no risks or additional benefits and only deduct the costs.
* @param swap user's order details
* @param effectiveAmountIn effective amount in which swap.fromAmount - totalCosts
* @param costDetails cost details to consider more factors
* @returns the amount to bid
**/
async bidAmount(swap: Swap, effectiveAmountIn: number, costDetails: SwiftCosts): Promise<number> {
return effectiveAmountIn * 0.97;
// if (effectiveAmountIn * 0.98 >= minOut) {
// return effectiveAmountIn * 0.98;
// } else if (effectiveAmountIn * 0.99 >= minOut) {
// return effectiveAmountIn * 0.99;
// } else if (effectiveAmountIn * 0.999 >= minOut) {
// return effectiveAmountIn * 0.999;
// }
// return effectiveAmountIn;
async getSolanaEquivalentOutput(
driverToken: Token,
effectiveAmountInDriverToken: number,
toToken: Token,
): Promise<bigint> {
let output: bigint;
if (driverToken.contract === toToken.contract) {
output = BigInt(Math.floor(effectiveAmountInDriverToken * 10 ** driverToken.decimals));
} else {
const quoteRes = await this.getJupQuoteWithRetry(
BigInt(Math.floor(effectiveAmountInDriverToken * 10 ** driverToken.decimals)),
driverToken.mint,
toToken.mint,
0.1, // 10%
);

if (!quoteRes || !quoteRes.raw) {
throw new Error('jupiter quote for bid in swift failed');
}

output = BigInt(Math.floor(Number(quoteRes.expectedAmountOut)));
}

return output;
}

async fulfillAmount(driverToken: Token, effectiveAmountIn: number, swap: Swap, costs: SwiftCosts): Promise<number> {
if (swap.fromAmount.toNumber() * costs.fromTokenPrice > driverConfig.volumeLimitUsd) {
throw new Error(`Volume limit exceeded for ${swap.sourceTxHash} and dropping fulfill`);
}

const normalizedMinAmountOut = BigInt(swap.minAmountOut64);

let output64: bigint;
if (swap.destChain === CHAIN_ID_SOLANA) {
output64 = await this.getSolanaEquivalentOutput(driverToken, effectiveAmountIn, swap.toToken);
} else {
output64 = await this.getEvmEquivalentOutput(
swap.destChain,
driverToken,
effectiveAmountIn,
normalizedMinAmountOut,
swap.toToken,
);
}
let output = Number(output64) / 10 ** swap.toToken.decimals;

const bpsFees = await this.calcProtocolAndRefBps(
swap.fromAmount64,
swap.fromToken,
swap.toToken,
swap.destChain,
swap.referrerBps,
);
const realMinAmountOut =
swap.toToken.decimals > 8
? normalizedMinAmountOut * BigInt(10 ** (swap.toToken.decimals - 8))
: normalizedMinAmountOut;
const minAmountNeededForFulfill64 = realMinAmountOut + (realMinAmountOut * bpsFees) / 10000n;
const minAmountNeededForFulfill = Number(minAmountNeededForFulfill64) / 10 ** swap.toToken.decimals;

const mappedMinAmountIn = minAmountNeededForFulfill * (effectiveAmountIn / output);
const mappedBpsAmountIn = (swap.fromAmount.toNumber() * Number(bpsFees)) / 10000; // upper estimate

if (mappedMinAmountIn > effectiveAmountIn - mappedBpsAmountIn) {
logger.warn(
`AuctionFulfillerConfig.normalizedBidAmount: mappedMinAmountIn > effectiveAmountIn ${mappedMinAmountIn} > ${effectiveAmountIn}`,
);
throw new Error(`mappedMinAmountIn > effectiveAmountIn for ${swap.sourceTxHash}`);
}

const aggressionPercent = this.fulfillAggressionPercent; // 0 - 100

const profitMargin = effectiveAmountIn - mappedMinAmountIn - mappedBpsAmountIn;

const finalAmountIn = mappedMinAmountIn + (profitMargin * aggressionPercent) / 100;

return finalAmountIn;
}

private async getJupQuoteWithRetry(
amountIn: bigint,
fromMint: string,
toMint: string,
slippage: number,
retry: number = 10,
): Promise<any> {
let res;
do {
try {
let params: any = {
inputMint: fromMint,
outputMint: toMint,
slippageBps: slippage * 10000,
maxAccounts: 64 - 7, // 7 accounts reserved for other instructions
amount: amountIn,
};
if (!!this.rpcConfig.jupApiKey) {
params['token'] = this.rpcConfig.jupApiKey;
}
const { data } = await axios.get(`${this.rpcConfig.jupV6Endpoint}/quote`, {
params: params,
});
res = data;
} catch (err) {
logger.warn(`error in fetch jupiter ${err} try ${retry}`);
} finally {
retry--;
}
} while ((!res || !res.outAmount) && retry > 0);

if (!res) {
logger.error(`juptier quote failed ${fromMint} ${toMint} ${amountIn}`);
return null;
}

return {
effectiveAmountIn: res.inAmount,
expectedAmountOut: res.outAmount,
priceImpact: res.priceImpactPct,
minAmountOut: res.otherAmountThreshold,
route: [],
raw: res,
};
}

async fulfillAmount(swap: Swap, effectiveAmountIn: number, costDetails: SwiftCosts): Promise<number> {
return effectiveAmountIn;
private async calcProtocolAndRefBps(
amountIn: bigint,
tokenIn: Token,
tokenOut: Token,
destChain: number,
referrerBps: number,
): Promise<bigint> {
if (referrerBps > 3) {
return BigInt(referrerBps * 2);
} else {
return BigInt(3 + referrerBps);
}
}
}
1 change: 1 addition & 0 deletions src/config/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface SwiftFeeParams {
}

export type GlobalConfig = {
ignoreReferrers: Set<string>;
blackListedReferrerAddresses: Set<string>;
auctionTimeSeconds: number;
batchUnlockThreshold: number; // Optimal Number of swaps to select for unlocking
Expand Down
1 change: 1 addition & 0 deletions src/config/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export async function fetchDynamicSdkParams(): Promise<{
singleBatchChainIds: string;
scheduleUnlockInterval: number;
feeParams: SwiftFeeParams;
ignoreReferrers: string[];
}> {
const result = await axios.get(initEndpoint);
const serverChains = Object.keys(result.data.swiftContracts).map((k) => +k);
Expand Down
36 changes: 36 additions & 0 deletions src/driver.conf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
CHAIN_ID_ARBITRUM,
CHAIN_ID_AVAX,
CHAIN_ID_BASE,
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CHAIN_ID_OPTIMISM,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
} from './config/chains';

export const driverConfig = {
bidAggressionPercent: 1, // 1% above minamout out
fulfillAggressionPercent: 99, // take 1% of approximated available profit
volumeLimitUsd: 20_000, // 20k USD
acceptedInputChains: new Set([
CHAIN_ID_BSC,
CHAIN_ID_AVAX,
CHAIN_ID_ETH,
CHAIN_ID_ARBITRUM,
CHAIN_ID_POLYGON,
CHAIN_ID_OPTIMISM,
CHAIN_ID_BASE,
CHAIN_ID_SOLANA,
]),
acceptedOutputChains: new Set([
CHAIN_ID_BSC,
CHAIN_ID_AVAX,
CHAIN_ID_ETH,
CHAIN_ID_ARBITRUM,
CHAIN_ID_POLYGON,
CHAIN_ID_OPTIMISM,
CHAIN_ID_BASE,
CHAIN_ID_SOLANA,
]),
};
Loading

0 comments on commit 93b3822

Please sign in to comment.