UNPKG

@ox-fun/drift-sdk

Version:

SDK for Drift Protocol

596 lines (522 loc) 16.7 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, } 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)); } return { totalCapacity, remainingCapacity }; } export function calculateInterestRate( bank: SpotMarketAccount, delta = ZERO ): BN { const utilization = calculateUtilization(bank, delta); let interestRate: BN; if (utilization.gt(new BN(bank.optimalUtilization))) { const surplusUtilization = utilization.sub(new BN(bank.optimalUtilization)); const borrowRateSlope = new BN(bank.maxBorrowRate - bank.optimalBorrowRate) .mul(SPOT_MARKET_UTILIZATION_PRECISION) .div( SPOT_MARKET_UTILIZATION_PRECISION.sub(new BN(bank.optimalUtilization)) ); interestRate = new BN(bank.optimalBorrowRate).add( surplusUtilization .mul(borrowRateSlope) .div(SPOT_MARKET_UTILIZATION_PRECISION) ); } else { const borrowRateSlope = new BN(bank.optimalBorrowRate) .mul(SPOT_MARKET_UTILIZATION_PRECISION) .div(new BN(bank.optimalUtilization)); interestRate = utilization .mul(borrowRateSlope) .div(SPOT_MARKET_UTILIZATION_PRECISION); } return interestRate; } export function calculateDepositRate( bank: SpotMarketAccount, delta = ZERO ): BN { // positive delta => adding to deposit // negative delta => adding to borrow const utilization = calculateUtilization(bank, delta); const borrowRate = calculateBorrowRate(bank, delta); 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): BN { return calculateInterestRate(bank, delta); } 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 ); const maxBorrowTokensTwap = BN.max( spotMarket.withdrawGuardThreshold, BN.min( BN.max( marketDepositTokenAmount.div(new BN(6)), borrowTokenTwapLive.add(lesserDepositAmount.div(new BN(10))) ), lesserDepositAmount.sub(lesserDepositAmount.div(new BN(5))) ) ); // between ~15-80% utilization with friction on twap 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 ); const 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 (withdrawLimit.eq(ZERO)) { borrowLimit = ZERO; } return { borrowLimit, withdrawLimit, maxBorrowAmount: maxBorrowTokens, minDepositAmount: minDepositTokens, currentDepositAmount: marketDepositTokenAmount, currentBorrowAmount: marketBorrowTokenAmount, }; }