UNPKG

@kamino-finance/klend-sdk

Version:

Typescript SDK for interacting with the Kamino Lending (klend) protocol

906 lines (903 loc) 56.8 kB
"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(