diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index 3bcab1333..3690181ec 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -6,9 +6,7 @@ import {IAToken} from '../../../interfaces/IAToken.sol'; import {IStableDebtToken} from '../../../interfaces/IStableDebtToken.sol'; import {IVariableDebtToken} from '../../../interfaces/IVariableDebtToken.sol'; import {IPriceOracleGetter} from '../../../interfaces/IPriceOracleGetter.sol'; -import { - VersionedInitializable -} from '../../libraries/aave-upgradeability/VersionedInitializable.sol'; +import {VersionedInitializable} from '../../libraries/aave-upgradeability/VersionedInitializable.sol'; import {ReserveLogic} from '../../libraries/logic/ReserveLogic.sol'; import {Helpers} from '../../libraries/helpers/Helpers.sol'; import {WadRayMath} from '../../libraries/math/WadRayMath.sol'; @@ -64,13 +62,14 @@ library LiquidationLogic { uint256 healthFactor; uint256 liquidatorPreviousATokenBalance; IAToken collateralAtoken; + ITokenWithTreasury tokenWithTreasury; IPriceOracleGetter oracle; bool isCollateralEnabled; DataTypes.InterestRateMode borrowRateMode; uint256 errorCode; string errorMsg; DataTypes.ReserveCache debtReserveCache; - + uint256 liquidationProtocolFeeAmount; } /** @@ -91,8 +90,10 @@ library LiquidationLogic { DataTypes.UserConfigurationMap storage userConfig = usersConfig[params.user]; vars.debtReserveCache = debtReserve.cache(); - - (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt(params.user, debtReserve); + (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt( + params.user, + debtReserve + ); vars.oracle = IPriceOracleGetter(params.priceOracle); ValidationLogic.validateLiquidationCall( @@ -108,7 +109,6 @@ library LiquidationLogic { ); vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); - vars.userCollateralBalance = vars.collateralAtoken.balanceOf(params.user); vars.maxLiquidatableDebt = (vars.userStableDebt + vars.userVariableDebt).percentMul( @@ -121,7 +121,8 @@ library LiquidationLogic { ( vars.maxCollateralToLiquidate, - vars.debtAmountNeeded + vars.debtAmountNeeded, + vars.liquidationProtocolFeeAmount ) = _calculateAvailableCollateralToLiquidate( collateralReserve, vars.debtReserveCache, @@ -149,7 +150,12 @@ library LiquidationLogic { vars.debtReserveCache.nextVariableBorrowIndex ); vars.debtReserveCache.refreshDebt(0, 0, 0, vars.actualDebtToLiquidate); - debtReserve.updateInterestRates(vars.debtReserveCache, params.debtAsset, vars.actualDebtToLiquidate, 0); + debtReserve.updateInterestRates( + vars.debtReserveCache, + params.debtAsset, + vars.actualDebtToLiquidate, + 0 + ); } else { // If the user doesn't have variable debt, no need to try to burn variable debt tokens if (vars.userVariableDebt > 0) { @@ -170,12 +176,21 @@ library LiquidationLogic { vars.userVariableDebt ); - debtReserve.updateInterestRates(vars.debtReserveCache, params.debtAsset, vars.actualDebtToLiquidate, 0); + debtReserve.updateInterestRates( + vars.debtReserveCache, + params.debtAsset, + vars.actualDebtToLiquidate, + 0 + ); } if (params.receiveAToken) { vars.liquidatorPreviousATokenBalance = IERC20(vars.collateralAtoken).balanceOf(msg.sender); - vars.collateralAtoken.transferOnLiquidation(params.user, msg.sender, vars.maxCollateralToLiquidate); + vars.collateralAtoken.transferOnLiquidation( + params.user, + msg.sender, + vars.maxCollateralToLiquidate + ); if (vars.liquidatorPreviousATokenBalance == 0) { DataTypes.UserConfigurationMap storage liquidatorConfig = usersConfig[msg.sender]; @@ -201,6 +216,16 @@ library LiquidationLogic { ); } + // Transfer fee to treasury if it is non-zero + if (vars.liquidationProtocolFeeAmount > 0) { + vars.tokenWithTreasury = ITokenWithTreasury(collateralReserve.aTokenAddress); + vars.collateralAtoken.transferOnLiquidation( + params.user, + vars.tokenWithTreasury.RESERVE_TREASURY_ADDRESS(), + vars.liquidationProtocolFeeAmount + ); + } + // If the collateral being liquidated is equal to the user balance, // we set the currency as not being used as collateral anymore if (vars.maxCollateralToLiquidate == vars.userCollateralBalance) { @@ -236,6 +261,10 @@ library LiquidationLogic { uint256 collateralDecimals; uint256 collateralAssetUnit; uint256 debtAssetUnit; + uint256 collateralAmount; + uint256 debtAmountNeeded; + uint256 liquidationProtocolFeePercentage; + uint256 liquidationProtocolFeeAmount; } /** @@ -261,10 +290,15 @@ library LiquidationLogic { uint256 debtToCover, uint256 userCollateralBalance, IPriceOracleGetter oracle - ) internal view returns (uint256, uint256) { - uint256 collateralAmount = 0; - uint256 debtAmountNeeded = 0; - + ) + internal + view + returns ( + uint256, + uint256, + uint256 + ) + { AvailableCollateralToLiquidateLocalVars memory vars; vars.collateralPrice = oracle.getAssetPrice(collateralAsset); @@ -273,13 +307,16 @@ library LiquidationLogic { (, , vars.liquidationBonus, vars.collateralDecimals, ) = collateralReserve .configuration .getParams(); - vars.debtAssetDecimals = debtReserveCache.reserveConfiguration.getDecimalsMemory(); unchecked { vars.collateralAssetUnit = 10**vars.collateralDecimals; vars.debtAssetUnit = 10**vars.debtAssetDecimals; } + vars.liquidationProtocolFeePercentage = collateralReserve + .configuration + .getLiquidationProtocolFee(); + // This is the maximum possible amount of the selected collateral that can be liquidated, given the // max amount of liquidatable debt vars.maxAmountCollateralToLiquidate = @@ -291,14 +328,28 @@ library LiquidationLogic { (vars.collateralPrice * vars.debtAssetUnit); if (vars.maxAmountCollateralToLiquidate > userCollateralBalance) { - collateralAmount = userCollateralBalance; - debtAmountNeeded = ((vars.collateralPrice * collateralAmount * vars.debtAssetUnit) / - (vars.debtAssetPrice * vars.collateralAssetUnit)) - .percentDiv(vars.liquidationBonus); + vars.collateralAmount = userCollateralBalance; + vars.debtAmountNeeded = ((vars.collateralPrice * vars.collateralAmount * vars.debtAssetUnit) / + (vars.debtAssetPrice * vars.collateralAssetUnit)).percentDiv(vars.liquidationBonus); } else { - collateralAmount = vars.maxAmountCollateralToLiquidate; - debtAmountNeeded = debtToCover; + vars.collateralAmount = vars.maxAmountCollateralToLiquidate; + vars.debtAmountNeeded = debtToCover; } - return (collateralAmount, debtAmountNeeded); + + if (vars.liquidationProtocolFeePercentage > 0) { + vars.liquidationProtocolFeeAmount = vars.collateralAmount.percentMul( + vars.liquidationProtocolFeePercentage + ); + return ( + vars.collateralAmount - vars.liquidationProtocolFeeAmount, + vars.debtAmountNeeded, + vars.liquidationProtocolFeeAmount + ); + } + return (vars.collateralAmount, vars.debtAmountNeeded, 0); } } + +interface ITokenWithTreasury { + function RESERVE_TREASURY_ADDRESS() external view returns (address); +} diff --git a/test-suites/liquidation-underlying.spec.ts b/test-suites/liquidation-underlying.spec.ts index 8efc7d714..2670fd0b5 100644 --- a/test-suites/liquidation-underlying.spec.ts +++ b/test-suites/liquidation-underlying.spec.ts @@ -377,7 +377,7 @@ makeSuite('Pool Liquidation: Liquidator receiving the underlying asset', (testEn .connect(liquidator.signer) .mint(await convertToCurrencyDecimals(usdc.address, '1000')); - //approve protocol to access depositor wallet + //approve protocol to access liquidator wallet await usdc.connect(liquidator.signer).approve(pool.address, MAX_UINT_AMOUNT); const userReserveDataBefore = await helpersContract.getUserReserveData( diff --git a/test-suites/liquidation-with-fee.spec.ts b/test-suites/liquidation-with-fee.spec.ts new file mode 100644 index 000000000..4d7a6a109 --- /dev/null +++ b/test-suites/liquidation-with-fee.spec.ts @@ -0,0 +1,562 @@ +import { expect } from 'chai'; +import { BigNumber, utils } from 'ethers'; +import { DRE, increaseTime } from '../helpers/misc-utils'; +import { MAX_UINT_AMOUNT, oneEther } from '../helpers/constants'; +import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'; +import { ProtocolErrors, RateMode } from '../helpers/types'; +import { ATokenFactory } from '../types'; +import { calcExpectedStableDebtTokenBalance } from './helpers/utils/calculations'; +import { getUserData } from './helpers/utils/helpers'; +import { makeSuite } from './helpers/make-suite'; + +makeSuite('Pool Liquidation: Add fee to liquidations', (testEnv) => { + const { INVALID_HF } = ProtocolErrors; + + it('Sets the WETH protocol liquidation fee to 1000 (10.00%)', async () => { + const { configurator, weth, aave, helpersContract } = testEnv; + + const wethLiquidationProtocolFeeInput = 1000; + const aaveLiquidationProtocolFeeInput = 500; + + expect( + await configurator.setLiquidationProtocolFee(weth.address, wethLiquidationProtocolFeeInput) + ) + .to.emit(configurator, 'LiquidationProtocolFeeChanged') + .withArgs(weth.address, wethLiquidationProtocolFeeInput); + expect( + await configurator.setLiquidationProtocolFee(aave.address, aaveLiquidationProtocolFeeInput) + ) + .to.emit(configurator, 'LiquidationProtocolFeeChanged') + .withArgs(aave.address, aaveLiquidationProtocolFeeInput); + + const wethLiquidationProtocolFee = await helpersContract.getLiquidationProtocolFee( + weth.address + ); + const aaveLiquidationProtocolFee = await helpersContract.getLiquidationProtocolFee( + aave.address + ); + + expect(wethLiquidationProtocolFee).to.be.equal(wethLiquidationProtocolFeeInput); + expect(aaveLiquidationProtocolFee).to.be.equal(aaveLiquidationProtocolFeeInput); + }); + + it('Deposits WETH, borrows DAI', async () => { + const { + dai, + weth, + users: [depositor, borrower], + pool, + oracle, + } = testEnv; + + //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, MAX_UINT_AMOUNT); + + //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, MAX_UINT_AMOUNT); + + 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, + userGlobalData.availableBorrowsBase.div(daiPrice).percentMul(9500).toString() + ); + + await pool + .connect(borrower.signer) + .borrow(dai.address, amountDAIToBorrow, RateMode.Stable, '0', borrower.address); + + const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); + + expect(userGlobalDataAfter.currentLiquidationThreshold).to.be.equal(8250, INVALID_HF); + }); + + it('Drop the health factor below 1', async () => { + const { + dai, + users: [, borrower], + pool, + oracle, + } = testEnv; + + const daiPrice = await oracle.getAssetPrice(dai.address); + + await oracle.setAssetPrice(dai.address, daiPrice.percentMul(11800)); + + const userGlobalData = await pool.getUserAccountData(borrower.address); + + expect(userGlobalData.healthFactor).to.be.lt(oneEther, INVALID_HF); + }); + + it('Liquidates the borrow', async () => { + const { + dai, + weth, + aWETH, + users: [, borrower, , liquidator], + pool, + oracle, + helpersContract, + } = testEnv; + + //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, MAX_UINT_AMOUNT); + + const daiReserveDataBefore = await helpersContract.getReserveData(dai.address); + const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); + + const liquidatorBalanceBefore = await weth.balanceOf(liquidator.address); + + const treasuryAddress = await aWETH.RESERVE_TREASURY_ADDRESS(); + const treasuryDataBefore = await helpersContract.getUserReserveData( + weth.address, + treasuryAddress + ); + const treasuryBalanceBefore = treasuryDataBefore.currentATokenBalance; + + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2); + + const wethLiquidationProtocolFee = await helpersContract.getLiquidationProtocolFee( + weth.address + ); + + 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 liquidatorBalanceAfter = await weth.balanceOf(liquidator.address); + + const treasuryDataAfter = await helpersContract.getUserReserveData( + weth.address, + treasuryAddress + ); + const treasuryBalanceAfter = treasuryDataAfter.currentATokenBalance; + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + + const collateralDecimals = (await helpersContract.getReserveConfigurationData(weth.address)) + .decimals; + const principalDecimals = (await helpersContract.getReserveConfigurationData(dai.address)) + .decimals; + + const totalCollateralLiquidated = principalPrice + .mul(amountToLiquidate) + .percentMul(10500) + .mul(BigNumber.from(10).pow(collateralDecimals)) + .div(collateralPrice.mul(BigNumber.from(10).pow(principalDecimals))); + + const liquidationProtocolFees = totalCollateralLiquidated.percentMul( + wethLiquidationProtocolFee + ); + const expectedLiquidationReward = totalCollateralLiquidated.sub(liquidationProtocolFees); + + if (!tx.blockNumber) { + expect(false, 'Invalid block number'); + return; + } + const txTimestamp = BigNumber.from( + (await DRE.ethers.provider.getBlock(tx.blockNumber)).timestamp + ); + + const stableDebtBeforeTx = calcExpectedStableDebtTokenBalance( + userReserveDataBefore.principalStableDebt, + userReserveDataBefore.stableBorrowRate, + userReserveDataBefore.stableRateLastUpdated, + txTimestamp + ); + + expect(userReserveDataAfter.currentStableDebt).to.be.closeTo( + stableDebtBeforeTx.sub(amountToLiquidate), + 2, + 'Invalid user debt after liquidation' + ); + + //the liquidity index of the principal reserve needs to be bigger than the index before + expect(daiReserveDataAfter.liquidityIndex).to.be.gte( + daiReserveDataBefore.liquidityIndex, + 'Invalid liquidity index' + ); + + //the principal APY after a liquidation needs to be lower than the APY before + expect(daiReserveDataAfter.liquidityRate).to.be.lt( + daiReserveDataBefore.liquidityRate, + 'Invalid liquidity APY' + ); + + expect(daiReserveDataAfter.availableLiquidity).to.be.closeTo( + daiReserveDataBefore.availableLiquidity.add(amountToLiquidate), + 2, + 'Invalid principal available liquidity' + ); + + expect(ethReserveDataAfter.availableLiquidity).to.be.closeTo( + ethReserveDataBefore.availableLiquidity.sub(expectedLiquidationReward), + 2, + 'Invalid collateral available liquidity' + ); + + expect(treasuryBalanceAfter).to.be.closeTo( + treasuryBalanceBefore.add(liquidationProtocolFees), + 2, + 'Invalid treasury increase' + ); + + expect(liquidatorBalanceAfter).to.be.closeTo( + liquidatorBalanceBefore.add(expectedLiquidationReward), + 2, + 'Invalid liquidator balance' + ); + }); + + it('User 3 deposits 1000 USDC, user 4 1 WETH, user 4 borrows - drops HF, liquidates the borrow', async () => { + const { + usdc, + users: [, , , depositor, borrower, liquidator], + pool, + oracle, + weth, + aWETH, + helpersContract, + } = testEnv; + + //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, MAX_UINT_AMOUNT); + + //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, MAX_UINT_AMOUNT); + + 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, + userGlobalData.availableBorrowsBase.div(usdcPrice).percentMul(9502).toString() + ); + + await pool + .connect(borrower.signer) + .borrow(usdc.address, amountUSDCToBorrow, RateMode.Stable, '0', borrower.address); + + //drops HF below 1 + await oracle.setAssetPrice(usdc.address, usdcPrice.percentMul(11200)); + + //mints usdc to the liquidator + await usdc + .connect(liquidator.signer) + .mint(await convertToCurrencyDecimals(usdc.address, '1000')); + + //approve protocol to access liquidator wallet + await usdc.connect(liquidator.signer).approve(pool.address, MAX_UINT_AMOUNT); + + const userReserveDataBefore = await helpersContract.getUserReserveData( + usdc.address, + borrower.address + ); + + const usdcReserveDataBefore = await helpersContract.getReserveData(usdc.address); + const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); + + const liquidatorBalanceBefore = await weth.balanceOf(liquidator.address); + + const treasuryAddress = await aWETH.RESERVE_TREASURY_ADDRESS(); + const treasuryDataBefore = await helpersContract.getUserReserveData( + weth.address, + treasuryAddress + ); + const treasuryBalanceBefore = treasuryDataBefore.currentATokenBalance; + + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2); + + const wethLiquidationProtocolFee = await helpersContract.getLiquidationProtocolFee( + weth.address + ); + + 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 liquidatorBalanceAfter = await weth.balanceOf(liquidator.address); + const treasuryDataAfter = await helpersContract.getUserReserveData( + weth.address, + treasuryAddress + ); + const treasuryBalanceAfter = treasuryDataAfter.currentATokenBalance; + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = (await helpersContract.getReserveConfigurationData(weth.address)) + .decimals; + const principalDecimals = (await helpersContract.getReserveConfigurationData(usdc.address)) + .decimals; + + const expectedCollateralLiquidated = principalPrice + .mul(BigNumber.from(amountToLiquidate)) + .percentMul(10500) + .mul(BigNumber.from(10).pow(collateralDecimals)) + .div(collateralPrice.mul(BigNumber.from(10).pow(principalDecimals))); + + const liquidationProtocolFees = expectedCollateralLiquidated.percentMul( + wethLiquidationProtocolFee + ); + const expectedLiquidationReward = expectedCollateralLiquidated.sub(liquidationProtocolFees); + + expect(userGlobalDataAfter.healthFactor).to.be.gt(oneEther, 'Invalid health factor'); + + expect(userReserveDataAfter.currentStableDebt).to.be.closeTo( + userReserveDataBefore.currentStableDebt.sub(amountToLiquidate), + 2, + 'Invalid user borrow balance after liquidation' + ); + + //the liquidity index of the principal reserve needs to be bigger than the index before + expect(usdcReserveDataAfter.liquidityIndex).to.be.gte( + usdcReserveDataBefore.liquidityIndex, + 'Invalid liquidity index' + ); + + //the principal APY after a liquidation needs to be lower than the APY before + expect(usdcReserveDataAfter.liquidityRate).to.be.lt( + usdcReserveDataBefore.liquidityRate, + 'Invalid liquidity APY' + ); + + expect(usdcReserveDataAfter.availableLiquidity).to.be.closeTo( + usdcReserveDataBefore.availableLiquidity.add(amountToLiquidate), + 2, + 'Invalid principal available liquidity' + ); + + expect(ethReserveDataAfter.availableLiquidity).to.be.closeTo( + ethReserveDataBefore.availableLiquidity.sub(expectedLiquidationReward), + 2, + 'Invalid collateral available liquidity' + ); + + expect(treasuryBalanceAfter).to.be.closeTo( + treasuryBalanceBefore.add(liquidationProtocolFees), + 2, + 'Invalid treasury increase' + ); + + expect(liquidatorBalanceAfter).to.be.closeTo( + liquidatorBalanceBefore.add(expectedLiquidationReward), + 2, + 'Invalid liquidator balance' + ); + }); + + it('User 4 deposits 10 AAVE - drops HF, liquidates the AAVE, which results on a lower amount being liquidated', async () => { + const { + aave, + usdc, + users: [, , , , borrower, liquidator], + pool, + oracle, + helpersContract, + } = testEnv; + + //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, MAX_UINT_AMOUNT); + + //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, usdcPrice.percentMul(11400)); + + //mints usdc to the liquidator + await usdc + .connect(liquidator.signer) + .mint(await convertToCurrencyDecimals(usdc.address, '1000')); + + //approve protocol to access liquidator wallet + await usdc.connect(liquidator.signer).approve(pool.address, MAX_UINT_AMOUNT); + + 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 = userReserveDataBefore.currentStableDebt.div(2); + + const collateralPrice = await oracle.getAssetPrice(aave.address); + const principalPrice = await oracle.getAssetPrice(usdc.address); + + const aaveTokenAddresses = await helpersContract.getReserveTokensAddresses(aave.address); + const aAaveTokenAddress = await aaveTokenAddresses.aTokenAddress; + const aAaveTokenContract = await ATokenFactory.connect(aAaveTokenAddress, DRE.ethers.provider); + const aAaveTokenBalanceBefore = await aAaveTokenContract.balanceOf(liquidator.address); + + const treasuryAddress = await aAaveTokenContract.RESERVE_TREASURY_ADDRESS(); + const treasuryDataBefore = await helpersContract.getUserReserveData( + aave.address, + treasuryAddress + ); + const treasuryBalanceBefore = treasuryDataBefore.currentATokenBalance; + + await pool + .connect(liquidator.signer) + .liquidationCall(aave.address, usdc.address, borrower.address, amountToLiquidate, true); + + 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; + const liquidationBonus = aaveConfiguration.liquidationBonus; + + const principalDecimals = (await helpersContract.getReserveConfigurationData(usdc.address)) + .decimals; + + const expectedCollateralLiquidated = oneEther.mul(10); + + const aaveLiquidationProtocolFee = await helpersContract.getLiquidationProtocolFee( + aave.address + ); + + const protocolFeeAmount = expectedCollateralLiquidated.percentMul(aaveLiquidationProtocolFee); + const liquidationReward = expectedCollateralLiquidated.sub(protocolFeeAmount); + + const expectedPrincipal = collateralPrice + .mul(expectedCollateralLiquidated) + .mul(BigNumber.from(10).pow(principalDecimals)) + .div(principalPrice.mul(BigNumber.from(10).pow(collateralDecimals))) + .percentDiv(liquidationBonus); + + const aAaveTokenBalanceAfter = await aAaveTokenContract.balanceOf(liquidator.address); + + const treasuryDataAfter = await helpersContract.getUserReserveData( + aave.address, + treasuryAddress + ); + const treasuryBalanceAfter = treasuryDataAfter.currentATokenBalance; + + expect(userGlobalDataAfter.healthFactor).to.be.gt(oneEther, 'Invalid health factor'); + + expect(userReserveDataAfter.currentStableDebt).to.be.closeTo( + userReserveDataBefore.currentStableDebt.sub(expectedPrincipal), + 2, + 'Invalid user borrow balance after liquidation' + ); + + expect(usdcReserveDataAfter.availableLiquidity).to.be.closeTo( + usdcReserveDataBefore.availableLiquidity.add(expectedPrincipal), + 2, + 'Invalid principal available liquidity' + ); + + expect(aaveReserveDataAfter.availableLiquidity).to.be.closeTo( + aaveReserveDataBefore.availableLiquidity, + 2, + 'Invalid collateral available liquidity' + ); + + expect(aAaveTokenBalanceBefore).to.be.equal( + aAaveTokenBalanceAfter.sub(liquidationReward), + 'Liquidator aToken balance incorrect' + ); + + expect(treasuryBalanceBefore).to.be.equal( + treasuryBalanceAfter.sub(protocolFeeAmount), + 'Treasury aToken balance incorrect' + ); + }); +});