UNPKG

@drift-labs/sdk

Version:
754 lines (656 loc) • 20.8 kB
import { SpotMarketAccount, SpotBalanceType, isVariant, MarginCategory, } from '../types'; import { BN } from '@coral-xyz/anchor'; import { SPOT_MARKET_UTILIZATION_PRECISION, ONE, TEN, ZERO, SPOT_MARKET_RATE_PRECISION, SPOT_MARKET_WEIGHT_PRECISION, ONE_YEAR, AMM_RESERVE_PRECISION, QUOTE_SPOT_MARKET_INDEX, } from '../constants/numericConstants'; import { calculateSizeDiscountAssetWeight, calculateSizePremiumLiabilityWeight, } from './margin'; import { OraclePriceData } from '../oracles/types'; import { PERCENTAGE_PRECISION } from '../constants/numericConstants'; import { divCeil } from './utils'; import { StrictOraclePrice } from '../oracles/strictOraclePrice'; /** * Calculates the balance of a given token amount including any accumulated interest. This * is the same as `SpotPosition.scaledBalance`. * * @param {BN} tokenAmount - the amount of tokens * @param {SpotMarketAccount} spotMarket - the spot market account * @param {SpotBalanceType} balanceType - the balance type ('deposit' or 'borrow') * @return {BN} the calculated balance, scaled by `SPOT_MARKET_BALANCE_PRECISION` */ export function getBalance( tokenAmount: BN, spotMarket: SpotMarketAccount, balanceType: SpotBalanceType ): BN { const precisionIncrease = TEN.pow(new BN(19 - spotMarket.decimals)); const cumulativeInterest = isVariant(balanceType, 'deposit') ? spotMarket.cumulativeDepositInterest : spotMarket.cumulativeBorrowInterest; let balance = tokenAmount.mul(precisionIncrease).div(cumulativeInterest); if (!balance.eq(ZERO) && isVariant(balanceType, 'borrow')) { balance = balance.add(ONE); } return balance; } /** * Calculates the spot token amount including any accumulated interest. * * @param {BN} balanceAmount - The balance amount, typically from `SpotPosition.scaledBalance` * @param {SpotMarketAccount} spotMarket - The spot market account details * @param {SpotBalanceType} balanceType - The balance type to be used for calculation * @returns {BN} The calculated token amount, scaled by `SpotMarketConfig.precision` */ export function getTokenAmount( balanceAmount: BN, spotMarket: SpotMarketAccount, balanceType: SpotBalanceType ): BN { const precisionDecrease = TEN.pow(new BN(19 - spotMarket.decimals)); if (isVariant(balanceType, 'deposit')) { return balanceAmount .mul(spotMarket.cumulativeDepositInterest) .div(precisionDecrease); } else { return divCeil( balanceAmount.mul(spotMarket.cumulativeBorrowInterest), precisionDecrease ); } } /** * Returns the signed (positive for deposit,negative for borrow) token amount based on the balance type. * * @param {BN} tokenAmount - The token amount to convert (from `getTokenAmount`) * @param {SpotBalanceType} balanceType - The balance type to determine the sign of the token amount. * @returns {BN} - The signed token amount, scaled by `SpotMarketConfig.precision` */ export function getSignedTokenAmount( tokenAmount: BN, balanceType: SpotBalanceType ): BN { if (isVariant(balanceType, 'deposit')) { return tokenAmount; } else { return tokenAmount.abs().neg(); } } /** * Calculates the value of a given token amount using the worst of the provided oracle price and its TWAP. * * @param {BN} tokenAmount - The amount of tokens to calculate the value for (from `getTokenAmount`) * @param {number} spotDecimals - The number of decimals in the token. * @param {StrictOraclePrice} strictOraclePrice - Contains oracle price and 5min twap. * @return {BN} The calculated value of the given token amount, scaled by `PRICE_PRECISION` */ export function getStrictTokenValue( tokenAmount: BN, spotDecimals: number, strictOraclePrice: StrictOraclePrice ): BN { if (tokenAmount.eq(ZERO)) { return ZERO; } let price; if (tokenAmount.gte(ZERO)) { price = strictOraclePrice.min(); } else { price = strictOraclePrice.max(); } const precisionDecrease = TEN.pow(new BN(spotDecimals)); return tokenAmount.mul(price).div(precisionDecrease); } /** * Calculates the value of a given token amount in relation to an oracle price data * * @param {BN} tokenAmount - The amount of tokens to calculate the value for (from `getTokenAmount`) * @param {number} spotDecimals - The number of decimal places of the token. * @param {OraclePriceData} oraclePriceData - The oracle price data (typically a token/USD oracle). * @return {BN} The value of the token based on the oracle, scaled by `PRICE_PRECISION` */ export function getTokenValue( tokenAmount: BN, spotDecimals: number, oraclePriceData: Pick<OraclePriceData, 'price'> ): BN { if (tokenAmount.eq(ZERO)) { return ZERO; } const precisionDecrease = TEN.pow(new BN(spotDecimals)); return tokenAmount.mul(oraclePriceData.price).div(precisionDecrease); } export function calculateAssetWeight( balanceAmount: BN, oraclePrice: BN, spotMarket: SpotMarketAccount, marginCategory: MarginCategory ): BN { const sizePrecision = TEN.pow(new BN(spotMarket.decimals)); let sizeInAmmReservePrecision; if (sizePrecision.gt(AMM_RESERVE_PRECISION)) { sizeInAmmReservePrecision = balanceAmount.div( sizePrecision.div(AMM_RESERVE_PRECISION) ); } else { sizeInAmmReservePrecision = balanceAmount .mul(AMM_RESERVE_PRECISION) .div(sizePrecision); } let assetWeight; switch (marginCategory) { case 'Initial': assetWeight = calculateSizeDiscountAssetWeight( sizeInAmmReservePrecision, new BN(spotMarket.imfFactor), calculateScaledInitialAssetWeight(spotMarket, oraclePrice) ); break; case 'Maintenance': assetWeight = calculateSizeDiscountAssetWeight( sizeInAmmReservePrecision, new BN(spotMarket.imfFactor), new BN(spotMarket.maintenanceAssetWeight) ); break; default: assetWeight = calculateScaledInitialAssetWeight(spotMarket, oraclePrice); break; } return assetWeight; } export function calculateScaledInitialAssetWeight( spotMarket: SpotMarketAccount, oraclePrice: BN ): BN { if (spotMarket.scaleInitialAssetWeightStart.eq(ZERO)) { return new BN(spotMarket.initialAssetWeight); } const deposits = getTokenAmount( spotMarket.depositBalance, spotMarket, SpotBalanceType.DEPOSIT ); const depositsValue = getTokenValue(deposits, spotMarket.decimals, { price: oraclePrice, }); if (depositsValue.lt(spotMarket.scaleInitialAssetWeightStart)) { return new BN(spotMarket.initialAssetWeight); } else { return new BN(spotMarket.initialAssetWeight) .mul(spotMarket.scaleInitialAssetWeightStart) .div(depositsValue); } } export function calculateLiabilityWeight( size: BN, spotMarket: SpotMarketAccount, marginCategory: MarginCategory ): BN { const sizePrecision = TEN.pow(new BN(spotMarket.decimals)); let sizeInAmmReservePrecision; if (sizePrecision.gt(AMM_RESERVE_PRECISION)) { sizeInAmmReservePrecision = size.div( sizePrecision.div(AMM_RESERVE_PRECISION) ); } else { sizeInAmmReservePrecision = size .mul(AMM_RESERVE_PRECISION) .div(sizePrecision); } let liabilityWeight; switch (marginCategory) { case 'Initial': liabilityWeight = calculateSizePremiumLiabilityWeight( sizeInAmmReservePrecision, new BN(spotMarket.imfFactor), new BN(spotMarket.initialLiabilityWeight), SPOT_MARKET_WEIGHT_PRECISION ); break; case 'Maintenance': liabilityWeight = calculateSizePremiumLiabilityWeight( sizeInAmmReservePrecision, new BN(spotMarket.imfFactor), new BN(spotMarket.maintenanceLiabilityWeight), SPOT_MARKET_WEIGHT_PRECISION ); break; default: liabilityWeight = new BN(spotMarket.initialLiabilityWeight); break; } return liabilityWeight; } export function calculateUtilization( bank: SpotMarketAccount, delta = ZERO ): BN { let tokenDepositAmount = getTokenAmount( bank.depositBalance, bank, SpotBalanceType.DEPOSIT ); let tokenBorrowAmount = getTokenAmount( bank.borrowBalance, bank, SpotBalanceType.BORROW ); if (delta.gt(ZERO)) { tokenDepositAmount = tokenDepositAmount.add(delta); } else if (delta.lt(ZERO)) { tokenBorrowAmount = tokenBorrowAmount.add(delta.abs()); } let utilization: BN; if (tokenBorrowAmount.eq(ZERO) && tokenDepositAmount.eq(ZERO)) { utilization = ZERO; } else if (tokenDepositAmount.eq(ZERO)) { utilization = SPOT_MARKET_UTILIZATION_PRECISION; } else { utilization = tokenBorrowAmount .mul(SPOT_MARKET_UTILIZATION_PRECISION) .div(tokenDepositAmount); } return utilization; } /** * calculates max borrow amount where rate would stay below targetBorrowRate * @param spotMarketAccount * @param targetBorrowRate * @returns : Precision: TOKEN DECIMALS */ export function calculateSpotMarketBorrowCapacity( spotMarketAccount: SpotMarketAccount, targetBorrowRate: BN ): { totalCapacity: BN; remainingCapacity: BN } { const currentBorrowRate = calculateBorrowRate(spotMarketAccount); const tokenDepositAmount = getTokenAmount( spotMarketAccount.depositBalance, spotMarketAccount, SpotBalanceType.DEPOSIT ); const tokenBorrowAmount = getTokenAmount( spotMarketAccount.borrowBalance, spotMarketAccount, SpotBalanceType.BORROW ); let targetUtilization; // target utilization past mid point if (targetBorrowRate.gte(new BN(spotMarketAccount.optimalBorrowRate))) { const borrowRateSlope = new BN( spotMarketAccount.maxBorrowRate - spotMarketAccount.optimalBorrowRate ) .mul(SPOT_MARKET_UTILIZATION_PRECISION) .div( SPOT_MARKET_UTILIZATION_PRECISION.sub( new BN(spotMarketAccount.optimalUtilization) ) ); const surplusTargetUtilization = targetBorrowRate .sub(new BN(spotMarketAccount.optimalBorrowRate)) .mul(SPOT_MARKET_UTILIZATION_PRECISION) .div(borrowRateSlope); targetUtilization = surplusTargetUtilization.add( new BN(spotMarketAccount.optimalUtilization) ); } else { const borrowRateSlope = new BN(spotMarketAccount.optimalBorrowRate) .mul(SPOT_MARKET_UTILIZATION_PRECISION) .div(new BN(spotMarketAccount.optimalUtilization)); targetUtilization = targetBorrowRate .mul(SPOT_MARKET_UTILIZATION_PRECISION) .div(borrowRateSlope); } const totalCapacity = tokenDepositAmount .mul(targetUtilization) .div(SPOT_MARKET_UTILIZATION_PRECISION); let remainingCapacity; if (currentBorrowRate.gte(targetBorrowRate)) { remainingCapacity = ZERO; } else { remainingCapacity = BN.max(ZERO, totalCapacity.sub(tokenBorrowAmount)); } if (spotMarketAccount.maxTokenBorrowsFraction > 0) { const maxTokenBorrows = spotMarketAccount.maxTokenDeposits .mul(new BN(spotMarketAccount.maxTokenBorrowsFraction)) .divn(10000); remainingCapacity = BN.min( remainingCapacity, BN.max(ZERO, maxTokenBorrows.sub(tokenBorrowAmount)) ); } return { totalCapacity, remainingCapacity }; } export function calculateInterestRate( bank: SpotMarketAccount, delta = ZERO, currentUtilization: BN = null ): BN { // todo: ensure both a delta and current util aren't pass? const utilization = currentUtilization || calculateUtilization(bank, delta); const optimalUtil = new BN(bank.optimalUtilization); const optimalRate = new BN(bank.optimalBorrowRate); const maxRate = new BN(bank.maxBorrowRate); const minRate = new BN(bank.minBorrowRate).mul( PERCENTAGE_PRECISION.divn(200) ); const weightsDivisor = new BN(1000); const segments: [BN, BN][] = [ [new BN(850_000), new BN(50)], [new BN(900_000), new BN(100)], [new BN(950_000), new BN(150)], [new BN(990_000), new BN(200)], [new BN(995_000), new BN(250)], [SPOT_MARKET_UTILIZATION_PRECISION, new BN(250)], ]; let rate: BN; if (utilization.lte(optimalUtil)) { // below optimal: linear ramp from 0 to optimalRate const slope = optimalRate .mul(SPOT_MARKET_UTILIZATION_PRECISION) .div(optimalUtil); rate = utilization.mul(slope).div(SPOT_MARKET_UTILIZATION_PRECISION); } else { // above optimal: piecewise segments const totalExtraRate = maxRate.sub(optimalRate); rate = optimalRate.clone(); let prevUtil = optimalUtil.clone(); for (const [bp, weight] of segments) { const segmentEnd = bp.gt(SPOT_MARKET_UTILIZATION_PRECISION) ? SPOT_MARKET_UTILIZATION_PRECISION : bp; const segmentRange = segmentEnd.sub(prevUtil); const segmentRateTotal = totalExtraRate.mul(weight).div(weightsDivisor); if (utilization.lte(segmentEnd)) { const partialUtil = utilization.sub(prevUtil); const partialRate = segmentRateTotal.mul(partialUtil).div(segmentRange); rate = rate.add(partialRate); break; } else { rate = rate.add(segmentRateTotal); prevUtil = segmentEnd; } } } return BN.max(minRate, rate); } export function calculateDepositRate( bank: SpotMarketAccount, delta = ZERO, currentUtilization: BN = null ): BN { // positive delta => adding to deposit // negative delta => adding to borrow const utilization = currentUtilization || calculateUtilization(bank, delta); const borrowRate = calculateBorrowRate(bank, delta, utilization); const depositRate = borrowRate .mul(PERCENTAGE_PRECISION.sub(new BN(bank.insuranceFund.totalFactor))) .mul(utilization) .div(SPOT_MARKET_UTILIZATION_PRECISION) .div(PERCENTAGE_PRECISION); return depositRate; } export function calculateBorrowRate( bank: SpotMarketAccount, delta = ZERO, currentUtilization: BN = null ): BN { return calculateInterestRate(bank, delta, currentUtilization); } export function calculateInterestAccumulated( bank: SpotMarketAccount, now: BN ): { borrowInterest: BN; depositInterest: BN } { const interestRate = calculateInterestRate(bank); const timeSinceLastUpdate = now.sub(bank.lastInterestTs); const modifiedBorrowRate = interestRate.mul(timeSinceLastUpdate); const utilization = calculateUtilization(bank); const modifiedDepositRate = modifiedBorrowRate .mul(utilization) .div(SPOT_MARKET_UTILIZATION_PRECISION); const borrowInterest = bank.cumulativeBorrowInterest .mul(modifiedBorrowRate) .div(ONE_YEAR) .div(SPOT_MARKET_RATE_PRECISION) .add(ONE); const depositInterest = bank.cumulativeDepositInterest .mul(modifiedDepositRate) .div(ONE_YEAR) .div(SPOT_MARKET_RATE_PRECISION); return { borrowInterest, depositInterest }; } export function calculateTokenUtilizationLimits( depositTokenAmount: BN, borrowTokenAmount: BN, spotMarket: SpotMarketAccount ): { minDepositTokensForUtilization: BN; maxBorrowTokensForUtilization: BN; } { // Calculates the allowable minimum deposit and maximum borrow amounts for immediate withdrawal based on market utilization. // First, it determines a maximum withdrawal utilization from the market's target and historic utilization. // Then, it deduces corresponding deposit/borrow amounts. // Note: For deposit sizes below the guard threshold, withdrawals aren't blocked. const maxWithdrawUtilization = BN.max( new BN(spotMarket.optimalUtilization), spotMarket.utilizationTwap.add( SPOT_MARKET_UTILIZATION_PRECISION.sub(spotMarket.utilizationTwap).div( new BN(2) ) ) ); let minDepositTokensForUtilization = borrowTokenAmount .mul(SPOT_MARKET_UTILIZATION_PRECISION) .div(maxWithdrawUtilization); // don't block withdraws for deposit sizes below guard threshold minDepositTokensForUtilization = BN.min( minDepositTokensForUtilization, depositTokenAmount.sub(spotMarket.withdrawGuardThreshold) ); let maxBorrowTokensForUtilization = maxWithdrawUtilization .mul(depositTokenAmount) .div(SPOT_MARKET_UTILIZATION_PRECISION); maxBorrowTokensForUtilization = BN.max( spotMarket.withdrawGuardThreshold, maxBorrowTokensForUtilization ); return { minDepositTokensForUtilization, maxBorrowTokensForUtilization, }; } export function calculateWithdrawLimit( spotMarket: SpotMarketAccount, now: BN ): { borrowLimit: BN; withdrawLimit: BN; minDepositAmount: BN; maxBorrowAmount: BN; currentDepositAmount; currentBorrowAmount; } { const marketDepositTokenAmount = getTokenAmount( spotMarket.depositBalance, spotMarket, SpotBalanceType.DEPOSIT ); const marketBorrowTokenAmount = getTokenAmount( spotMarket.borrowBalance, spotMarket, SpotBalanceType.BORROW ); const twentyFourHours = new BN(60 * 60 * 24); const sinceLast = now.sub(spotMarket.lastTwapTs); const sinceStart = BN.max(ZERO, twentyFourHours.sub(sinceLast)); const borrowTokenTwapLive = spotMarket.borrowTokenTwap .mul(sinceStart) .add(marketBorrowTokenAmount.mul(sinceLast)) .div(sinceLast.add(sinceStart)); const depositTokenTwapLive = spotMarket.depositTokenTwap .mul(sinceStart) .add(marketDepositTokenAmount.mul(sinceLast)) .div(sinceLast.add(sinceStart)); const lesserDepositAmount = BN.min( marketDepositTokenAmount, depositTokenTwapLive ); let maxBorrowTokensTwap; if (spotMarket.poolId == 0) { maxBorrowTokensTwap = BN.max( spotMarket.withdrawGuardThreshold, BN.min( BN.max( marketDepositTokenAmount.div(new BN(3)), borrowTokenTwapLive.add(lesserDepositAmount.div(new BN(7))) ), lesserDepositAmount.sub(lesserDepositAmount.div(new BN(8))) ) ); // main pool between ~30-92.5% utilization with friction on twap in 20% increments } else { maxBorrowTokensTwap = BN.max( spotMarket.withdrawGuardThreshold, BN.min( BN.max( marketDepositTokenAmount.div(new BN(2)), borrowTokenTwapLive.add(lesserDepositAmount.div(new BN(3))) ), lesserDepositAmount.sub(lesserDepositAmount.div(new BN(20))) ) ); // isolated pools between 50-95% utilization with friction on twap in 33% increments } const minDepositTokensTwap = depositTokenTwapLive.sub( BN.max( depositTokenTwapLive.div(new BN(4)), BN.min(spotMarket.withdrawGuardThreshold, depositTokenTwapLive) ) ); const { minDepositTokensForUtilization, maxBorrowTokensForUtilization } = calculateTokenUtilizationLimits( marketDepositTokenAmount, marketBorrowTokenAmount, spotMarket ); const minDepositTokens = BN.max( minDepositTokensForUtilization, minDepositTokensTwap ); let maxBorrowTokens = BN.min( maxBorrowTokensForUtilization, maxBorrowTokensTwap ); const withdrawLimit = BN.max( marketDepositTokenAmount.sub(minDepositTokens), ZERO ); let borrowLimit = maxBorrowTokens.sub(marketBorrowTokenAmount); borrowLimit = BN.min( borrowLimit, marketDepositTokenAmount.sub(marketBorrowTokenAmount) ); if (spotMarket.maxTokenBorrowsFraction > 0) { const maxTokenBorrowsByFraction = spotMarket.maxTokenDeposits .mul(new BN(spotMarket.maxTokenBorrowsFraction)) .divn(10000); const trueMaxBorrowTokensAvailable = maxTokenBorrowsByFraction.sub( marketBorrowTokenAmount ); maxBorrowTokens = BN.min(maxBorrowTokens, trueMaxBorrowTokensAvailable); borrowLimit = BN.min(borrowLimit, maxBorrowTokens); } if (withdrawLimit.eq(ZERO) || isVariant(spotMarket.assetTier, 'protected')) { borrowLimit = ZERO; } return { borrowLimit, withdrawLimit, maxBorrowAmount: maxBorrowTokens, minDepositAmount: minDepositTokens, currentDepositAmount: marketDepositTokenAmount, currentBorrowAmount: marketBorrowTokenAmount, }; } export function getSpotAssetValue( tokenAmount: BN, strictOraclePrice: StrictOraclePrice, spotMarketAccount: SpotMarketAccount, maxMarginRatio: number, marginCategory?: MarginCategory ): BN { let assetValue = getStrictTokenValue( tokenAmount, spotMarketAccount.decimals, strictOraclePrice ); if (marginCategory !== undefined) { let weight = calculateAssetWeight( tokenAmount, strictOraclePrice.current, spotMarketAccount, marginCategory ); if ( marginCategory === 'Initial' && spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX ) { const userCustomAssetWeight = BN.max( ZERO, SPOT_MARKET_WEIGHT_PRECISION.subn(maxMarginRatio) ); weight = BN.min(weight, userCustomAssetWeight); } assetValue = assetValue.mul(weight).div(SPOT_MARKET_WEIGHT_PRECISION); } return assetValue; } export function getSpotLiabilityValue( tokenAmount: BN, strictOraclePrice: StrictOraclePrice, spotMarketAccount: SpotMarketAccount, maxMarginRatio: number, marginCategory?: MarginCategory, liquidationBuffer?: BN ): BN { let liabilityValue = getStrictTokenValue( tokenAmount, spotMarketAccount.decimals, strictOraclePrice ); if (marginCategory !== undefined) { let weight = calculateLiabilityWeight( tokenAmount, spotMarketAccount, marginCategory ); if ( marginCategory === 'Initial' && spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX ) { weight = BN.max( weight, SPOT_MARKET_WEIGHT_PRECISION.addn(maxMarginRatio) ); } if (liquidationBuffer !== undefined) { weight = weight.add(liquidationBuffer); } liabilityValue = liabilityValue .mul(weight) .div(SPOT_MARKET_WEIGHT_PRECISION); } return liabilityValue; }