@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
906 lines (903 loc) • 56.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.KaminoObligation = void 0;
exports.isKaminoObligation = isKaminoObligation;
/* eslint-disable max-classes-per-file */
const web3_js_1 = require("@solana/web3.js");
const decimal_js_1 = __importDefault(require("decimal.js"));
const accounts_1 = require("../idl_codegen/accounts");
const bn_js_1 = __importDefault(require("bn.js"));
const fraction_1 = require("./fraction");
const types_1 = require("../idl_codegen/types");
const utils_1 = require("./utils");
const utils_2 = require("../utils");
const obligationOrder_1 = require("./obligationOrder");
class KaminoObligation {
obligationAddress;
state;
/**
* Deposits stored in a map of reserve address to position
*/
deposits;
/**
* Borrows stored in a map of reserve address to position
*/
borrows;
refreshedStats;
obligationTag;
/**
* 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, obligationAddress, obligation, collateralExchangeRates, cumulativeBorrowRates) {
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();
}
getObligationId(market, mintAddress1 = web3_js_1.PublicKey.default, mintAddress2 = web3_js_1.PublicKey.default) {
if (!this.state.lendingMarket.equals(new web3_js_1.PublicKey(market.address))) {
throw new Error('Obligation does not belong to this market');
}
let obligationId;
const type = (0, utils_2.getObligationType)(market, this.obligationTag, mintAddress1, mintAddress2);
const baseArgs = type.toArgs();
for (let i = 0; i < utils_2.TOTAL_NUMBER_OF_IDS_TO_CHECK; i++) {
const pda = (0, utils_2.getObligationPdaWithArgs)(new web3_js_1.PublicKey(market.address), this.state.owner, {
...baseArgs,
id: i,
}, market.programId);
if (pda.equals(this.obligationAddress)) {
obligationId = i;
break;
}
}
if (obligationId === undefined) {
throw new Error(`obligation id not found for obligation ${this.obligationAddress.toString()}`);
}
return obligationId;
}
static async load(kaminoMarket, obligationAddress) {
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 = accounts_1.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, obligationAddresses, slot) {
let currentSlot = slot;
let obligations;
if (!currentSlot) {
[currentSlot, obligations] = await Promise.all([
kaminoMarket.getConnection().getSlot(),
accounts_1.Obligation.fetchMultiple(kaminoMarket.getConnection(), obligationAddresses, kaminoMarket.programId),
]);
}
else {
obligations = await accounts_1.Obligation.fetchMultiple(kaminoMarket.getConnection(), obligationAddresses, kaminoMarket.programId);
}
const cumulativeBorrowRates = new utils_2.PubkeyHashMap();
const collateralExchangeRates = new utils_2.PubkeyHashMap();
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() {
return [...this.borrows.values()];
}
/**
* @returns the obligation borrows as a list
*/
getDeposits() {
return [...this.deposits.values()];
}
/**
* Returns obligation orders (including the null ones, i.e. non-active positions in the orders' array).
*/
getOrders() {
return this.state.orders.map((order) => obligationOrder_1.KaminoObligationOrder.fromState(order));
}
/**
* Returns active obligation orders (i.e. ones that *may* have their condition met).
*/
getActiveOrders() {
return this.getOrders().filter((order) => order !== null);
}
/**
* @returns the total deposited value of the obligation (sum of all deposits)
*/
getDepositedValue() {
return new fraction_1.Fraction(this.state.depositedValueSf).toDecimal();
}
/**
* @returns the total borrowed value of the obligation (sum of all borrows -- no borrow factor)
*/
getBorrowedMarketValue() {
return new fraction_1.Fraction(this.state.borrowedAssetsMarketValueSf).toDecimal();
}
/**
* @returns the total borrowed value of the obligation (sum of all borrows -- with borrow factor weighting)
*/
getBorrowedMarketValueBFAdjusted() {
return new fraction_1.Fraction(this.state.borrowFactorAdjustedDebtValueSf).toDecimal();
}
/**
* @returns total borrow power of the obligation, relative to max LTV of each asset's reserve
*/
getMaxAllowedBorrowValue() {
return new fraction_1.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() {
return new fraction_1.Fraction(this.state.unhealthyBorrowValueSf).toDecimal();
}
/**
*
* @returns Market value of the deposit in the specified obligation collateral/deposit asset (USD)
*/
getDepositMarketValue(deposit) {
return new fraction_1.Fraction(deposit.marketValueSf).toDecimal();
}
getBorrowByReserve(reserve) {
return this.borrows.get(reserve);
}
getDepositByReserve(reserve) {
return this.deposits.get(reserve);
}
getBorrowByMint(mint) {
for (const value of this.borrows.values()) {
if (value.mintAddress.equals(mint)) {
return value;
}
}
return undefined;
}
getBorrowAmountByReserve(reserve) {
const amountLamports = this.getBorrowByMint(reserve.getLiquidityMint())?.amount ?? new decimal_js_1.default(0);
return amountLamports.div(reserve.getMintFactor());
}
getDepositByMint(mint) {
for (const value of this.deposits.values()) {
if (value.mintAddress.equals(mint)) {
return value;
}
}
return undefined;
}
getDepositAmountByReserve(reserve) {
const amountLamports = this.getDepositByMint(reserve.getLiquidityMint())?.amount ?? new decimal_js_1.default(0);
return amountLamports.div(reserve.getMintFactor());
}
/**
*
* @returns Market value of the borrow in the specified obligation liquidity/borrow asset (USD) (no borrow factor weighting)
*/
getBorrowMarketValue(borrow) {
return new fraction_1.Fraction(borrow.marketValueSf).toDecimal();
}
/**
*
* @returns Market value of the borrow in the specified obligation liquidity/borrow asset (USD) (with borrow factor weighting)
*/
getBorrowMarketValueBFAdjusted(borrow) {
return new fraction_1.Fraction(borrow.borrowFactorAdjustedMarketValueSf).toDecimal();
}
/**
* Calculates the current ratio of borrowed value to deposited value (taking *all* deposits into account).
*
* Please note that the denominator here is different from the one found in `refreshedStats`:
* - the {@link ObligationStats#loanToValue} contains a value appropriate for display on the UI (i.e. taking into
* account *only* the deposits having `reserve.loanToValue > 0`).
* - the computation below follows the logic used by the KLend smart contract, and is appropriate e.g. for evaluating
* LTV-based obligation orders.
*/
loanToValue() {
if (this.refreshedStats.userTotalDeposit.eq(0)) {
return new decimal_js_1.default(0);
}
return this.refreshedStats.userTotalBorrowBorrowFactorAdjusted.div(this.refreshedStats.userTotalDeposit);
}
/**
* Calculates the ratio of borrowed value to deposited value (taking *all* deposits into account) at which the
* obligation is subject to liquidation.
*
* Please note that the denominator here is different from the one found in `refreshedStats`:
* - the {@link ObligationStats#liquidationLtv} contains a value appropriate for display on the UI (i.e. taking into
* account *only* the deposits having `reserve.liquidationLtv > 0`).
* - the computation below follows the logic used by the KLend smart contract, and is appropriate e.g. for evaluating
* LTV-based obligation orders.
*/
liquidationLtv() {
if (this.refreshedStats.userTotalDeposit.eq(0)) {
return new decimal_js_1.default(0);
}
return this.refreshedStats.borrowLiquidationLimit.div(this.refreshedStats.userTotalDeposit);
}
/**
* Calculate the current ratio of borrowed value to deposited value, disregarding the borrow factor.
*/
noBfLoanToValue() {
if (this.refreshedStats.userTotalDeposit.eq(0)) {
return new decimal_js_1.default(0);
}
return this.refreshedStats.userTotalBorrow.div(this.refreshedStats.userTotalDeposit);
}
/**
* @returns the total number of positions (deposits + borrows)
*/
getNumberOfPositions() {
return this.deposits.size + this.borrows.size;
}
getNetAccountValue() {
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
*/
getLtvForReserve(market, reserveAddress) {
return KaminoObligation.getLtvForReserve(market, market.getExistingReserveByAddress(reserveAddress), this.state.elevationGroup);
}
/**
* @returns the potential elevation groups the obligation qualifies for
*/
getElevationGroups(kaminoMarket) {
const reserves = new utils_2.PubkeyHashMap();
for (const deposit of this.state.deposits.values()) {
if ((0, utils_2.isNotNullPubkey)(deposit.depositReserve) && !reserves.has(deposit.depositReserve)) {
reserves.set(deposit.depositReserve, kaminoMarket.getReserveByAddress(deposit.depositReserve));
}
}
for (const borrow of this.state.borrows.values()) {
if ((0, utils_2.isNotNullPubkey)(borrow.borrowReserve) && !reserves.has(borrow.borrowReserve)) {
reserves.set(borrow.borrowReserve, kaminoMarket.getReserveByAddress(borrow.borrowReserve));
}
}
return KaminoObligation.getElevationGroupsForReserves([...reserves.values()]);
}
static getElevationGroupsForReserves(reserves) {
const elevationGroupsCounts = new Map();
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();
for (const [group, count] of elevationGroupsCounts.entries()) {
if (count === reserves.length) {
activeElevationGroups.push(group);
}
}
return activeElevationGroups;
}
simulateDepositChange(obligationDeposits, changeInLamports, changeReserve, collateralExchangeRates) {
const newDeposits = [];
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 = { ...obligationDeposits[i] };
const exchangeRate = collateralExchangeRates.get(changeReserve);
const changeInCollateral = new decimal_js_1.default(changeInLamports).mul(exchangeRate).toFixed(0);
const updatedDeposit = new decimal_js_1.default(obligationDeposits[i].depositedAmount.toNumber()).add(changeInCollateral);
coll.depositedAmount = new bn_js_1.default((0, utils_1.positiveOrZero)(updatedDeposit).toString());
newDeposits.push(new types_1.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(web3_js_1.PublicKey.default));
if (firstBorrowIndexAvailable === -1) {
throw new Error('No available borrows to modify');
}
const coll = { ...obligationDeposits[firstBorrowIndexAvailable] };
const exchangeRate = collateralExchangeRates.get(changeReserve);
const changeInCollateral = new decimal_js_1.default(changeInLamports).mul(exchangeRate).toFixed(0);
coll.depositedAmount = new bn_js_1.default((0, utils_1.positiveOrZero)(new decimal_js_1.default(changeInCollateral)).toString());
coll.depositReserve = changeReserve;
newDeposits[firstBorrowIndexAvailable] = new types_1.ObligationCollateral(coll);
}
return newDeposits;
}
simulateBorrowChange(obligationBorrows, changeInLamports, changeReserve, cumulativeBorrowRate) {
const newBorrows = [];
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 = { ...obligationBorrows[borrowIndex] };
const newBorrowedAmount = new fraction_1.Fraction(borrow.borrowedAmountSf).toDecimal().add(changeInLamports);
const newBorrowedAmountSf = fraction_1.Fraction.fromDecimal((0, utils_1.positiveOrZero)(newBorrowedAmount)).getValue();
borrow.borrowedAmountSf = newBorrowedAmountSf;
newBorrows.push(new types_1.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(web3_js_1.PublicKey.default));
if (firstBorrowIndexAvailable === -1) {
throw new Error('No available borrows to modify');
}
const borrow = { ...obligationBorrows[firstBorrowIndexAvailable] };
borrow.borrowedAmountSf = fraction_1.Fraction.fromDecimal(new decimal_js_1.default(changeInLamports)).getValue();
borrow.borrowReserve = changeReserve;
borrow.cumulativeBorrowRateBsf = {
padding: [],
value: [fraction_1.Fraction.fromDecimal(cumulativeBorrowRate).getValue(), new bn_js_1.default(0), new bn_js_1.default(0), new bn_js_1.default(0)],
};
newBorrows[firstBorrowIndexAvailable] = new types_1.ObligationLiquidity(borrow);
}
return newBorrows;
}
/**
* Calculate the newly modified stats of the obligation
*/
// TODO: Shall we set up position limits?
getSimulatedObligationStats(params) {
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 = new utils_2.PubkeyHashMap([...this.deposits.entries()]);
let newBorrows = new utils_2.PubkeyHashMap([...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;
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);
newStats = refreshedStats;
newDeposits = deposits;
newBorrows = borrows;
newStats.netAccountValue = newStats.userTotalDeposit.minus(newStats.userTotalBorrow);
newStats.loanToValue = (0, utils_1.valueOrZero)(newStats.userTotalBorrowBorrowFactorAdjusted.dividedBy(newStats.userTotalCollateralDeposit));
newStats.leverage = (0, utils_1.valueOrZero)(newStats.userTotalDeposit.dividedBy(newStats.netAccountValue));
return {
stats: newStats,
deposits: newDeposits,
borrows: newBorrows,
};
}
/**
* Calculates the stats of the obligation after a hypothetical collateral swap.
*/
getPostSwapCollObligationStats(params) {
const { withdrawAmountLamports, withdrawReserveAddress, depositAmountLamports, depositReserveAddress, newElevationGroup, market, slot, } = params;
const additionalReserves = [withdrawReserveAddress, depositReserveAddress].filter((reserveAddress) => !market.isReserveInObligation(this, reserveAddress));
const { collateralExchangeRates } = KaminoObligation.getRatesForObligation(market, this.state, slot, additionalReserves);
let newObligationDeposits = this.state.deposits;
newObligationDeposits = this.simulateDepositChange(newObligationDeposits, withdrawAmountLamports.neg().toNumber(), withdrawReserveAddress, collateralExchangeRates);
newObligationDeposits = this.simulateDepositChange(newObligationDeposits, depositAmountLamports.toNumber(), depositReserveAddress, collateralExchangeRates);
const { refreshedStats } = this.calculatePositions(market, newObligationDeposits, this.state.borrows, newElevationGroup, collateralExchangeRates, null);
const newStats = refreshedStats;
newStats.netAccountValue = newStats.userTotalDeposit.minus(newStats.userTotalBorrow);
newStats.loanToValue = (0, utils_1.valueOrZero)(newStats.userTotalBorrowBorrowFactorAdjusted.dividedBy(newStats.userTotalCollateralDeposit));
newStats.leverage = (0, utils_1.valueOrZero)(newStats.userTotalDeposit.dividedBy(newStats.netAccountValue));
return newStats;
}
estimateObligationInterestRate = (market, reserve, borrow, currentSlot) => {
const newCumulativeBorrowRate = reserve.getEstimatedCumulativeBorrowRate(currentSlot, market.state.referralFeeBps);
const formerCumulativeBorrowRate = KaminoObligation.getCumulativeBorrowRate(borrow);
if (newCumulativeBorrowRate.gt(formerCumulativeBorrowRate)) {
return newCumulativeBorrowRate.div(formerCumulativeBorrowRate);
}
return new decimal_js_1.default(0);
};
static getOraclePx = (reserve) => {
return reserve.getOracleMarketPrice();
};
calculatePositions(market, obligationDeposits, obligationBorrows, elevationGroup, collateralExchangeRates, cumulativeBorrowRates, getOraclePx = KaminoObligation.getOraclePx) {
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,
userTotalLiquidatableDeposit: depositStatsOraclePrice.userTotalLiquidatableDeposit,
liquidationLtv: depositStatsOraclePrice.liquidationLtv,
borrowUtilization: borrowStatsOraclePrice.userTotalBorrowBorrowFactorAdjusted.dividedBy(depositStatsOraclePrice.borrowLimit),
netAccountValue: netAccountValueScopeRefreshed,
leverage: depositStatsOraclePrice.userTotalDeposit.dividedBy(netAccountValueScopeRefreshed),
loanToValue: borrowStatsOraclePrice.userTotalBorrowBorrowFactorAdjusted.dividedBy(depositStatsOraclePrice.userTotalCollateralDeposit),
potentialElevationGroupUpdate,
},
};
}
static calculateObligationDeposits(market, obligationDeposits, collateralExchangeRates, elevationGroup, getPx) {
let userTotalDeposit = new decimal_js_1.default(0);
let userTotalCollateralDeposit = new decimal_js_1.default(0);
let userTotalLiquidatableDeposit = new decimal_js_1.default(0);
let borrowLimit = new decimal_js_1.default(0);
let borrowLiquidationLimit = new decimal_js_1.default(0);
const deposits = new utils_2.PubkeyHashMap();
for (let i = 0; i < obligationDeposits.length; i++) {
if (!(0, utils_2.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;
if (collateralExchangeRates !== null) {
exchangeRate = collateralExchangeRates.get(reserve.address);
}
else {
exchangeRate = reserve.getCollateralExchangeRate();
}
const supplyAmount = new decimal_js_1.default(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);
}
if (!liquidationLtv.eq('0')) {
userTotalLiquidatableDeposit = userTotalLiquidatableDeposit.add(depositValueUsd);
}
borrowLimit = borrowLimit.add(depositValueUsd.mul(maxLtv));
borrowLiquidationLimit = borrowLiquidationLimit.add(depositValueUsd.mul(liquidationLtv));
const position = {
reserveAddress: reserve.address,
mintAddress: reserve.getLiquidityMint(),
mintFactor: reserve.getMintFactor(),
amount: supplyAmount,
marketValueRefreshed: depositValueUsd,
};
deposits.set(reserve.address, position);
}
return {
deposits,
userTotalDeposit,
userTotalCollateralDeposit,
userTotalLiquidatableDeposit,
borrowLimit,
liquidationLtv: (0, utils_1.valueOrZero)(borrowLiquidationLimit.div(userTotalLiquidatableDeposit)),
borrowLiquidationLimit,
};
}
static calculateObligationBorrows(market, obligationBorrows, cumulativeBorrowRates, elevationGroup, getPx) {
let userTotalBorrow = new decimal_js_1.default(0);
let userTotalBorrowBorrowFactorAdjusted = new decimal_js_1.default(0);
let positions = 0;
const borrows = new utils_2.PubkeyHashMap();
for (let i = 0; i < obligationBorrows.length; i++) {
if (!(0, utils_2.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_js_1.default('0'))) {
positions += 1;
}
userTotalBorrow = userTotalBorrow.plus(borrowValueUsd);
userTotalBorrowBorrowFactorAdjusted = userTotalBorrowBorrowFactorAdjusted.plus(borrowValueBorrowFactorAdjustedUsd);
const position = {
reserveAddress: reserve.address,
mintAddress: reserve.getLiquidityMint(),
mintFactor: reserve.getMintFactor(),
amount: borrowAmount,
marketValueRefreshed: borrowValueUsd,
};
borrows.set(reserve.address, position);
}
return {
borrows,
userTotalBorrow,
userTotalBorrowBorrowFactorAdjusted,
positions,
};
}
getMaxLoanLtvGivenElevationGroup(market, elevationGroup, slot) {
const getOraclePx = (reserve) => reserve.getOracleMarketPrice();
const { collateralExchangeRates } = KaminoObligation.getRatesForObligation(market, this.state, slot);
const { borrowLimit, userTotalCollateralDeposit } = KaminoObligation.calculateObligationDeposits(market, this.state.deposits, collateralExchangeRates, elevationGroup, getOraclePx);
if (borrowLimit.eq(0) || userTotalCollateralDeposit.eq(0)) {
return new decimal_js_1.default(0);
}
return borrowLimit.div(userTotalCollateralDeposit);
}
/*
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, liquidityMint, slot, elevationGroup = this.state.elevationGroup) {
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) => 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_js_1.default(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_js_1.default(1)));
const borrowFee = maxObligationBorrowPower.mul(originationFeeRate);
const maxBorrowAmount = maxObligationBorrowPower.sub(borrowFee);
return decimal_js_1.default.max(new decimal_js_1.default(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, liquidityMint, slot, elevationGroup = this.state.elevationGroup) {
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_js_1.default.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_js_1.default(0) };
const liquidityAvailablePostMigration = decimal_js_1.default.max(0, liquidityAvailable.minus(debtThisReserve));
return decimal_js_1.default.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, slot, elevationGroup) {
// - 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 = Array.from(this.deposits.keys());
const reserveBorrows = 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) => 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) {
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, liquidityMint, slot, requestElevationGroup) {
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.getExistingReserveByAddress(value.reserveAddress);
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_js_1.default(0);
}
});
let maxBorrowAmount = decimal_js_1.default.min(maxObligationBorrowPower, reserveAvailableAmount, reserveBorrowCapRemained);
const debtWithdrawalCap = reserve.getDebtWithdrawalCapCapacity().sub(reserve.getDebtWithdrawalCapCurrent(slot));
maxBorrowAmount = reserve.getDebtWithdrawalCapCapacity().gt(0)
? decimal_js_1.default.min(maxBorrowAmount, debtWithdrawalCap)
: maxBorrowAmount;
let originationFeeRate = reserve.getBorrowFee();
// Inclusive fee rate
originationFeeRate = originationFeeRate.div(originationFeeRate.add(new decimal_js_1.default(1)));
const borrowFee = maxBorrowAmount.mul(originationFeeRate);
maxBorrowAmount = maxBorrowAmount.sub(borrowFee);
const utilizationRatioLimit = reserve.state.config.utilizationLimitBlockBorrowingAbovePct / 100;
const currentUtilizationRatio = reserve.calculateUtilizationRatio();
if (utilizationRatioLimit > 0 && currentUtilizationRatio > utilizationRatioLimit) {
return new decimal_js_1.default(0);
}
else if (utilizationRatioLimit > 0 && currentUtilizationRatio < utilizationRatioLimit) {
const maxBorrowBasedOnUtilization = new decimal_js_1.default(utilizationRatioLimit - currentUtilizationRatio).mul(reserve.getTotalSupply());
maxBorrowAmount = decimal_js_1.default.min(maxBorrowAmount, maxBorrowBasedOnUtilization);
}
let borrowLimitDependentOnElevationGroup = new decimal_js_1.default(utils_2.U64_MAX);
if (!elevationGroupActivated) {
borrowLimitDependentOnElevationGroup = reserve
.getBorrowLimitOutsideElevationGroup()
.sub(reserve.getBorrowedAmountOutsideElevationGroup());
}
else {
let maxDebtTakenAgainstCollaterals = new decimal_js_1.default(utils_2.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_js_1.default.max(new decimal_js_1.default(0), decimal_js_1.default.min(maxDebtAllowedAgainstCollateral, maxDebtTakenAgainstCollaterals));
}
borrowLimitDependentOnElevationGroup = maxDebtTakenAgainstCollaterals;
}
maxBorrowAmount = decimal_js_1.default.min(maxBorrowAmount, borrowLimitDependentOnElevationGroup);
return decimal_js_1.default.max(new decimal_js_1.default(0), maxBorrowAmount);
}
getMaxWithdrawAmount(market, tokenMint, slot) {
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_js_1.default(0))) {
return new decimal_js_1.default(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_js_1.default(0);
}
let maxWithdrawValue;
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_js_1.default.max(0, decimal_js_1.default.min(userDepositPositionAmount, maxWithdrawAmount, reserveAvailableLiquidity, withdrawalCapRemained));
}
getObligationLiquidityByReserve(reserveAddress) {
const obligationLiquidity = this.state.borrows.find((borrow) => borrow.borrowReserve.equals(reserveAddress));
if (!obligationLiquidity) {
throw new Error(`Obligation liquidity not found given reserve ${reserveAddress.toString()}`);
}
return obligationLiquidity;
}
/**
*
* @returns Total borrowed amount for the specified obligation liquidity/borrow asset
*/
static getBorrowAmount(