import BigNumber from 'bignumber.js'; import {BRE} 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 {getUserData, getReserveData} from './helpers/utils/helpers'; const chai = require('chai'); const {expect} = chai; makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => { const { HEALTH_FACTOR_NOT_BELOW_THRESHOLD, INVALID_HF, SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER, COLLATERAL_CANNOT_BE_LIQUIDATED, } = ProtocolErrors; it('LIQUIDATION - Deposits WETH, borrows DAI/Check liquidation fails because health factor is above 1', 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, '0'); const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); //mints WETH to borrower await weth.connect(borrower.signer).mint(amountETHtoDeposit); //approve protocol to access borrower wallet await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); //user 2 deposits 1 WETH await pool.connect(borrower.signer).deposit(weth.address, amountETHtoDeposit, '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'); const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); expect(userGlobalDataAfter.currentLiquidationThreshold.toString()).to.be.bignumber.equal( '8000', 'Invalid liquidation threshold' ); //someone tries to liquidate user 2 await expect( pool.liquidationCall(weth.address, dai.address, borrower.address, 1, true) ).to.be.revertedWith(HEALTH_FACTOR_NOT_BELOW_THRESHOLD); }); it('LIQUIDATION - Drop the health factor below 1', async () => { const {dai, 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.15).toFixed(0) ); const userGlobalData = await pool.getUserAccountData(borrower.address); expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt(oneEther, INVALID_HF); }); it('LIQUIDATION - Tries to liquidate a different currency than the loan principal', async () => { const {pool, users, weth} = testEnv; const borrower = users[1]; //user 2 tries to borrow await expect( pool.liquidationCall(weth.address, weth.address, borrower.address, oneEther.toString(), true) ).revertedWith(SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER); }); it('LIQUIDATION - Tries to liquidate a different collateral than the borrower collateral', async () => { const {pool, dai, users} = testEnv; const borrower = users[1]; await expect( pool.liquidationCall(dai.address, dai.address, borrower.address, oneEther.toString(), true) ).revertedWith(COLLATERAL_CANNOT_BE_LIQUIDATED); }); it('LIQUIDATION - Liquidates the borrow', async () => { const {pool, dai, weth, users, oracle} = testEnv; const borrower = users[1]; //mints dai to the caller await dai.mint(await convertToCurrencyDecimals(dai.address, '1000')); //approve protocol to access depositor wallet await dai.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const daiReserveDataBefore = await getReserveData(pool, dai.address); const ethReserveDataBefore = await pool.getReserveData(weth.address); const userReserveDataBefore = await getUserData(pool, dai.address, borrower.address); const amountToLiquidate = new BigNumber(userReserveDataBefore.currentVariableDebt.toString()) .div(2) .toFixed(0); const tx = await pool.liquidationCall( weth.address, dai.address, borrower.address, amountToLiquidate, true ); const userReserveDataAfter = await pool.getUserReserveData(dai.address, borrower.address); const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); const daiReserveDataAfter = await pool.getReserveData(dai.address); const ethReserveDataAfter = await pool.getReserveData(weth.address); const collateralPrice = (await oracle.getAssetPrice(weth.address)).toString(); const principalPrice = (await oracle.getAssetPrice(dai.address)).toString(); const collateralDecimals = ( await pool.getReserveConfigurationData(weth.address) ).decimals.toString(); const principalDecimals = ( await pool.getReserveConfigurationData(dai.address) ).decimals.toString(); const expectedCollateralLiquidated = new BigNumber(principalPrice) .times(new BigNumber(amountToLiquidate).times(105)) .times(new BigNumber(10).pow(collateralDecimals)) .div(new BigNumber(collateralPrice).times(new BigNumber(10).pow(principalDecimals))) .decimalPlaces(0, BigNumber.ROUND_DOWN); if (!tx.blockNumber) { expect(false, 'Invalid block number'); return; } const txTimestamp = new BigNumber( (await BRE.ethers.provider.getBlock(tx.blockNumber)).timestamp ); const variableDebtBeforeTx = calcExpectedVariableDebtTokenBalance( daiReserveDataBefore, userReserveDataBefore, txTimestamp ); expect(userGlobalDataAfter.healthFactor.toString()).to.be.bignumber.gt( oneEther.toFixed(0), 'Invalid health factor' ); expect(userReserveDataAfter.currentVariableDebt.toString()).to.be.bignumber.almostEqual( new BigNumber(variableDebtBeforeTx).minus(amountToLiquidate).toFixed(0), 'Invalid user borrow balance after liquidation' ); expect(daiReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(daiReserveDataBefore.availableLiquidity.toString()) .plus(amountToLiquidate) .toFixed(0), 'Invalid principal available liquidity' ); //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(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(ethReserveDataBefore.availableLiquidity.toString()).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 {users, pool, usdc, oracle, weth} = testEnv; const depositor = users[3]; const borrower = users[4]; //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); //user 3 deposits 1000 USDC const amountUSDCtoDeposit = await convertToCurrencyDecimals(usdc.address, '1000'); await pool.connect(depositor.signer).deposit(usdc.address, amountUSDCtoDeposit, '0'); //user 4 deposits 1 ETH const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); //mints WETH to borrower await weth.connect(borrower.signer).mint(amountETHtoDeposit); //approve protocol to access borrower wallet await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool.connect(borrower.signer).deposit(weth.address, amountETHtoDeposit, '0'); //user 4 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.Stable, '0'); //drops HF below 1 await oracle.setAssetPrice( usdc.address, new BigNumber(usdcPrice.toString()).multipliedBy(1.2).toFixed(0) ); //mints dai to the liquidator await usdc.mint(await convertToCurrencyDecimals(usdc.address, '1000')); //approve protocol to access depositor wallet await usdc.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const userReserveDataBefore = await pool.getUserReserveData(usdc.address, borrower.address); const usdcReserveDataBefore = await pool.getReserveData(usdc.address); const ethReserveDataBefore = await pool.getReserveData(weth.address); const amountToLiquidate = new BigNumber(userReserveDataBefore.currentStableDebt.toString()) .multipliedBy(0.5) .toFixed(0); await pool.liquidationCall( weth.address, usdc.address, borrower.address, amountToLiquidate, true ); const userReserveDataAfter = await pool.getUserReserveData(usdc.address, borrower.address); const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); const usdcReserveDataAfter = await pool.getReserveData(usdc.address); const ethReserveDataAfter = await pool.getReserveData(weth.address); const collateralPrice = (await oracle.getAssetPrice(weth.address)).toString(); const principalPrice = (await oracle.getAssetPrice(usdc.address)).toString(); const collateralDecimals = ( await pool.getReserveConfigurationData(weth.address) ).decimals.toString(); const principalDecimals = ( await pool.getReserveConfigurationData(usdc.address) ).decimals.toString(); const expectedCollateralLiquidated = new BigNumber(principalPrice) .times(new BigNumber(amountToLiquidate).times(105)) .times(new BigNumber(10).pow(collateralDecimals)) .div(new BigNumber(collateralPrice).times(new BigNumber(10).pow(principalDecimals))) .decimalPlaces(0, BigNumber.ROUND_DOWN); expect(userGlobalDataAfter.healthFactor.toString()).to.be.bignumber.gt( oneEther.toFixed(0), 'Invalid health factor' ); expect(userReserveDataAfter.currentStableDebt.toString()).to.be.bignumber.almostEqual( new BigNumber(userReserveDataBefore.currentStableDebt.toString()) .minus(amountToLiquidate) .toFixed(0), 'Invalid user borrow balance after liquidation' ); expect(usdcReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(usdcReserveDataBefore.availableLiquidity.toString()) .plus(amountToLiquidate) .toFixed(0), 'Invalid principal available liquidity' ); //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(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(ethReserveDataBefore.availableLiquidity.toString()).toFixed(0), 'Invalid collateral available liquidity' ); }); });