diff --git a/solend-lite/package.json b/solend-lite/package.json index 406b52cf..d5dba9b0 100644 --- a/solend-lite/package.json +++ b/solend-lite/package.json @@ -48,6 +48,7 @@ "react-no-ssr": "^1.1.0", "react-svg": "^16.0.0", "react-timer-hook": "^3.0.5", + "recharts": "^2.7.2", "sass": "^1.57.1", "string-comparison": "^1.1.0", "typescript": "4.9.4" diff --git a/solend-lite/src/components/InterestGraph/InterestGraph.tsx b/solend-lite/src/components/InterestGraph/InterestGraph.tsx new file mode 100644 index 00000000..1736393c --- /dev/null +++ b/solend-lite/src/components/InterestGraph/InterestGraph.tsx @@ -0,0 +1,165 @@ +import React, { ReactElement } from 'react'; +import { ReserveType } from '@solendprotocol/solend-sdk'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + ReferenceDot, + Label, +} from 'recharts'; +import { themeConfig } from 'theme/theme'; +import { Box, Text } from '@chakra-ui/react'; +import { formatPercent } from 'utils/numberFormatter'; + +const CustomTooltip = ({ + active, + payload, + label, +}: { + active?: boolean; + payload?: Array<{ + payload?: { name: string }; + value?: number; + }>; + label?: number; +}) => { + if (active && payload && payload.length) { + return ( + + {payload[0]?.payload?.name} + {`Utilization: ${formatPercent( + (payload[0]?.value ?? 0) / 100, + )}`} + {`Interest: ${formatPercent( + (label ?? 0) / 100, + )}`} +
+ + Click to return to parameter view + +
+ ); + } + + return null; +}; + +function InterestGraph({ reserve }: { reserve: ReserveType }): ReactElement { + const data = [ + { + name: 'Min borrow rate', + utilization: 0, + interest: reserve.minBorrowApr * 100, + current: false, + }, + { + name: 'Target rate', + utilization: reserve.targetUtilization * 100, + interest: reserve.targetBorrowApr * 100, + current: false, + }, + { + name: 'Max rate', + utilization: reserve.maxUtilizationRate * 100, + interest: reserve.maxBorrowApr * 100, + current: false, + }, + { + name: 'Supermax rate', + utilization: 100, + interest: reserve.superMaxBorrowRate * 100, + current: false, + }, + { + name: 'Current', + utilization: reserve.reserveUtilization.toNumber() * 100, + interest: reserve.borrowInterest.toNumber() * 100, + current: true, + }, + ].sort((a, b) => a.utilization - b.utilization); + + return ( + + + + + + + + ; + label?: number; + }) => ( + + )} + /> + + + + + ); +} + +export default InterestGraph; diff --git a/solend-lite/src/components/Metric/Metric.module.scss b/solend-lite/src/components/Metric/Metric.module.scss index a6c2f45f..51d38c8a 100644 --- a/solend-lite/src/components/Metric/Metric.module.scss +++ b/solend-lite/src/components/Metric/Metric.module.scss @@ -25,3 +25,10 @@ } } } + +.rowMetric { + .label { + align-items: center; + gap: 4px; + } +} diff --git a/solend-lite/src/components/Metric/Metric.tsx b/solend-lite/src/components/Metric/Metric.tsx index 22dbd538..016dac30 100644 --- a/solend-lite/src/components/Metric/Metric.tsx +++ b/solend-lite/src/components/Metric/Metric.tsx @@ -35,7 +35,11 @@ function Metric({ align={row ? 'center' : undefined} justify='space-between' style={style} - className={classNames(styles.alignCenter, !alignCenter && styles.metric)} + className={classNames( + 'metric', + styles.alignCenter, + !alignCenter && (row ? styles.rowMetric : styles.metric), + )} > {label && ( diff --git a/solend-lite/src/components/Pool/PoolTable/PoolTable.tsx b/solend-lite/src/components/Pool/PoolTable/PoolTable.tsx index da86d23d..18ed0131 100644 --- a/solend-lite/src/components/Pool/PoolTable/PoolTable.tsx +++ b/solend-lite/src/components/Pool/PoolTable/PoolTable.tsx @@ -47,17 +47,19 @@ export default function PoolTable({ header: 'Open LTV / BW', meta: { isNumeric: true }, cell: ({ row: { original: reserve } }) => ( - - {formatPercent(reserve.loanToValueRatio, false, 0)} - + <> + + {formatPercent(reserve.loanToValueRatio, false, 0)} + {' '} + / - - + {' '} + {reserve.addedBorrowWeightBPS.toString() !== U64_MAX ? formatToken(reserve.borrowWeight.toString(), 2, false, true) : '∞'} - + ), }), columnHelper.accessor('totalSupplyUsd', { diff --git a/solend-lite/src/components/TransactionTakeover/ReserveStats/ReserveStats.module.scss b/solend-lite/src/components/TransactionTakeover/ReserveStats/ReserveStats.module.scss index 9a9f3399..5ebbe861 100644 --- a/solend-lite/src/components/TransactionTakeover/ReserveStats/ReserveStats.module.scss +++ b/solend-lite/src/components/TransactionTakeover/ReserveStats/ReserveStats.module.scss @@ -12,7 +12,7 @@ .params { background-color: var(--chakra-colors-neutralAlt); padding-top: 8px; - padding-bottom: 32px; + padding-bottom: 8px; margin-left: -36px; margin-right: -36px; padding-left: 36px; @@ -37,3 +37,19 @@ .reserveAddress { cursor: pointer; } + +.rateSection { + &:hover { + opacity: 0.5; + :global(.metric) { + filter: blur(0.25px); + } + .graphHover { + visibility: visible; + } + } +} + +.graphHover { + visibility: hidden; +} diff --git a/solend-lite/src/components/TransactionTakeover/ReserveStats/ReserveStats.tsx b/solend-lite/src/components/TransactionTakeover/ReserveStats/ReserveStats.tsx index 3e6d88d1..751e7b96 100644 --- a/solend-lite/src/components/TransactionTakeover/ReserveStats/ReserveStats.tsx +++ b/solend-lite/src/components/TransactionTakeover/ReserveStats/ReserveStats.tsx @@ -11,6 +11,7 @@ import { computeExtremeRates } from '@solendprotocol/solend-sdk'; import { useAtom } from 'jotai'; import humanizeDuration from 'humanize-duration'; import { SLOT_RATE } from 'utils/utils'; +import InterestGraph from 'components/InterestGraph/InterestGraph'; // certain oracles do not match their underlying asset, hence this mapping const PYTH_ORACLE_MAPPING: Record = { @@ -49,6 +50,7 @@ function ReserveStats({ }: ReserveStatsPropsType): ReactElement { const [rateLimiter] = useAtom(rateLimiterAtom); const [showParams, setShowParams] = useState(false); + const [showGraph, setShowGraph] = useState(false); let newBorrowLimitDisplay = null; if (newBorrowLimit) { const nbuObj = newBorrowLimit; @@ -166,10 +168,112 @@ function ReserveStats({ showParams ? styles.visible : styles.hidden, )} style={{ - maxHeight: showParams ? 500 : 0, + maxHeight: showParams ? 1000 : 0, display: showParams ? 'visible' : 'hidden', }} > + {showGraph && ( + setShowGraph(false)}> + + + Interest rate curve + + + + + )} + {!showGraph && ( + setShowGraph(true)} + > + + Percentage of the asset being lent out. Utilization determines + interest rates via a function.{' '} + + Learn more + + . + + } + /> + + + + + + + + + Click to show graph + + + + )} {reserve.reserveSupplyLimit && ( + + {formatPercent(reserve.protocolLiquidationFee)}} + tooltip='Liquidation penalty increases past close LTV until max close LTV, where max liquidation penalty occurs.' + /> {formatPercent(reserve.interestRateSpread)}} /> - - - - - Percentage of the asset being lent out. Utilization determines - interest rates via a function.{' '} - - Learn more - - . - - } - /> } /> + {rateLimiter && ( + + {formatToken( + new BigNumber( + rateLimiter.config.maxOutflow.toString(), + reserve.decimals, + ).toString(), + )}{' '} + {reserve.symbol} per{' '} + {humanizeDuration( + (rateLimiter.config.windowDuration.toNumber() / SLOT_RATE) * + 1000, + )} + + ) + } + tooltip={ + <> + For the safety of the pool, amounts being withdrawn or borrowed + from the pool are limited by this rate.
+ Remaining outflow this window:{' '} + {formatUsd( + rateLimiter.remainingOutflow?.toString() ?? '0', + false, + true, + )} + + } + /> + )} + {!new BigNumber(reserve.borrowWeight).isEqualTo(new BigNumber(0)) && ( + + )} {reserve.pythOracle !== 'nu11111111111111111111111111111111111111111' && ( } /> - {rateLimiter && ( - - {formatToken( - new BigNumber( - rateLimiter.config.maxOutflow.toString(), - reserve.decimals, - ).toString(), - )}{' '} - {reserve.symbol} per{' '} - {humanizeDuration( - (rateLimiter.config.windowDuration.toNumber() / SLOT_RATE) * - 1000, - )} - - ) - } - tooltip={ - <> - For the safety of the pool, amounts being withdrawn or borrowed - from the pool are limited by this rate.
- Remaining outflow this window:{' '} - {formatUsd( - rateLimiter.remainingOutflow?.toString() ?? '0', - false, - true, - )} - - } - /> - )} - {!new BigNumber(reserve.borrowWeight).isEqualTo(new BigNumber(0)) && ( - - )}
); diff --git a/solend-lite/src/components/TransactionTakeover/TransactionTakeover.tsx b/solend-lite/src/components/TransactionTakeover/TransactionTakeover.tsx index 77bdadb0..6cfe100f 100644 --- a/solend-lite/src/components/TransactionTakeover/TransactionTakeover.tsx +++ b/solend-lite/src/components/TransactionTakeover/TransactionTakeover.tsx @@ -62,10 +62,10 @@ export default function TransactionTakeover() { selectedReserve, walletAssets, selectedObligation, - rateLimiter + rateLimiter, ) : BigNumber(0), - [selectedReserve, walletAssets, selectedObligation], + [selectedReserve, walletAssets, selectedObligation, rateLimiter], ); const withdrawMax = useMemo( () => @@ -74,10 +74,10 @@ export default function TransactionTakeover() { selectedReserve, walletAssets, selectedObligation, - rateLimiter + rateLimiter, ) : BigNumber(0), - [selectedReserve, walletAssets, selectedObligation], + [selectedReserve, walletAssets, selectedObligation, rateLimiter], ); const repayMax = useMemo( () => @@ -177,7 +177,7 @@ export default function TransactionTakeover() { const useMax = new BigNumber(value).isGreaterThanOrEqualTo( borrowMax, ); - + console.log(useMax); return borrowConfigs.action( useMax ? U64_MAX @@ -246,7 +246,7 @@ export default function TransactionTakeover() { selectedObligation, selectedReserve, walletAssets, - rateLimiter + rateLimiter, )} getNewCalculations={withdrawConfigs.getNewCalculations} /> diff --git a/solend-lite/src/components/Wallet/Wallet.tsx b/solend-lite/src/components/Wallet/Wallet.tsx index ea05fe28..d92f7874 100644 --- a/solend-lite/src/components/Wallet/Wallet.tsx +++ b/solend-lite/src/components/Wallet/Wallet.tsx @@ -47,7 +47,7 @@ export default function Wallet() { - Wallet asset + Wallet assets diff --git a/solend-sdk/src/core/utils/pools.ts b/solend-sdk/src/core/utils/pools.ts index 9ab6eecc..d9d94c50 100644 --- a/solend-sdk/src/core/utils/pools.ts +++ b/solend-sdk/src/core/utils/pools.ts @@ -114,7 +114,10 @@ export function formatReserve( ).shiftedBy(-decimals), targetBorrowApr: reserve.info.config.optimalBorrowRate / 100, targetUtilization: reserve.info.config.optimalUtilizationRate / 100, + maxUtilizationRate: reserve.info.config.maxUtilizationRate / 100, + minBorrowApr: reserve.info.config.minBorrowRate / 100, maxBorrowApr: reserve.info.config.maxBorrowRate / 100, + superMaxBorrowRate: reserve.info.config.superMaxBorrowRate / 100, supplyInterest: calculateSupplyInterest(reserve.info, false), borrowInterest: calculateBorrowInterest(reserve.info, false), totalSupply, @@ -126,7 +129,9 @@ export function formatReserve( availableAmountUsd: availableAmount.times(priceResolved), loanToValueRatio: reserve.info.config.loanToValueRatio / 100, liquidationThreshold: reserve.info.config.liquidationThreshold / 100, + maxLiquidationThreshold: reserve.info.config.maxLiquidationThreshold / 100, liquidationPenalty: reserve.info.config.liquidationBonus / 100, + maxLiquidationPenalty: reserve.info.config.maxLiquidationBonus / 100, liquidityAddress: reserve.info.liquidity.supplyPubkey.toBase58(), cTokenLiquidityAddress: reserve.info.collateral.supplyPubkey.toBase58(), liquidityFeeReceiverAddress: reserve.info.config.feeReceiver.toBase58(), diff --git a/solend-sdk/src/core/utils/rates.ts b/solend-sdk/src/core/utils/rates.ts index 405a7352..1c805c48 100644 --- a/solend-sdk/src/core/utils/rates.ts +++ b/solend-sdk/src/core/utils/rates.ts @@ -30,34 +30,50 @@ const calculateBorrowAPR = (reserve: Reserve) => { const optimalUtilization = new BigNumber( reserve.config.optimalUtilizationRate / 100 ); - + const maxUtilizationRate = new BigNumber( + reserve.config.maxUtilizationRate + ); let borrowAPR; if ( - optimalUtilization.isEqualTo(1) || - currentUtilization.isLessThan(optimalUtilization) + currentUtilization.isLessThanOrEqualTo( + optimalUtilization + ) ) { + const minBorrowRate = new BigNumber(reserve.config.minBorrowRate / 100) + if (optimalUtilization.isEqualTo(0)) { + return minBorrowRate; + } const normalizedFactor = currentUtilization.dividedBy(optimalUtilization); + const optimalBorrowRate = new BigNumber( reserve.config.optimalBorrowRate / 100 ); - const minBorrowRate = new BigNumber(reserve.config.minBorrowRate / 100); + borrowAPR = normalizedFactor .times(optimalBorrowRate.minus(minBorrowRate)) .plus(minBorrowRate); - } else { - if (reserve.config.optimalBorrowRate === reserve.config.maxBorrowRate) { - return new BigNumber( - computeExtremeRates(reserve.config.maxBorrowRate / 100) + } else if (currentUtilization.isLessThanOrEqualTo(maxUtilizationRate)) { + const weight = currentUtilization + .minus(optimalUtilization) + .dividedBy(maxUtilizationRate.minus(optimalUtilization)); + + const optimalBorrowRate = new BigNumber( + reserve.config.optimalBorrowRate / 100 ); - } - const normalizedFactor = currentUtilization + const maxBorrowRate = new BigNumber(reserve.config.maxBorrowRate / 100); + + borrowAPR = weight.times(maxBorrowRate.minus(optimalBorrowRate)).plus(optimalBorrowRate) + } else { + const weight = currentUtilization .minus(optimalUtilization) - .dividedBy(new BigNumber(1).minus(optimalUtilization)); - const optimalBorrowRate = reserve.config.optimalBorrowRate / 100; - const maxBorrowRate = reserve.config.maxBorrowRate / 100; - borrowAPR = normalizedFactor - .times(maxBorrowRate - optimalBorrowRate) - .plus(optimalBorrowRate); + .dividedBy(maxUtilizationRate.minus(optimalUtilization)); + + const maxBorrowRate = new BigNumber(reserve.config.maxBorrowRate / 100) + const superMaxBorrowRate = new BigNumber(reserve.config.superMaxBorrowRate.toString()).dividedBy(100); + + borrowAPR = weight + .times(superMaxBorrowRate.minus(maxBorrowRate)) + .plus(maxBorrowRate); } return borrowAPR; diff --git a/solend-sdk/src/instructions/index.ts b/solend-sdk/src/instructions/index.ts index d8ec71c7..17dde38c 100644 --- a/solend-sdk/src/instructions/index.ts +++ b/solend-sdk/src/instructions/index.ts @@ -15,4 +15,7 @@ export * from "./initReserve"; export * from "./updateReserveConfig"; export * from "./flashBorrowReserveLiquidity"; export * from "./flashRepayReserveLiquidity"; -export * from "./instruction"; +export * from "./forgiveDebt"; +export * from "./setLendingMarketOwnerAndConfig"; +export * from "./updateMetadata"; +export * from "./instruction"; \ No newline at end of file diff --git a/solend-sdk/src/state/reserve.ts b/solend-sdk/src/state/reserve.ts index f25380b5..9c598875 100644 --- a/solend-sdk/src/state/reserve.ts +++ b/solend-sdk/src/state/reserve.ts @@ -52,7 +52,7 @@ export interface ReserveConfig { minBorrowRate: number; optimalBorrowRate: number; maxBorrowRate: number; - superMaxBorrowRate: BN; + superMaxBorrowRate: number; fees: { borrowFeeWad: BN; flashLoanFeeWad: BN; @@ -183,16 +183,16 @@ function decodeReserve(buffer: Buffer): Reserve { }, config: { optimalUtilizationRate: reserve.optimalUtilizationRate, - maxUtilizationRate: reserve.maxUtilizationRate, + maxUtilizationRate: Math.max(reserve.maxUtilizationRate, reserve.optimalUtilizationRate), loanToValueRatio: reserve.loanToValueRatio, liquidationBonus: reserve.liquidationBonus, - maxLiquidationBonus: reserve.maxLiquidationBonus, + maxLiquidationBonus: Math.max(reserve.maxLiquidationBonus, reserve.liquidationBonus), liquidationThreshold: reserve.liquidationThreshold, - maxLiquidationThreshold: reserve.maxLiquidationThreshold, + maxLiquidationThreshold: Math.max(reserve.maxLiquidationThreshold, reserve.liquidationThreshold), minBorrowRate: reserve.minBorrowRate, optimalBorrowRate: reserve.optimalBorrowRate, maxBorrowRate: reserve.maxBorrowRate, - superMaxBorrowRate: reserve.superMaxBorrowRate, + superMaxBorrowRate: Math.max(reserve.superMaxBorrowRate, reserve.maxBorrowRate), fees: { borrowFeeWad: reserve.borrowFeeWad, flashLoanFeeWad: reserve.flashLoanFeeWad, @@ -201,7 +201,7 @@ function decodeReserve(buffer: Buffer): Reserve { depositLimit: reserve.depositLimit, borrowLimit: reserve.borrowLimit, feeReceiver: reserve.feeReceiver, - protocolLiquidationFee: reserve.protocolLiquidationFee, + protocolLiquidationFee: reserve.protocolLiquidationFee * 10, protocolTakeRate: reserve.protocolTakeRate, addedBorrowWeightBPS: reserve.addedBorrowWeightBPS, borrowWeight: new BigNumber(reserve.addedBorrowWeightBPS.toString())