-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Simple market-price auction driver implementation using exactIn swaps (…
…#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
Showing
8 changed files
with
332 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
]), | ||
}; |
Oops, something went wrong.