/ src / components / transactions / Repay / CollateralRepayModalContent.tsx
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  }