import BigNumber from 'bignumber.js'; import {BRE} from '../helpers/misc-utils'; import {APPROVAL_AMOUNT_LENDING_POOL, MOCK_ETH_ADDRESS, oneEther} from '../helpers/constants'; import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; import {makeSuite} from './helpers/make-suite'; import {ProtocolErrors, RateMode} from '../helpers/types'; import {borrow} from './helpers/actions'; const chai = require('chai'); chai.use(require('chai-bignumber')()); const {expect} = chai; const almostEqual: any = function (this: any, expected: any, actual: any): any { this.assert( expected.plus(new BigNumber(1)).eq(actual) || expected.plus(new BigNumber(2)).eq(actual) || actual.plus(new BigNumber(1)).eq(expected) || actual.plus(new BigNumber(2)).eq(expected) || expected.eq(actual), 'expected #{act} to be almost equal #{exp}', 'expected #{act} to be different from #{exp}', expected.toString(), actual.toString() ); }; chai.use(function (chai: any, utils: any) { chai.Assertion.overwriteMethod('almostEqual', function (original: any) { return function (this: any, value: any) { if (utils.flag(this, 'bignumber')) { var expected = new BigNumber(value); var actual = new BigNumber(this._obj); almostEqual.apply(this, [expected, actual]); } else { original.apply(this, arguments); } }; }); }); makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', (testEnv) => { const { HF_IS_NOT_BELLOW_THRESHOLD, INVALID_HF, USER_DID_NOT_BORROW_SPECIFIED, INVALID_COLLATERAL_TO_LIQUIDATE, } = ProtocolErrors; it('LIQUIDATION - Deposits ETH, borrows DAI', async () => { const {dai, 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'); //user 2 deposits 1 ETH const amountETHtoDeposit = await convertToCurrencyDecimals(MOCK_ETH_ADDRESS, '1'); await pool.connect(borrower.signer).deposit(MOCK_ETH_ADDRESS, amountETHtoDeposit, '0', { value: amountETHtoDeposit, }); //user 2 borrows const userGlobalData: any = await pool.getUserAccountData(borrower.address); const daiPrice = await oracle.getAssetPrice(dai.address); const amountDAIToBorrow = await convertToCurrencyDecimals( dai.address, new BigNumber(userGlobalData.availableBorrowsETH) .div(daiPrice.toString()) .multipliedBy(0.95) .toFixed(0) ); await pool .connect(borrower.signer) .borrow(dai.address, amountDAIToBorrow, RateMode.Stable, '0'); const userGlobalDataAfter: any = await pool.getUserAccountData(borrower.address); expect(userGlobalDataAfter.currentLiquidationThreshold.toString()).to.be.bignumber.equal( '80', 'Invalid liquidation 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.25).toFixed(0) ); const userGlobalData: any = await pool.getUserAccountData(borrower.address); expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt( oneEther.toFixed(0), 'Invalid health factor' ); }); it('LIQUIDATION - Liquidates the borrow', async () => { const {dai, users, pool, oracle, addressesProvider} = testEnv; const liquidator = users[3]; 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.connect(liquidator.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const userReserveDataBefore: any = await pool.getUserReserveData(dai.address, borrower.address); const daiReserveDataBefore: any = await pool.getReserveData(dai.address); const ethReserveDataBefore: any = await pool.getReserveData(MOCK_ETH_ADDRESS); const amountToLiquidate = new BigNumber(userReserveDataBefore.currentBorrowBalance) .div(2) .toFixed(0); await pool.liquidationCall( MOCK_ETH_ADDRESS, dai.address, borrower.address, amountToLiquidate, false ); const userReserveDataAfter: any = await pool.getUserReserveData(dai.address, borrower.address); const daiReserveDataAfter: any = await pool.getReserveData(dai.address); const ethReserveDataAfter: any = await pool.getReserveData(MOCK_ETH_ADDRESS); const collateralPrice = await oracle.getAssetPrice(MOCK_ETH_ADDRESS); const principalPrice = await oracle.getAssetPrice(dai.address); const collateralDecimals = await pool.getReserveDecimals(MOCK_ETH_ADDRESS); const principalDecimals = await pool.getReserveDecimals(dai.address); 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); const expectedFeeLiquidated = new BigNumber(principalPrice.toString()) .times(new BigNumber(userReserveDataBefore.originationFee).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); const feeAddress = await addressesProvider.getTokenDistributor(); const feeAddressBalance = await BRE.ethers.provider.getBalance(feeAddress); expect(userReserveDataAfter.originationFee.toString()).to.be.bignumber.eq( '0', 'Origination fee should be repaid' ); expect(feeAddressBalance).to.be.bignumber.gt('0'); expect(userReserveDataAfter.principalBorrowBalance.toString()).to.be.bignumber.almostEqual( new BigNumber(userReserveDataBefore.currentBorrowBalance).minus(amountToLiquidate).toFixed(0), 'Invalid user borrow balance after liquidation' ); expect(daiReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(daiReserveDataBefore.availableLiquidity).plus(amountToLiquidate).toFixed(0), 'Invalid principal available liquidity' ); expect(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(ethReserveDataBefore.availableLiquidity) .minus(expectedFeeLiquidated) .minus(expectedCollateralLiquidated) .toFixed(0), 'Invalid collateral available liquidity' ); }); it('User 3 deposits 1000 USDC, user 4 1 ETH, user 4 borrows - drops HF, liquidates the borrow', async () => { const {usdc, users, pool, oracle, addressesProvider} = 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, '0'); //borrower deposits 1 ETH const amountETHtoDeposit = await convertToCurrencyDecimals(MOCK_ETH_ADDRESS, '1'); await pool.connect(borrower.signer).deposit(MOCK_ETH_ADDRESS, amountETHtoDeposit, '0', { value: amountETHtoDeposit, }); //borrower borrows const userGlobalData: any = await pool.getUserAccountData(borrower.address); const usdcPrice = await oracle.getAssetPrice(usdc.address); const amountUSDCToBorrow = await convertToCurrencyDecimals( usdc.address, new BigNumber(userGlobalData.availableBorrowsETH).div(usdcPrice.toString()).multipliedBy(0.95).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.connect(liquidator.signer).mint(await convertToCurrencyDecimals(usdc.address, '1000')); //approve protocol to access depositor wallet await usdc.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const userReserveDataBefore: any = await pool.getUserReserveData(usdc.address, borrower.address); const usdcReserveDataBefore: any = await pool.getReserveData(usdc.address); const ethReserveDataBefore: any = await pool.getReserveData(MOCK_ETH_ADDRESS); const amountToLiquidate = new BigNumber(userReserveDataBefore.currentBorrowBalance) .div(2) .decimalPlaces(0, BigNumber.ROUND_DOWN) .toFixed(0); await pool.connect(liquidator.signer).liquidationCall(MOCK_ETH_ADDRESS, usdc.address, borrower.address, amountToLiquidate, false); const userReserveDataAfter: any = await pool.getUserReserveData(usdc.address, borrower.address); const userGlobalDataAfter: any = await pool.getUserAccountData(borrower.address); const usdcReserveDataAfter: any = await pool.getReserveData(usdc.address); const ethReserveDataAfter: any = await pool.getReserveData(MOCK_ETH_ADDRESS); const feeAddress = await addressesProvider.getTokenDistributor(); const feeAddressBalance = await BRE.ethers.provider.getBalance(feeAddress); const collateralPrice = await oracle.getAssetPrice(MOCK_ETH_ADDRESS); const principalPrice = await oracle.getAssetPrice(usdc.address); const collateralDecimals = await pool.getReserveDecimals(MOCK_ETH_ADDRESS); const principalDecimals = await pool.getReserveDecimals(usdc.address); 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); const expectedFeeLiquidated = new BigNumber(principalPrice.toString()) .times(new BigNumber(userReserveDataBefore.originationFee).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.originationFee.toString()).to.be.bignumber.eq( '0', 'Origination fee should be repaid' ); expect(feeAddressBalance.toString()).to.be.bignumber.gt('0'); expect(userReserveDataAfter.principalBorrowBalance.toString()).to.be.bignumber.almostEqual( new BigNumber(userReserveDataBefore.currentBorrowBalance.toString()) .minus(amountToLiquidate) .toFixed(0), 'Invalid user borrow balance after liquidation' ); expect(usdcReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(usdcReserveDataBefore.availableLiquidity).plus(amountToLiquidate).toFixed(0), 'Invalid principal available liquidity' ); expect(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(ethReserveDataBefore.availableLiquidity) .minus(expectedFeeLiquidated) .minus(expectedCollateralLiquidated) .toFixed(0), 'Invalid collateral available liquidity' ); }); it('User 4 deposits 1000 LEND - drops HF, liquidates the LEND, which results on a lower amount being liquidated', async () => { const {lend, usdc, users, pool, oracle, addressesProvider} = testEnv; const depositor = users[3]; const borrower = users[4]; const liquidator = users[5]; //mints LEND to borrower await lend.connect(borrower.signer).mint(await convertToCurrencyDecimals(lend.address, '1000')); //approve protocol to access the borrower wallet await lend.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); //borrower deposits 1000 LEND const amountLENDtoDeposit = await convertToCurrencyDecimals(lend.address, '1000'); await pool.connect(borrower.signer).deposit(lend.address, amountLENDtoDeposit, '0'); const usdcPrice = await oracle.getAssetPrice(usdc.address); //drops HF below 1 await oracle.setAssetPrice(usdc.address, new BigNumber(usdcPrice.toString()).multipliedBy(1.1).toFixed(0)); //mints usdc 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: any = await pool.getUserReserveData(usdc.address, borrower.address); const usdcReserveDataBefore: any = await pool.getReserveData(usdc.address); const lendReserveDataBefore: any = await pool.getReserveData(lend.address); const amountToLiquidate = new BigNumber(userReserveDataBefore.currentBorrowBalance) .div(2) .decimalPlaces(0, BigNumber.ROUND_DOWN) .toFixed(0); const collateralPrice = await oracle.getAssetPrice(lend.address); const principalPrice = await oracle.getAssetPrice(usdc.address); await pool.connect(liquidator.signer).liquidationCall( lend.address, usdc.address, borrower.address, amountToLiquidate, false ); const userReserveDataAfter: any = await pool.getUserReserveData(usdc.address, borrower.address); const userGlobalDataAfter: any = await pool.getUserAccountData(borrower.address); const usdcReserveDataAfter: any = await pool.getReserveData(usdc.address); const lendReserveDataAfter: any = await pool.getReserveData(lend.address); const collateralDecimals = await pool.getReserveDecimals(lend.address); const principalDecimals = await pool.getReserveDecimals(usdc.address); const expectedCollateralLiquidated = oneEther.multipliedBy('1000'); const liquidationBonus = await pool.getReserveLiquidationBonus(lend.address); 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(100) .div(liquidationBonus.toString()) .decimalPlaces(0, BigNumber.ROUND_DOWN); expect(userGlobalDataAfter.healthFactor.toString()).to.be.bignumber.gt( oneEther.toFixed(0), 'Invalid health factor' ); expect(userReserveDataAfter.originationFee.toString()).to.be.bignumber.eq( '0', 'Origination fee should be repaid' ); expect(userReserveDataAfter.principalBorrowBalance.toString()).to.be.bignumber.almostEqual( new BigNumber(userReserveDataBefore.currentBorrowBalance).minus(expectedPrincipal).toFixed(0), 'Invalid user borrow balance after liquidation' ); expect(usdcReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(usdcReserveDataBefore.availableLiquidity).plus(expectedPrincipal).toFixed(0), 'Invalid principal available liquidity' ); expect(lendReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(lendReserveDataBefore.availableLiquidity) .minus(expectedCollateralLiquidated) .toFixed(0), 'Invalid collateral available liquidity' ); }); });