import BigNumber from 'bignumber.js'; import { DRE, increaseTime } from '../../helpers/misc-utils'; import { APPROVAL_AMOUNT_LENDING_POOL, oneEther } from '../../helpers/constants'; import { convertToCurrencyDecimals } from '../../helpers/contracts-helpers'; import { makeSuite } from './helpers/make-suite'; import { ProtocolErrors, RateMode } from '../../helpers/types'; import { calcExpectedVariableDebtTokenBalance } from './helpers/utils/calculations'; import { getReserveData, getUserData } from './helpers/utils/helpers'; import { CommonsConfig } from '../../markets/lp/commons'; import { parseEther } from 'ethers/lib/utils'; const chai = require('chai'); const { expect } = chai; makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', (testEnv) => { const { INVALID_HF } = ProtocolErrors; before('Before LendingPool liquidation: set config', () => { BigNumber.config({ DECIMAL_PLACES: 0, ROUNDING_MODE: BigNumber.ROUND_DOWN }); }); after('After LendingPool liquidation: reset config', () => { BigNumber.config({ DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP }); }); it("It's not possible to liquidate on a non-active collateral or a non active principal", async () => { const { configurator, weth, pool, users, dai } = testEnv; const user = users[1]; await configurator.deactivateReserve(weth.address); await expect( pool.liquidationCall(weth.address, dai.address, user.address, parseEther('1000'), false) ).to.be.revertedWith('2'); await configurator.activateReserve(weth.address); await configurator.deactivateReserve(dai.address); await expect( pool.liquidationCall(weth.address, dai.address, user.address, parseEther('1000'), false) ).to.be.revertedWith('2'); await configurator.activateReserve(dai.address); }); it('Deposits WETH, borrows DAI', async () => { const { dai, weth, users, pool, oracle } = testEnv; const depositor = users[0]; const borrower = users[1]; //mints DAI to depositor await dai.connect(depositor.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); //approve protocol to access depositor wallet await dai.connect(depositor.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); //user 1 deposits 1000 DAI const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); await pool .connect(depositor.signer) .deposit(dai.address, amountDAItoDeposit, depositor.address, '0'); //user 2 deposits 1 ETH const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); //mints WETH to borrower await weth.connect(borrower.signer).mint(await convertToCurrencyDecimals(weth.address, '1000')); //approve protocol to access the borrower wallet await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool .connect(borrower.signer) .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); //user 2 borrows const userGlobalData = await pool.getUserAccountData(borrower.address); const daiPrice = await oracle.getAssetPrice(dai.address); const amountDAIToBorrow = await convertToCurrencyDecimals( dai.address, new BigNumber(userGlobalData.availableBorrowsETH.toString()) .div(daiPrice.toString()) .multipliedBy(0.95) .toFixed(0) ); await pool .connect(borrower.signer) .borrow(dai.address, amountDAIToBorrow, RateMode.Variable, '0', borrower.address); const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); expect(userGlobalDataAfter.currentLiquidationThreshold.toString()).to.be.bignumber.equal( '8250', INVALID_HF ); }); it('Drop the health factor below 1', async () => { const { dai, weth, users, pool, oracle } = testEnv; const borrower = users[1]; const daiPrice = await oracle.getAssetPrice(dai.address); await oracle.setAssetPrice( dai.address, new BigNumber(daiPrice.toString()).multipliedBy(1.18).toFixed(0) ); const userGlobalData = await pool.getUserAccountData(borrower.address); expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt( oneEther.toFixed(0), INVALID_HF ); }); it('Liquidates the borrow', async () => { const { dai, weth, users, pool, oracle, helpersContract } = testEnv; const liquidator = users[3]; const borrower = users[1]; //mints dai to the liquidator await dai.connect(liquidator.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); //approve protocol to access the liquidator wallet await dai.connect(liquidator.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const daiReserveBefore = await getReserveData(helpersContract, dai.address); const daiReserveDataBefore = await helpersContract.getReserveData(dai.address); const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); const userReserveDataBefore = await getUserData( pool, helpersContract, dai.address, borrower.address ); const amountToLiquidate = userReserveDataBefore.currentVariableDebt.div(2).toFixed(0); await increaseTime(100); const tx = await pool .connect(liquidator.signer) .liquidationCall(weth.address, dai.address, borrower.address, amountToLiquidate, false); const userReserveDataAfter = await getUserData( pool, helpersContract, dai.address, borrower.address ); const daiReserveDataAfter = await helpersContract.getReserveData(dai.address); const ethReserveDataAfter = await helpersContract.getReserveData(weth.address); const collateralPrice = await oracle.getAssetPrice(weth.address); const principalPrice = await oracle.getAssetPrice(dai.address); const collateralDecimals = ( await helpersContract.getReserveConfigurationData(weth.address) ).decimals.toString(); const principalDecimals = ( await helpersContract.getReserveConfigurationData(dai.address) ).decimals.toString(); const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) .times(new BigNumber(amountToLiquidate).times(105)) .times(new BigNumber(10).pow(collateralDecimals)) .div( new BigNumber(collateralPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) ) .div(100) .decimalPlaces(0, BigNumber.ROUND_DOWN); if (!tx.blockNumber) { expect(false, 'Invalid block number'); return; } const txTimestamp = new BigNumber( (await DRE.ethers.provider.getBlock(tx.blockNumber)).timestamp ); const reserve = await getReserveData const variableDebtBeforeTx = calcExpectedVariableDebtTokenBalance( daiReserveBefore, userReserveDataBefore, txTimestamp ); expect(userReserveDataAfter.currentVariableDebt.toString()).to.be.bignumber.almostEqual( variableDebtBeforeTx.minus(amountToLiquidate).toFixed(0), 'Invalid user debt after liquidation' ); //the liquidity index of the principal reserve needs to be bigger than the index before expect(daiReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gte( daiReserveDataBefore.liquidityIndex.toString(), 'Invalid liquidity index' ); //the principal APY after a liquidation needs to be lower than the APY before expect(daiReserveDataAfter.liquidityRate.toString()).to.be.bignumber.lt( daiReserveDataBefore.liquidityRate.toString(), 'Invalid liquidity APY' ); expect(daiReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(daiReserveDataBefore.availableLiquidity.toString()) .plus(amountToLiquidate) .toFixed(0), 'Invalid principal available liquidity' ); expect(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(ethReserveDataBefore.availableLiquidity.toString()) .minus(expectedCollateralLiquidated) .toFixed(0), 'Invalid collateral available liquidity' ); }); it('User 3 deposits 1000 USDC, user 4 1 WETH, user 4 borrows - drops HF, liquidates the borrow', async () => { const { usdc, users, pool, oracle, weth, helpersContract } = testEnv; const depositor = users[3]; const borrower = users[4]; const liquidator = users[5]; //mints USDC to depositor await usdc .connect(depositor.signer) .mint(await convertToCurrencyDecimals(usdc.address, '1000')); //approve protocol to access depositor wallet await usdc.connect(depositor.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); //depositor deposits 1000 USDC const amountUSDCtoDeposit = await convertToCurrencyDecimals(usdc.address, '1000'); await pool .connect(depositor.signer) .deposit(usdc.address, amountUSDCtoDeposit, depositor.address, '0'); //borrower deposits 1 ETH const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); //mints WETH to borrower await weth.connect(borrower.signer).mint(await convertToCurrencyDecimals(weth.address, '1000')); //approve protocol to access the borrower wallet await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool .connect(borrower.signer) .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); //borrower borrows const userGlobalData = await pool.getUserAccountData(borrower.address); const usdcPrice = await oracle.getAssetPrice(usdc.address); const amountUSDCToBorrow = await convertToCurrencyDecimals( usdc.address, new BigNumber(userGlobalData.availableBorrowsETH.toString()) .div(usdcPrice.toString()) .multipliedBy(0.9502) .toFixed(0) ); await pool .connect(borrower.signer) .borrow(usdc.address, amountUSDCToBorrow, RateMode.Variable, '0', borrower.address); //drops HF below 1 await oracle.setAssetPrice( usdc.address, new BigNumber(usdcPrice.toString()).multipliedBy(1.12).toFixed(0) ); //mints dai to the liquidator await usdc .connect(liquidator.signer) .mint(await convertToCurrencyDecimals(usdc.address, '1000')); //approve protocol to access depositor wallet await usdc.connect(liquidator.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const userReserveDataBefore = await helpersContract.getUserReserveData( usdc.address, borrower.address ); const usdcReserveDataBefore = await helpersContract.getReserveData(usdc.address); const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); const amountToLiquidate = DRE.ethers.BigNumber.from( userReserveDataBefore.currentVariableDebt.toString() ) .div(2) .toString(); await pool .connect(liquidator.signer) .liquidationCall(weth.address, usdc.address, borrower.address, amountToLiquidate, false); const userReserveDataAfter = await helpersContract.getUserReserveData( usdc.address, borrower.address ); const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); const usdcReserveDataAfter = await helpersContract.getReserveData(usdc.address); const ethReserveDataAfter = await helpersContract.getReserveData(weth.address); const collateralPrice = await oracle.getAssetPrice(weth.address); const principalPrice = await oracle.getAssetPrice(usdc.address); const collateralDecimals = ( await helpersContract.getReserveConfigurationData(weth.address) ).decimals.toString(); const principalDecimals = ( await helpersContract.getReserveConfigurationData(usdc.address) ).decimals.toString(); const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) .times(new BigNumber(amountToLiquidate).times(105)) .times(new BigNumber(10).pow(collateralDecimals)) .div( new BigNumber(collateralPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) ) .div(100) .decimalPlaces(0, BigNumber.ROUND_DOWN); expect(userGlobalDataAfter.healthFactor.toString()).to.be.bignumber.gt( oneEther.toFixed(0), 'Invalid health factor' ); expect(userReserveDataAfter.currentVariableDebt.toString()).to.be.bignumber.almostEqual( new BigNumber(userReserveDataBefore.currentVariableDebt.toString()) .minus(amountToLiquidate) .toFixed(0), 'Invalid user borrow balance after liquidation' ); //the liquidity index of the principal reserve needs to be bigger than the index before expect(usdcReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gte( usdcReserveDataBefore.liquidityIndex.toString(), 'Invalid liquidity index' ); //the principal APY after a liquidation needs to be lower than the APY before expect(usdcReserveDataAfter.liquidityRate.toString()).to.be.bignumber.lt( usdcReserveDataBefore.liquidityRate.toString(), 'Invalid liquidity APY' ); expect(usdcReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(usdcReserveDataBefore.availableLiquidity.toString()) .plus(amountToLiquidate) .toFixed(0), 'Invalid principal available liquidity' ); expect(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(ethReserveDataBefore.availableLiquidity.toString()) .minus(expectedCollateralLiquidated) .toFixed(0), 'Invalid collateral available liquidity' ); }); it('User 4 deposits 10 AAVE - drops HF, liquidates the AAVE, which results on a lower amount being liquidated', async () => { const { aave, usdc, users, pool, oracle, helpersContract } = testEnv; const depositor = users[3]; const borrower = users[4]; const liquidator = users[5]; //mints AAVE to borrower await aave.connect(borrower.signer).mint(await convertToCurrencyDecimals(aave.address, '10')); //approve protocol to access the borrower wallet await aave.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); //borrower deposits 10 AAVE const amountToDeposit = await convertToCurrencyDecimals(aave.address, '10'); await pool .connect(borrower.signer) .deposit(aave.address, amountToDeposit, borrower.address, '0'); const usdcPrice = await oracle.getAssetPrice(usdc.address); //drops HF below 1 await oracle.setAssetPrice( usdc.address, new BigNumber(usdcPrice.toString()).multipliedBy(1.14).toFixed(0) ); //mints usdc to the liquidator await usdc .connect(liquidator.signer) .mint(await convertToCurrencyDecimals(usdc.address, '1000')); //approve protocol to access depositor wallet await usdc.connect(liquidator.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const userReserveDataBefore = await helpersContract.getUserReserveData( usdc.address, borrower.address ); const usdcReserveDataBefore = await helpersContract.getReserveData(usdc.address); const aaveReserveDataBefore = await helpersContract.getReserveData(aave.address); const amountToLiquidate = new BigNumber(userReserveDataBefore.currentVariableDebt.toString()) .div(2) .decimalPlaces(0, BigNumber.ROUND_DOWN) .toFixed(0); const collateralPrice = await oracle.getAssetPrice(aave.address); const principalPrice = await oracle.getAssetPrice(usdc.address); await pool .connect(liquidator.signer) .liquidationCall(aave.address, usdc.address, borrower.address, amountToLiquidate, false); const userReserveDataAfter = await helpersContract.getUserReserveData( usdc.address, borrower.address ); const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); const usdcReserveDataAfter = await helpersContract.getReserveData(usdc.address); const aaveReserveDataAfter = await helpersContract.getReserveData(aave.address); const aaveConfiguration = await helpersContract.getReserveConfigurationData(aave.address); const collateralDecimals = aaveConfiguration.decimals.toString(); const liquidationBonus = aaveConfiguration.liquidationBonus.toString(); const principalDecimals = ( await helpersContract.getReserveConfigurationData(usdc.address) ).decimals.toString(); const expectedCollateralLiquidated = oneEther.multipliedBy('10'); const expectedPrincipal = new BigNumber(collateralPrice.toString()) .times(expectedCollateralLiquidated) .times(new BigNumber(10).pow(principalDecimals)) .div( new BigNumber(principalPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) ) .times(10000) .div(liquidationBonus.toString()) .decimalPlaces(0, BigNumber.ROUND_DOWN); expect(userGlobalDataAfter.healthFactor.toString()).to.be.bignumber.gt( oneEther.toFixed(0), 'Invalid health factor' ); expect(userReserveDataAfter.currentVariableDebt.toString()).to.be.bignumber.almostEqual( new BigNumber(userReserveDataBefore.currentVariableDebt.toString()) .minus(expectedPrincipal) .toFixed(0), 'Invalid user borrow balance after liquidation' ); expect(usdcReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(usdcReserveDataBefore.availableLiquidity.toString()) .plus(expectedPrincipal) .toFixed(0), 'Invalid principal available liquidity' ); expect(aaveReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(aaveReserveDataBefore.availableLiquidity.toString()) .minus(expectedCollateralLiquidated) .toFixed(0), 'Invalid collateral available liquidity' ); }); });