UNPKG

@symmetry-hq/liquidity-sdk

Version:

Exchange functionality using symmetry funds liquidity

452 lines (405 loc) 16.3 kB
import { PublicKey } from "@solana/web3.js"; import { CURVE_DATA_ADDRESS, CurveChainData, FUNDS_PROGRAM_ID, FUNDS_PROGRAM_PDA, FundStateChainData, OraclePrice, RouteData, SWAP_FEE_ACCOUNT, TOKEN_LIST_ADDRESS, TokenPriceData, TokenSettings } from "./types"; export const BPS_DIVIDER = 10000; export const WEIGHT_MULTIPLIER = 10000; export function mul_div(a: number, b: number, c: number): number { if (c == 0) return 0; return Math.floor(a * b / c); } export function amount_to_usd_value(amount: number, decimals: number, price: number): number { return mul_div(amount, price, 10 ** decimals) } export function usd_value_to_amount(worth: number, decimals:number, price:number): number { return mul_div(worth, 10 ** decimals, price) } export function compute_value_of_sold_token( amount: number, token_settings: TokenSettings, price: OraclePrice, start_amount: number, target_amount: number, curve_data: TokenPriceData ): number { let current_amount = start_amount; let curve_offset = (start_amount > target_amount) ? (start_amount - target_amount) : 0; let current_output_value = 0; let amount_left = amount; let current_price = price.sell_price; let total_fees = 0; for (let step = 0; step <= 10; step++) { let step_amount = (step < 10) ? curve_data.amount[step] : amount_left; if (step < 10 && curve_data.price[step] < current_price) { if (token_settings.useCurveData == true) { current_price = curve_data.price[step]; } } if (step == 10) { curve_offset = 0; } if (step_amount <= curve_offset) { curve_offset -= curve_data.amount[step]; continue; } let amount_in_interval = step_amount - curve_offset; curve_offset = 0; if (amount_in_interval > amount_left) { amount_in_interval = amount_left }; let amount_before_tw = amount_in_interval; if (current_amount >= target_amount) { amount_before_tw = 0; } else if (current_amount + amount_in_interval >= target_amount) { amount_before_tw -= current_amount + amount_in_interval - target_amount; } let amount_after_tw = amount_in_interval - amount_before_tw; let value_before_tw = amount_to_usd_value( amount_before_tw, token_settings.decimals, current_price ); let value_after_tw = amount_to_usd_value( amount_after_tw, token_settings.decimals, current_price ); let fees = mul_div(value_before_tw, token_settings.tokenSwapFeeBeforeTwBps, BPS_DIVIDER) + mul_div(value_after_tw, token_settings.tokenSwapFeeAfterTwBps, BPS_DIVIDER); total_fees += fees; current_output_value += value_before_tw + value_after_tw - fees; amount_left -= amount_in_interval; current_amount += amount_in_interval; if (amount_left == 0) { break; } }; let fee_bps_x100 = mul_div(total_fees, BPS_DIVIDER * 100, total_fees + current_output_value); return current_output_value; } export function compute_amount_of_bought_token( value: number, token_settings: TokenSettings, price: OraclePrice, start_amount: number, target_amount: number, curve_data: TokenPriceData, ): number { let current_amount = start_amount; let curve_offset = (start_amount < target_amount) ? (target_amount - start_amount) : 0; let current_output_amount = 0; let value_left = value; let current_price = price.buy_price; let total_fees = 0; for (let step = 0; step <= 10; step++){ let step_amount = (step < 10) ? curve_data.amount[step]: usd_value_to_amount(value_left * 2, token_settings.decimals, current_price); if (step < 10 && curve_data.price[step] > current_price) { if (token_settings.useCurveData == true) { current_price = curve_data.price[step]; }; } if (step == 10) { curve_offset = 0; } if (step_amount <= curve_offset) { curve_offset -= curve_data.amount[step]; continue; } let amount_in_interval = step_amount - curve_offset; curve_offset = 0; let value_in_interval = amount_to_usd_value(amount_in_interval, token_settings.decimals, current_price); if (value_in_interval > value_left) { value_in_interval = value_left; amount_in_interval = usd_value_to_amount(value_in_interval, token_settings.decimals, current_price); } let value_before_tw = value_in_interval; if (current_amount <= target_amount) { value_before_tw = 0; } else if (current_amount <= target_amount + amount_in_interval) { value_before_tw -= amount_to_usd_value(target_amount + amount_in_interval - current_amount, token_settings.decimals, current_price)} let value_after_tw = value_in_interval - value_before_tw; let fees = mul_div(value_before_tw, token_settings.tokenSwapFeeBeforeTwBps, BPS_DIVIDER) + mul_div(value_after_tw, token_settings.tokenSwapFeeAfterTwBps, BPS_DIVIDER); let amount_bought = usd_value_to_amount(value_in_interval - fees, token_settings.decimals, current_price); current_output_amount += amount_bought; value_left -= value_in_interval; total_fees += fees; if (amount_bought > current_amount) { current_amount = 0; } else { current_amount -= amount_bought; } if (value_left == 0) { break; } }; let fee_bps_x100 = mul_div(total_fees, BPS_DIVIDER * 100, value); return current_output_amount } export function computeOutputAmount( from_token_id: number, to_token_id: number, from_amount: number, fundState: FundStateChainData, oracle_prices: OraclePrice[], curve_data: CurveChainData, token_list: {list: TokenSettings[]}, ) { fundState.currentCompToken = fundState.currentCompToken.map(x => x.toNumber()); fundState.currentCompAmount = fundState.currentCompAmount.map(x => x.toNumber()); fundState.targetWeight = fundState.targetWeight.map(x => x.toNumber()); fundState.weightSum = fundState.weightSum.toNumber(); fundState.numOfTokens = fundState.numOfTokens.toNumber(); let from_token_settings = token_list.list[from_token_id]; let to_token_settings = token_list.list[to_token_id]; let from_token_index = fundState.currentCompToken.findIndex((x: any) => x == from_token_id); let to_token_index = fundState.currentCompToken.findIndex((x: any) => x == to_token_id); if (!from_token_settings.isLive || !to_token_settings.isLive) return { toAmount: undefined, feePct: undefined } if (from_token_index == -1 || to_token_index == -1) return { toAmount: undefined, feePct: undefined } let fund_worth = 0; for (let i=0;i<fundState.numOfTokens; i++) { let token = fundState.currentCompToken[i]; let token_settings = token_list.list[token]; let token_price = oracle_prices[token]; fund_worth += amount_to_usd_value( fundState.currentCompAmount[i], token_settings.decimals, token_price.avg_price ); } let from_token_price = oracle_prices[from_token_id]; let to_token_price = oracle_prices[to_token_id]; let from_token_target_amount = usd_value_to_amount( mul_div(fundState.targetWeight[from_token_index], fund_worth, fundState.weightSum), from_token_settings.decimals, from_token_price.avg_price ); let to_token_target_amount = usd_value_to_amount( mul_div(fundState.targetWeight[to_token_index], fund_worth, fundState.weightSum), to_token_settings.decimals, to_token_price.avg_price, ); let value = compute_value_of_sold_token( from_amount, from_token_settings, from_token_price, fundState.currentCompAmount[from_token_index], from_token_target_amount, curve_data.sell[from_token_id], ); let to_amount = compute_amount_of_bought_token( value, to_token_settings, to_token_price, fundState.currentCompAmount[to_token_index], to_token_target_amount, curve_data.buy[to_token_id], ); let amount_without_fees = usd_value_to_amount( amount_to_usd_value( from_amount, from_token_settings.decimals, from_token_price.sell_price ), to_token_settings.decimals, to_token_price.buy_price ); let fair_amount = usd_value_to_amount( amount_to_usd_value( from_amount, from_token_settings.decimals, from_token_price.avg_price ), to_token_settings.decimals, to_token_price.avg_price ); if (amount_without_fees > fundState.currentCompAmount[to_token_index]) { amount_without_fees = fundState.currentCompAmount[to_token_index]; } if (to_amount > amount_without_fees) { to_amount = amount_without_fees } let total_fees = amount_without_fees - to_amount; let symmetry_bps = token_list.list[0].additionalData[60]; let symmetry_fee = mul_div(total_fees, symmetry_bps, 100); let host_bps = token_list.list[0].additionalData[61]; let host_fee = mul_div(total_fees, host_bps, 100); let manager_bps = token_list.list[0].additionalData[62]; let manager_fee = mul_div(total_fees, manager_bps, 100); let fund_fee = total_fees - symmetry_fee - host_fee - manager_fee; let confidence_bps = mul_div( fair_amount - amount_without_fees, BPS_DIVIDER * 100, fair_amount ); let fee_bps = mul_div( amount_without_fees - to_amount, BPS_DIVIDER * 100, fair_amount ); let from_token_worth_before_swap = amount_to_usd_value( fundState.currentCompAmount[from_token_index], from_token_settings.decimals, from_token_price.avg_price ); let to_token_worth_before_swap = amount_to_usd_value( fundState.currentCompAmount[to_token_index], to_token_settings.decimals, to_token_price.avg_price ); let toTokenAfter = fundState.currentCompAmount[to_token_index] - (amount_without_fees - fund_fee); let fromTokenAfter = fundState.currentCompAmount[from_token_index] + from_amount; let from_token_worth_after_swap = amount_to_usd_value( fromTokenAfter, from_token_settings.decimals, from_token_price.avg_price ); let to_token_worth_after_swap= amount_to_usd_value( toTokenAfter, to_token_settings.decimals, to_token_price.avg_price ); let from_old_weight = mul_div( from_token_worth_before_swap, WEIGHT_MULTIPLIER, fund_worth ); let to_old_weight = mul_div( to_token_worth_before_swap, WEIGHT_MULTIPLIER, fund_worth ); fund_worth = fund_worth - from_token_worth_before_swap; fund_worth = fund_worth - to_token_worth_before_swap; fund_worth = fund_worth + from_token_worth_after_swap; fund_worth = fund_worth + to_token_worth_after_swap; let from_new_weight = mul_div( from_token_worth_after_swap, WEIGHT_MULTIPLIER, fund_worth ); let to_new_weight = mul_div( to_token_worth_after_swap, WEIGHT_MULTIPLIER, fund_worth ); let allowed_offset = fundState.rebalanceThreshold.toNumber() * fundState.lpOffsetThreshold.toNumber(); let allowed_from_target_weight = mul_div( fundState.targetWeight[from_token_index], BPS_DIVIDER * BPS_DIVIDER + allowed_offset, BPS_DIVIDER * BPS_DIVIDER ); let allowed_to_target_weight = mul_div( fundState.targetWeight[to_token_index], BPS_DIVIDER * BPS_DIVIDER - allowed_offset, BPS_DIVIDER * BPS_DIVIDER ); if (allowed_from_target_weight > WEIGHT_MULTIPLIER) { allowed_from_target_weight = WEIGHT_MULTIPLIER; } let removing_dust = (from_token_id == 0 && fundState.targetWeight[to_token_index] == 0); if (from_new_weight > allowed_from_target_weight && (!removing_dust)) return { toAmount: undefined, feePct: undefined } if (to_new_weight < allowed_to_target_weight) return { toAmount: undefined, feePct: undefined } return { toAmount: Math.floor(to_amount), feePct: fee_bps/100, } } export function checkForLiquidity( tokenListData: TokenSettings[], curveChainData: CurveChainData, fundAddress: PublicKey, fund: FundStateChainData, oraclePrices: OraclePrice[], tokenFrom: number, tokenTo: number, fromAmount: number, ): RouteData|undefined { if (tokenFrom == undefined || tokenTo == undefined) throw new Error("From or To tokens are not defined"); let initialAmount = fromAmount; let {toAmount, feePct} = computeOutputAmount( tokenFrom, tokenTo, fromAmount, Object.assign({},fund), oraclePrices, Object.assign({}, curveChainData), {list: tokenListData} ) if (toAmount == undefined || feePct == undefined) return undefined; let oracleAccounts = []; for (let i = 0; i < fund.numOfTokens.toNumber(); i++) oracleAccounts.push({ pubkey: new PublicKey( tokenListData[fund.currentCompToken[i].toNumber()].oracleAccount ), isSigner: false, isWritable: false, }) return { fromAmount: initialAmount / (10 ** tokenListData[tokenFrom].decimals), toAmount: toAmount / (10 ** tokenListData[tokenTo].decimals), fromTokenId: tokenFrom, toTokenId: tokenTo, feePct: feePct, priceImpactPct: feePct, swapAccounts: { program: FUNDS_PROGRAM_ID, fundState: fundAddress, authority: FUNDS_PROGRAM_PDA, source: new PublicKey(tokenListData[tokenFrom].pdaTokenAccount), destination: new PublicKey(tokenListData[tokenTo].pdaTokenAccount), fees: { smfWallet: SWAP_FEE_ACCOUNT, hostWallet: fund.hostPubkey, managerWallet: fund.manager, feeTokenMint: new PublicKey(tokenListData[tokenTo].tokenMint) }, tokenList: TOKEN_LIST_ADDRESS, curveData: CURVE_DATA_ADDRESS, remainingAccounts: oracleAccounts, } } } export function loadRouteData( tokenListData: TokenSettings[], curveChainData: CurveChainData, funds: {pubkey: PublicKey, fund: FundStateChainData}[], oraclePrices: OraclePrice[], tokenFrom: number, tokenTo: number, fromAmount: number, ): RouteData { if (tokenFrom == undefined || tokenTo == undefined) throw new Error("From or To tokens are not defined"); let fromTokenAmount = fromAmount * 10 ** tokenListData[tokenFrom].decimals; let bestRouteData: RouteData = { fromAmount: 0, toAmount: 0, fromTokenId: 0, toTokenId: 0, feePct: 0, priceImpactPct: 0, swapAccounts: { program: FUNDS_PROGRAM_ID, fundState: PublicKey.default, authority: FUNDS_PROGRAM_PDA, source: PublicKey.default, destination: PublicKey.default, fees: { smfWallet: SWAP_FEE_ACCOUNT, hostWallet: PublicKey.default, managerWallet: PublicKey.default, feeTokenMint: PublicKey.default }, tokenList: TOKEN_LIST_ADDRESS, curveData: CURVE_DATA_ADDRESS, remainingAccounts: [], } }; for (let i = 0; i < funds.length; i++) { let routeData = checkForLiquidity( tokenListData, curveChainData, funds[i].pubkey, funds[i].fund, oraclePrices, tokenFrom, tokenTo, fromTokenAmount, ); if(!routeData) continue; if (routeData.toAmount > bestRouteData.toAmount) bestRouteData = routeData; } return bestRouteData; }