@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
1,407 lines (1,223 loc) • 52.3 kB
text/typescript
/* eslint-disable max-classes-per-file */
import { PublicKey } from '@solana/web3.js';
import Decimal from 'decimal.js';
import { KaminoReserve } from './reserve';
import { Obligation } from '../idl_codegen/accounts';
import { ElevationGroupDescription, KaminoMarket } from './market';
import BN from 'bn.js';
import { Fraction } from './fraction';
import {
ObligationCollateral,
ObligationCollateralFields,
ObligationLiquidity,
ObligationLiquidityFields,
} from '../idl_codegen/types';
import { positiveOrZero, valueOrZero } from './utils';
import { isNotNullPubkey, PubkeyHashMap, U64_MAX } from '../utils';
import { ActionType } from './action';
export type Position = {
reserveAddress: PublicKey;
mintAddress: PublicKey;
/**
* Amount of tokens in lamports, including decimal places for interest accrued (no borrow factor weighting)
*/
amount: Decimal;
/**
* Market value of the position in USD (no borrow factor weighting)
*/
marketValueRefreshed: Decimal;
};
export type ObligationStats = {
userTotalDeposit: Decimal;
userTotalCollateralDeposit: Decimal;
userTotalBorrow: Decimal;
userTotalBorrowBorrowFactorAdjusted: Decimal;
borrowLimit: Decimal;
borrowLiquidationLimit: Decimal;
borrowUtilization: Decimal;
netAccountValue: Decimal;
loanToValue: Decimal;
liquidationLtv: Decimal;
leverage: Decimal;
potentialElevationGroupUpdate: number;
};
interface BorrowStats {
borrows: Map<PublicKey, Position>;
userTotalBorrow: Decimal;
userTotalBorrowBorrowFactorAdjusted: Decimal;
positions: number;
}
interface DepositStats {
deposits: Map<PublicKey, Position>;
userTotalDeposit: Decimal;
userTotalCollateralDeposit: Decimal;
borrowLimit: Decimal;
liquidationLtv: Decimal;
borrowLiquidationLimit: Decimal;
}
export class KaminoObligation {
obligationAddress: PublicKey;
state: Obligation;
/**
* Deposits stored in a map of reserve address to position
*/
deposits: Map<PublicKey, Position>;
/**
* Borrows stored in a map of reserve address to position
*/
borrows: Map<PublicKey, Position>;
refreshedStats: ObligationStats;
obligationTag: number;
/**
* Initialise a new Obligation from the deserialized state
* @param market
* @param obligationAddress
* @param obligation
* @param collateralExchangeRates - rates from the market by reserve address, will be calculated if not provided
* @param cumulativeBorrowRates - rates from the market by reserve address, will be calculated if not provided
*/
constructor(
market: KaminoMarket,
obligationAddress: PublicKey,
obligation: Obligation,
collateralExchangeRates: Map<PublicKey, Decimal>,
cumulativeBorrowRates: Map<PublicKey, Decimal>
) {
this.obligationAddress = obligationAddress;
this.state = obligation;
const { borrows, deposits, refreshedStats } = this.calculatePositions(
market,
obligation.deposits,
obligation.borrows,
obligation.elevationGroup,
collateralExchangeRates,
cumulativeBorrowRates
);
this.deposits = deposits;
this.borrows = borrows;
this.refreshedStats = refreshedStats;
this.obligationTag = obligation.tag.toNumber();
}
static async load(kaminoMarket: KaminoMarket, obligationAddress: PublicKey): Promise<KaminoObligation | null> {
const res = await kaminoMarket.getConnection().getAccountInfoAndContext(obligationAddress);
if (res.value === null) {
return null;
}
const accInfo = res.value;
if (!accInfo.owner.equals(kaminoMarket.programId)) {
throw new Error("account doesn't belong to this program");
}
const obligation = Obligation.decode(accInfo.data);
if (obligation === null) {
return null;
}
const { collateralExchangeRates, cumulativeBorrowRates } = KaminoObligation.getRatesForObligation(
kaminoMarket,
obligation,
res.context.slot
);
return new KaminoObligation(
kaminoMarket,
obligationAddress,
obligation,
collateralExchangeRates,
cumulativeBorrowRates
);
}
static async loadAll(
kaminoMarket: KaminoMarket,
obligationAddresses: PublicKey[],
slot?: number
): Promise<(KaminoObligation | null)[]> {
let currentSlot = slot;
let obligations: (Obligation | null)[];
if (!currentSlot) {
[currentSlot, obligations] = await Promise.all([
kaminoMarket.getConnection().getSlot(),
Obligation.fetchMultiple(kaminoMarket.getConnection(), obligationAddresses, kaminoMarket.programId),
]);
} else {
obligations = await Obligation.fetchMultiple(
kaminoMarket.getConnection(),
obligationAddresses,
kaminoMarket.programId
);
}
const cumulativeBorrowRates = new PubkeyHashMap<PublicKey, Decimal>();
const collateralExchangeRates = new PubkeyHashMap<PublicKey, Decimal>();
for (const obligation of obligations) {
if (obligation !== null) {
KaminoObligation.addRatesForObligation(
kaminoMarket,
obligation,
collateralExchangeRates,
cumulativeBorrowRates,
currentSlot
);
}
}
return obligations.map((obligation, i) => {
if (obligation === null) {
return null;
}
return new KaminoObligation(
kaminoMarket,
obligationAddresses[i],
obligation,
collateralExchangeRates,
cumulativeBorrowRates
);
});
}
/**
* @returns the obligation borrows as a list
*/
getBorrows(): Array<Position> {
return [...this.borrows.values()];
}
/**
* @returns the obligation borrows as a list
*/
getDeposits(): Array<Position> {
return [...this.deposits.values()];
}
/**
* @returns the total deposited value of the obligation (sum of all deposits)
*/
getDepositedValue(): Decimal {
return new Fraction(this.state.depositedValueSf).toDecimal();
}
/**
* @returns the total borrowed value of the obligation (sum of all borrows -- no borrow factor)
*/
getBorrowedMarketValue(): Decimal {
return new Fraction(this.state.borrowedAssetsMarketValueSf).toDecimal();
}
/**
* @returns the total borrowed value of the obligation (sum of all borrows -- with borrow factor weighting)
*/
getBorrowedMarketValueBFAdjusted(): Decimal {
return new Fraction(this.state.borrowFactorAdjustedDebtValueSf).toDecimal();
}
/**
* @returns total borrow power of the obligation, relative to max LTV of each asset's reserve
*/
getAllowedBorrowValue(): Decimal {
return new Fraction(this.state.allowedBorrowValueSf).toDecimal();
}
/**
* @returns the borrow value at which the obligation gets liquidatable
* (relative to the liquidation threshold of each asset's reserve)
*/
getUnhealthyBorrowValue(): Decimal {
return new Fraction(this.state.unhealthyBorrowValueSf).toDecimal();
}
/**
*
* @returns Market value of the deposit in the specified obligation collateral/deposit asset (USD)
*/
getDepositMarketValue(deposit: ObligationCollateral): Decimal {
return new Fraction(deposit.marketValueSf).toDecimal();
}
getBorrowByReserve(reserve: PublicKey): Position | undefined {
return this.borrows.get(reserve);
}
getDepositByReserve(reserve: PublicKey): Position | undefined {
return this.deposits.get(reserve);
}
getBorrowByMint(mint: PublicKey): Position | undefined {
for (const value of this.borrows.values()) {
if (value.mintAddress.equals(mint)) {
return value;
}
}
return undefined;
}
getDepositByMint(mint: PublicKey): Position | undefined {
for (const value of this.deposits.values()) {
if (value.mintAddress.equals(mint)) {
return value;
}
}
return undefined;
}
/**
*
* @returns Market value of the borrow in the specified obligation liquidity/borrow asset (USD) (no borrow factor weighting)
*/
getBorrowMarketValue(borrow: ObligationLiquidity): Decimal {
return new Fraction(borrow.marketValueSf).toDecimal();
}
/**
*
* @returns Market value of the borrow in the specified obligation liquidity/borrow asset (USD) (with borrow factor weighting)
*/
getBorrowMarketValueBFAdjusted(borrow: ObligationLiquidity): Decimal {
return new Fraction(borrow.borrowFactorAdjustedMarketValueSf).toDecimal();
}
/**
* Calculate the current ratio of borrowed value to deposited value
*/
loanToValue(): Decimal {
if (this.refreshedStats.userTotalDeposit.eq(0)) {
return new Decimal(0);
}
return this.refreshedStats.userTotalBorrowBorrowFactorAdjusted.div(this.refreshedStats.userTotalDeposit);
}
/**
* @returns the total number of positions (deposits + borrows)
*/
getNumberOfPositions(): number {
return this.deposits.size + this.borrows.size;
}
getNetAccountValue(): Decimal {
return this.refreshedStats.netAccountValue;
}
/**
* Get the loan to value and liquidation loan to value for a collateral token reserve as ratios, accounting for the obligation elevation group if it is active
* @param market
* @param reserve
*/
public getLtvForReserve(market: KaminoMarket, reserve: KaminoReserve): { maxLtv: Decimal; liquidationLtv: Decimal } {
return KaminoObligation.getLtvForReserve(market, reserve, this.state.elevationGroup);
}
/**
* @returns the potential elevation groups the obligation qualifies for
*/
getElevationGroups(kaminoMarket: KaminoMarket): Array<number> {
const reserves = new PubkeyHashMap<PublicKey, KaminoReserve>();
for (const deposit of this.state.deposits.values()) {
if (isNotNullPubkey(deposit.depositReserve) && !reserves.has(deposit.depositReserve)) {
reserves.set(deposit.depositReserve, kaminoMarket.getReserveByAddress(deposit.depositReserve)!);
}
}
for (const borrow of this.state.borrows.values()) {
if (isNotNullPubkey(borrow.borrowReserve) && !reserves.has(borrow.borrowReserve)) {
reserves.set(borrow.borrowReserve, kaminoMarket.getReserveByAddress(borrow.borrowReserve)!);
}
}
return KaminoObligation.getElevationGroupsForReserves([...reserves.values()]);
}
static getElevationGroupsForReserves(reserves: Array<KaminoReserve>): Array<number> {
const elevationGroupsCounts = new Map<number, number>();
for (const reserve of reserves) {
for (const elevationGroup of reserve.state.config.elevationGroups) {
if (elevationGroup !== 0) {
const count = elevationGroupsCounts.get(elevationGroup);
if (count) {
elevationGroupsCounts.set(elevationGroup, count + 1);
} else {
elevationGroupsCounts.set(elevationGroup, 1);
}
}
}
}
const activeElevationGroups = new Array<number>();
for (const [group, count] of elevationGroupsCounts.entries()) {
if (count === reserves.length) {
activeElevationGroups.push(group);
}
}
return activeElevationGroups;
}
simulateDepositChange(
obligationDeposits: ObligationCollateral[],
changeInLamports: number,
changeReserve: PublicKey,
collateralExchangeRates: Map<PublicKey, Decimal>
): ObligationCollateral[] {
const newDeposits: ObligationCollateral[] = [];
const depositIndex = obligationDeposits.findIndex((deposit) => deposit.depositReserve.equals(changeReserve));
// Always copy the previous deposits and modify the changeReserve one if it exists
for (let i = 0; i < obligationDeposits.length; i++) {
if (obligationDeposits[i].depositReserve.equals(changeReserve)) {
const coll: ObligationCollateralFields = { ...obligationDeposits[i] };
const exchangeRate = collateralExchangeRates.get(changeReserve)!;
const changeInCollateral = new Decimal(changeInLamports).mul(exchangeRate).toFixed(0);
const updatedDeposit = new Decimal(obligationDeposits[i].depositedAmount.toNumber()).add(changeInCollateral);
coll.depositedAmount = new BN(positiveOrZero(updatedDeposit).toString());
newDeposits.push(new ObligationCollateral(coll));
} else {
newDeposits.push(obligationDeposits[i]);
}
}
if (depositIndex === -1) {
// If the reserve is not in the obligation, we add it
const firstBorrowIndexAvailable = obligationDeposits.findIndex((deposit) =>
deposit.depositReserve.equals(PublicKey.default)
);
if (firstBorrowIndexAvailable === -1) {
throw new Error('No available borrows to modify');
}
const coll: ObligationCollateralFields = { ...obligationDeposits[firstBorrowIndexAvailable] };
const exchangeRate = collateralExchangeRates.get(changeReserve)!;
const changeInCollateral = new Decimal(changeInLamports).mul(exchangeRate).toFixed(0);
coll.depositedAmount = new BN(positiveOrZero(new Decimal(changeInCollateral)).toString());
coll.depositReserve = changeReserve;
newDeposits[firstBorrowIndexAvailable] = new ObligationCollateral(coll);
}
return newDeposits;
}
simulateBorrowChange(
obligationBorrows: ObligationLiquidity[],
changeInLamports: number,
changeReserve: PublicKey,
cumulativeBorrowRate: Decimal
): ObligationLiquidity[] {
const newBorrows: ObligationLiquidity[] = [];
const borrowIndex = obligationBorrows.findIndex((borrow) => borrow.borrowReserve.equals(changeReserve));
// Always copy the previous borrows and modify the changeReserve one if it exists
for (let i = 0; i < obligationBorrows.length; i++) {
if (obligationBorrows[i].borrowReserve.equals(changeReserve)) {
const borrow: ObligationLiquidityFields = { ...obligationBorrows[borrowIndex] };
const newBorrowedAmount: Decimal = new Fraction(borrow.borrowedAmountSf).toDecimal().add(changeInLamports);
const newBorrowedAmountSf = Fraction.fromDecimal(positiveOrZero(newBorrowedAmount)).getValue();
borrow.borrowedAmountSf = newBorrowedAmountSf;
newBorrows.push(new ObligationLiquidity(borrow));
} else {
newBorrows.push(obligationBorrows[i]);
}
}
if (borrowIndex === -1) {
// If the reserve is not in the obligation, we add it
const firstBorrowIndexAvailable = obligationBorrows.findIndex((borrow) =>
borrow.borrowReserve.equals(PublicKey.default)
);
if (firstBorrowIndexAvailable === -1) {
throw new Error('No available borrows to modify');
}
const borrow: ObligationLiquidityFields = { ...obligationBorrows[firstBorrowIndexAvailable] };
borrow.borrowedAmountSf = Fraction.fromDecimal(new Decimal(changeInLamports)).getValue();
borrow.borrowReserve = changeReserve;
borrow.cumulativeBorrowRateBsf = {
padding: [],
value: [Fraction.fromDecimal(cumulativeBorrowRate).getValue(), new BN(0), new BN(0), new BN(0)],
};
newBorrows[firstBorrowIndexAvailable] = new ObligationLiquidity(borrow);
}
return newBorrows;
}
/**
* Calculate the newly modified stats of the obligation
*/
// TODO: Shall we set up position limits?
getSimulatedObligationStats(params: {
amountCollateral?: Decimal;
amountDebt?: Decimal;
action: ActionType;
mintCollateral?: PublicKey;
mintDebt?: PublicKey;
market: KaminoMarket;
reserves: Map<PublicKey, KaminoReserve>;
slot: number;
elevationGroupOverride?: number;
}): {
stats: ObligationStats;
deposits: Map<PublicKey, Position>;
borrows: Map<PublicKey, Position>;
} {
const { amountCollateral, amountDebt, action, mintCollateral, mintDebt, market } = params;
let newStats = { ...this.refreshedStats };
const collateralReservePk = mintCollateral ? market.getReserveByMint(mintCollateral)!.address : undefined;
const debtReservePk = mintDebt ? market.getReserveByMint(mintDebt)!.address : undefined;
const additionalReserves = [];
if (collateralReservePk !== undefined) {
additionalReserves.push(collateralReservePk);
}
if (debtReservePk !== undefined) {
additionalReserves.push(debtReservePk);
}
const { collateralExchangeRates } = KaminoObligation.getRatesForObligation(
market,
this.state,
params.slot,
additionalReserves
);
const elevationGroup = params.elevationGroupOverride ?? this.state.elevationGroup;
let newDeposits: Map<PublicKey, Position> = new PubkeyHashMap<PublicKey, Position>([...this.deposits.entries()]);
let newBorrows: Map<PublicKey, Position> = new PubkeyHashMap<PublicKey, Position>([...this.borrows.entries()]);
// Any action can impact both deposit stats and borrow stats if elevation group is changed
// so we have to recalculate the entire position, not just an updated deposit or borrow
// as both LTVs and borrow factors can change, affecting all calcs
const debtReserveCumulativeBorrowRate = mintDebt
? market.getReserveByMint(mintDebt)!.getCumulativeBorrowRate()
: undefined;
let newObligationDeposits = this.state.deposits;
let newObligationBorrows = this.state.borrows;
// Print deposits and borrows before
for (const deposit of this.state.deposits) {
console.log(`Before Deposit: ${deposit.depositReserve.toBase58()} - ${deposit.depositedAmount}`);
}
for (const borrow of this.state.borrows) {
console.log(`Before Borrow: ${borrow.borrowReserve.toBase58()} - ${borrow.borrowedAmountSf}`);
}
switch (action) {
case 'deposit': {
if (amountCollateral === undefined || mintCollateral === undefined) {
throw Error('amountCollateral & mintCollateral are required for deposit action');
}
newObligationDeposits = this.simulateDepositChange(
this.state.deposits,
amountCollateral.toNumber(),
collateralReservePk!,
collateralExchangeRates
);
break;
}
case 'borrow': {
if (amountDebt === undefined || mintDebt === undefined) {
throw Error('amountDebt & mintDebt are required for borrow action');
}
newObligationBorrows = this.simulateBorrowChange(
this.state.borrows,
amountDebt.toNumber(),
debtReservePk!,
debtReserveCumulativeBorrowRate!
);
break;
}
case 'repay': {
if (amountDebt === undefined || mintDebt === undefined) {
throw Error('amountDebt & mintDebt are required for repay action');
}
newObligationBorrows = this.simulateBorrowChange(
this.state.borrows,
amountDebt.neg().toNumber(),
debtReservePk!,
debtReserveCumulativeBorrowRate!
);
break;
}
case 'withdraw': {
if (amountCollateral === undefined || mintCollateral === undefined) {
throw Error('amountCollateral & mintCollateral are required for withdraw action');
}
newObligationDeposits = this.simulateDepositChange(
this.state.deposits,
amountCollateral.neg().toNumber(),
collateralReservePk!,
collateralExchangeRates
);
break;
}
case 'depositAndBorrow': {
if (
amountCollateral === undefined ||
amountDebt === undefined ||
mintCollateral === undefined ||
mintDebt === undefined
) {
throw Error('amountColl & amountDebt & mintCollateral & mintDebt are required for depositAndBorrow action');
}
newObligationDeposits = this.simulateDepositChange(
this.state.deposits,
amountCollateral.toNumber(),
collateralReservePk!,
collateralExchangeRates
);
newObligationBorrows = this.simulateBorrowChange(
this.state.borrows,
amountDebt.toNumber(),
debtReservePk!,
debtReserveCumulativeBorrowRate!
);
break;
}
case 'repayAndWithdraw': {
if (
amountCollateral === undefined ||
amountDebt === undefined ||
mintCollateral === undefined ||
mintDebt === undefined
) {
throw Error('amountColl & amountDebt & mintCollateral & mintDebt are required for repayAndWithdraw action');
}
newObligationDeposits = this.simulateDepositChange(
this.state.deposits,
amountCollateral.neg().toNumber(),
collateralReservePk!,
collateralExchangeRates
);
newObligationBorrows = this.simulateBorrowChange(
this.state.borrows,
amountDebt.neg().toNumber(),
debtReservePk!,
debtReserveCumulativeBorrowRate!
);
break;
}
default: {
throw Error(`Invalid action type ${action} for getSimulatedObligationStats`);
}
}
const { borrows, deposits, refreshedStats } = this.calculatePositions(
market,
newObligationDeposits,
newObligationBorrows,
elevationGroup,
collateralExchangeRates,
null
);
// Print deposits and borrows after
for (const deposit of newObligationDeposits) {
console.log(`After Deposit: ${deposit.depositReserve.toBase58()} - ${deposit.depositedAmount}`);
}
for (const borrow of newObligationBorrows) {
console.log(`After Borrow: ${borrow.borrowReserve.toBase58()} - ${borrow.borrowedAmountSf}`);
}
newStats = refreshedStats;
newDeposits = deposits;
newBorrows = borrows;
newStats.netAccountValue = newStats.userTotalDeposit.minus(newStats.userTotalBorrow);
newStats.loanToValue = valueOrZero(
newStats.userTotalBorrowBorrowFactorAdjusted.dividedBy(newStats.userTotalDeposit)
);
newStats.leverage = valueOrZero(newStats.userTotalDeposit.dividedBy(newStats.netAccountValue));
return {
stats: newStats,
deposits: newDeposits,
borrows: newBorrows,
};
}
estimateObligationInterestRate = (
market: KaminoMarket,
reserve: KaminoReserve,
borrow: ObligationLiquidity,
currentSlot: number
): Decimal => {
const estimatedCumulativeBorrowRate = reserve.getEstimatedCumulativeBorrowRate(
currentSlot,
market.state.referralFeeBps
);
const currentCumulativeBorrowRate = KaminoObligation.getCumulativeBorrowRate(borrow);
if (estimatedCumulativeBorrowRate.gt(currentCumulativeBorrowRate)) {
return estimatedCumulativeBorrowRate.div(currentCumulativeBorrowRate);
}
return new Decimal(0);
};
private calculatePositions(
market: KaminoMarket,
obligationDeposits: ObligationCollateral[],
obligationBorrows: ObligationLiquidity[],
elevationGroup: number,
collateralExchangeRates: Map<PublicKey, Decimal>,
cumulativeBorrowRates: Map<PublicKey, Decimal> | null
): {
borrows: Map<PublicKey, Position>;
deposits: Map<PublicKey, Position>;
refreshedStats: ObligationStats;
} {
const getOraclePx = (reserve: KaminoReserve) => reserve.getOracleMarketPrice();
const depositStatsOraclePrice = KaminoObligation.calculateObligationDeposits(
market,
obligationDeposits,
collateralExchangeRates,
elevationGroup,
getOraclePx
);
const borrowStatsOraclePrice = KaminoObligation.calculateObligationBorrows(
market,
obligationBorrows,
cumulativeBorrowRates,
elevationGroup,
getOraclePx
);
const netAccountValueScopeRefreshed = depositStatsOraclePrice.userTotalDeposit.minus(
borrowStatsOraclePrice.userTotalBorrow
);
// TODO: Fix this?
const potentialElevationGroupUpdate = 0;
return {
deposits: depositStatsOraclePrice.deposits,
borrows: borrowStatsOraclePrice.borrows,
refreshedStats: {
borrowLimit: depositStatsOraclePrice.borrowLimit,
borrowLiquidationLimit: depositStatsOraclePrice.borrowLiquidationLimit,
userTotalBorrow: borrowStatsOraclePrice.userTotalBorrow,
userTotalBorrowBorrowFactorAdjusted: borrowStatsOraclePrice.userTotalBorrowBorrowFactorAdjusted,
userTotalDeposit: depositStatsOraclePrice.userTotalDeposit,
userTotalCollateralDeposit: depositStatsOraclePrice.userTotalCollateralDeposit,
liquidationLtv: depositStatsOraclePrice.liquidationLtv,
borrowUtilization: borrowStatsOraclePrice.userTotalBorrowBorrowFactorAdjusted.dividedBy(
depositStatsOraclePrice.borrowLimit
),
netAccountValue: netAccountValueScopeRefreshed,
leverage: depositStatsOraclePrice.userTotalDeposit.dividedBy(netAccountValueScopeRefreshed),
loanToValue: borrowStatsOraclePrice.userTotalBorrowBorrowFactorAdjusted.dividedBy(
depositStatsOraclePrice.userTotalDeposit
),
potentialElevationGroupUpdate,
},
};
}
public static calculateObligationDeposits(
market: KaminoMarket,
obligationDeposits: ObligationCollateral[],
collateralExchangeRates: Map<PublicKey, Decimal> | null,
elevationGroup: number,
getPx: (reserve: KaminoReserve) => Decimal
): DepositStats {
let userTotalDeposit = new Decimal(0);
let userTotalCollateralDeposit = new Decimal(0);
let borrowLimit = new Decimal(0);
let borrowLiquidationLimit = new Decimal(0);
const deposits = new PubkeyHashMap<PublicKey, Position>();
for (let i = 0; i < obligationDeposits.length; i++) {
if (!isNotNullPubkey(obligationDeposits[i].depositReserve)) {
continue;
}
const deposit = obligationDeposits[i];
const reserve = market.getReserveByAddress(deposit.depositReserve);
if (!reserve) {
throw new Error(
`Obligation contains a deposit belonging to reserve: ${deposit.depositReserve} but the reserve was not found on the market. Deposit amount: ${deposit.depositedAmount}`
);
}
const { maxLtv, liquidationLtv } = KaminoObligation.getLtvForReserve(market, reserve, elevationGroup);
let exchangeRate: Decimal;
if (collateralExchangeRates !== null) {
exchangeRate = collateralExchangeRates.get(reserve.address)!;
} else {
exchangeRate = reserve.getCollateralExchangeRate();
}
const supplyAmount = new Decimal(deposit.depositedAmount.toString()).div(exchangeRate);
const depositValueUsd = supplyAmount.mul(getPx(reserve)).div(reserve.getMintFactor());
userTotalDeposit = userTotalDeposit.add(depositValueUsd);
if (!maxLtv.eq('0')) {
userTotalCollateralDeposit = userTotalCollateralDeposit.add(depositValueUsd);
}
borrowLimit = borrowLimit.add(depositValueUsd.mul(maxLtv));
borrowLiquidationLimit = borrowLiquidationLimit.add(depositValueUsd.mul(liquidationLtv));
const position: Position = {
reserveAddress: reserve.address,
mintAddress: reserve.getLiquidityMint(),
amount: supplyAmount,
marketValueRefreshed: depositValueUsd,
};
deposits.set(reserve.address, position);
}
return {
deposits,
userTotalDeposit,
userTotalCollateralDeposit,
borrowLimit,
liquidationLtv: valueOrZero(borrowLiquidationLimit.div(userTotalDeposit)),
borrowLiquidationLimit,
};
}
public static calculateObligationBorrows(
market: KaminoMarket,
obligationBorrows: ObligationLiquidity[],
cumulativeBorrowRates: Map<PublicKey, Decimal> | null,
elevationGroup: number,
getPx: (reserve: KaminoReserve) => Decimal
): BorrowStats {
let userTotalBorrow = new Decimal(0);
let userTotalBorrowBorrowFactorAdjusted = new Decimal(0);
let positions = 0;
const borrows = new PubkeyHashMap<PublicKey, Position>();
for (let i = 0; i < obligationBorrows.length; i++) {
if (!isNotNullPubkey(obligationBorrows[i].borrowReserve)) {
continue;
}
const borrow = obligationBorrows[i];
const reserve = market.getReserveByAddress(borrow.borrowReserve);
if (!reserve) {
throw new Error(
`Obligation contains a borrow belonging to reserve: ${
borrow.borrowReserve
} but the reserve was not found on the market. Borrow amount: ${KaminoObligation.getBorrowAmount(borrow)}`
);
}
const obligationCumulativeBorrowRate = KaminoObligation.getCumulativeBorrowRate(borrow);
let cumulativeBorrowRate;
if (cumulativeBorrowRates !== null) {
cumulativeBorrowRate = cumulativeBorrowRates.get(reserve.address)!;
} else {
cumulativeBorrowRate = reserve.getCumulativeBorrowRate();
}
const borrowAmount = KaminoObligation.getBorrowAmount(borrow)
.mul(cumulativeBorrowRate)
.dividedBy(obligationCumulativeBorrowRate);
const borrowValueUsd = borrowAmount.mul(getPx(reserve)).dividedBy(reserve.getMintFactor());
const borrowFactor = KaminoObligation.getBorrowFactorForReserve(reserve, elevationGroup);
const borrowValueBorrowFactorAdjustedUsd = borrowValueUsd.mul(borrowFactor);
if (!borrowAmount.eq(new Decimal('0'))) {
positions += 1;
}
userTotalBorrow = userTotalBorrow.plus(borrowValueUsd);
userTotalBorrowBorrowFactorAdjusted = userTotalBorrowBorrowFactorAdjusted.plus(
borrowValueBorrowFactorAdjustedUsd
);
const position: Position = {
reserveAddress: reserve.address,
mintAddress: reserve.getLiquidityMint(),
amount: borrowAmount,
marketValueRefreshed: borrowValueUsd,
};
borrows.set(reserve.address, position);
}
return {
borrows,
userTotalBorrow,
userTotalBorrowBorrowFactorAdjusted,
positions,
};
}
getMaxLoanLtvGivenElevationGroup(market: KaminoMarket, elevationGroup: number, slot: number): Decimal {
const getOraclePx = (reserve: KaminoReserve) => reserve.getOracleMarketPrice();
const { collateralExchangeRates } = KaminoObligation.getRatesForObligation(market, this.state, slot);
const { borrowLimit, userTotalDeposit } = KaminoObligation.calculateObligationDeposits(
market,
this.state.deposits,
collateralExchangeRates,
elevationGroup,
getOraclePx
);
if (borrowLimit.eq(0) || userTotalDeposit.eq(0)) {
return new Decimal(0);
}
return borrowLimit.div(userTotalDeposit);
}
/*
How much of a given token can a user borrow extra given an elevation group,
regardless of caps and liquidity or assuming infinite liquidity and infinite caps,
until it hits max LTV.
This is purely a function about the borrow power of an obligation,
not a reserve-specific, caps-specific, liquidity-specific function.
* @param market - The KaminoMarket instance.
* @param liquidityMint - The liquidity mint PublicKey.
* @param slot - The slot number.
* @param elevationGroup - The elevation group number (default: this.state.elevationGroup).
* @returns The borrow power as a Decimal.
* @throws Error if the reserve is not found.
*/
getBorrowPower(
market: KaminoMarket,
liquidityMint: PublicKey,
slot: number,
elevationGroup: number = this.state.elevationGroup
): Decimal {
const reserve = market.getReserveByMint(liquidityMint);
if (!reserve) {
throw new Error('Reserve not found');
}
const elevationGroupActivated =
reserve.state.config.elevationGroups.includes(elevationGroup) && elevationGroup !== 0;
const borrowFactor = KaminoObligation.getBorrowFactorForReserve(reserve, elevationGroup);
const getOraclePx = (reserve: KaminoReserve) => reserve.getOracleMarketPrice();
const { collateralExchangeRates, cumulativeBorrowRates } = KaminoObligation.getRatesForObligation(
market,
this.state,
slot
);
const { borrowLimit } = KaminoObligation.calculateObligationDeposits(
market,
this.state.deposits,
collateralExchangeRates,
elevationGroup,
getOraclePx
);
const { userTotalBorrowBorrowFactorAdjusted } = KaminoObligation.calculateObligationBorrows(
market,
this.state.borrows,
cumulativeBorrowRates,
elevationGroup,
getOraclePx
);
const maxObligationBorrowPower = borrowLimit // adjusted available amount
.minus(userTotalBorrowBorrowFactorAdjusted)
.div(borrowFactor)
.div(reserve.getOracleMarketPrice())
.mul(reserve.getMintFactor());
// If it has any collateral outside emode, then return 0
for (const [_, value] of this.deposits.entries()) {
const depositReserve = market.getReserveByAddress(value.reserveAddress);
if (!depositReserve) {
throw new Error('Reserve not found');
}
if (depositReserve.state.config.disableUsageAsCollOutsideEmode && !elevationGroupActivated) {
return new Decimal(0);
}
}
// This is not amazing because it assumes max borrow, which is not true
let originationFeeRate = reserve.getBorrowFee();
// Inclusive fee rate
originationFeeRate = originationFeeRate.div(originationFeeRate.add(new Decimal(1)));
const borrowFee = maxObligationBorrowPower.mul(originationFeeRate);
const maxBorrowAmount = maxObligationBorrowPower.sub(borrowFee);
return Decimal.max(new Decimal(0), maxBorrowAmount);
}
/*
How much of a given token can a user borrow extra given an elevation group,
and a specific reserve, until it hits max LTV and given available liquidity and caps.
* @param market - The KaminoMarket instance.
* @param liquidityMint - The liquidity mint PublicKey.
* @param slot - The slot number.
* @param elevationGroup - The elevation group number (default: this.state.elevationGroup).
* @returns The maximum borrow amount as a Decimal.
* @throws Error if the reserve is not found.
*/
getMaxBorrowAmountV2(
market: KaminoMarket,
liquidityMint: PublicKey,
slot: number,
elevationGroup: number = this.state.elevationGroup
): Decimal {
const reserve = market.getReserveByMint(liquidityMint);
if (!reserve) {
throw new Error('Reserve not found');
}
const liquidityAvailable = reserve.getLiquidityAvailableForDebtReserveGivenCaps(
market,
[elevationGroup],
Array.from(this.deposits.keys())
)[0];
const maxBorrowAmount = this.getBorrowPower(market, liquidityMint, slot, elevationGroup);
if (elevationGroup === this.state.elevationGroup) {
return Decimal.min(maxBorrowAmount, liquidityAvailable);
} else {
// TODO: this is wrong, most liquidity caps are global, we should add up only the ones that are specific to this mode
const { amount: debtThisReserve } = this.borrows.get(reserve.address) || { amount: new Decimal(0) };
const liquidityAvailablePostMigration = Decimal.max(0, liquidityAvailable.minus(debtThisReserve));
return Decimal.min(maxBorrowAmount, liquidityAvailablePostMigration);
}
}
/*
Returns true if the loan is eligible for the elevation group, including for the default one.
* @param market - The KaminoMarket object representing the market.
* @param slot - The slot number of the loan.
* @param elevationGroup - The elevation group number.
* @returns A boolean indicating whether the loan is eligible for elevation.
*/
isLoanEligibleForElevationGroup(market: KaminoMarket, slot: number, elevationGroup: number): boolean {
// - isLoanEligibleForEmode(obligation, emode: 0 | number): <boolean, ErrorMessage>
// - essentially checks if a loan can be migrated or not
// - [x] due to collateral / debt reserves combination
// - [x] due to LTV, etc
const reserveDeposits: PublicKey[] = Array.from(this.deposits.keys());
const reserveBorrows: PublicKey[] = Array.from(this.borrows.keys());
if (reserveBorrows.length > 1) {
return false;
}
if (elevationGroup > 0) {
// Elevation group 0 doesn't need to do reserve checks, as all are included by default
const allElevationGroups = market.getMarketElevationGroupDescriptions();
const elevationGroupDescription = allElevationGroups[elevationGroup - 1];
// Has to be a subset
const allCollsIncluded = reserveDeposits.every((reserve) =>
elevationGroupDescription.collateralReserves.contains(reserve)
);
const allDebtsIncluded =
reserveBorrows.length === 0 ||
(reserveBorrows.length === 1 && elevationGroupDescription.debtReserve.equals(reserveBorrows[0]));
if (!allCollsIncluded || !allDebtsIncluded) {
return false;
}
}
// Check if the loan can be migrated based on LTV
const getOraclePx = (reserve: KaminoReserve) => reserve.getOracleMarketPrice();
const { collateralExchangeRates } = KaminoObligation.getRatesForObligation(market, this.state, slot);
const { borrowLimit } = KaminoObligation.calculateObligationDeposits(
market,
this.state.deposits,
collateralExchangeRates,
elevationGroup,
getOraclePx
);
const isEligibleBasedOnLtv = this.refreshedStats.userTotalBorrowBorrowFactorAdjusted.lte(borrowLimit);
return isEligibleBasedOnLtv;
}
/*
Returns all elevation groups for a given obligation, except the default one
* @param market - The KaminoMarket instance.
* @returns An array of ElevationGroupDescription objects representing the elevation groups for the obligation.
*/
getElevationGroupsForObligation(market: KaminoMarket): ElevationGroupDescription[] {
if (this.borrows.size > 1) {
return [];
}
const collReserves = Array.from(this.deposits.keys());
if (this.borrows.size === 0) {
return market.getElevationGroupsForReservesCombination(collReserves);
} else {
const debtReserve = Array.from(this.borrows.keys())[0];
return market.getElevationGroupsForReservesCombination(collReserves, debtReserve);
}
}
/* Deprecated function, also broken */
getMaxBorrowAmount(
market: KaminoMarket,
liquidityMint: PublicKey,
slot: number,
requestElevationGroup: boolean
): Decimal {
const reserve = market.getReserveByMint(liquidityMint);
if (!reserve) {
throw new Error('Reserve not found');
}
const groups = market.state.elevationGroups;
const emodeGroupsDebtReserve = reserve.state.config.elevationGroups;
let commonElevationGroups = [...emodeGroupsDebtReserve].filter(
(item) => item !== 0 && groups[item - 1].debtReserve.equals(reserve.address)
);
for (const [_, value] of this.deposits.entries()) {
const depositReserve = market.getReserveByAddress(value.reserveAddress);
if (!depositReserve) {
throw new Error('Reserve not found');
}
const depositReserveEmodeGroups = depositReserve.state.config.elevationGroups;
commonElevationGroups = commonElevationGroups.filter((item) => depositReserveEmodeGroups.includes(item));
}
let elevationGroup = this.state.elevationGroup;
if (commonElevationGroups.length != 0) {
const eModeGroupWithMaxLtvAndDebtReserve = commonElevationGroups.reduce((prev, curr) => {
const prevGroup = groups.find((group) => group.id === prev);
const currGroup = groups.find((group) => group.id === curr);
return prevGroup!.ltvPct > currGroup!.ltvPct ? prev : curr;
});
if (requestElevationGroup) {
elevationGroup = eModeGroupWithMaxLtvAndDebtReserve;
}
}
const elevationGroupActivated =
reserve.state.config.elevationGroups.includes(elevationGroup) && elevationGroup !== 0;
const borrowFactor = KaminoObligation.getBorrowFactorForReserve(reserve, elevationGroup);
const maxObligationBorrowPower = this.refreshedStats.borrowLimit // adjusted available amount
.minus(this.refreshedStats.userTotalBorrowBorrowFactorAdjusted)
.div(borrowFactor)
.div(reserve.getOracleMarketPrice())
.mul(reserve.getMintFactor());
const reserveAvailableAmount = reserve.getLiquidityAvailableAmount();
let reserveBorrowCapRemained = reserve.stats.reserveBorrowLimit.sub(reserve.getBorrowedAmount());
this.deposits.forEach((deposit) => {
const depositReserve = market.getReserveByAddress(deposit.reserveAddress);
if (!depositReserve) {
throw new Error('Reserve not found');
}
if (depositReserve.state.config.disableUsageAsCollOutsideEmode && !elevationGroupActivated) {
reserveBorrowCapRemained = new Decimal(0);
}
});
let maxBorrowAmount = Decimal.min(maxObligationBorrowPower, reserveAvailableAmount, reserveBorrowCapRemained);
const debtWithdrawalCap = reserve.getDebtWithdrawalCapCapacity().sub(reserve.getDebtWithdrawalCapCurrent(slot));
maxBorrowAmount = reserve.getDebtWithdrawalCapCapacity().gt(0)
? Decimal.min(maxBorrowAmount, debtWithdrawalCap)
: maxBorrowAmount;
let originationFeeRate = reserve.getBorrowFee();
// Inclusive fee rate
originationFeeRate = originationFeeRate.div(originationFeeRate.add(new Decimal(1)));
const borrowFee = maxBorrowAmount.mul(originationFeeRate);
maxBorrowAmount = maxBorrowAmount.sub(borrowFee);
const utilizationRatioLimit = reserve.state.config.utilizationLimitBlockBorrowingAbove / 100;
const currentUtilizationRatio = reserve.calculateUtilizationRatio();
if (utilizationRatioLimit > 0 && currentUtilizationRatio > utilizationRatioLimit) {
return new Decimal(0);
} else if (utilizationRatioLimit > 0 && currentUtilizationRatio < utilizationRatioLimit) {
const maxBorrowBasedOnUtilization = new Decimal(utilizationRatioLimit - currentUtilizationRatio).mul(
reserve.getTotalSupply()
);
maxBorrowAmount = Decimal.min(maxBorrowAmount, maxBorrowBasedOnUtilization);
}
let borrowLimitDependentOnElevationGroup = new Decimal(U64_MAX);
if (!elevationGroupActivated) {
borrowLimitDependentOnElevationGroup = reserve
.getBorrowLimitOutsideElevationGroup()
.sub(reserve.getBorrowedAmountOutsideElevationGroup());
} else {
let maxDebtTakenAgainstCollaterals = new Decimal(U64_MAX);
for (const [_, value] of this.deposits.entries()) {
const depositReserve = market.getReserveByAddress(value.reserveAddress);
if (!depositReserve) {
throw new Error('Reserve not found');
}
const maxDebtAllowedAgainstCollateral = depositReserve
.getBorrowLimitAgainstCollateralInElevationGroup(elevationGroup - 1)
.sub(depositReserve.getBorrowedAmountAgainstCollateralInElevationGroup(elevationGroup - 1));
maxDebtTakenAgainstCollaterals = Decimal.max(
new Decimal(0),
Decimal.min(maxDebtAllowedAgainstCollateral, maxDebtTakenAgainstCollaterals)
);
}
borrowLimitDependentOnElevationGroup = maxDebtTakenAgainstCollaterals;
}
maxBorrowAmount = Decimal.min(maxBorrowAmount, borrowLimitDependentOnElevationGroup);
return Decimal.max(new Decimal(0), maxBorrowAmount);
}
getMaxWithdrawAmount(market: KaminoMarket, tokenMint: PublicKey, slot: number): Decimal {
const depositReserve = market.getReserveByMint(tokenMint);
if (!depositReserve) {
throw new Error('Reserve not found');
}
const userDepositPosition = this.getDepositByReserve(depositReserve.address);
if (!userDepositPosition) {
throw new Error('Deposit reserve not found');
}
const userDepositPositionAmount = userDepositPosition.amount;
if (this.refreshedStats.userTotalBorrowBorrowFactorAdjusted.equals(new Decimal(0))) {
return new Decimal(userDepositPositionAmount);
}
const { maxLtv: reserveMaxLtv } = KaminoObligation.getLtvForReserve(
market,
depositReserve,
this.state.elevationGroup
);
// bf adjusted debt value > allowed_borrow_value
if (this.refreshedStats.userTotalBorrowBorrowFactorAdjusted.gte(this.refreshedStats.borrowLimit)) {
return new Decimal(0);
}
let maxWithdrawValue: Decimal;
if (reserveMaxLtv.eq(0)) {
maxWithdrawValue = userDepositPositionAmount;
} else {
// borrowLimit / userTotalDeposit = maxLtv
// maxWithdrawValue = userTotalDeposit - userTotalBorrow / maxLtv
maxWithdrawValue = this.refreshedStats.borrowLimit
.sub(this.refreshedStats.userTotalBorrowBorrowFactorAdjusted)
.div(reserveMaxLtv)
.mul(0.999); // remove 0.1% to prevent going over max ltv
}
const maxWithdrawAmount = maxWithdrawValue
.div(depositReserve.getOracleMarketPrice())
.mul(depositReserve.getMintFactor());
const reserveAvailableLiquidity = depositReserve.getLiquidityAvailableAmount();
const withdrawalCapRemained = depositReserve
.getDepositWithdrawalCapCapacity()
.sub(depositReserve.getDepositWithdrawalCapCurrent(slot));
return Decimal.max(
0,
Decimal.min(userDepositPositionAmount, maxWithdrawAmount, reserveAvailableLiquidity, withdrawalCapRemained)
);
}
/**
*
* @returns Total borrowed amount for the specified obligation liquidity/borrow asset
*/
static getBorrowAmount(borrow: ObligationLiquidity): Decimal {
return new Fraction(borrow.borrowedAmountSf).toDecimal();
}
/**
*
* @returns Cumulative borrow rate for the specified obligation liquidity/borrow asset
*/
static getCumulativeBorrowRate(borrow: ObligationLiquidity): Decimal {
let accSf = new BN(0);
for (const value of borrow.cumulativeBorrowRateBsf.value.reverse()) {
accSf = accSf.add(value);
accSf.shrn(64);
}
return new Fraction(accSf).toDecimal();
}
public static getRatesForObligation(
kaminoMarket: KaminoMarket,
obligation: Obligation,
slot: number,
additionalReserves: PublicKey[] = []
): {
collateralExchangeRates: Map<PublicKey, Decimal>;
cumulativeBorrowRates: Map<PublicKey, Decimal>;
} {
const collateralExchangeRates = KaminoObligation.getCollateralExchangeRatesForObligation(
kaminoMarket,
obligation,
slot,
additionalReserves
);
const cumulativeBorrowRates = KaminoObligation.getCumulativeBorrowRatesForObligation(
kaminoMarket,
obligation,
slot,
additionalReserves
);
return {
collateralExchangeRates,
cumulativeBorrowRates,
};
}
public static addRatesForObligation(
kaminoMarket: KaminoMarket,
obligation: Obligation,
collateralExchangeRates: Map<PublicKey, Decimal>,
cumulativeBorrowRates: Map<PublicKey, Decimal>,
slot: number
): void {
KaminoObligation.addCollateralExchangeRatesForObligation(kaminoMarket, collateralExchangeRates, obligation, slot);
KaminoObligation.addCumulativeBorrowRatesForObligation(kaminoMarket, cumulativeBorrowRates, obligation, slot);
}
static getCollateralExchangeRatesForObligation(
kaminoMarket: KaminoMarket,
obligation: Obligation,
slot: number,
additionalReserves: PublicKey[]
): Map<PublicKey, Decimal> {
const collateralExchangeRates = new PubkeyHashMap<PublicKey, Decimal>();
// Create a set of all reserves coming from deposit plus additional reserves
const allReserves = new Set<PublicKey>();
for (let i = 0; i < obligation.deposits.length; i++) {
const deposit = obligation.deposits[i];
if (isNotNullPubkey(deposit.depositReserve)) {
allReserves.add(deposit.depositReserve);
}
}
for (let i = 0; i < additionalReserves.length; i++) {
if (isNotNullPubkey(additionalReserves[i])) {
allReserves.add(additionalReserves[i]);
}
}
// Run through all reserves and get the exchange rate
for (const reserve of allReserves) {
const reserveInstance = kaminoMarket.getReserveByAddress(reserve)!;
const collateralExchangeRate = reserveInstance.getEstimatedCollateralExchangeRate(
slot,
kaminoMarket.state.referralFeeBps
);
collateralExchangeRates.set(reserve, collateralExchangeRate);
}
return collateralExchangeRates;
}
static addCollateralExchangeRatesForObligation(
kaminoMarket: KaminoMarket,
collateralExchangeRates: Map<PublicKey, Decimal>,
obligation: Obligation,
slot: number
) {
for (let i = 0; i < obligation.deposits.length; i++) {
const deposit = obligation.deposits[i];
if (isNotNullPubkey(deposit.depositReserve) && !collateralExchangeRates.has(deposit.depositReserve)) {
const reserve = kaminoMarket.getReserveByAddress(deposit.depositReserve)!;
const collateralExchangeRate = reserve.getEstimatedCollateralExchangeRate(
slot,
kaminoMarket.state.referralFeeBps
);
collateralExchangeRates.set(reserve.address, collateralExchangeRate);
}
}
}
static getCumulativeBorrowRatesForObligation(
kaminoMarket: KaminoMarket,
obligation: Obligation,
slot: number,
additionalReserves: PublicKey[] = []
): Map<PublicKey, Decimal> {
const allReserves = new Set<PublicKey>();
for (let i = 0; i < obligation.borrows.length; i++) {
const borrow = obligation.borrows[i];
if (isNotNullPubkey(borrow.borrowReserve)) {
allReserves.add(borrow.borrowReserve);
}
}
// Add additional reserves
for (let i = 0; i < additionalReserves.length; i++) {
if (isNotNullPubkey(additionalReserves[i])) {
allReserves.add(additionalReserves[i]);
}
}
const cumulativeBorrowRates = new PubkeyHashMap<PublicKey, Decimal>();
// Run through all reserves and get the cumulative borrow rate
for (const reserve of allReserves) {
const reserveInstance = kaminoMarket.getReserveByAddress(reserve)!;
const cumulativeBorrowRate = reserveInstance.getEstimatedCumulativeBorrowRate(
slot,
kaminoMarket.state.referralFeeBps
);
cumulat