@symmetry-hq/liquidity-sdk
Version:
Exchange functionality using symmetry funds liquidity
452 lines (405 loc) • 16.3 kB
text/typescript
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;
}