From 7419f9de5c94e1cf9604441c448ed786c4571e3a Mon Sep 17 00:00:00 2001 From: Hayden Shively <17186559+haydenshively@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:44:53 -0600 Subject: [PATCH 1/4] Initial --- .../src/components/advanced/BorrowMetrics.tsx | 10 +- .../components/advanced/GlobalStatsTable.tsx | 40 +++-- .../advanced/UniswapPositionList.tsx | 7 +- .../advanced/modal/AddCollateralModal.tsx | 34 +--- .../components/advanced/modal/BorrowModal.tsx | 33 ++-- .../advanced/modal/RemoveCollateralModal.tsx | 10 +- .../components/advanced/modal/RepayModal.tsx | 6 +- .../components/boost/ImportBoostWidget.tsx | 80 ++-------- earn/src/components/lend/LendPairCard.tsx | 12 +- .../components/markets/modal/BorrowModal.tsx | 7 +- .../markets/modal/BorrowModalUniswap.tsx | 4 +- .../modal/content/BorrowModalContent.tsx | 13 +- .../portfolio/LendingPairPeerCard.tsx | 4 +- earn/src/data/FactoryData.ts | 17 ++ earn/src/data/LendingPair.ts | 71 ++++---- earn/src/data/MarketInfo.ts | 145 ----------------- earn/src/data/OracleData.ts | 15 ++ earn/src/data/hooks/UseLendingPairs.ts | 24 +++ earn/src/pages/AdvancedPage.tsx | 151 ++---------------- earn/src/pages/MarketsPage.tsx | 8 +- 20 files changed, 187 insertions(+), 504 deletions(-) create mode 100644 earn/src/data/FactoryData.ts delete mode 100644 earn/src/data/MarketInfo.ts create mode 100644 earn/src/data/OracleData.ts diff --git a/earn/src/components/advanced/BorrowMetrics.tsx b/earn/src/components/advanced/BorrowMetrics.tsx index b3d79f82..339f3b95 100644 --- a/earn/src/components/advanced/BorrowMetrics.tsx +++ b/earn/src/components/advanced/BorrowMetrics.tsx @@ -9,7 +9,6 @@ import styled from 'styled-components'; import { computeLiquidationThresholds, getAssets, sqrtRatioToPrice } from '../../data/BalanceSheet'; import { RESPONSIVE_BREAKPOINT_MD, RESPONSIVE_BREAKPOINT_SM } from '../../data/constants/Breakpoints'; import { MarginAccount } from '../../data/MarginAccount'; -import { UniswapPosition } from '../../data/Uniswap'; const BORROW_TITLE_TEXT_COLOR = 'rgba(130, 160, 182, 1)'; const MAX_HEALTH = 10; @@ -161,12 +160,11 @@ export type BorrowMetricsProps = { marginAccount?: MarginAccount; dailyInterest0: number; dailyInterest1: number; - uniswapPositions: readonly UniswapPosition[]; userHasNoMarginAccounts: boolean; }; export function BorrowMetrics(props: BorrowMetricsProps) { - const { marginAccount, dailyInterest0, dailyInterest1, uniswapPositions, userHasNoMarginAccounts } = props; + const { marginAccount, dailyInterest0, dailyInterest1, userHasNoMarginAccounts } = props; const maxSafeCollateralFall = useMemo(() => { if (!marginAccount) return null; @@ -174,7 +172,7 @@ export function BorrowMetrics(props: BorrowMetricsProps) { const { lowerSqrtRatio, upperSqrtRatio, minSqrtRatio, maxSqrtRatio } = computeLiquidationThresholds( marginAccount.assets, marginAccount.liabilities, - uniswapPositions, + marginAccount.uniswapPositions ?? [], marginAccount.sqrtPriceX96, marginAccount.iv, marginAccount.nSigma, @@ -191,7 +189,7 @@ export function BorrowMetrics(props: BorrowMetricsProps) { const assets = getAssets( marginAccount.assets.token0Raw, marginAccount.assets.token1Raw, - uniswapPositions, + marginAccount.uniswapPositions ?? [], lowerSqrtRatio, upperSqrtRatio, marginAccount.token0.decimals, @@ -229,7 +227,7 @@ export function BorrowMetrics(props: BorrowMetricsProps) { // Since we don't know whether the user is thinking in terms of "X per Y" or "Y per X", // we return the minimum. Error on the side of being too conservative. return Math.min(percentChange0, percentChange1); - }, [marginAccount, uniswapPositions]); + }, [marginAccount]); if (!marginAccount) return ( diff --git a/earn/src/components/advanced/GlobalStatsTable.tsx b/earn/src/components/advanced/GlobalStatsTable.tsx index 83fa46d6..4585fd6d 100644 --- a/earn/src/components/advanced/GlobalStatsTable.tsx +++ b/earn/src/components/advanced/GlobalStatsTable.tsx @@ -5,8 +5,7 @@ import { roundPercentage } from 'shared/lib/util/Numbers'; import styled from 'styled-components'; import { RESPONSIVE_BREAKPOINT_XS } from '../../data/constants/Breakpoints'; -import { MarginAccount } from '../../data/MarginAccount'; -import { MarketInfo } from '../../data/MarketInfo'; +import { LendingPair } from '../../data/LendingPair'; const STAT_LABEL_TEXT_COLOR = 'rgba(130, 160, 182, 1)'; const STAT_VALUE_TEXT_COLOR = 'rgba(255, 255, 255, 1)'; @@ -49,12 +48,11 @@ const StatContainer = styled.div` `; export type GlobalStatsTableProps = { - marginAccount?: MarginAccount; - marketInfo?: MarketInfo; + market?: LendingPair; }; export default function GlobalStatsTable(props: GlobalStatsTableProps) { - const { marginAccount, marketInfo } = props; + const { market } = props; return ( @@ -62,66 +60,66 @@ export default function GlobalStatsTable(props: GlobalStatsTableProps) { - {marginAccount?.token0.symbol} Total Supply + {market?.token0.symbol} Total Supply - {marketInfo?.lender0TotalAssets.toString(GNFormat.LOSSY_HUMAN) ?? '-'} + {market?.kitty0Info.totalAssets.toString(GNFormat.LOSSY_HUMAN) ?? '-'} - {marginAccount?.token1.symbol} Total Supply + {market?.token1.symbol} Total Supply - {marketInfo?.lender1TotalAssets.toString(GNFormat.LOSSY_HUMAN) ?? '-'} + {market?.kitty1Info.totalAssets.toString(GNFormat.LOSSY_HUMAN) ?? '-'} - {marginAccount?.token0.symbol} Borrows + {market?.token0.symbol} Borrows - {marketInfo?.lender0TotalBorrows.toString(GNFormat.LOSSY_HUMAN) ?? '-'} + {market?.kitty0Info.totalBorrows.toString(GNFormat.LOSSY_HUMAN) ?? '-'} - {marginAccount?.token1.symbol} Borrows + {market?.token1.symbol} Borrows - {marketInfo?.lender1TotalBorrows.toString(GNFormat.LOSSY_HUMAN) ?? '-'} + {market?.kitty1Info.totalBorrows.toString(GNFormat.LOSSY_HUMAN) ?? '-'} - {marginAccount?.token0.symbol} Utilization + {market?.token0.symbol} Utilization - {marketInfo ? roundPercentage(marketInfo.lender0Utilization * 100, 2) : '-'}% + {market ? roundPercentage(market.kitty0Info.utilization * 100, 2) : '-'}% - {marginAccount?.token1.symbol} Utilization + {market?.token1.symbol} Utilization - {marketInfo ? roundPercentage(marketInfo.lender1Utilization * 100, 2) : '-'}% + {market ? roundPercentage(market.kitty1Info.utilization * 100, 2) : '-'}% - {marginAccount?.token0.symbol} Borrow APR + {market?.token0.symbol} Borrow APR - {marketInfo ? roundPercentage(marketInfo.borrowerAPR0 * 100, 2) : '-'}% + {market ? roundPercentage(market.kitty0Info.borrowAPR * 100, 2) : '-'}% - {marginAccount?.token1.symbol} Borrow APR + {market?.token1.symbol} Borrow APR - {marketInfo ? roundPercentage(marketInfo.borrowerAPR1 * 100, 2) : '-'}% + {market ? roundPercentage(market.kitty1Info.borrowAPR * 100, 2) : '-'}% diff --git a/earn/src/components/advanced/UniswapPositionList.tsx b/earn/src/components/advanced/UniswapPositionList.tsx index 414d92ff..f8bdbd5c 100644 --- a/earn/src/components/advanced/UniswapPositionList.tsx +++ b/earn/src/components/advanced/UniswapPositionList.tsx @@ -168,13 +168,12 @@ function UniswapPositionCard(props: UniswapPositionCardProps) { export type UniswapPositionListProps = { borrower?: BorrowerNftBorrower; - uniswapPositions: readonly UniswapPosition[]; withdrawableUniswapNFTs: Map; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; export function UniswapPositionList(props: UniswapPositionListProps) { - const { borrower, uniswapPositions, withdrawableUniswapNFTs, setPendingTxn } = props; + const { borrower, withdrawableUniswapNFTs, setPendingTxn } = props; const [selectedUniswapPosition, setSelectedUniswapPosition] = useState(null); return ( @@ -187,7 +186,7 @@ export function UniswapPositionList(props: UniswapPositionListProps) { {slot} { setSelectedUniswapPosition(null); diff --git a/earn/src/components/advanced/modal/AddCollateralModal.tsx b/earn/src/components/advanced/modal/AddCollateralModal.tsx index 4c0a832f..46fa865a 100644 --- a/earn/src/components/advanced/modal/AddCollateralModal.tsx +++ b/earn/src/components/advanced/modal/AddCollateralModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { SendTransactionResult } from '@wagmi/core'; import { FilledGradientButton } from 'shared/lib/components/common/Buttons'; @@ -7,8 +7,7 @@ import { Text } from 'shared/lib/components/common/Typography'; import { BorrowerNftBorrower } from '../../../data/BorrowerNft'; import { MAX_UNISWAP_POSITIONS } from '../../../data/constants/Values'; -import { MarketInfo } from '../../../data/MarketInfo'; -import { UniswapNFTPosition, UniswapPosition } from '../../../data/Uniswap'; +import { UniswapNFTPosition } from '../../../data/Uniswap'; import { AddCollateralTab } from './tab/AddCollateralTab'; import { AddUniswapNFTAsCollateralTab } from './tab/AddUniswapNFTAsCollateralTab'; @@ -22,9 +21,6 @@ enum AddCollateralModalState { export type AddCollateralModalProps = { borrower: BorrowerNftBorrower; - marketInfo: MarketInfo; - isLoadingUniswapPositions: boolean; - existingUniswapPositions: readonly UniswapPosition[]; uniswapNFTPositions: Map; isOpen: boolean; setIsOpen: (open: boolean) => void; @@ -32,23 +28,13 @@ export type AddCollateralModalProps = { }; export default function AddCollateralModal(props: AddCollateralModalProps) { - const { - borrower, - isLoadingUniswapPositions, - existingUniswapPositions, - uniswapNFTPositions, - isOpen, - setIsOpen, - setPendingTxn, - } = props; + const { borrower, uniswapNFTPositions, isOpen, setIsOpen, setPendingTxn } = props; + const existingUniswapPositions = useMemo(() => borrower.uniswapPositions ?? [], [borrower]); + const [modalState, setModalState] = useState(() => { // Only show the select collateral type modal if there are uniswap NFT positions and the user has not already // added the maximum number of uniswap positions. - if ( - uniswapNFTPositions.size > 0 && - existingUniswapPositions.length < MAX_UNISWAP_POSITIONS && - !isLoadingUniswapPositions - ) { + if (uniswapNFTPositions.size > 0 && existingUniswapPositions.length < MAX_UNISWAP_POSITIONS) { return AddCollateralModalState.SELECT_COLLATERAL_TYPE; } return AddCollateralModalState.TOKENS; @@ -59,17 +45,13 @@ export default function AddCollateralModal(props: AddCollateralModalProps) { setModalState(() => { // Only show the select collateral type modal if there are uniswap NFT positions and the user has not already // added the maximum number of uniswap positions. - if ( - uniswapNFTPositions.size > 0 && - existingUniswapPositions.length < MAX_UNISWAP_POSITIONS && - !isLoadingUniswapPositions - ) { + if (uniswapNFTPositions.size > 0 && existingUniswapPositions.length < MAX_UNISWAP_POSITIONS) { return AddCollateralModalState.SELECT_COLLATERAL_TYPE; } return AddCollateralModalState.TOKENS; }); } - }, [existingUniswapPositions.length, isLoadingUniswapPositions, isOpen, uniswapNFTPositions.size]); + }, [existingUniswapPositions, isOpen, uniswapNFTPositions.size]); const defaultUniswapNFTPosition = uniswapNFTPositions.size > 0 ? Array.from(uniswapNFTPositions.entries())[0] : null; diff --git a/earn/src/components/advanced/modal/BorrowModal.tsx b/earn/src/components/advanced/modal/BorrowModal.tsx index 82a37f82..292c6d7d 100644 --- a/earn/src/components/advanced/modal/BorrowModal.tsx +++ b/earn/src/components/advanced/modal/BorrowModal.tsx @@ -23,10 +23,8 @@ import { useAccount, useContractRead, useContractWrite, usePrepareContractWrite import { ChainContext } from '../../../App'; import { isHealthy, maxBorrowAndWithdraw } from '../../../data/BalanceSheet'; import { BorrowerNftBorrower } from '../../../data/BorrowerNft'; +import { LendingPair } from '../../../data/LendingPair'; import { Liabilities } from '../../../data/MarginAccount'; -import { MarketInfo } from '../../../data/MarketInfo'; -import { RateModel, yieldPerSecondToAPR } from '../../../data/RateModel'; -import { UniswapPosition } from '../../../data/Uniswap'; import HealthBar from '../../common/HealthBar'; import TokenAmountSelectInput from '../../portfolio/TokenAmountSelectInput'; @@ -179,8 +177,7 @@ function BorrowButton(props: BorrowButtonProps) { export type BorrowModalProps = { borrower: BorrowerNftBorrower; - uniswapPositions: readonly UniswapPosition[]; - marketInfo: MarketInfo; + market: LendingPair; accountEtherBalance?: GN; isOpen: boolean; setIsOpen: (open: boolean) => void; @@ -188,7 +185,7 @@ export type BorrowModalProps = { }; export default function BorrowModal(props: BorrowModalProps) { - const { borrower, uniswapPositions, marketInfo, accountEtherBalance, isOpen, setIsOpen, setPendingTxn } = props; + const { borrower, market, accountEtherBalance, isOpen, setIsOpen, setPendingTxn } = props; const { activeChain } = useContext(ChainContext); const [borrowAmountStr, setBorrowAmountStr] = useState(''); @@ -234,20 +231,22 @@ export default function BorrowModal(props: BorrowModalProps) { return null; } - const gnMaxBorrowsBasedOnMarket = isToken0 ? marketInfo.lender0AvailableAssets : marketInfo.lender1AvailableAssets; // TODO: use GN const maxBorrowsBasedOnHealth = maxBorrowAndWithdraw( borrower.assets, borrower.liabilities, - uniswapPositions, + borrower.uniswapPositions ?? [], borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, borrower.token0.decimals, borrower.token1.decimals )[isToken0 ? 0 : 1]; - // TODO: use GN - const max = Math.min(maxBorrowsBasedOnHealth, gnMaxBorrowsBasedOnMarket.toNumber()); + + const max = Math.min( + maxBorrowsBasedOnHealth, + market[isToken0 ? 'kitty0Info' : 'kitty1Info'].availableAssets.toNumber() + ); // Mitigate the case when the number is represented in scientific notation const gnEightyPercentMax = GN.fromNumber(max, borrowToken.decimals).recklessMul(80).recklessDiv(100); const maxString = gnEightyPercentMax.toString(GNFormat.DECIMAL); @@ -261,7 +260,7 @@ export default function BorrowModal(props: BorrowModalProps) { const { health: newHealth } = isHealthy( borrower.assets, newLiabilities, - uniswapPositions, + borrower.uniswapPositions ?? [], borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, @@ -269,20 +268,12 @@ export default function BorrowModal(props: BorrowModalProps) { borrower.token1.decimals ); - const availableAssets = isToken0 ? marketInfo.lender0AvailableAssets : marketInfo.lender1AvailableAssets; - const remainingAvailableAssets = availableAssets.sub(borrowAmount); - - const lenderTotalAssets = isToken0 ? marketInfo.lender0TotalAssets : marketInfo.lender1TotalAssets; - // TODO: use GN - const newUtilization = lenderTotalAssets.isGtZero() - ? 1 - remainingAvailableAssets.div(lenderTotalAssets).toNumber() - : 0; - const apr = yieldPerSecondToAPR(RateModel.computeYieldPerSecond(newUtilization)) * 100; + const apr = market[isToken0 ? 'kitty0Info' : 'kitty1Info'].hypotheticalBorrowAPR(borrowAmount) * 100; // A user is considered unhealthy if their health is 1 or less const isUnhealthy = newHealth <= 1; // A user cannot borrow more than the total supply of the market - const notEnoughSupply = gnMaxBorrowsBasedOnMarket.lt(borrowAmount); + const notEnoughSupply = borrowAmount.gt(market[isToken0 ? 'kitty0Info' : 'kitty1Info'].availableAssets); return ( diff --git a/earn/src/components/advanced/modal/RemoveCollateralModal.tsx b/earn/src/components/advanced/modal/RemoveCollateralModal.tsx index 7489bffa..1b598697 100644 --- a/earn/src/components/advanced/modal/RemoveCollateralModal.tsx +++ b/earn/src/components/advanced/modal/RemoveCollateralModal.tsx @@ -23,8 +23,6 @@ import { ChainContext } from '../../../App'; import { isHealthy, maxWithdraws } from '../../../data/BalanceSheet'; import { BorrowerNftBorrower } from '../../../data/BorrowerNft'; import { Assets } from '../../../data/MarginAccount'; -import { MarketInfo } from '../../../data/MarketInfo'; -import { UniswapPosition } from '../../../data/Uniswap'; import HealthBar from '../../common/HealthBar'; import TokenAmountSelectInput from '../../portfolio/TokenAmountSelectInput'; @@ -159,15 +157,13 @@ function RemoveCollateralButton(props: RemoveCollateralButtonProps) { export type RemoveCollateralModalProps = { borrower: BorrowerNftBorrower; - uniswapPositions: readonly UniswapPosition[]; - marketInfo: MarketInfo; isOpen: boolean; setIsOpen: (open: boolean) => void; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; export default function RemoveCollateralModal(props: RemoveCollateralModalProps) { - const { borrower, uniswapPositions, isOpen, setIsOpen, setPendingTxn } = props; + const { borrower, isOpen, setIsOpen, setPendingTxn } = props; const [collateralAmountStr, setCollateralAmountStr] = useState(''); const [collateralToken, setCollateralToken] = useState(borrower.token0); @@ -197,7 +193,7 @@ export default function RemoveCollateralModal(props: RemoveCollateralModalProps) const maxWithdrawBasedOnHealth = maxWithdraws( borrower.assets, borrower.liabilities, - uniswapPositions, + borrower.uniswapPositions ?? [], borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, @@ -220,7 +216,7 @@ export default function RemoveCollateralModal(props: RemoveCollateralModalProps) const { health: newHealth } = isHealthy( newAssets, borrower.liabilities, - uniswapPositions, + borrower.uniswapPositions ?? [], borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, diff --git a/earn/src/components/advanced/modal/RepayModal.tsx b/earn/src/components/advanced/modal/RepayModal.tsx index cc4821dd..8295b7e6 100644 --- a/earn/src/components/advanced/modal/RepayModal.tsx +++ b/earn/src/components/advanced/modal/RepayModal.tsx @@ -18,7 +18,6 @@ import { useAccount, usePrepareContractWrite, useContractWrite, useBalance, Addr import { ChainContext } from '../../../App'; import { isHealthy } from '../../../data/BalanceSheet'; import { Liabilities, MarginAccount } from '../../../data/MarginAccount'; -import { UniswapPosition } from '../../../data/Uniswap'; import HealthBar from '../../common/HealthBar'; import TokenAmountSelectInput from '../../portfolio/TokenAmountSelectInput'; @@ -195,14 +194,13 @@ function RepayButton(props: RepayButtonProps) { export type RepayModalProps = { marginAccount: MarginAccount; - uniswapPositions: readonly UniswapPosition[]; isOpen: boolean; setIsOpen: (open: boolean) => void; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; export default function RepayModal(props: RepayModalProps) { - const { marginAccount, uniswapPositions, isOpen, setIsOpen, setPendingTxn } = props; + const { marginAccount, isOpen, setIsOpen, setPendingTxn } = props; const { activeChain } = useContext(ChainContext); const [repayAmountStr, setRepayAmountStr] = useState(''); @@ -251,7 +249,7 @@ export default function RepayModal(props: RepayModalProps) { const { health: newHealth } = isHealthy( marginAccount.assets, newLiabilities, - uniswapPositions, + marginAccount.uniswapPositions ?? [], marginAccount.sqrtPriceX96, marginAccount.iv, marginAccount.nSigma, diff --git a/earn/src/components/boost/ImportBoostWidget.tsx b/earn/src/components/boost/ImportBoostWidget.tsx index e156b0ff..cce68e05 100644 --- a/earn/src/components/boost/ImportBoostWidget.tsx +++ b/earn/src/components/boost/ImportBoostWidget.tsx @@ -8,15 +8,11 @@ import Big from 'big.js'; import { ethers } from 'ethers'; import JSBI from 'jsbi'; import { borrowerNftAbi } from 'shared/lib/abis/BorrowerNft'; -import { factoryAbi } from 'shared/lib/abis/Factory'; -import { lenderLensAbi } from 'shared/lib/abis/LenderLens'; import { FilledGradientButton } from 'shared/lib/components/common/Buttons'; import { Text, Display } from 'shared/lib/components/common/Typography'; import { ALOE_II_BOOST_MANAGER_ADDRESS, ALOE_II_BORROWER_NFT_ADDRESS, - ALOE_II_FACTORY_ADDRESS, - ALOE_II_LENDER_LENS_ADDRESS, UNISWAP_NONFUNGIBLE_POSITION_MANAGER_ADDRESS, } from 'shared/lib/data/constants/ChainSpecific'; import { TERMS_OF_SERVICE_URL } from 'shared/lib/data/constants/Values'; @@ -43,9 +39,8 @@ import { import { ChainContext } from '../../App'; import { fetchListOfBorrowerNfts } from '../../data/BorrowerNft'; import { API_PRICE_RELAY_LATEST_URL } from '../../data/constants/Values'; -import { fetchMarketInfoFor, MarketInfo } from '../../data/MarketInfo'; +import { useLendingPair } from '../../data/hooks/UseLendingPairs'; import { PriceRelayLatestResponse } from '../../data/PriceRelayResponse'; -import { RateModel, yieldPerSecondToAPR } from '../../data/RateModel'; import { BoostCardInfo } from '../../data/Uniboost'; import { getValueOfLiquidity, UniswapPosition, UniswapV3GraphQL24HourPoolDataQueryResponse } from '../../data/Uniswap'; import { BOOST_MAX, BOOST_MIN } from '../../pages/boost/ImportBoostPage'; @@ -183,7 +178,6 @@ export type ImportBoostWidgetProps = { export default function ImportBoostWidget(props: ImportBoostWidgetProps) { const { cardInfo, boostFactor, iv, setBoostFactor, setPendingTxn } = props; const { activeChain } = useContext(ChainContext); - const [marketInfo, setMarketInfo] = useSafeState(null); const [twentyFourHourPoolData, setTwentyFourHourPoolData] = useSafeState( undefined ); @@ -196,6 +190,8 @@ export default function ImportBoostWidget(props: ImportBoostWidgetProps) { const provider = useProvider({ chainId: activeChain.id }); const { address: userAddress } = useAccount(); + const lendingPair = useLendingPair(cardInfo.token0.address, cardInfo.token1.address); + // Generate labels for input range (slider) const labels: string[] = []; for (let i = BOOST_MIN; i <= BOOST_MAX; i += 1) { @@ -230,63 +226,17 @@ export default function ImportBoostWidget(props: ImportBoostWidgetProps) { })(); }, [activeChain.id, cardInfo.token0.symbol, cardInfo.token1.symbol, setTokenQuotes]); - useEffect(() => { - async function fetchMarketInfo() { - // Checking each of these individually since we don't want to fetch market info when the boost factor changes - if (!provider || !cardInfo.lender0 || !cardInfo.lender1 || !cardInfo.token0 || !cardInfo.token1) return; - const lenderLensContract = new ethers.Contract( - ALOE_II_LENDER_LENS_ADDRESS[activeChain.id], - lenderLensAbi, - provider - ); - const marketInfo = await fetchMarketInfoFor( - lenderLensContract, - cardInfo.lender0, - cardInfo.lender1, - cardInfo.token0.decimals, - cardInfo.token1.decimals - ); - setMarketInfo(marketInfo); - } - fetchMarketInfo(); - }, [activeChain.id, cardInfo.lender0, cardInfo.lender1, cardInfo.token0, cardInfo.token1, provider, setMarketInfo]); - - const { data: marketParameters } = useContractRead({ - abi: factoryAbi, - address: ALOE_II_FACTORY_ADDRESS[activeChain.id], - functionName: 'getParameters', - args: [cardInfo.uniswapPool], - chainId: activeChain.id, - }); - const borrowAmount0 = GN.fromNumber(cardInfo.amount0() * (boostFactor - 1), cardInfo.token0.decimals); const borrowAmount1 = GN.fromNumber(cardInfo.amount1() * (boostFactor - 1), cardInfo.token1.decimals); const { apr0, apr1 } = useMemo(() => { - if (!marketInfo) { - return { apr0: null, apr1: null }; - } + if (!lendingPair) return { apr0: null, apr1: null }; - const availableAssets0 = marketInfo.lender0AvailableAssets; - const availableAssets1 = marketInfo.lender1AvailableAssets; - const remainingAvailableAssets0 = availableAssets0.sub(borrowAmount0); - const remainingAvailableAssets1 = availableAssets1.sub(borrowAmount1); - - const lenderTotalAssets0 = marketInfo.lender0TotalAssets; - const lenderTotalAssets1 = marketInfo.lender1TotalAssets; - - const newUtilization0 = lenderTotalAssets0.isGtZero() - ? 1 - remainingAvailableAssets0.div(lenderTotalAssets0).toNumber() - : 0; - - const newUtilization1 = lenderTotalAssets1.isGtZero() - ? 1 - remainingAvailableAssets1.div(lenderTotalAssets1).toNumber() - : 0; - - const apr0 = yieldPerSecondToAPR(RateModel.computeYieldPerSecond(newUtilization0)) * 100; - const apr1 = yieldPerSecondToAPR(RateModel.computeYieldPerSecond(newUtilization1)) * 100; - return { apr0, apr1 }; - }, [marketInfo, borrowAmount0, borrowAmount1]); + return { + apr0: lendingPair.kitty0Info.hypotheticalBorrowAPR(borrowAmount0) * 100, + apr1: lendingPair.kitty1Info.hypotheticalBorrowAPR(borrowAmount1) * 100, + }; + }, [lendingPair, borrowAmount0, borrowAmount1]); useEffect(() => { (async () => { @@ -435,15 +385,9 @@ export default function ImportBoostWidget(props: ImportBoostWidgetProps) { }); const ethToSend = useMemo(() => { - if (!marketParameters) { - return ethers.BigNumber.from(0); - } - if (availableNft !== undefined && borrowerBalance !== undefined) { - if (borrowerBalance.value.lt(marketParameters.ante)) return marketParameters.ante.sub(borrowerBalance.value); - return ethers.BigNumber.from(0); - } - return marketParameters.ante; - }, [marketParameters, availableNft, borrowerBalance]); + if (!lendingPair || !borrowerBalance || !availableNft) return ethers.BigNumber.from(0); + return lendingPair.amountEthRequiredBeforeBorrowing(borrowerBalance.value); + }, [lendingPair, borrowerBalance, availableNft]); // Prepare for actual import/mint transaction const borrowerNft = useMemo(() => new ethers.utils.Interface(borrowerNftAbi), []); diff --git a/earn/src/components/lend/LendPairCard.tsx b/earn/src/components/lend/LendPairCard.tsx index 3442b645..cf92c071 100644 --- a/earn/src/components/lend/LendPairCard.tsx +++ b/earn/src/components/lend/LendPairCard.tsx @@ -138,7 +138,11 @@ export default function LendPairCard(props: LendPairCardProps) { - + {isCardHovered && ( : } @@ -161,7 +165,11 @@ export default function LendPairCard(props: LendPairCardProps) { - + {isCardHovered && ( : } diff --git a/earn/src/components/markets/modal/BorrowModal.tsx b/earn/src/components/markets/modal/BorrowModal.tsx index f8a55426..a14a5319 100644 --- a/earn/src/components/markets/modal/BorrowModal.tsx +++ b/earn/src/components/markets/modal/BorrowModal.tsx @@ -163,10 +163,7 @@ export default function BorrowModal(props: BorrowModalProps) { if (selectedBorrow === undefined) { return null; } - return GN.fromNumber( - selectedLendingPair[isBorrowingToken0 ? 'kitty0Info' : 'kitty1Info'].totalSupply, - selectedBorrow.decimals - ); + return selectedLendingPair[isBorrowingToken0 ? 'kitty0Info' : 'kitty1Info'].availableAssets; }, [selectedBorrow, selectedLendingPair, isBorrowingToken0]); const maxBorrowHealthConstraint = useMemo(() => { @@ -207,7 +204,7 @@ export default function BorrowModal(props: BorrowModalProps) { const estimatedApr = useMemo(() => { const { kitty0Info, kitty1Info } = selectedLendingPair; - const numericLenderTotalAssets = isBorrowingToken0 ? kitty0Info.totalSupply : kitty1Info.totalSupply; + const numericLenderTotalAssets = (isBorrowingToken0 ? kitty0Info.totalAssets : kitty1Info.totalAssets).toNumber(); const lenderTotalAssets = GN.fromNumber(numericLenderTotalAssets, selectedBorrow.decimals); const lenderUtilization = isBorrowingToken0 ? kitty0Info.utilization : kitty1Info.utilization; diff --git a/earn/src/components/markets/modal/BorrowModalUniswap.tsx b/earn/src/components/markets/modal/BorrowModalUniswap.tsx index eb4d2b85..5f1201c4 100644 --- a/earn/src/components/markets/modal/BorrowModalUniswap.tsx +++ b/earn/src/components/markets/modal/BorrowModalUniswap.tsx @@ -148,7 +148,7 @@ export default function BorrowModalUniswap(props: BorrowModalProps) { return null; } const lenderInfo = selectedLendingPair[isBorrowingToken0 ? 'kitty0Info' : 'kitty1Info']; - return lenderInfo.inventory * (1 - lenderInfo.utilization); + return lenderInfo.availableAssets.toNumber(); }, [selectedBorrow, selectedLendingPair, isBorrowingToken0]); const maxBorrowHealthConstraint = useMemo(() => { @@ -186,7 +186,7 @@ export default function BorrowModalUniswap(props: BorrowModalProps) { const estimatedApr = useMemo(() => { const { kitty0Info, kitty1Info } = selectedLendingPair; - const numericLenderTotalAssets = isBorrowingToken0 ? kitty0Info.totalSupply : kitty1Info.totalSupply; + const numericLenderTotalAssets = (isBorrowingToken0 ? kitty0Info.totalAssets : kitty1Info.totalAssets).toNumber(); const lenderTotalAssets = GN.fromNumber(numericLenderTotalAssets, selectedBorrow.decimals); const lenderUtilization = isBorrowingToken0 ? kitty0Info.utilization : kitty1Info.utilization; diff --git a/earn/src/components/markets/modal/content/BorrowModalContent.tsx b/earn/src/components/markets/modal/content/BorrowModalContent.tsx index 15dac34e..fe2f4a8a 100644 --- a/earn/src/components/markets/modal/content/BorrowModalContent.tsx +++ b/earn/src/components/markets/modal/content/BorrowModalContent.tsx @@ -25,7 +25,6 @@ import { isHealthy, maxBorrowAndWithdraw } from '../../../../data/BalanceSheet'; import { BorrowerNftBorrower } from '../../../../data/BorrowerNft'; import { LendingPair } from '../../../../data/LendingPair'; import { Liabilities } from '../../../../data/MarginAccount'; -import { RateModel, yieldPerSecondToAPR } from '../../../../data/RateModel'; import HealthBar from '../../../common/HealthBar'; const GAS_ESTIMATE_WIGGLE_ROOM = 110; @@ -219,14 +218,8 @@ export default function BorrowModalContent(props: BorrowModalContentProps) { accountEtherBalance !== undefined && accountEtherBalance.lt(ante) ? ante.sub(accountEtherBalance) : GN.zero(18); const lenderInfo = lendingPair?.[isBorrowingToken0 ? 'kitty0Info' : 'kitty1Info']; - const inventoryTotal = lenderInfo?.inventory || 0; - const inventoryAvailable = inventoryTotal * (lenderInfo?.utilization || 0); - // Compute updated utilization and apr - const inventoryAvailableNew = inventoryAvailable - borrowAmount.toNumber(); - let utilizationNew = inventoryTotal > 0 ? 1 - inventoryAvailableNew / inventoryTotal : 0; - if (inventoryAvailableNew < 0) utilizationNew = 1; - const apr = yieldPerSecondToAPR(RateModel.computeYieldPerSecond(utilizationNew)) * 100; + const apr = (lenderInfo?.hypotheticalBorrowAPR(borrowAmount) || 0) * 100; // TODO: use GN const maxBorrowsBasedOnHealth = maxBorrowAndWithdraw( @@ -241,7 +234,7 @@ export default function BorrowModalContent(props: BorrowModalContentProps) { )[isBorrowingToken0 ? 0 : 1]; // TODO: use GN - const max = Math.min(maxBorrowsBasedOnHealth, inventoryAvailable); + const max = Math.min(maxBorrowsBasedOnHealth, lenderInfo?.availableAssets.toNumber() || 0); // Mitigate the case when the number is represented in scientific notation const eightyPercentMaxBorrowAmount = GN.fromNumber(max, borrowToken.decimals).recklessMul(80).recklessDiv(100); @@ -266,7 +259,7 @@ export default function BorrowModalContent(props: BorrowModalContentProps) { // A user is considered unhealthy if their health is 1 or less const isUnhealthy = newHealth <= 1; // A user cannot borrow more than the total supply of the market - const notEnoughSupply = borrowAmount.toNumber() > inventoryAvailable; + const notEnoughSupply = lenderInfo !== undefined && borrowAmount.gt(lenderInfo.availableAssets); return ( <> diff --git a/earn/src/components/portfolio/LendingPairPeerCard.tsx b/earn/src/components/portfolio/LendingPairPeerCard.tsx index 2e7bc4c8..e45d68d4 100644 --- a/earn/src/components/portfolio/LendingPairPeerCard.tsx +++ b/earn/src/components/portfolio/LendingPairPeerCard.tsx @@ -127,9 +127,9 @@ function getActiveUtilizationAndTotalSupply(activeAsset: Token, selectedLendingP const token0Address = selectedLendingPair.token0.address; const token1Address = selectedLendingPair.token1.address; if (activeAssetAddress === token0Address) { - return [selectedLendingPair.kitty0Info.utilization, selectedLendingPair.kitty0Info.inventory]; + return [selectedLendingPair.kitty0Info.utilization, selectedLendingPair.kitty0Info.totalAssets.toNumber()]; } else if (activeAssetAddress === token1Address) { - return [selectedLendingPair.kitty1Info.utilization, selectedLendingPair.kitty1Info.inventory]; + return [selectedLendingPair.kitty1Info.utilization, selectedLendingPair.kitty1Info.totalAssets.toNumber()]; } else { return [0, 0]; } diff --git a/earn/src/data/FactoryData.ts b/earn/src/data/FactoryData.ts new file mode 100644 index 00000000..5d2aff87 --- /dev/null +++ b/earn/src/data/FactoryData.ts @@ -0,0 +1,17 @@ +import { GN } from 'shared/lib/data/GoodNumber'; + +export type FactoryData = { + ante: GN; + nSigma: number; + manipulationThresholdDivisor: number; + pausedUntilTime: number; +}; + +export function asFactoryData(multicallResult: any[]): FactoryData { + return { + ante: GN.fromBigNumber(multicallResult[0], 18), + nSigma: (multicallResult[1] as number) / 10, + manipulationThresholdDivisor: multicallResult[2], + pausedUntilTime: multicallResult[3], + }; +} diff --git a/earn/src/data/LendingPair.ts b/earn/src/data/LendingPair.ts index 2aef6319..efc942e6 100644 --- a/earn/src/data/LendingPair.ts +++ b/earn/src/data/LendingPair.ts @@ -27,33 +27,33 @@ import { ContractCallReturnContextEntries, convertBigNumbersForReturnContexts } import { computeLTV } from './BalanceSheet'; import { UNISWAP_POOL_DENYLIST } from './constants/Addresses'; import { TOPIC0_CREATE_MARKET_EVENT } from './constants/Signatures'; -import { borrowAPRToLendAPY } from './RateModel'; +import { asFactoryData, FactoryData } from './FactoryData'; +import { asOracleData, OracleData } from './OracleData'; +import { borrowAPRToLendAPY, RateModel, yieldPerSecondToAPR } from './RateModel'; class KittyInfo { + public readonly availableAssets: GN; + public readonly utilization: number; public readonly lendAPY: number; constructor( - public readonly inventory: number, - public readonly totalSupply: number, + public readonly totalAssets: GN, + public readonly totalBorrows: GN, + public readonly totalSupply: GN, public readonly borrowAPR: number, - public readonly utilization: number, public readonly reserveFactor: number ) { - this.lendAPY = borrowAPRToLendAPY(borrowAPR, utilization, reserveFactor); + this.availableAssets = totalAssets.sub(totalBorrows); + this.utilization = totalBorrows.div(totalAssets).toNumber(); + this.lendAPY = borrowAPRToLendAPY(borrowAPR, this.utilization, reserveFactor); } -} - -type FactoryData = { - ante: GN; - nSigma: number; - manipulationThresholdDivisor: number; - pausedUntilTime: number; -}; -type OracleData = { - iv: GN; - manipulationMetric: number; -}; + hypotheticalBorrowAPR(additionalBorrowAmount: GN) { + const hypotheticalUtilization = this.totalBorrows.add(additionalBorrowAmount).div(this.totalAssets); + // TODO: This only works for the current RateModel. If there are others, we'll need to update this. + return yieldPerSecondToAPR(RateModel.computeYieldPerSecond(hypotheticalUtilization.toNumber())); + } +} export class LendingPair { constructor( @@ -86,6 +86,11 @@ export class LendingPair { get manipulationThreshold() { return -Math.log(this.ltv) / Math.log(1.0001) / this.factoryData.manipulationThresholdDivisor; } + + amountEthRequiredBeforeBorrowing(currentBorrowerBalance: BigNumber) { + const ante = this.factoryData.ante.toBigNumber(); + return currentBorrowerBalance.gte(ante) ? BigNumber.from(0) : ante.sub(currentBorrowerBalance); + } } export type LendingPairBalances = { @@ -253,26 +258,14 @@ export async function getAvailableLendingPairs( const borrowAPR0 = toImpreciseNumber(basics0[1].mul(secondsInYear), 12); const borrowAPR1 = toImpreciseNumber(basics1[1].mul(secondsInYear), 12); - const utilization0 = toImpreciseNumber(basics0[2], 18); - const utilization1 = toImpreciseNumber(basics1[2], 18); - - const inventory0 = toImpreciseNumber(basics0[3], token0.decimals); - const inventory1 = toImpreciseNumber(basics1[3], token1.decimals); + const totalAssets0 = GN.fromBigNumber(basics0[3], token0.decimals); + const totalAssets1 = GN.fromBigNumber(basics1[3], token1.decimals); - const totalSupply0 = toImpreciseNumber(basics0[5], kitty0.decimals); - const totalSupply1 = toImpreciseNumber(basics1[5], kitty1.decimals); + const totalBorrows0 = GN.fromBigNumber(basics0[4], token0.decimals); + const totalBorrows1 = GN.fromBigNumber(basics1[4], token1.decimals); - const oracleData = { - iv: GN.fromBigNumber(oracleResult[2], 12), - manipulationMetric: oracleResult[0].toNumber(), - }; - - const factoryData = { - ante: GN.fromBigNumber(factoryResult[0], 18), - nSigma: (factoryResult[1] as number) / 10, - manipulationThresholdDivisor: factoryResult[2], - pausedUntilTime: factoryResult[3], - }; + const totalSupply0 = GN.fromBigNumber(basics0[5], kitty0.decimals); + const totalSupply1 = GN.fromBigNumber(basics1[5], kitty1.decimals); lendingPairs.push( new LendingPair( @@ -280,14 +273,14 @@ export async function getAvailableLendingPairs( token1, kitty0, kitty1, - new KittyInfo(inventory0, totalSupply0, borrowAPR0, utilization0, basics0[6]), - new KittyInfo(inventory1, totalSupply1, borrowAPR1, utilization1, basics1[6]), + new KittyInfo(totalAssets0, totalBorrows0, totalSupply0, borrowAPR0, basics0[6]), + new KittyInfo(totalAssets1, totalBorrows1, totalSupply1, borrowAPR1, basics1[6]), uniswapPool as Address, NumericFeeTierToEnum(feeTier[0]), toImpreciseNumber(basics0[7], 18), // rewardsRate0 toImpreciseNumber(basics1[7], 18), // rewardsRate1 - factoryData, - oracleData + asFactoryData(factoryResult), + asOracleData(oracleResult) ) ); }); diff --git a/earn/src/data/MarketInfo.ts b/earn/src/data/MarketInfo.ts deleted file mode 100644 index 124d24b2..00000000 --- a/earn/src/data/MarketInfo.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { secondsInYear } from 'date-fns'; -import { Multicall } from 'ethereum-multicall'; -import { CallContext, ContractCallContext } from 'ethereum-multicall/dist/esm/models'; -import { ethers } from 'ethers'; -import { lenderLensAbi } from 'shared/lib/abis/LenderLens'; -import { ALOE_II_LENDER_LENS_ADDRESS, MULTICALL_ADDRESS } from 'shared/lib/data/constants/ChainSpecific'; -import { GN } from 'shared/lib/data/GoodNumber'; -import { toBig, toImpreciseNumber } from 'shared/lib/util/Numbers'; -import { Address } from 'wagmi'; - -import { convertBigNumbersForReturnContexts } from '../util/Multicall'; - -// TODO: This is completely unnecessary. All info is available in `lendingPairs` (just need to refactor for GN) -export type MarketInfo = { - lender0: Address; - lender1: Address; - borrowerAPR0: number; - borrowerAPR1: number; - lender0Utilization: number; - lender1Utilization: number; - lender0TotalAssets: GN; - lender1TotalAssets: GN; - lender0TotalBorrows: GN; - lender1TotalBorrows: GN; - lender0AvailableAssets: GN; - lender1AvailableAssets: GN; -}; - -export type Market = { - lender0: Address; - lender1: Address; - token0Decimals: number; - token1Decimals: number; -}; - -// TODO: This is completely unnecessary. All info is available in `lendingPairs` (just need to refactor for GN) -export async function fetchMarketInfos( - markets: Market[], - chainId: number, - provider: ethers.providers.Provider -): Promise> { - if (markets.length === 0) return []; - const multicall = new Multicall({ - ethersProvider: provider, - tryAggregate: true, - multicallCustomContractAddress: MULTICALL_ADDRESS[chainId], - }); - const marketCallContexts: CallContext[] = []; - markets.forEach(({ lender0, lender1 }) => { - marketCallContexts.push({ - methodName: 'readBasics', - methodParameters: [lender0], - reference: 'lender0', - }); - marketCallContexts.push({ - methodName: 'readBasics', - methodParameters: [lender1], - reference: 'lender1', - }); - }); - - const marketCallContext: ContractCallContext[] = [ - { - abi: lenderLensAbi as any, - calls: marketCallContexts, - contractAddress: ALOE_II_LENDER_LENS_ADDRESS[chainId], - reference: 'lenderLens', - }, - ]; - - const lenderLensResults = (await multicall.call(marketCallContext)).results['lenderLens']; - - const marketInfoResults = convertBigNumbersForReturnContexts(lenderLensResults.callsReturnContext); - - return markets.map(({ lender0, lender1, token0Decimals, token1Decimals }, index) => { - const lender0Basics = marketInfoResults[index * 2]; - const lender1Basics = marketInfoResults[index * 2 + 1]; - - const interestRate0 = toBig(lender0Basics.returnValues[1]); - const borrowAPR0 = interestRate0.eq('0') ? 0 : interestRate0.div(1e12).toNumber() * secondsInYear; - - const interestRate1 = toBig(lender1Basics.returnValues[1]); - const borrowAPR1 = interestRate1.eq('0') ? 0 : interestRate1.div(1e12).toNumber() * secondsInYear; - - const lender0Utilization = toImpreciseNumber(lender0Basics.returnValues[2], 18); - const lender1Utilization = toImpreciseNumber(lender1Basics.returnValues[2], 18); - const lender0Inventory = GN.fromBigNumber(lender0Basics.returnValues[3], token0Decimals); - const lender1Inventory = GN.fromBigNumber(lender1Basics.returnValues[3], token1Decimals); - const lender0TotalBorrows = GN.fromBigNumber(lender0Basics.returnValues[4], token0Decimals); - const lender1TotalBorrows = GN.fromBigNumber(lender1Basics.returnValues[4], token1Decimals); - - return { - lender0, - lender1, - borrowerAPR0: borrowAPR0, - borrowerAPR1: borrowAPR1, - lender0Utilization: lender0Utilization, - lender1Utilization: lender1Utilization, - lender0TotalAssets: lender0Inventory, - lender1TotalAssets: lender1Inventory, - lender0TotalBorrows: lender0TotalBorrows, - lender1TotalBorrows: lender1TotalBorrows, - lender0AvailableAssets: lender0Inventory.sub(lender0TotalBorrows), - lender1AvailableAssets: lender1Inventory.sub(lender1TotalBorrows), - } as MarketInfo; - }); -} - -export async function fetchMarketInfoFor( - lenderLensContract: ethers.Contract, - lender0: Address, - lender1: Address, - token0Decimals: number, - token1Decimals: number -): Promise { - const [lender0Basics, lender1Basics] = await Promise.all([ - lenderLensContract.readBasics(lender0), - lenderLensContract.readBasics(lender1), - ]); - - const interestRate0 = toBig(lender0Basics.interestRate); - const borrowAPR0 = interestRate0.eq('0') ? 0 : interestRate0.div(1e12).toNumber() * secondsInYear; - const interestRate1 = toBig(lender1Basics.interestRate); - const borrowAPR1 = interestRate1.eq('0') ? 0 : interestRate1.div(1e12).toNumber() * secondsInYear; - const lender0Utilization = toImpreciseNumber(lender0Basics.utilization, 18); - const lender1Utilization = toImpreciseNumber(lender1Basics.utilization, 18); - const lender0Inventory = GN.fromBigNumber(lender0Basics.inventory, token0Decimals); - const lender1Inventory = GN.fromBigNumber(lender1Basics.inventory, token1Decimals); - const lender0TotalBorrows = GN.fromBigNumber(lender0Basics.totalBorrows, token0Decimals); - const lender1TotalBorrows = GN.fromBigNumber(lender1Basics.totalBorrows, token1Decimals); - return { - lender0, - lender1, - borrowerAPR0: borrowAPR0, - borrowerAPR1: borrowAPR1, - lender0Utilization: lender0Utilization, - lender1Utilization: lender1Utilization, - lender0TotalAssets: lender0Inventory, - lender1TotalAssets: lender1Inventory, - lender0TotalBorrows: lender0TotalBorrows, - lender1TotalBorrows: lender1TotalBorrows, - lender0AvailableAssets: lender0Inventory.sub(lender0TotalBorrows), - lender1AvailableAssets: lender1Inventory.sub(lender1TotalBorrows), - }; -} diff --git a/earn/src/data/OracleData.ts b/earn/src/data/OracleData.ts new file mode 100644 index 00000000..a81956e1 --- /dev/null +++ b/earn/src/data/OracleData.ts @@ -0,0 +1,15 @@ +import { GN } from 'shared/lib/data/GoodNumber'; + +export type OracleData = { + iv: GN; + sqrtPriceX96: GN; + manipulationMetric: number; +}; + +export function asOracleData(multicallResult: any[]): OracleData { + return { + iv: GN.fromBigNumber(multicallResult[2], 12), + sqrtPriceX96: GN.fromBigNumber(multicallResult[1], 96, 2), + manipulationMetric: multicallResult[0].toNumber(), + }; +} diff --git a/earn/src/data/hooks/UseLendingPairs.ts b/earn/src/data/hooks/UseLendingPairs.ts index 55a9ada7..e333589f 100644 --- a/earn/src/data/hooks/UseLendingPairs.ts +++ b/earn/src/data/hooks/UseLendingPairs.ts @@ -1,5 +1,7 @@ import { createContext, useContext, useMemo } from 'react'; +import { Address } from 'wagmi'; + import { LendingPair } from '../LendingPair'; export const LendingPairsContext = createContext(null); @@ -15,3 +17,25 @@ export function useLendingPairs() { [ctxt] ); } + +export function useLendingPair(token0?: Address, token1?: Address) { + const ctxt = useContext(LendingPairsContext); + + const { lendingPairs } = useMemo( + () => ({ + isLoading: ctxt === null, + lendingPairs: ctxt ?? [], + }), + [ctxt] + ); + + return useMemo( + () => + lendingPairs.find( + (pair) => + pair.token0.address.toLowerCase() === token0?.toLowerCase() && + pair.token1.address.toLowerCase() === token1?.toLowerCase() + ), + [lendingPairs, token0, token1] + ); +} diff --git a/earn/src/pages/AdvancedPage.tsx b/earn/src/pages/AdvancedPage.tsx index 0c75001f..644f158d 100644 --- a/earn/src/pages/AdvancedPage.tsx +++ b/earn/src/pages/AdvancedPage.tsx @@ -5,26 +5,19 @@ import { SendTransactionResult } from '@wagmi/core'; import { ethers } from 'ethers'; import JSBI from 'jsbi'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { borrowerAbi } from 'shared/lib/abis/Borrower'; import { borrowerLensAbi } from 'shared/lib/abis/BorrowerLens'; -import { lenderLensAbi } from 'shared/lib/abis/LenderLens'; import Banner from 'shared/lib/components/banner/Banner'; import AppPage from 'shared/lib/components/common/AppPage'; import { Text } from 'shared/lib/components/common/Typography'; -import { - ALOE_II_LENDER_LENS_ADDRESS, - ALOE_II_BORROWER_LENS_ADDRESS, - ALOE_II_BORROWER_NFT_ADDRESS, -} from 'shared/lib/data/constants/ChainSpecific'; +import { ALOE_II_BORROWER_LENS_ADDRESS, ALOE_II_BORROWER_NFT_ADDRESS } from 'shared/lib/data/constants/ChainSpecific'; import { GetNumericFeeTier } from 'shared/lib/data/FeeTier'; import { GN } from 'shared/lib/data/GoodNumber'; import { useChainDependentState } from 'shared/lib/data/hooks/UseChainDependentState'; -import { useDebouncedEffect } from 'shared/lib/data/hooks/UseDebouncedEffect'; import useSafeState from 'shared/lib/data/hooks/UseSafeState'; import { Token } from 'shared/lib/data/Token'; import { getEtherscanUrlForChain } from 'shared/lib/util/Chains'; import styled from 'styled-components'; -import { Address, useAccount, useContract, useProvider, useContractRead, useBalance } from 'wagmi'; +import { Address, useAccount, useContract, useProvider, useBalance } from 'wagmi'; import { ChainContext } from '../App'; import { ReactComponent as InfoIcon } from '../assets/svg/info.svg'; @@ -44,18 +37,11 @@ import { BorrowerNftBorrower, fetchListOfBorrowerNfts } from '../data/BorrowerNf import { RESPONSIVE_BREAKPOINT_SM } from '../data/constants/Breakpoints'; import { primeUrl } from '../data/constants/Values'; import useAvailablePools from '../data/hooks/UseAvailablePools'; +import { useLendingPair } from '../data/hooks/UseLendingPairs'; import { fetchBorrowerDatas } from '../data/MarginAccount'; -import { fetchMarketInfoFor, MarketInfo } from '../data/MarketInfo'; -import { - fetchUniswapNFTPositions, - fetchUniswapPositions, - UniswapNFTPosition, - UniswapPosition, - UniswapPositionPrior, -} from '../data/Uniswap'; +import { fetchUniswapNFTPositions, UniswapNFTPosition } from '../data/Uniswap'; const BORROW_TITLE_TEXT_COLOR = 'rgba(130, 160, 182, 1)'; -const FETCH_UNISWAP_POSITIONS_DEBOUNCE_MS = 500; const SELECTED_MARGIN_ACCOUNT_KEY = 'account'; const Container = styled.div` @@ -153,14 +139,7 @@ export default function AdvancedPage() { null, activeChain.id ); - const [cachedUniswapPositionsMap, setCachedUniswapPositionsMap] = useSafeState< - Map - >(new Map()); - const [isLoadingUniswapPositions, setIsLoadingUniswapPositions] = useSafeState(true); - const [uniswapPositions, setUniswapPositions] = useSafeState([]); const [uniswapNFTPositions, setUniswapNFTPositions] = useSafeState>(new Map()); - const [cachedMarketInfos, setCachedMarketInfos] = useSafeState>(new Map()); - const [selectedMarketInfo, setSelectedMarketInfo] = useSafeState(undefined); const [newSmartWalletModalOpen, setNewSmartWalletModalOpen] = useState(false); const [isAddCollateralModalOpen, setIsAddCollateralModalOpen] = useState(false); const [isRemoveCollateralModalOpen, setIsRemoveCollateralModalOpen] = useState(false); @@ -183,18 +162,13 @@ export default function AdvancedPage() { ); }, [borrowerNftBorrowers, searchParams]); + const market = useLendingPair(selectedMarginAccount?.token0.address, selectedMarginAccount?.token1.address); + const borrowerLensContract = useContract({ abi: borrowerLensAbi, address: ALOE_II_BORROWER_LENS_ADDRESS[activeChain.id], signerOrProvider: provider, }); - const { data: uniswapPositionTicks } = useContractRead({ - address: selectedMarginAccount?.address ?? '0x', - abi: borrowerAbi, - functionName: 'getUniswapPositions', - chainId: activeChain.id, - enabled: Boolean(selectedMarginAccount), - }); const availablePools = useAvailablePools(); @@ -232,94 +206,6 @@ export default function AdvancedPage() { } }, [borrowerNftBorrowers?.length, searchParams, selectedMarginAccount, setSearchParams]); - // MARK: Fetch market info - useEffect(() => { - const cachedMarketInfo = cachedMarketInfos.get(selectedMarginAccount?.address ?? ''); - if (cachedMarketInfo !== undefined) { - setSelectedMarketInfo(cachedMarketInfo); - return; - } - (async () => { - if (selectedMarginAccount == null) return; - const lenderLensContract = new ethers.Contract( - ALOE_II_LENDER_LENS_ADDRESS[activeChain.id], - lenderLensAbi, - provider - ); - const result = await fetchMarketInfoFor( - lenderLensContract, - selectedMarginAccount.lender0, - selectedMarginAccount.lender1, - selectedMarginAccount.token0.decimals, - selectedMarginAccount.token1.decimals - ); - setCachedMarketInfos((prev) => { - return new Map(prev).set(selectedMarginAccount.address, result); - }); - setSelectedMarketInfo(result); - })(); - }, [selectedMarginAccount, provider, cachedMarketInfos, activeChain.id, setSelectedMarketInfo, setCachedMarketInfos]); - - // MARK: Fetch Uniswap positions for this MarginAccount (debounced to avoid double-fetching) - useDebouncedEffect( - () => { - setIsLoadingUniswapPositions(true); - const cachedUniswapPositions = cachedUniswapPositionsMap.get(selectedMarginAccount?.address ?? ''); - if (cachedUniswapPositions !== undefined) { - // If we have cached positions, set them and return (no need to fetch) - setUniswapPositions(cachedUniswapPositions); - setIsLoadingUniswapPositions(false); - return; - } - (async () => { - if (!Array.isArray(uniswapPositionTicks)) { - setCachedUniswapPositionsMap((prev) => { - return new Map(prev).set(selectedMarginAccount?.address ?? '', []); - }); - setIsLoadingUniswapPositions(false); - return; - } - - // Convert the ticks into UniswapPositionPriors - const uniswapPositionPriors: UniswapPositionPrior[] = []; - for (let i = 0; i < uniswapPositionTicks.length; i += 2) { - uniswapPositionPriors.push({ - lower: uniswapPositionTicks[i] as number, - upper: uniswapPositionTicks[i + 1] as number, - }); - } - if (uniswapPositionPriors.length === 0 || selectedMarginAccount === undefined) { - setCachedUniswapPositionsMap((prev) => { - return new Map(prev).set(selectedMarginAccount?.address ?? '', []); - }); - setIsLoadingUniswapPositions(false); - return; - } - - // Fetch the positions - const fetchedUniswapPositionsMap = await fetchUniswapPositions( - uniswapPositionPriors, - selectedMarginAccount.address, - selectedMarginAccount.uniswapPool, - provider, - activeChain - ); - // We only want the values, not the keys - const fetchedUniswapPositions = Array.from(fetchedUniswapPositionsMap.values()); - - // Cache the positions - setCachedUniswapPositionsMap((prev) => { - return new Map(prev).set(selectedMarginAccount.address, fetchedUniswapPositions); - }); - // Set the positions - setUniswapPositions(fetchedUniswapPositions); - setIsLoadingUniswapPositions(false); - })(); - }, - FETCH_UNISWAP_POSITIONS_DEBOUNCE_MS, - [uniswapPositionTicks] - ); - useEffect(() => { (async () => { if (userAddress === undefined) return; @@ -370,7 +256,7 @@ export default function AdvancedPage() { const withdrawableUniswapNFTPositions = useMemo(() => { const filteredPositions: Map = new Map(); if (selectedMarginAccount == null) return filteredPositions; - uniswapPositions.forEach((uniswapPosition) => { + selectedMarginAccount.uniswapPositions?.forEach((uniswapPosition) => { const isNonZero = JSBI.greaterThan(uniswapPosition.liquidity, JSBI.BigInt('0')); const matchingNFTPosition = Array.from(uniswapNFTPositions.entries()).find(([, position]) => { return position.lower === uniswapPosition.lower && position.upper === uniswapPosition.upper; @@ -380,14 +266,14 @@ export default function AdvancedPage() { } }); return filteredPositions; - }, [selectedMarginAccount, uniswapPositions, uniswapNFTPositions]); + }, [selectedMarginAccount, uniswapNFTPositions]); const defaultPool = Array.from(availablePools.keys())[0]; const dailyInterest0 = - ((selectedMarketInfo?.borrowerAPR0 || 0) / 365) * (selectedMarginAccount?.liabilities.amount0 || 0); + ((market?.kitty0Info.borrowAPR || 0) / 365) * (selectedMarginAccount?.liabilities.amount0 || 0); const dailyInterest1 = - ((selectedMarketInfo?.borrowerAPR1 || 0) / 365) * (selectedMarginAccount?.liabilities.amount1 || 0); + ((market?.kitty1Info.borrowAPR || 0) / 365) * (selectedMarginAccount?.liabilities.amount1 || 0); const baseEtherscanUrl = getEtherscanUrlForChain(activeChain); const selectedMarginAccountEtherscanUrl = `${baseEtherscanUrl}/address/${selectedMarginAccount?.address}`; @@ -458,8 +344,6 @@ export default function AdvancedPage() { // selectedMarginAccount, selectedMarketInfo, and uniswapPositions // setSelectedMarginAccount(account); setSearchParams({ [SELECTED_MARGIN_ACCOUNT_KEY]: account.address }); - setSelectedMarketInfo(cachedMarketInfos.get(account.address) ?? undefined); - setUniswapPositions(cachedUniswapPositionsMap.get(account.address) ?? []); }} key={account.address} /> @@ -476,16 +360,14 @@ export default function AdvancedPage() { marginAccount={selectedMarginAccount} dailyInterest0={dailyInterest0} dailyInterest1={dailyInterest1} - uniswapPositions={uniswapPositions} userHasNoMarginAccounts={userHasNoMarginAccounts} /> - + {selectedMarginAccount && (
@@ -517,13 +399,10 @@ export default function AdvancedPage() { setPendingTxn={setPendingTxn} /> )} - {selectedMarginAccount && selectedMarketInfo && ( + {selectedMarginAccount && market && ( <> Date: Thu, 22 Feb 2024 21:35:32 -0600 Subject: [PATCH 2/4] More --- .../src/components/advanced/BorrowMetrics.tsx | 29 +++----- .../advanced/UniswapPositionList.tsx | 4 +- .../advanced/modal/AddCollateralModal.tsx | 12 ++-- .../components/advanced/modal/BorrowModal.tsx | 2 - .../advanced/modal/RemoveCollateralModal.tsx | 20 ++---- .../components/advanced/modal/RepayModal.tsx | 1 - .../advanced/modal/tab/AddCollateralTab.tsx | 32 +++------ earn/src/components/boost/BurnBoostModal.tsx | 6 +- .../markets/borrow/BorrowingWidget.tsx | 26 +++---- .../markets/modal/BorrowModalUniswap.tsx | 11 ++- .../markets/modal/UpdateCollateralModal.tsx | 4 +- .../content/AddCollateralModalContent.tsx | 18 ++--- .../modal/content/BorrowModalContent.tsx | 2 - .../content/RemoveCollateralModalContent.tsx | 21 +++--- .../modal/content/RepayModalContent.tsx | 1 - .../content/ToUniswapNFTModalContent.tsx | 3 +- earn/src/data/BalanceSheet.ts | 67 +++---------------- earn/src/data/MarginAccount.ts | 51 +++++++++----- earn/src/data/Uniboost.ts | 35 +++------- earn/src/pages/AdvancedPage.tsx | 2 +- earn/src/pages/boost/ImportBoostPage.tsx | 8 +-- 21 files changed, 130 insertions(+), 225 deletions(-) diff --git a/earn/src/components/advanced/BorrowMetrics.tsx b/earn/src/components/advanced/BorrowMetrics.tsx index 339f3b95..bfa26c89 100644 --- a/earn/src/components/advanced/BorrowMetrics.tsx +++ b/earn/src/components/advanced/BorrowMetrics.tsx @@ -6,7 +6,7 @@ import { GREY_700 } from 'shared/lib/data/constants/Colors'; import { formatTokenAmount } from 'shared/lib/util/Numbers'; import styled from 'styled-components'; -import { computeLiquidationThresholds, getAssets, sqrtRatioToPrice } from '../../data/BalanceSheet'; +import { computeLiquidationThresholds, getAssets, sqrtRatioToPrice, sqrtRatioToTick } from '../../data/BalanceSheet'; import { RESPONSIVE_BREAKPOINT_MD, RESPONSIVE_BREAKPOINT_SM } from '../../data/constants/Breakpoints'; import { MarginAccount } from '../../data/MarginAccount'; @@ -166,13 +166,18 @@ export type BorrowMetricsProps = { export function BorrowMetrics(props: BorrowMetricsProps) { const { marginAccount, dailyInterest0, dailyInterest1, userHasNoMarginAccounts } = props; + const [token0Collateral, token1Collateral] = useMemo( + () => marginAccount?.assets.amountsAt(sqrtRatioToTick(marginAccount.sqrtPriceX96)) ?? [0, 0], + [marginAccount] + ); + const maxSafeCollateralFall = useMemo(() => { if (!marginAccount) return null; const { lowerSqrtRatio, upperSqrtRatio, minSqrtRatio, maxSqrtRatio } = computeLiquidationThresholds( marginAccount.assets, marginAccount.liabilities, - marginAccount.uniswapPositions ?? [], + marginAccount.assets.uniswapPositions, marginAccount.sqrtPriceX96, marginAccount.iv, marginAccount.nSigma, @@ -186,22 +191,11 @@ export function BorrowMetrics(props: BorrowMetricsProps) { sqrtRatioToPrice(sp, marginAccount.token0.decimals, marginAccount.token1.decimals) ); - const assets = getAssets( - marginAccount.assets.token0Raw, - marginAccount.assets.token1Raw, - marginAccount.uniswapPositions ?? [], - lowerSqrtRatio, - upperSqrtRatio, - marginAccount.token0.decimals, - marginAccount.token1.decimals - ); + const assets = getAssets(marginAccount.assets, lowerSqrtRatio, upperSqrtRatio); // Compute the value of all assets (collateral) at 3 different prices (current, lower, and upper) // Denominated in units of token1 - let assetValueCurrent = - (marginAccount.assets.token0Raw + marginAccount.assets.uni0) * current + - marginAccount.assets.token1Raw + - marginAccount.assets.uni1; + let assetValueCurrent = token0Collateral * current + token1Collateral; let assetValueAtLower = assets.amount0AtA * lower + assets.amount1AtA; let assetValueAtUpper = assets.amount0AtB * upper + assets.amount1AtB; @@ -227,7 +221,7 @@ export function BorrowMetrics(props: BorrowMetricsProps) { // Since we don't know whether the user is thinking in terms of "X per Y" or "Y per X", // we return the minimum. Error on the side of being too conservative. return Math.min(percentChange0, percentChange1); - }, [marginAccount]); + }, [marginAccount, token0Collateral, token1Collateral]); if (!marginAccount) return ( @@ -252,9 +246,6 @@ export function BorrowMetrics(props: BorrowMetricsProps) { else liquidationDistanceText = `${(maxSafeCollateralFall * 100).toPrecision(2)}% drop in collateral value`; } - const token0Collateral = marginAccount.assets.token0Raw + marginAccount.assets.uni0; - const token1Collateral = marginAccount.assets.token1Raw + marginAccount.assets.uni1; - return ( diff --git a/earn/src/components/advanced/UniswapPositionList.tsx b/earn/src/components/advanced/UniswapPositionList.tsx index f8bdbd5c..7d496f88 100644 --- a/earn/src/components/advanced/UniswapPositionList.tsx +++ b/earn/src/components/advanced/UniswapPositionList.tsx @@ -186,7 +186,7 @@ export function UniswapPositionList(props: UniswapPositionListProps) { {slot} { setSelectedUniswapPosition(null); diff --git a/earn/src/components/advanced/modal/AddCollateralModal.tsx b/earn/src/components/advanced/modal/AddCollateralModal.tsx index 46fa865a..d43e905a 100644 --- a/earn/src/components/advanced/modal/AddCollateralModal.tsx +++ b/earn/src/components/advanced/modal/AddCollateralModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { SendTransactionResult } from '@wagmi/core'; import { FilledGradientButton } from 'shared/lib/components/common/Buttons'; @@ -29,12 +29,11 @@ export type AddCollateralModalProps = { export default function AddCollateralModal(props: AddCollateralModalProps) { const { borrower, uniswapNFTPositions, isOpen, setIsOpen, setPendingTxn } = props; - const existingUniswapPositions = useMemo(() => borrower.uniswapPositions ?? [], [borrower]); const [modalState, setModalState] = useState(() => { // Only show the select collateral type modal if there are uniswap NFT positions and the user has not already // added the maximum number of uniswap positions. - if (uniswapNFTPositions.size > 0 && existingUniswapPositions.length < MAX_UNISWAP_POSITIONS) { + if (uniswapNFTPositions.size > 0 && borrower.assets.uniswapPositions.length < MAX_UNISWAP_POSITIONS) { return AddCollateralModalState.SELECT_COLLATERAL_TYPE; } return AddCollateralModalState.TOKENS; @@ -45,13 +44,13 @@ export default function AddCollateralModal(props: AddCollateralModalProps) { setModalState(() => { // Only show the select collateral type modal if there are uniswap NFT positions and the user has not already // added the maximum number of uniswap positions. - if (uniswapNFTPositions.size > 0 && existingUniswapPositions.length < MAX_UNISWAP_POSITIONS) { + if (uniswapNFTPositions.size > 0 && borrower.assets.uniswapPositions.length < MAX_UNISWAP_POSITIONS) { return AddCollateralModalState.SELECT_COLLATERAL_TYPE; } return AddCollateralModalState.TOKENS; }); } - }, [existingUniswapPositions, isOpen, uniswapNFTPositions.size]); + }, [borrower.assets.uniswapPositions, isOpen, uniswapNFTPositions.size]); const defaultUniswapNFTPosition = uniswapNFTPositions.size > 0 ? Array.from(uniswapNFTPositions.entries())[0] : null; @@ -83,7 +82,6 @@ export default function AddCollateralModal(props: AddCollateralModalProps) { {modalState === AddCollateralModalState.TOKENS && ( void; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; export function AddCollateralTab(props: AddCollateralTabProps) { - const { marginAccount, uniswapPositions, isOpen, setIsOpen, setPendingTxn } = props; + const { marginAccount, isOpen, setIsOpen, setPendingTxn } = props; const { activeChain } = useContext(ChainContext); const [collateralAmountStr, setCollateralAmountStr] = useState(''); @@ -167,33 +165,23 @@ export function AddCollateralTab(props: AddCollateralTabProps) { const tokenOptions = [marginAccount.token0, marginAccount.token1]; - const existingCollateralRaw = - collateralToken.address === marginAccount.token0.address - ? marginAccount.assets.token0Raw - : marginAccount.assets.token1Raw; + const isToken0 = collateralToken.address === marginAccount.token0.address; + + const existingCollateral = isToken0 ? marginAccount.assets.amount0 : marginAccount.assets.amount1; const collateralAmount = GN.fromDecimalString(collateralAmountStr || '0', collateralToken.decimals); const userBalance = GN.fromDecimalString(userBalanceResult?.formatted ?? '0', collateralToken.decimals); - const newCollateral = GN.fromNumber(existingCollateralRaw, collateralToken.decimals).add(collateralAmount); + const newCollateral = existingCollateral.add(collateralAmount); - // TODO: Utilize GN for this - const newAssets: Assets = { - token0Raw: - collateralToken.address === marginAccount.token0.address - ? existingCollateralRaw + collateralAmount.toNumber() - : marginAccount.assets.token0Raw, - token1Raw: - collateralToken.address === marginAccount.token1.address - ? existingCollateralRaw + collateralAmount.toNumber() - : marginAccount.assets.token1Raw, - uni0: marginAccount.assets.uni0, - uni1: marginAccount.assets.uni1, - }; + const newAssets = new Assets( + isToken0 ? newCollateral : marginAccount.assets.amount0, + isToken0 ? marginAccount.assets.amount1 : newCollateral, + marginAccount.assets.uniswapPositions + ); const { health: newHealth } = isHealthy( newAssets, marginAccount.liabilities, - uniswapPositions, marginAccount.sqrtPriceX96, marginAccount.iv, marginAccount.nSigma, diff --git a/earn/src/components/boost/BurnBoostModal.tsx b/earn/src/components/boost/BurnBoostModal.tsx index 30857e8e..388829ec 100644 --- a/earn/src/components/boost/BurnBoostModal.tsx +++ b/earn/src/components/boost/BurnBoostModal.tsx @@ -12,6 +12,7 @@ import { GN } from 'shared/lib/data/GoodNumber'; import { useAccount, useContractWrite, usePrepareContractWrite } from 'wagmi'; import { ChainContext } from '../../App'; +import { sqrtRatioToTick } from '../../data/BalanceSheet'; import { MarginAccount } from '../../data/MarginAccount'; import { BoostCardInfo } from '../../data/Uniboost'; import MaxSlippageInput from '../common/MaxSlippageInput'; @@ -42,9 +43,10 @@ function getConfirmButton(state: ConfirmButtonState): { text: string; enabled: b function calculateShortfall(borrower: MarginAccount): { shortfall0: GN; shortfall1: GN } { if (!borrower) return { shortfall0: GN.zero(0), shortfall1: GN.zero(0) }; const { assets, liabilities } = borrower; + const [assets0, assets1] = assets.amountsAt(sqrtRatioToTick(borrower.sqrtPriceX96)); return { - shortfall0: GN.fromNumber(liabilities.amount0 - (assets.token0Raw + assets.uni0), borrower.token0.decimals), - shortfall1: GN.fromNumber(liabilities.amount1 - (assets.token1Raw + assets.uni1), borrower.token1.decimals), + shortfall0: GN.fromNumber(liabilities.amount0 - assets0, borrower.token0.decimals), + shortfall1: GN.fromNumber(liabilities.amount1 - assets1, borrower.token1.decimals), }; } diff --git a/earn/src/components/markets/borrow/BorrowingWidget.tsx b/earn/src/components/markets/borrow/BorrowingWidget.tsx index be44ac23..25c035e5 100644 --- a/earn/src/components/markets/borrow/BorrowingWidget.tsx +++ b/earn/src/components/markets/borrow/BorrowingWidget.tsx @@ -8,7 +8,7 @@ import { Display, Text } from 'shared/lib/components/common/Typography'; import { UNISWAP_NONFUNGIBLE_POSITION_MANAGER_ADDRESS } from 'shared/lib/data/constants/ChainSpecific'; import { GREY_600, GREY_700 } from 'shared/lib/data/constants/Colors'; import { GetNumericFeeTier } from 'shared/lib/data/FeeTier'; -import { GN } from 'shared/lib/data/GoodNumber'; +import { GN, GNFormat } from 'shared/lib/data/GoodNumber'; import { useChainDependentState } from 'shared/lib/data/hooks/UseChainDependentState'; import { Token } from 'shared/lib/data/Token'; import { formatTokenAmount, roundPercentage } from 'shared/lib/util/Numbers'; @@ -280,15 +280,15 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) { {borrowers && borrowers.map((account) => { const hasNoCollateral = - account.assets.token0Raw === 0 && - account.assets.token1Raw === 0 && - (account.uniswapPositions ?? []).every((pos) => JSBI.EQ(pos.liquidity, '0')); + account.assets.amount0.isZero() && + account.assets.amount1.isZero() && + account.assets.uniswapPositions.every((pos) => JSBI.EQ(pos.liquidity, '0')); if (hasNoCollateral) return null; - const uniswapPosition = account.uniswapPositions?.at(0); - const collateral = account.assets.token0Raw > 0 ? account.token0 : account.token1; + const uniswapPosition = account.assets.uniswapPositions.at(0); + const collateral = account.assets.amount0.isGtZero() ? account.token0 : account.token1; const collateralAmount = collateral.equals(account.token0) - ? account.assets.token0Raw - : account.assets.token1Raw; + ? account.assets.amount0 + : account.assets.amount1; const collateralColor = tokenColors.get(collateral.address); const ltvPercentage = computeLTV(account.iv, account.nSigma) * 100; return ( @@ -319,7 +319,7 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) {
- {formatTokenAmount(collateralAmount)}  {collateral.symbol} + {collateralAmount.toString(GNFormat.LOSSY_HUMAN)}  {collateral.symbol}
)} @@ -335,7 +335,7 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) {
{borrowers && borrowers.map((borrower) => { - const hasNoCollateral = borrower.assets.token0Raw === 0 && borrower.assets.token1Raw === 0; + const hasNoCollateral = borrower.assets.amount0.isZero() && borrower.assets.amount1.isZero(); if (hasNoCollateral) return null; return (
@@ -356,9 +356,9 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) { {borrowers && borrowers.map((account) => { const hasNoCollateral = - account.assets.token0Raw === 0 && - account.assets.token1Raw === 0 && - (account.uniswapPositions?.length || 0) === 0; + account.assets.amount0.isZero() && + account.assets.amount1.isZero() && + account.assets.uniswapPositions.length === 0; if (hasNoCollateral) return null; const isBorrowingToken0 = account.liabilities.amount0 > 0; diff --git a/earn/src/components/markets/modal/BorrowModalUniswap.tsx b/earn/src/components/markets/modal/BorrowModalUniswap.tsx index 5f1201c4..5360aa0e 100644 --- a/earn/src/components/markets/modal/BorrowModalUniswap.tsx +++ b/earn/src/components/markets/modal/BorrowModalUniswap.tsx @@ -29,6 +29,7 @@ import { erc721ABI, useAccount, useBalance, useContractRead, useContractWrite, u import { ChainContext } from '../../../App'; import { maxBorrowAndWithdraw } from '../../../data/BalanceSheet'; import { LendingPair } from '../../../data/LendingPair'; +import { Assets } from '../../../data/MarginAccount'; import { RateModel, yieldPerSecondToAPR } from '../../../data/RateModel'; import { UniswapNFTPosition, zip } from '../../../data/Uniswap'; @@ -157,14 +158,10 @@ export default function BorrowModalUniswap(props: BorrowModalProps) { } const [max0, max1] = maxBorrowAndWithdraw( - { - token0Raw: 0, - token1Raw: 0, - uni0: 0, - uni1: 0, - }, + new Assets(GN.zero(selectedLendingPair.token0.decimals), GN.zero(selectedLendingPair.token1.decimals), [ + uniswapPosition, + ]), { amount0: 0, amount1: 0 }, - [uniswapPosition], new Big(consultData[1].toString()), selectedLendingPair.iv, selectedLendingPair.factoryData.nSigma, diff --git a/earn/src/components/markets/modal/UpdateCollateralModal.tsx b/earn/src/components/markets/modal/UpdateCollateralModal.tsx index 3fabcabe..b3708441 100644 --- a/earn/src/components/markets/modal/UpdateCollateralModal.tsx +++ b/earn/src/components/markets/modal/UpdateCollateralModal.tsx @@ -61,8 +61,8 @@ export default function UpdateCollateralModal(props: UpdateCollateralModalProps) const { isOpen, borrower, uniswapPositions, setIsOpen, setPendingTxn } = props; const [confirmationType, setConfirmationType] = useState(ConfirmationType.DEPOSIT); - if ((borrower.uniswapPositions?.length || 0) > 0) { - const positionToWithdraw = borrower.uniswapPositions![0]; + if (borrower.assets.uniswapPositions.length > 0) { + const positionToWithdraw = borrower.assets.uniswapPositions[0]; const uniswapNftId = uniswapPositions.find( (nft) => nft.lower === positionToWithdraw.lower && nft.upper === positionToWithdraw.upper )?.tokenId; diff --git a/earn/src/components/markets/modal/content/AddCollateralModalContent.tsx b/earn/src/components/markets/modal/content/AddCollateralModalContent.tsx index eb2cc082..1adc1053 100644 --- a/earn/src/components/markets/modal/content/AddCollateralModalContent.tsx +++ b/earn/src/components/markets/modal/content/AddCollateralModalContent.tsx @@ -131,7 +131,7 @@ export default function AddCollateralModalContent(props: AddCollateralModalConte const { address: userAddress } = useAccount(); // TODO this logic needs to change once we support more complex borrowing - const isDepositingToken0 = borrower.assets.token0Raw > 0; + const isDepositingToken0 = borrower.assets.amount0.isGtZero(); // TODO: This logic needs to change once we support more complex borrowing const collateralToken = isDepositingToken0 ? borrower.token0 : borrower.token1; @@ -145,25 +145,21 @@ export default function AddCollateralModalContent(props: AddCollateralModalConte // TODO: This assumes that the borrowing token is always the opposite of the collateral token // and that only one token is borrowed and one token is collateralized - const numericCollateralAmount = isDepositingToken0 ? borrower.assets.token0Raw : borrower.assets.token1Raw; - const currentCollateralAmount = GN.fromNumber(numericCollateralAmount, collateralToken.decimals); + const currentCollateralAmount = isDepositingToken0 ? borrower.assets.amount0 : borrower.assets.amount1; const depositAmount = GN.fromDecimalString(depositAmountStr || '0', collateralToken.decimals); const newCollateralAmount = currentCollateralAmount.add(depositAmount); const maxDepositAmount = GN.fromDecimalString(balanceData?.formatted || '0', collateralToken.decimals); const maxDepositAmountStr = maxDepositAmount.toString(GNFormat.DECIMAL); - // TODO: use GN - const newAssets: Assets = { - token0Raw: isDepositingToken0 ? newCollateralAmount.toNumber() : borrower.assets.token0Raw, - token1Raw: isDepositingToken0 ? borrower.assets.token1Raw : newCollateralAmount.toNumber(), - uni0: 0, // TODO: add uniswap positions - uni1: 0, // TODO: add uniswap positions - }; + const newAssets = new Assets( + isDepositingToken0 ? newCollateralAmount : borrower.assets.amount0, + isDepositingToken0 ? borrower.assets.amount1 : newCollateralAmount, + borrower.assets.uniswapPositions + ); const { health: newHealth } = isHealthy( newAssets, borrower.liabilities, - [], // TODO: add uniswap positions borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, diff --git a/earn/src/components/markets/modal/content/BorrowModalContent.tsx b/earn/src/components/markets/modal/content/BorrowModalContent.tsx index fe2f4a8a..ce44bd77 100644 --- a/earn/src/components/markets/modal/content/BorrowModalContent.tsx +++ b/earn/src/components/markets/modal/content/BorrowModalContent.tsx @@ -225,7 +225,6 @@ export default function BorrowModalContent(props: BorrowModalContentProps) { const maxBorrowsBasedOnHealth = maxBorrowAndWithdraw( borrower.assets, borrower.liabilities, - borrower.uniswapPositions ?? [], borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, @@ -248,7 +247,6 @@ export default function BorrowModalContent(props: BorrowModalContentProps) { const { health: newHealth } = isHealthy( borrower.assets, newLiabilities, - borrower.uniswapPositions ?? [], borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, diff --git a/earn/src/components/markets/modal/content/RemoveCollateralModalContent.tsx b/earn/src/components/markets/modal/content/RemoveCollateralModalContent.tsx index 82229860..1e9f301c 100644 --- a/earn/src/components/markets/modal/content/RemoveCollateralModalContent.tsx +++ b/earn/src/components/markets/modal/content/RemoveCollateralModalContent.tsx @@ -198,22 +198,20 @@ export default function RemoveCollateralModalContent(props: RemoveCollateralModa const { address: accountAddress } = useAccount(); // TODO: This logic needs to change once we support more complex borrowing - const isWithdrawingToken0 = borrower.assets.token0Raw > 0; + const isWithdrawingToken0 = borrower.assets.amount0.isGtZero(); // TODO: This logic needs to change once we support more complex borrowing const collateralToken = isWithdrawingToken0 ? borrower.token0 : borrower.token1; // TODO: This assumes that the borrowing token is always the opposite of the collateral token // and that only one token is borrowed and one token is collateralized - const numericExistingCollateral = isWithdrawingToken0 ? borrower.assets.token0Raw : borrower.assets.token1Raw; - const existingCollateral = GN.fromNumber(numericExistingCollateral, collateralToken.decimals); + const existingCollateral = isWithdrawingToken0 ? borrower.assets.amount0 : borrower.assets.amount1; const withdrawAmount = GN.fromDecimalString(withdrawAmountStr || '0', collateralToken.decimals); const newCollateralAmount = existingCollateral.sub(withdrawAmount); const numericMaxWithdrawAmount = maxWithdraws( borrower.assets, borrower.liabilities, - [], // TODO: Add uniswap positions borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, @@ -226,18 +224,15 @@ export default function RemoveCollateralModalContent(props: RemoveCollateralModa const max = maxWithdrawAmount; const maxStr = max.toString(GNFormat.DECIMAL); - // TODO: use GN - const newAssets: Assets = { - token0Raw: isWithdrawingToken0 ? newCollateralAmount.toNumber() : borrower.assets.token0Raw, - token1Raw: isWithdrawingToken0 ? borrower.assets.token1Raw : newCollateralAmount.toNumber(), - uni0: 0, // TODO: add uniswap positions - uni1: 0, // TODO: add uniswap positions - }; + const newAssets = new Assets( + isWithdrawingToken0 ? newCollateralAmount : borrower.assets.amount0, + isWithdrawingToken0 ? borrower.assets.amount1 : newCollateralAmount, + borrower.assets.uniswapPositions + ); const { health: newHealth } = isHealthy( newAssets, borrower.liabilities, - [], // TODO: add uniswap positions borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, @@ -245,7 +240,7 @@ export default function RemoveCollateralModalContent(props: RemoveCollateralModa borrower.token1.decimals ); - const shouldWithdrawAnte = newAssets.token0Raw < Number.EPSILON && newAssets.token1Raw < Number.EPSILON; + const shouldWithdrawAnte = newAssets.amount0.isZero() && newAssets.amount1.isZero(); return ( <> diff --git a/earn/src/components/markets/modal/content/RepayModalContent.tsx b/earn/src/components/markets/modal/content/RepayModalContent.tsx index 4c89d690..31bd0703 100644 --- a/earn/src/components/markets/modal/content/RepayModalContent.tsx +++ b/earn/src/components/markets/modal/content/RepayModalContent.tsx @@ -238,7 +238,6 @@ export default function RepayModalContent(props: RepayModalContentProps) { const { health: newHealth } = isHealthy( borrower.assets, newLiabilities, - borrower.uniswapPositions ?? [], borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, diff --git a/earn/src/components/markets/modal/content/ToUniswapNFTModalContent.tsx b/earn/src/components/markets/modal/content/ToUniswapNFTModalContent.tsx index 81944dfe..a230a425 100644 --- a/earn/src/components/markets/modal/content/ToUniswapNFTModalContent.tsx +++ b/earn/src/components/markets/modal/content/ToUniswapNFTModalContent.tsx @@ -73,7 +73,7 @@ function ConfirmButton(props: ConfirmButtonProps) { positionToWithdraw.upper, positionToWithdraw.liquidity.toString(10), zip( - borrower.uniswapPositions!.filter((position) => { + borrower.assets.uniswapPositions.filter((position) => { return position.lower !== positionToWithdraw.lower || position.upper !== positionToWithdraw.upper; }), '0x83ee755b' @@ -144,7 +144,6 @@ export default function ToUniswapNFTModalContent(props: RemoveCollateralModalCon const { health: newHealth } = isHealthy( borrower.assets, borrower.liabilities, - [], borrower.sqrtPriceX96, borrower.iv, borrower.nSigma, diff --git a/earn/src/data/BalanceSheet.ts b/earn/src/data/BalanceSheet.ts index 00fa908e..431c0927 100644 --- a/earn/src/data/BalanceSheet.ts +++ b/earn/src/data/BalanceSheet.ts @@ -5,7 +5,7 @@ import { areWithinNSigDigs } from 'shared/lib/util/Numbers'; import { ALOE_II_LIQUIDATION_INCENTIVE, ALOE_II_MAX_LEVERAGE, BIGQ96 } from './constants/Values'; import { Assets, Liabilities } from './MarginAccount'; -import { getAmountsForLiquidity, UniswapPosition } from './Uniswap'; +import { UniswapPosition } from './Uniswap'; const MIN_SQRT_RATIO = new Big('4295128740'); const MAX_SQRT_RATIO = new Big('1461446703485210103287273052203988822378723970341'); @@ -61,32 +61,12 @@ function _solvency( }; } -export function getAssets( - token0Raw: number, - token1Raw: number, - uniswapPositions: readonly UniswapPosition[], - a: Big, - b: Big, - token0Decimals: number, - token1Decimals: number -) { +export function getAssets(assets: Assets, a: Big, b: Big) { const tickA = TickMath.getTickAtSqrtRatio(JSBI.BigInt(a.toFixed(0))); const tickB = TickMath.getTickAtSqrtRatio(JSBI.BigInt(b.toFixed(0))); - let amount0AtA = token0Raw; - let amount1AtA = token1Raw; - let amount0AtB = token0Raw; - let amount1AtB = token1Raw; - - for (const position of uniswapPositions) { - let temp: [number, number]; - temp = getAmountsForLiquidity(position, tickA, token0Decimals, token1Decimals); - amount0AtA += temp[0]; - amount1AtA += temp[1]; - temp = getAmountsForLiquidity(position, tickB, token0Decimals, token1Decimals); - amount0AtB += temp[0]; - amount1AtB += temp[1]; - } + const [amount0AtA, amount1AtA] = assets.amountsAt(tickA); + const [amount0AtB, amount1AtB] = assets.amountsAt(tickB); return { amount0AtA, @@ -107,7 +87,6 @@ function _augmentLiabilities(liabilities0Or1: number, assets0Or1: number) { export function isHealthy( assets: Assets, liabilities: Liabilities, - uniswapPositions: readonly UniswapPosition[], sqrtPriceX96: Big, iv: number, nSigma: number, @@ -115,7 +94,7 @@ export function isHealthy( token1Decimals: number ) { const [a, b] = _computeProbePrices(sqrtPriceX96, iv, nSigma); - const mem = getAssets(assets.token0Raw, assets.token1Raw, uniswapPositions, a, b, token0Decimals, token1Decimals); + const mem = getAssets(assets, a, b); const { amount0: liabilities0, amount1: liabilities1 } = liabilities; @@ -219,7 +198,6 @@ function _minAssets0AllElseConstant(price: number, assets1: number, liabilities0 export function maxWithdraws( assets: Assets, liabilities: Liabilities, - uniswapPositions: readonly UniswapPosition[], sqrtPriceX96: Big, iv: number, nSigma: number, @@ -227,7 +205,7 @@ export function maxWithdraws( token1Decimals: number ) { const [a, b] = _computeProbePrices(sqrtPriceX96, iv, nSigma); - const mem = getAssets(assets.token0Raw, assets.token1Raw, uniswapPositions, a, b, token0Decimals, token1Decimals); + const mem = getAssets(assets, a, b); const priceA = sqrtRatioToPrice(a, token0Decimals, token1Decimals); const priceB = sqrtRatioToPrice(b, token0Decimals, token1Decimals); @@ -239,13 +217,12 @@ export function maxWithdraws( const min0 = Math.max(min0A, min0B); const min1 = Math.max(min1A, min1B); - return [Math.max(0, assets.token0Raw - min0), Math.max(0, assets.token1Raw - min1)]; + return [Math.max(0, assets.amount0.toNumber() - min0), Math.max(0, assets.amount1.toNumber() - min1)]; } export function maxBorrowAndWithdraw( assets: Assets, liabilities: Liabilities, - uniswapPositions: readonly UniswapPosition[], sqrtPriceX96: Big, iv: number, nSigma: number, @@ -253,7 +230,7 @@ export function maxBorrowAndWithdraw( token1Decimals: number ) { const [a, b] = _computeProbePrices(sqrtPriceX96, iv, nSigma); - const mem = getAssets(assets.token0Raw, assets.token1Raw, uniswapPositions, a, b, token0Decimals, token1Decimals); + const mem = getAssets(assets, a, b); const priceA = sqrtRatioToPrice(a, token0Decimals, token1Decimals); const priceB = sqrtRatioToPrice(b, token0Decimals, token1Decimals); @@ -298,16 +275,7 @@ export function computeLiquidationThresholds( }; // Find lower liquidation threshold - const isSolventAtMin = isHealthy( - assets, - liabilities, - uniswapPositions, - MINPRICE, - iv, - nSigma, - token0Decimals, - token1Decimals - ); + const isSolventAtMin = isHealthy(assets, liabilities, MINPRICE, iv, nSigma, token0Decimals, token1Decimals); if (isSolventAtMin.atA && isSolventAtMin.atB) { // if solvent at beginning, short-circuit result.lowerSqrtRatio = MINPRICE; @@ -326,7 +294,6 @@ export function computeLiquidationThresholds( const isSolventAtSearchPrice = isHealthy( assets, liabilities, - uniswapPositions, searchPrice, iv, nSigma, @@ -346,16 +313,7 @@ export function computeLiquidationThresholds( } // Find upper liquidation threshold - const isSolventAtMax = isHealthy( - assets, - liabilities, - uniswapPositions, - MAXPRICE, - iv, - nSigma, - token0Decimals, - token1Decimals - ); + const isSolventAtMax = isHealthy(assets, liabilities, MAXPRICE, iv, nSigma, token0Decimals, token1Decimals); if (isSolventAtMax.atA && isSolventAtMax.atB) { // if solvent at end, short-circuit result.upperSqrtRatio = MAXPRICE; @@ -374,7 +332,6 @@ export function computeLiquidationThresholds( const isSolventAtSearchPrice = isHealthy( assets, liabilities, - uniswapPositions, searchPrice, iv, nSigma, @@ -396,10 +353,6 @@ export function computeLiquidationThresholds( return result; } -export function sumAssetsPerToken(assets: Assets): [number, number] { - return [assets.token0Raw + assets.uni0, assets.token1Raw + assets.uni1]; -} - export function computeLTV(iv: number, nSigma: number) { const ltv = 1 / ((1 + 1 / ALOE_II_MAX_LEVERAGE + 1 / ALOE_II_LIQUIDATION_INCENTIVE) * Math.exp(iv * nSigma)); return Math.max(0.1, Math.min(ltv, 0.9)); diff --git a/earn/src/data/MarginAccount.ts b/earn/src/data/MarginAccount.ts index 9d1e6a4c..a170e8cb 100644 --- a/earn/src/data/MarginAccount.ts +++ b/earn/src/data/MarginAccount.ts @@ -15,6 +15,7 @@ import { } from 'shared/lib/data/constants/ChainSpecific'; import { Q32 } from 'shared/lib/data/constants/Values'; import { FeeTier, NumericFeeTierToEnum } from 'shared/lib/data/FeeTier'; +import { GN } from 'shared/lib/data/GoodNumber'; import { Token } from 'shared/lib/data/Token'; import { getToken } from 'shared/lib/data/TokenData'; import { toBig, toImpreciseNumber } from 'shared/lib/util/Numbers'; @@ -22,14 +23,33 @@ import { Address } from 'wagmi'; import { ContractCallReturnContextEntries, convertBigNumbersForReturnContexts } from '../util/Multicall'; import { TOPIC0_CREATE_BORROWER_EVENT } from './constants/Signatures'; -import { UniswapPosition } from './Uniswap'; +import { getAmountsForLiquidity, UniswapPosition } from './Uniswap'; -export type Assets = { - token0Raw: number; - token1Raw: number; - uni0: number; - uni1: number; -}; +export class Assets { + constructor( + public readonly amount0: GN, + public readonly amount1: GN, + public readonly uniswapPositions: UniswapPosition[] + ) {} + + amountsAt(tick: number) { + let amount0 = this.amount0.toNumber(); + let amount1 = this.amount1.toNumber(); + for (const uniswapPosition of this.uniswapPositions) { + const [temp0, temp1] = getAmountsForLiquidity( + uniswapPosition, + tick, + this.amount0.resolution, + this.amount1.resolution + ); + + amount0 += temp0; + amount1 += temp1; + } + + return [amount0, amount1]; + } +} export type Liabilities = { amount0: number; @@ -53,7 +73,6 @@ export type MarginAccount = { lender1: Address; iv: number; nSigma: number; - uniswapPositions?: UniswapPosition[]; }; /** @@ -271,15 +290,6 @@ export async function fetchBorrowerDatas( const nSigma = convertBigNumbersForReturnContexts(value.nSigma.callsReturnContext)[0].returnValues[1] / 10; const health = toImpreciseNumber(healthData[0].lt(healthData[1]) ? healthData[0] : healthData[1], 18); - const assets: Assets = { - token0Raw: toImpreciseNumber(token0Balance, token0.decimals), - token1Raw: toImpreciseNumber(token1Balance, token1.decimals), - uni0: 0, - uni1: 0, - // TODO: BEFORE LAUNCH, Get the uniswap balances data again - // uni0: toImpreciseNumber(assetsData[4], token0.decimals), - // uni1: toImpreciseNumber(assetsData[5], token1.decimals), - }; const liabilities: Liabilities = { amount0: toImpreciseNumber(liabilitiesData[0], token0.decimals), amount1: toImpreciseNumber(liabilitiesData[1], token1.decimals), @@ -299,6 +309,12 @@ export async function fetchBorrowerDatas( }); // const uniswapPositionFees = uniswapPositionData[2]; + const assets = new Assets( + GN.fromBigNumber(token0Balance, token0.decimals), + GN.fromBigNumber(token1Balance, token1.decimals), + uniswapPositions + ); + const lender0 = accountReturnContexts[0].returnValues[0]; const lender1 = accountReturnContexts[1].returnValues[0]; const oracleReturnValues = convertBigNumbersForReturnContexts(oracleResults.callsReturnContext)[0].returnValues; @@ -316,7 +332,6 @@ export async function fetchBorrowerDatas( lender0, lender1, nSigma, - uniswapPositions, }; marginAccounts.push(marginAccount); }); diff --git a/earn/src/data/Uniboost.ts b/earn/src/data/Uniboost.ts index 54370cf5..f70be0a7 100644 --- a/earn/src/data/Uniboost.ts +++ b/earn/src/data/Uniboost.ts @@ -1,4 +1,3 @@ -import { TickMath } from '@uniswap/v3-sdk'; import Big from 'big.js'; import { ContractCallContext, Multicall } from 'ethereum-multicall'; import { ethers } from 'ethers'; @@ -19,9 +18,9 @@ import { NumericFeeTierToEnum } from 'shared/lib/data/FeeTier'; import { GN } from 'shared/lib/data/GoodNumber'; import { Token } from 'shared/lib/data/Token'; import { getToken } from 'shared/lib/data/TokenData'; -import { String1E } from 'shared/lib/util/Numbers'; import { Address, erc20ABI } from 'wagmi'; +import { sqrtRatioToTick } from './BalanceSheet'; import { fetchListOfBorrowerNfts } from './BorrowerNft'; import { Assets, Liabilities, MarginAccount } from './MarginAccount'; import { getAmountsForLiquidity, getValueOfLiquidity, tickToPrice, UniswapPosition } from './Uniswap'; @@ -97,8 +96,9 @@ export class BoostCardInfo { const uniswapValue = getValueOfLiquidity(this.position, this.currentTick, this.token1.decimals); // Compute total debt - const debt0 = this.borrower.liabilities.amount0 - this.borrower.assets.token0Raw; - const debt1 = this.borrower.liabilities.amount1 - this.borrower.assets.token1Raw; + const [assets0, assets1] = this.borrower.assets.amountsAt(sqrtRatioToTick(this.borrower.sqrtPriceX96)); + const debt0 = this.borrower.liabilities.amount0 - assets0; + const debt1 = this.borrower.liabilities.amount1 - assets1; const price = tickToPrice(this.currentTick, this.token0.decimals, this.token1.decimals, true); const debtValue = debt0 * price + debt1; @@ -300,27 +300,14 @@ export async function fetchBoostBorrower( liquidity: JSBI.BigInt(liquidity.toString()), }; - const token0Raw = new Big( - ethers.BigNumber.from(extraResults['token0.balanceOf'].callsReturnContext[0].returnValues[0].hex).toString() - ); - const token1Raw = new Big( - ethers.BigNumber.from(extraResults['token1.balanceOf'].callsReturnContext[0].returnValues[0].hex).toString() + const token0Raw = ethers.BigNumber.from(extraResults['token0.balanceOf'].callsReturnContext[0].returnValues[0].hex); + const token1Raw = ethers.BigNumber.from(extraResults['token1.balanceOf'].callsReturnContext[0].returnValues[0].hex); + + const assets = new Assets( + GN.fromBigNumber(token0Raw, token0.decimals), + GN.fromBigNumber(token1Raw, token1.decimals), + [uniswapPosition] ); - let [uni0, uni1] = [0, 0]; - if (hasPosition) { - [uni0, uni1] = getAmountsForLiquidity( - uniswapPosition, - TickMath.getTickAtSqrtRatio(sqrtPriceX96), - token0.decimals, - token1.decimals - ); - } - const assets: Assets = { - token0Raw: token0Raw.div(String1E(token0.decimals)).toNumber(), - token1Raw: token1Raw.div(String1E(token1.decimals)).toNumber(), - uni0, - uni1, - }; const borrower: MarginAccount = { address: borrowerAddress, diff --git a/earn/src/pages/AdvancedPage.tsx b/earn/src/pages/AdvancedPage.tsx index 644f158d..1c2d2613 100644 --- a/earn/src/pages/AdvancedPage.tsx +++ b/earn/src/pages/AdvancedPage.tsx @@ -256,7 +256,7 @@ export default function AdvancedPage() { const withdrawableUniswapNFTPositions = useMemo(() => { const filteredPositions: Map = new Map(); if (selectedMarginAccount == null) return filteredPositions; - selectedMarginAccount.uniswapPositions?.forEach((uniswapPosition) => { + selectedMarginAccount.assets.uniswapPositions.forEach((uniswapPosition) => { const isNonZero = JSBI.greaterThan(uniswapPosition.liquidity, JSBI.BigInt('0')); const matchingNFTPosition = Array.from(uniswapNFTPositions.entries()).find(([, position]) => { return position.lower === uniswapPosition.lower && position.upper === uniswapPosition.upper; diff --git a/earn/src/pages/boost/ImportBoostPage.tsx b/earn/src/pages/boost/ImportBoostPage.tsx index 8f3ee30f..acc1a9ae 100644 --- a/earn/src/pages/boost/ImportBoostPage.tsx +++ b/earn/src/pages/boost/ImportBoostPage.tsx @@ -23,6 +23,7 @@ import { ChainContext } from '../../App'; import BoostCard from '../../components/boost/BoostCard'; import ImportBoostWidget from '../../components/boost/ImportBoostWidget'; import PendingTxnModal, { PendingTxnModalStatus } from '../../components/common/PendingTxnModal'; +import { Assets } from '../../data/MarginAccount'; import { BoostCardInfo, BoostCardType } from '../../data/Uniboost'; import { UniswapNFTPosition, computePoolAddress, fetchUniswapNFTPosition } from '../../data/Uniswap'; import { getProminentColor, rgb } from '../../util/Colors'; @@ -191,12 +192,7 @@ export default function ImportBoostPage() { uniswapPool: cardInfo.uniswapPool, token0: cardInfo.token0, token1: cardInfo.token1, - assets: { - token0Raw: 0, - token1Raw: 0, - uni0: 0, - uni1: 0, - }, + assets: new Assets(GN.zero(cardInfo.token0.decimals), GN.zero(cardInfo.token1.decimals), [position]), liabilities: { amount0: cardInfo.amount0() * (boostFactor - 1), amount1: cardInfo.amount1() * (boostFactor - 1), From 3d11f4f0f801ce272b648adf514894fa75796a50 Mon Sep 17 00:00:00 2001 From: Hayden Shively <17186559+haydenshively@users.noreply.github.com> Date: Thu, 22 Feb 2024 21:38:25 -0600 Subject: [PATCH 3/4] Correct --- earn/src/pages/boost/ImportBoostPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/earn/src/pages/boost/ImportBoostPage.tsx b/earn/src/pages/boost/ImportBoostPage.tsx index acc1a9ae..acbddb46 100644 --- a/earn/src/pages/boost/ImportBoostPage.tsx +++ b/earn/src/pages/boost/ImportBoostPage.tsx @@ -192,7 +192,7 @@ export default function ImportBoostPage() { uniswapPool: cardInfo.uniswapPool, token0: cardInfo.token0, token1: cardInfo.token1, - assets: new Assets(GN.zero(cardInfo.token0.decimals), GN.zero(cardInfo.token1.decimals), [position]), + assets: new Assets(GN.zero(cardInfo.token0.decimals), GN.zero(cardInfo.token1.decimals), []), liabilities: { amount0: cardInfo.amount0() * (boostFactor - 1), amount1: cardInfo.amount1() * (boostFactor - 1), From ed9dadcb9fe5e7bb3fa9df8515b9382d67ae75df Mon Sep 17 00:00:00 2001 From: Hayden Shively <17186559+haydenshively@users.noreply.github.com> Date: Thu, 22 Feb 2024 21:58:11 -0600 Subject: [PATCH 4/4] Fix --- .../src/components/markets/modal/BorrowModal.tsx | 16 ++-------------- .../markets/modal/BorrowModalUniswap.tsx | 16 ++-------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/earn/src/components/markets/modal/BorrowModal.tsx b/earn/src/components/markets/modal/BorrowModal.tsx index a14a5319..c0f9ec9c 100644 --- a/earn/src/components/markets/modal/BorrowModal.tsx +++ b/earn/src/components/markets/modal/BorrowModal.tsx @@ -32,7 +32,6 @@ import { ChainContext } from '../../../App'; import { computeLTV } from '../../../data/BalanceSheet'; import { BorrowerNft, fetchListOfBorrowerNfts } from '../../../data/BorrowerNft'; import { LendingPair } from '../../../data/LendingPair'; -import { RateModel, yieldPerSecondToAPR } from '../../../data/RateModel'; const MAX_BORROW_PERCENTAGE = 0.8; const SECONDARY_COLOR = '#CCDFED'; @@ -204,19 +203,8 @@ export default function BorrowModal(props: BorrowModalProps) { const estimatedApr = useMemo(() => { const { kitty0Info, kitty1Info } = selectedLendingPair; - const numericLenderTotalAssets = (isBorrowingToken0 ? kitty0Info.totalAssets : kitty1Info.totalAssets).toNumber(); - const lenderTotalAssets = GN.fromNumber(numericLenderTotalAssets, selectedBorrow.decimals); - - const lenderUtilization = isBorrowingToken0 ? kitty0Info.utilization : kitty1Info.utilization; - const lenderUsedAssets = GN.fromNumber(numericLenderTotalAssets * lenderUtilization, selectedBorrow.decimals); - - const remainingAvailableAssets = lenderTotalAssets.sub(lenderUsedAssets).sub(borrowAmount); - const newUtilization = lenderTotalAssets.isGtZero() - ? 1 - remainingAvailableAssets.div(lenderTotalAssets).toNumber() - : 0; - - return yieldPerSecondToAPR(RateModel.computeYieldPerSecond(newUtilization)) * 100; - }, [selectedLendingPair, selectedBorrow, isBorrowingToken0, borrowAmount]); + return (isBorrowingToken0 ? kitty0Info : kitty1Info).hypotheticalBorrowAPR(borrowAmount) * 100; + }, [selectedLendingPair, isBorrowingToken0, borrowAmount]); // The NFT index we will use if minting const { data: nextNftPtrIdx } = useContractRead({ diff --git a/earn/src/components/markets/modal/BorrowModalUniswap.tsx b/earn/src/components/markets/modal/BorrowModalUniswap.tsx index 5360aa0e..f177c9c3 100644 --- a/earn/src/components/markets/modal/BorrowModalUniswap.tsx +++ b/earn/src/components/markets/modal/BorrowModalUniswap.tsx @@ -30,7 +30,6 @@ import { ChainContext } from '../../../App'; import { maxBorrowAndWithdraw } from '../../../data/BalanceSheet'; import { LendingPair } from '../../../data/LendingPair'; import { Assets } from '../../../data/MarginAccount'; -import { RateModel, yieldPerSecondToAPR } from '../../../data/RateModel'; import { UniswapNFTPosition, zip } from '../../../data/Uniswap'; const MAX_BORROW_PERCENTAGE = 0.8; @@ -183,19 +182,8 @@ export default function BorrowModalUniswap(props: BorrowModalProps) { const estimatedApr = useMemo(() => { const { kitty0Info, kitty1Info } = selectedLendingPair; - const numericLenderTotalAssets = (isBorrowingToken0 ? kitty0Info.totalAssets : kitty1Info.totalAssets).toNumber(); - const lenderTotalAssets = GN.fromNumber(numericLenderTotalAssets, selectedBorrow.decimals); - - const lenderUtilization = isBorrowingToken0 ? kitty0Info.utilization : kitty1Info.utilization; - const lenderUsedAssets = GN.fromNumber(numericLenderTotalAssets * lenderUtilization, selectedBorrow.decimals); - - const remainingAvailableAssets = lenderTotalAssets.sub(lenderUsedAssets).sub(borrowAmount); - const newUtilization = lenderTotalAssets.isGtZero() - ? 1 - remainingAvailableAssets.div(lenderTotalAssets).toNumber() - : 0; - - return yieldPerSecondToAPR(RateModel.computeYieldPerSecond(newUtilization)) * 100; - }, [selectedLendingPair, selectedBorrow, isBorrowingToken0, borrowAmount]); + return (isBorrowingToken0 ? kitty0Info : kitty1Info).hypotheticalBorrowAPR(borrowAmount) * 100; + }, [selectedLendingPair, isBorrowingToken0, borrowAmount]); // The NFT index we will use if minting const { data: nextNftPtrIdx } = useContractRead({