CollateralRepayModalContent.tsx
1 import { InterestRate } from '@aave/contract-helpers'; 2 import { valueToBigNumber } from '@aave/math-utils'; 3 import { ArrowDownIcon } from '@heroicons/react/outline'; 4 import { Trans } from '@lingui/macro'; 5 import { Box, Stack, SvgIcon, Typography } from '@mui/material'; 6 import { BigNumber } from 'bignumber.js'; 7 import { useRef, useState } from 'react'; 8 import { PriceImpactTooltip } from 'src/components/infoTooltips/PriceImpactTooltip'; 9 import { FormattedNumber } from 'src/components/primitives/FormattedNumber'; 10 import { TokenIcon } from 'src/components/primitives/TokenIcon'; 11 import { 12 ComputedReserveData, 13 ExtendedFormattedUser, 14 useAppDataContext, 15 } from 'src/hooks/app-data-provider/useAppDataProvider'; 16 import { 17 maxInputAmountWithSlippage, 18 minimumReceivedAfterSlippage, 19 SwapVariant, 20 } from 'src/hooks/paraswap/common'; 21 import { useCollateralRepaySwap } from 'src/hooks/paraswap/useCollateralRepaySwap'; 22 import { useModalContext } from 'src/hooks/useModal'; 23 import { useZeroLTVBlockingWithdraw } from 'src/hooks/useZeroLTVBlockingWithdraw'; 24 import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; 25 import { ListSlippageButton } from 'src/modules/dashboard/lists/SlippageList'; 26 import { useRootStore } from 'src/store/root'; 27 import { calculateHFAfterRepay } from 'src/utils/hfUtils'; 28 import { useShallow } from 'zustand/shallow'; 29 30 import { Asset, AssetInput } from '../AssetInput'; 31 import { ModalWrapperProps } from '../FlowCommons/ModalWrapper'; 32 import { TxSuccessView } from '../FlowCommons/Success'; 33 import { 34 DetailsHFLine, 35 DetailsNumberLineWithSub, 36 TxModalDetails, 37 } from '../FlowCommons/TxModalDetails'; 38 import { ErrorType, useFlashloan } from '../utils'; 39 import { ParaswapErrorDisplay } from '../Warnings/ParaswapErrorDisplay'; 40 import { CollateralRepayActions } from './CollateralRepayActions'; 41 42 export function CollateralRepayModalContent({ 43 poolReserve, 44 symbol, 45 debtType, 46 userReserve, 47 isWrongNetwork, 48 user, 49 }: ModalWrapperProps & { debtType: InterestRate; user: ExtendedFormattedUser }) { 50 const { reserves, userReserves } = useAppDataContext(); 51 const { gasLimit, txError, mainTxState } = useModalContext(); 52 const [currentChainId, currentNetworkConfig] = useRootStore( 53 useShallow((store) => [store.currentChainId, store.currentNetworkConfig]) 54 ); 55 const { currentAccount } = useWeb3Context(); 56 57 // List of tokens eligble to repay with, ordered by USD value 58 const repayTokens = user.userReservesData 59 .filter( 60 (userReserve) => 61 userReserve.underlyingBalance !== '0' && 62 userReserve.underlyingAsset !== poolReserve.underlyingAsset && 63 userReserve.reserve.symbol !== 'stETH' 64 ) 65 .map((userReserve) => ({ 66 address: userReserve.underlyingAsset, 67 balance: userReserve.underlyingBalance, 68 balanceUSD: userReserve.underlyingBalanceUSD, 69 symbol: userReserve.reserve.symbol, 70 iconSymbol: userReserve.reserve.iconSymbol, 71 decimals: userReserve.reserve.decimals, 72 })) 73 .sort((a, b) => Number(b.balanceUSD) - Number(a.balanceUSD)); 74 const [tokenToRepayWith, setTokenToRepayWith] = useState<Asset>(repayTokens[0]); 75 const tokenToRepayWithBalance = tokenToRepayWith.balance || '0'; 76 77 // const [swapVariant, setSwapVariant] = useState<SwapVariant>('exactOut'); 78 const swapVariant: SwapVariant = 'exactOut'; 79 80 const [amount, setAmount] = useState(''); 81 const [maxSlippage, setMaxSlippage] = useState('0.5'); 82 83 const amountRef = useRef<string>(''); 84 85 const collateralReserveData = reserves.find( 86 (reserve) => reserve.underlyingAsset === tokenToRepayWith.address 87 ) as ComputedReserveData; 88 89 const debt = userReserve?.variableBorrows || '0'; 90 91 let safeAmountToRepayAll = valueToBigNumber(debt); 92 // Add in the approximate interest accrued over the next 30 minutes 93 safeAmountToRepayAll = safeAmountToRepayAll.plus( 94 safeAmountToRepayAll.multipliedBy(poolReserve.variableBorrowAPY).dividedBy(360 * 24 * 2) 95 ); 96 97 const isMaxSelected = amount === '-1'; 98 const repayAmount = isMaxSelected ? safeAmountToRepayAll.toString() : amount; 99 const repayAmountUsdValue = valueToBigNumber(repayAmount) 100 .multipliedBy(poolReserve.priceInUSD) 101 .toString(); 102 103 // The slippage is factored into the collateral amount because when we swap for 'exactOut', positive slippage is applied on the collateral amount. 104 const collateralAmountRequiredToCoverDebt = safeAmountToRepayAll 105 .multipliedBy(poolReserve.priceInUSD) 106 .multipliedBy(100 + Number(maxSlippage)) 107 .dividedBy(100) 108 .dividedBy(collateralReserveData.priceInUSD); 109 110 const swapIn = { ...collateralReserveData, amount: tokenToRepayWithBalance }; 111 const swapOut = { ...poolReserve, amount: amountRef.current }; 112 // if (swapVariant === 'exactIn') { 113 // swapIn.amount = tokenToRepayWithBalance; 114 // swapOut.amount = '0'; 115 // } 116 117 const repayAllDebt = 118 isMaxSelected && 119 valueToBigNumber(tokenToRepayWithBalance).gte(collateralAmountRequiredToCoverDebt); 120 121 const { 122 inputAmountUSD, 123 inputAmount, 124 outputAmount, 125 outputAmountUSD, 126 loading: routeLoading, 127 error, 128 buildTxFn, 129 } = useCollateralRepaySwap({ 130 chainId: currentNetworkConfig.underlyingChainId || currentChainId, 131 userAddress: currentAccount, 132 swapVariant: swapVariant, 133 swapIn, 134 swapOut, 135 max: repayAllDebt, 136 skip: mainTxState.loading || false, 137 maxSlippage: Number(maxSlippage), 138 }); 139 140 const loadingSkeleton = routeLoading && inputAmountUSD === '0'; 141 142 const handleRepayAmountChange = (value: string) => { 143 const maxSelected = value === '-1'; 144 amountRef.current = maxSelected ? safeAmountToRepayAll.toString(10) : value; 145 setAmount(value); 146 147 // if ( 148 // maxSelected && 149 // valueToBigNumber(tokenToRepayWithBalance).lt(collateralAmountRequiredToCoverDebt) 150 // ) { 151 // // The selected collateral amount is not enough to pay the full debt. We'll try to do a swap using the exact amount of collateral. 152 // // The amount won't be known until we fetch the swap data, so we'll clear it out. Once the swap data is fetched, we'll set the amount. 153 // amountRef.current = ''; 154 // setAmount(''); 155 // setSwapVariant('exactIn'); 156 // } else { 157 // amountRef.current = maxSelected ? safeAmountToRepayAll.toString(10) : value; 158 // setAmount(value); 159 // setSwapVariant('exactOut'); 160 // } 161 }; 162 163 // for v3 we need hf after withdraw collateral, because when removing collateral to repay 164 // debt, hf could go under 1 then it would fail. If that is the case then we need 165 // to use flashloan path 166 const repayWithUserReserve = userReserves.find( 167 (userReserve) => userReserve.underlyingAsset === tokenToRepayWith.address 168 ); 169 const { hfAfterSwap, hfEffectOfFromAmount } = calculateHFAfterRepay({ 170 amountToReceiveAfterSwap: outputAmount, 171 amountToSwap: inputAmount, 172 fromAssetData: collateralReserveData, 173 user, 174 toAssetData: poolReserve, 175 repayWithUserReserve, 176 debt, 177 }); 178 179 // If the selected collateral asset is frozen, a flashloan must be used. When a flashloan isn't used, 180 // the remaining amount after the swap is deposited into the pool, which will fail for frozen assets. 181 const shouldUseFlashloan = 182 useFlashloan(user.healthFactor, hfEffectOfFromAmount.toString()) || 183 collateralReserveData?.isFrozen; 184 185 // we need to get the min as minimumReceived can be greater than debt as we are swapping 186 // a safe amount to repay all. When this happens amountAfterRepay would be < 0 and 187 // this would show as certain amount left to repay when we are actually repaying all debt 188 const amountAfterRepay = valueToBigNumber(debt).minus(BigNumber.min(outputAmount, debt)); 189 const displayAmountAfterRepayInUsd = amountAfterRepay.multipliedBy(poolReserve.priceInUSD); 190 const collateralAmountAfterRepay = tokenToRepayWithBalance 191 ? valueToBigNumber(tokenToRepayWithBalance).minus(inputAmount) 192 : valueToBigNumber('0'); 193 const collateralAmountAfterRepayUSD = collateralAmountAfterRepay.multipliedBy( 194 collateralReserveData.priceInUSD 195 ); 196 197 const exactOutputAmount = repayAmount; // swapVariant === 'exactIn' ? outputAmount : repayAmount; 198 const exactOutputUsd = repayAmountUsdValue; // swapVariant === 'exactIn' ? outputAmountUSD : repayAmountUsdValue; 199 200 const assetsBlockingWithdraw = useZeroLTVBlockingWithdraw(); 201 202 let blockingError: ErrorType | undefined = undefined; 203 204 if ( 205 assetsBlockingWithdraw.length > 0 && 206 !assetsBlockingWithdraw.includes(tokenToRepayWith.symbol) 207 ) { 208 blockingError = ErrorType.ZERO_LTV_WITHDRAW_BLOCKED; 209 } else if (valueToBigNumber(tokenToRepayWithBalance).lt(inputAmount)) { 210 blockingError = ErrorType.NOT_ENOUGH_COLLATERAL_TO_REPAY_WITH; 211 } else if (shouldUseFlashloan && !collateralReserveData.flashLoanEnabled) { 212 blockingError = ErrorType.FLASH_LOAN_NOT_AVAILABLE; 213 } 214 215 const BlockingError: React.FC = () => { 216 switch (blockingError) { 217 case ErrorType.NOT_ENOUGH_COLLATERAL_TO_REPAY_WITH: 218 return <Trans>Not enough collateral to repay this amount of debt with</Trans>; 219 case ErrorType.ZERO_LTV_WITHDRAW_BLOCKED: 220 return ( 221 <Trans> 222 Assets with zero LTV ({assetsBlockingWithdraw.join(', ')}) must be withdrawn or disabled 223 as collateral to perform this action 224 </Trans> 225 ); 226 case ErrorType.FLASH_LOAN_NOT_AVAILABLE: 227 return ( 228 <Trans> 229 Due to health factor impact, a flashloan is required to perform this transaction, but 230 Aave Governance has disabled flashloan availability for this asset. Try lowering the 231 amount or supplying additional collateral. 232 </Trans> 233 ); 234 default: 235 return null; 236 } 237 }; 238 239 const inputAmountWithSlippage = maxInputAmountWithSlippage( 240 inputAmount, 241 maxSlippage, 242 tokenToRepayWith.decimals || 18 243 ); 244 245 const outputAmountWithSlippage = minimumReceivedAfterSlippage( 246 outputAmount, 247 maxSlippage, 248 poolReserve.decimals 249 ); 250 251 if (mainTxState.success) 252 return ( 253 <TxSuccessView 254 action={<Trans>Repaid</Trans>} 255 amount={swapVariant === 'exactOut' ? outputAmount : outputAmountWithSlippage} 256 symbol={poolReserve.symbol} 257 /> 258 ); 259 260 return ( 261 <> 262 <AssetInput 263 value={exactOutputAmount} 264 onChange={handleRepayAmountChange} 265 usdValue={exactOutputUsd} 266 symbol={poolReserve.symbol} 267 assets={[ 268 { 269 address: poolReserve.underlyingAsset, 270 symbol: poolReserve.symbol, 271 iconSymbol: poolReserve.iconSymbol, 272 balance: debt, 273 }, 274 ]} 275 isMaxSelected={isMaxSelected} 276 maxValue={debt} 277 inputTitle={<Trans>Expected amount to repay</Trans>} 278 balanceText={<Trans>Borrow balance</Trans>} 279 /> 280 <Box sx={{ padding: '18px', pt: '14px', display: 'flex', justifyContent: 'space-between' }}> 281 <SvgIcon sx={{ fontSize: '18px !important' }}> 282 <ArrowDownIcon /> 283 </SvgIcon> 284 285 <PriceImpactTooltip 286 loading={loadingSkeleton} 287 outputAmountUSD={outputAmountUSD} 288 inputAmountUSD={inputAmountUSD} 289 /> 290 </Box> 291 <AssetInput 292 value={swapVariant === 'exactOut' ? inputAmount : tokenToRepayWithBalance} 293 usdValue={inputAmountUSD} 294 symbol={tokenToRepayWith.symbol} 295 assets={repayTokens} 296 onSelect={setTokenToRepayWith} 297 onChange={handleRepayAmountChange} 298 inputTitle={<Trans>Collateral to repay with</Trans>} 299 balanceText={<Trans>Borrow balance</Trans>} 300 maxValue={tokenToRepayWithBalance} 301 loading={loadingSkeleton} 302 disableInput 303 /> 304 {error && !loadingSkeleton && ( 305 <Typography variant="helperText" color="error.main"> 306 {error} 307 </Typography> 308 )} 309 {blockingError !== undefined && ( 310 <Typography variant="helperText" color="error.main"> 311 <BlockingError /> 312 </Typography> 313 )} 314 315 <TxModalDetails 316 gasLimit={gasLimit} 317 slippageSelector={ 318 <ListSlippageButton 319 selectedSlippage={maxSlippage} 320 setSlippage={setMaxSlippage} 321 slippageTooltipHeader={ 322 <Stack direction="row" alignItems="center"> 323 {false ? ( 324 <> 325 <Trans>Minimum amount of debt to be repaid</Trans> 326 <Stack alignItems="end"> 327 <Stack direction="row"> 328 <TokenIcon 329 symbol={poolReserve.iconSymbol} 330 sx={{ mr: 1, fontSize: '14px' }} 331 /> 332 <FormattedNumber value={outputAmountWithSlippage} variant="secondary12" /> 333 </Stack> 334 </Stack> 335 </> 336 ) : ( 337 <> 338 <Trans>Maximum collateral amount to use</Trans> 339 <Stack alignItems="end"> 340 <Stack direction="row"> 341 <TokenIcon 342 symbol={tokenToRepayWith.iconSymbol || ''} 343 sx={{ mr: 1, fontSize: '14px' }} 344 /> 345 <FormattedNumber value={inputAmountWithSlippage} variant="secondary12" /> 346 </Stack> 347 </Stack> 348 </> 349 )} 350 </Stack> 351 } 352 /> 353 } 354 > 355 <DetailsHFLine 356 visibleHfChange={swapVariant === 'exactOut' ? !!amount : !!inputAmount} 357 healthFactor={user?.healthFactor} 358 futureHealthFactor={hfAfterSwap.toString(10)} 359 loading={loadingSkeleton} 360 /> 361 <DetailsNumberLineWithSub 362 description={<Trans>Borrow balance after repay</Trans>} 363 futureValue={amountAfterRepay.toString()} 364 futureValueUSD={displayAmountAfterRepayInUsd.toString()} 365 symbol={symbol} 366 tokenIcon={poolReserve.iconSymbol} 367 loading={loadingSkeleton} 368 hideSymbolSuffix 369 /> 370 <DetailsNumberLineWithSub 371 description={<Trans>Collateral balance after repay</Trans>} 372 futureValue={collateralAmountAfterRepay.toString()} 373 futureValueUSD={collateralAmountAfterRepayUSD.toString()} 374 symbol={tokenToRepayWith.symbol} 375 tokenIcon={tokenToRepayWith.iconSymbol} 376 loading={loadingSkeleton} 377 hideSymbolSuffix 378 /> 379 </TxModalDetails> 380 381 {txError && <ParaswapErrorDisplay txError={txError} />} 382 383 <CollateralRepayActions 384 poolReserve={poolReserve} 385 fromAssetData={collateralReserveData} 386 repayAmount={outputAmount} 387 repayWithAmount={inputAmountWithSlippage} 388 repayAllDebt={repayAllDebt} 389 useFlashLoan={shouldUseFlashloan} 390 isWrongNetwork={isWrongNetwork} 391 symbol={symbol} 392 rateMode={debtType} 393 blocked={blockingError !== undefined || error !== ''} 394 loading={routeLoading} 395 buildTxFn={buildTxFn} 396 /> 397 </> 398 ); 399 }