@drift-labs/sdk
Version:
SDK for Drift Protocol
754 lines (656 loc) • 20.8 kB
text/typescript
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;
}