@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
238 lines (215 loc) • 8.69 kB
text/typescript
import Decimal from 'decimal.js';
import { KaminoMarket, KaminoObligation, KaminoReserve, numberToLamportsDecimal } from '../classes';
import { PublicKey } from '@solana/web3.js';
import { lamportsToDecimal } from '../classes/utils';
import {
MaxWithdrawLtvCheck,
getMaxCollateralFromRepayAmount,
getMaxWithdrawLtvCheck,
} from './repay_with_collateral_operations';
export function calcRepayAmountWithSlippage(
kaminoMarket: KaminoMarket,
debtReserve: KaminoReserve,
currentSlot: number,
obligation: KaminoObligation,
amount: Decimal,
referrer: PublicKey
): {
repayAmount: Decimal;
repayAmountLamports: Decimal;
flashRepayAmountLamports: Decimal;
} {
const interestRateAccrued = obligation
.estimateObligationInterestRate(
kaminoMarket,
debtReserve,
obligation.state.borrows.find((borrow) => borrow.borrowReserve.equals(debtReserve.address))!,
currentSlot
)
.toDecimalPlaces(debtReserve.state.liquidity.mintDecimals.toNumber(), Decimal.ROUND_CEIL);
// add 0.1% to interestRateAccrued because we don't want to estimate slightly less than SC and end up not repaying enough
const repayAmountIrAdjusted = amount
.mul(interestRateAccrued.mul(new Decimal('1.001')))
.toDecimalPlaces(debtReserve.state.liquidity.mintDecimals.toNumber(), Decimal.ROUND_CEIL);
let repayAmount: Decimal;
// Ensure when repaying close to the full amount, we repay the full amount as otherwise we might end up having a small amount left
if (
repayAmountIrAdjusted.greaterThanOrEqualTo(
lamportsToDecimal(
obligation.getBorrowByReserve(debtReserve.address)?.amount || new Decimal(0),
debtReserve.stats.decimals
)
)
) {
repayAmount = repayAmountIrAdjusted;
} else {
repayAmount = amount;
}
const repayAmountLamports = numberToLamportsDecimal(repayAmount, debtReserve.stats.decimals);
const { flashRepayAmountLamports } = calcFlashRepayAmount({
reserve: debtReserve,
referralFeeBps: kaminoMarket.state.referralFeeBps,
hasReferral: !referrer.equals(PublicKey.default),
flashBorrowAmountLamports: repayAmountLamports,
});
return { repayAmount, repayAmountLamports, flashRepayAmountLamports };
}
export const calcFlashRepayAmount = (props: {
reserve: KaminoReserve;
referralFeeBps: number;
hasReferral: boolean;
flashBorrowAmountLamports: Decimal;
}): {
flashRepayAmountLamports: Decimal;
} => {
const { reserve, referralFeeBps, hasReferral, flashBorrowAmountLamports } = props;
const { referrerFees, protocolFees } = reserve.calculateFlashLoanFees(
flashBorrowAmountLamports,
referralFeeBps,
hasReferral
);
const flashRepayAmountLamports = flashBorrowAmountLamports.add(referrerFees).add(protocolFees);
return {
flashRepayAmountLamports,
};
};
export function calcMaxWithdrawCollateral(
market: KaminoMarket,
obligation: KaminoObligation,
collReserveAddr: PublicKey,
debtReserveAddr: PublicKey,
repayAmountLamports: Decimal
): {
maxWithdrawableCollLamports: Decimal;
canWithdrawAllColl: boolean;
repayingAllDebt: boolean;
} {
const deposit = obligation.getDepositByReserve(collReserveAddr)!;
const borrow = obligation.getBorrowByReserve(debtReserveAddr)!;
const depositReserve = market.getReserveByAddress(deposit.reserveAddress)!;
const debtReserve = market.getReserveByAddress(borrow.reserveAddress)!;
const depositTotalLamports = deposit.amount.floor(); // TODO: can remove floor, we have lamports only for deposits
// Calculate the market value of the remaining debt after repaying
const remainingBorrowLamports = borrow.amount.sub(repayAmountLamports).ceil();
const remainingBorrowAmount = remainingBorrowLamports.div(debtReserve.getMintFactor());
let remainingBorrowsValue = remainingBorrowAmount.mul(debtReserve.getOracleMarketPrice());
if (obligation.getBorrows().length > 1) {
remainingBorrowsValue = obligation
.getBorrows()
.filter((p) => !p.reserveAddress.equals(borrow.reserveAddress))
.reduce((acc, b) => acc.add(b.marketValueRefreshed), new Decimal('0'));
}
const hypotheticalWithdrawLamports = getMaxCollateralFromRepayAmount(
repayAmountLamports.div(debtReserve.getMintFactor()),
debtReserve,
depositReserve
);
// Calculate the max withdraw ltv we can withdraw up to
const maxWithdrawLtvCheck = getMaxWithdrawLtvCheck(
obligation,
repayAmountLamports,
debtReserve,
hypotheticalWithdrawLamports,
depositReserve
);
// Calculate the max borrowable value remaining against deposits
let maxBorrowableValueRemainingAgainstDeposits = new Decimal('0');
if (obligation.getDeposits().length > 1) {
maxBorrowableValueRemainingAgainstDeposits = obligation
.getDeposits()
.filter((p) => !p.reserveAddress.equals(deposit.reserveAddress))
.reduce((acc, d) => {
const { maxLtv, liquidationLtv } = obligation.getLtvForReserve(market, d.reserveAddress);
const maxWithdrawLtv =
maxWithdrawLtvCheck === MaxWithdrawLtvCheck.LIQUIDATION_THRESHOLD ? liquidationLtv : maxLtv;
return acc.add(d.marketValueRefreshed.mul(maxWithdrawLtv));
}, new Decimal('0'));
}
// if the remaining borrow value is less than the
// this means that the user's ltv is less or equal to the max ltv
if (maxBorrowableValueRemainingAgainstDeposits.gte(remainingBorrowsValue)) {
return {
maxWithdrawableCollLamports: depositTotalLamports,
canWithdrawAllColl: true,
repayingAllDebt: repayAmountLamports.gte(borrow.amount),
};
} else {
const { maxLtv: collMaxLtv, liquidationLtv: collLiquidationLtv } = obligation.getLtvForReserve(
market,
depositReserve.address
);
const maxWithdrawLtv =
maxWithdrawLtvCheck === MaxWithdrawLtvCheck.LIQUIDATION_THRESHOLD ? collLiquidationLtv : collMaxLtv;
const numerator = deposit.marketValueRefreshed
.mul(maxWithdrawLtv)
.add(maxBorrowableValueRemainingAgainstDeposits)
.sub(remainingBorrowsValue);
const denominator = depositReserve.getOracleMarketPrice().mul(maxWithdrawLtv);
const maxCollWithdrawAmount = numerator.div(denominator);
const maxWithdrawableCollLamports = maxCollWithdrawAmount.mul(depositReserve.getMintFactor()).floor();
return {
maxWithdrawableCollLamports,
canWithdrawAllColl: false,
repayingAllDebt: repayAmountLamports.gte(borrow.amount),
};
}
}
export function estimateDebtRepaymentWithColl(props: {
collAmount: Decimal; // in decimals
priceDebtToColl: Decimal;
slippagePct: Decimal;
flashLoanFeePct: Decimal;
kaminoMarket: KaminoMarket;
debtTokenMint: PublicKey;
obligation: KaminoObligation;
currentSlot: number;
}): Decimal {
const {
collAmount,
priceDebtToColl,
slippagePct,
flashLoanFeePct,
kaminoMarket,
debtTokenMint,
obligation,
currentSlot,
} = props;
const slippageMultiplier = new Decimal(1.0).add(slippagePct.div('100'));
const flashLoanFeeMultiplier = new Decimal(1.0).add(flashLoanFeePct.div('100'));
const debtReserve = kaminoMarket.getExistingReserveByMint(debtTokenMint);
const debtAfterSwap = collAmount.div(slippageMultiplier).div(priceDebtToColl);
const debtAfterFlashLoanRepay = debtAfterSwap.div(flashLoanFeeMultiplier);
const accruedInterestRate = obligation
.estimateObligationInterestRate(
kaminoMarket,
debtReserve,
obligation.getObligationLiquidityByReserve(debtReserve.address),
currentSlot
)
.toDecimalPlaces(debtReserve.state.liquidity.mintDecimals.toNumber(), Decimal.ROUND_CEIL);
// Estimate slightly more, by adding 1% to IR in order to avoid the case where UI users can repay the max we allow them
const debtIrAdjusted = debtAfterFlashLoanRepay
.div(accruedInterestRate.mul(new Decimal('1.01')))
.toDecimalPlaces(debtReserve.state.liquidity.mintDecimals.toNumber(), Decimal.ROUND_CEIL);
return debtIrAdjusted;
}
export function estimateCollNeededForDebtRepayment(props: {
debtAmount: Decimal; // in decimals
priceDebtToColl: Decimal;
slippagePct: Decimal;
flashLoanFeePct: Decimal;
}): Decimal {
const {
debtAmount, // in decimals
priceDebtToColl,
slippagePct,
flashLoanFeePct,
} = props;
const slippageRatio = slippagePct.div('100');
const flashLoanFeeRatio = flashLoanFeePct.div('100');
const slippageMultiplier = new Decimal(1.0).add(slippageRatio);
const flashLoanFeeMultiplier = new Decimal(1.0).add(flashLoanFeeRatio);
const debtFlashLoanRepay = debtAmount.mul(flashLoanFeeMultiplier);
const collToSwap = debtFlashLoanRepay.mul(slippageMultiplier).mul(priceDebtToColl);
return collToSwap;
}