This article has been cross-posted from my personal website: https://stermi.xyz/blog/aave-bug-bounty-wrong-debtprice-in-liquidation-emode
—
Important Note: each of the issue I have found have been already fixed and deployed with the release of Aave 3.0.2
On May 15th 2023, Aave have officially released a post on their Governance forum to disclose different bug bounty submissions. Three of them have been submitted by me, and you can’t understand how much proud of myself I am right now!
For each issue that I have disclosed, I will create a blog post with an in-depth explanation about it. Let’s deep dive into the first one!
I don’t remember currently which snapshot of the GitHub codebase was deployed at the time of the bug bounty, so I’m going to pick one that is as much recent but that still contains the bug: https://github.com/aave/aave-v3-core/blob/94e571f3a7465201881a59555314cd550ccfda57
Summary of the issue
If the user is in e-mode (efficiency mode) it means that all the assets that have been supplied and borrowed belong to the same e-mode category of the user.
During the liquidation process, Aave is making the wrong assumption that, if the user is in e-mode and the e-mode category has been configured with a custom oracle, both the collateral and debt asset are using the same e-mode category custom oracle.
This assumption would be normally correct (if you are in e-mode you can only supply and borrow assets that are in the same e-mode category) but there are some specific edge cases where it would not be true.
If the borrowed asset depegs from the e-mode oracle price and Aave removes such asset from the e-mode category, two different scenarios could happen:
- Scenario 1: borrowed asset has a higher price compared to the e-mode oracle price. In this case, Aave will be left with bad debts because liquidators will not be incentivized to liquidate such debts
- Scenario 2: borrowed asset has a lower price compared to the e-mode oracle price. Liquidators would get more collateral than deserved to liquidate the debt
How the amount of debt and collateral is calculated during a liquidation process
The LiquidationLogic.executeLiquidationCall
uses the following code to determine the collateralAToken
, collateralPriceSource
, debtPriceSource
and liquidationBonus
to be used during the liquidation process
(
vars.collateralAToken,
vars.collateralPriceSource,
vars.debtPriceSource,
vars.liquidationBonus
) = _getConfigurationData(eModeCategories, collateralReserve, params);
If we look inside the code of _getConfigurationData
we can see that if the user has e-mode enabled (userEModeCategory != 0
) the function checks if the price source to be used for the collateralPriceSource
and debtPriceSource
is the one from the e-mode or the default one of the asset itself.
function _getConfigurationData(
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.ReserveData storage collateralReserve,
DataTypes.ExecuteLiquidationCallParams memory params
)
internal
view
returns (
IAToken,
address,
address,
uint256
)
{
IAToken collateralAToken = IAToken(collateralReserve.aTokenAddress);
uint256 liquidationBonus = collateralReserve.configuration.getLiquidationBonus();
address collateralPriceSource = params.collateralAsset;
address debtPriceSource = params.debtAsset;
if (params.userEModeCategory != 0) {
address eModePriceSource = eModeCategories[params.userEModeCategory].priceSource;
if (
EModeLogic.isInEModeCategory(
params.userEModeCategory,
collateralReserve.configuration.getEModeCategory()
)
) {
liquidationBonus = eModeCategories[params.userEModeCategory].liquidationBonus;
if (eModePriceSource != address(0)) {
collateralPriceSource = eModePriceSource;
}
}
// when in eMode, debt will always be in the same eMode category, can skip matching category check
if (eModePriceSource != address(0)) {
debtPriceSource = eModePriceSource;
}
}
return (collateralAToken, collateralPriceSource, debtPriceSource, liquidationBonus);
}
In particular, we can see that if the user is in e-mode, the function assumes that if the eModePriceSource != address(0)
the debtPriceSource
must be equal to the price source used for the e-mode category.
This assumption is made because if the user is in e-mode, he should be able to borrow only assets that belong to the user’s e-mode.
This is not a correct assumption because an asset could be removed from the e-mode (there’s nothing that prevents it) by the poolAdmin
.
Let’s assume this scenario:
- e-mode is using a custom oracle to determine the price (
eModePriceSource != address(0)
) - The debt asset that is being liquidated has been removed from e-mode
Given such premises, the liquidation function is going to use the wrong debtPriceSource
to calculate the amount of collateral that the liquidator will receive by liquidating the debt. In the specific case, they will use the e-mode custom oracle price instead of the current and real asset's price.
Let’s build some examples to explain the problem and see the consequences. All the examples will start with a common scenario.
DAI
and USDC
belong to the same e-mode category. Let's assume that the category has the following parameters
- LTV: 98%
- LT: 98%
- LB: 0% (In the test case it’s 1% because otherwise it would revert for misconfiguration, but it’s fine for just showcasing here)
- Oracle Price: $1
Legends:
- LTV = Loan to Value
- LT = Liquidation Threshold
- LB = Liquidation Bonus
- HF = Health Factor
Scenario 1: USDC
depegs to $2, Aave left with bad debts that liquidators won't liquidate
- Alice supplies
100 DAI
worth $100 - Bob supplies
100 USDC
worth $100 - Alice borrows
98 USDC
worth $98 (LTV 98%, LT 98%)
At this point, the USDC
price depegs from the e-mode category oracle price and increases to $2. Aave will remove USDC
from the e-mode category list because the price has depegged, and it does not meet the same criteria to belong to an e-mode category.
I think that removing it from the e-mode is the only option because otherwise Aave would have to remove the oracle price and all the assets in the e-mode category would default to their original price. Considering that in the same e-category basket there are more than two assets (not like we have in our simple example) the side effects would be much worse than removing only one asset that has depegged (it’s the one that diverge from the e-mode standard) is a better solution.
Because of USDC
depegs going up to $2 and because it has been removed from the e-mode asset list, the new Alice's HF has been reduced, and it's lower than the CLOSE_FACTOR_HF_THRESHOLD (0.95e18)
threshold. Because of this, Alice collateral can be fully liquidated at 100% (see _calculateDebt
). To be clear, the problem would still be there even if the close factor would be the default one (50%).
Let’s follow the path of the liquidation process now via LiquidationLogic.executeLiquidationCall
where a liquidator tries to liquidate the USDC
debt to get the DAI
collateral of Alice.
After calculating the HF, the logic executes
(vars.userVariableDebt, vars.userTotalDebt, vars.actualDebtToLiquidate) = _calculateDebt(
vars.debtReserveCache,
params,
vars.healthFactor
);
userVariableDebt = 98 USDC
userTotalDebt = 98 USDC
actualDebtToLiquidate = 98 USDC
ValidationLogic.validateLiquidationCall
will pass because the user can be liquidated
At this point, the logic executes
(
vars.collateralAToken,
vars.collateralPriceSource,
vars.debtPriceSource,
vars.liquidationBonus
) = _getConfigurationData(eModeCategories, collateralReserve, params);
As we already saw inside the function, we have the main issue where Aave assumes that if the user is in e-mode, it means that the borrowed asset price source is the same one used in the e-mode
function _getConfigurationData(
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.ReserveData storage collateralReserve,
DataTypes.ExecuteLiquidationCallParams memory params
)
internal
view
returns (
IAToken,
address,
address,
uint256
)
{
IAToken collateralAToken = IAToken(collateralReserve.aTokenAddress);
uint256 liquidationBonus = collateralReserve.configuration.getLiquidationBonus();
address collateralPriceSource = params.collateralAsset;
address debtPriceSource = params.debtAsset;
if (params.userEModeCategory != 0) {
address eModePriceSource = eModeCategories[params.userEModeCategory].priceSource;
if (
EModeLogic.isInEModeCategory(
params.userEModeCategory,
collateralReserve.configuration.getEModeCategory()
)
) {
liquidationBonus = eModeCategories[params.userEModeCategory].liquidationBonus;
if (eModePriceSource != address(0)) {
collateralPriceSource = eModePriceSource;
}
}
// when in eMode, debt will always be in the same eMode category, can skip matching category check
if (eModePriceSource != address(0)) {
debtPriceSource = eModePriceSource;
}
}
return (collateralAToken, collateralPriceSource, debtPriceSource, liquidationBonus);
}
To be specific, the problem is inside this part of the code
// when in eMode, debt will always be in the same eMode category, can skip matching category check
if (eModePriceSource != address(0)) {
debtPriceSource = eModePriceSource;
}
The result of the execution is the following
collateralAToken
theAToken
of the underlying collateralcollateralPriceSource
will be equal to the Oracle Price source (fromeModePriceSource
) (in our example) but it does not matterdebtPriceSource
will be equal to the Oracle Price source (fromeModePriceSource
), while it should be equal to theUSDC
oracle priceliquidationBonus
the LB that in this case comes from the e-mode category becauseDAI
is in e-mode and matches the user's e-mode, but it is not relevant for the issue
Now we calculate the amount of collateral to be liquidated, the debt to be liquidated and the protocol fee
(
vars.actualCollateralToLiquidate,
vars.actualDebtToLiquidate,
vars.liquidationProtocolFeeAmount
) = _calculateAvailableCollateralToLiquidate(
collateralReserve,
vars.debtReserveCache,
vars.collateralPriceSource,
vars.debtPriceSource,
vars.actualDebtToLiquidate,
vars.userCollateralBalance,
vars.liquidationBonus,
IPriceOracleGetter(params.priceOracle)
);
Let’s assume that there’s no protocol fees (just remove complexity for the example) and see the calculation done by the function
// This is the base collateral to liquidate based on the given debt to cover
vars.baseCollateral =
((vars.debtAssetPrice * debtToCover * vars.collateralAssetUnit)) /
(vars.collateralPrice * vars.debtAssetUnit);
vars.maxCollateralToLiquidate = vars.baseCollateral.percentMul(liquidationBonus);
if (vars.maxCollateralToLiquidate > userCollateralBalance) {
vars.collateralAmount = userCollateralBalance;
vars.debtAmountNeeded = ((vars.collateralPrice * vars.collateralAmount * vars.debtAssetUnit) /
(vars.debtAssetPrice * vars.collateralAssetUnit)).percentDiv(liquidationBonus);
} else {
vars.collateralAmount = vars.maxCollateralToLiquidate;
vars.debtAmountNeeded = debtToCover;
}
Following the calculation and simplifying things
baseCollateral = (USDC_e-mode-oracle-price * 98 USDC) / DAI_e-mode-oracle-price = ($1 * 98 USDC) / $1 = 98 DAI
and in this case, we enter the else
(but even entering the if
would result in the same problem).
By following the calculation, baseCollateral
will be equal to 98 DAI because debtAssetPrice == $1
(it's using the e-mode category price oracle and not the USDC
oracle price) so
vars.actualCollateralToLiquidate = 98 DAI
equal to $98vars.actualDebtToLiquidate = 98 USDC
worth $196 (because the real USDC price is $2)vars.liquidationProtocolFeeAmount = 0
(just for simplicity, as we explained)
In a normal scenario (without the bug) to repay 98 USDC
debt (worth $196) the liquidator would get 196 DAI
+ the liquidation bonus (in this very specific case)
At this point, we follow the normal flow of the liquidation.
Conclusion & Results about Scenario 1
The conclusion is that the liquidator to liquidate 98 USDC
worth $196 of debt would only get 98 DAI
worth $98.
No liquidator would want to liquidate that debt, and Aave would remain with all the bad debt generated by the USDC
borrows made by the users. Without liquidating the debt, suppliers would not be able to withdraw the supplied USDC
.
Scenario 2: USDC
depegs to $0.5, liquidators would get more collateral than deserved to liquidate the debt
- Alice supplies 100
DAI
worth $100 - Alice supplies 1
wETH
worth $1000 - Bob supplies 200
USDC
worth $100 - Alice borrows 200
USDC
worth $200
In this example, two things happen to be able to make Alice liquidable
USDC
price depegs and decreases to $0.5wETH
just goes to $1 just to make the HF factor of Alice low enough to be able to be liquidated. But it really is not relevant, we just need to make Alice be liquidable for any reason.
At this point, Aave will follow the same procedure followed for the scenario explained before and will remove USDC
from the asset that belongs to the e-mode category. Let's skip all the explanation done before because the problem is the same, and we will just look at the final calculations
// This is the base collateral to liquidate based on the given debt to cover
vars.baseCollateral =
((vars.debtAssetPrice * debtToCover * vars.collateralAssetUnit)) /
(vars.collateralPrice * vars.debtAssetUnit);
vars.maxCollateralToLiquidate = vars.baseCollateral.percentMul(liquidationBonus);
if (vars.maxCollateralToLiquidate > userCollateralBalance) {
vars.collateralAmount = userCollateralBalance;
vars.debtAmountNeeded = ((vars.collateralPrice * vars.collateralAmount * vars.debtAssetUnit) /
(vars.debtAssetPrice * vars.collateralAssetUnit)).percentDiv(liquidationBonus);
} else {
vars.collateralAmount = vars.maxCollateralToLiquidate;
vars.debtAmountNeeded = debtToCover;
}
Like before baseCollateral = (USDC_e-mode-oracle-price * 200 USDC) / DAI_e-mode-oracle-price = ($1 * 200 USDC) / $1
In this case, would enter the if
branch and recalculate the values because Alice has not enough collateral (she only owns 100 DAI
worth $100) and we end up with
collateralAmount = 100 DAI
debtAmountNeeded = (DAI_e-mode-oracle-price * 100 DAI) / USDC_e-mode-oracle-price = ($1 * 100 DAI) / $1 = 100 USDC
This means that to repay a debt of 100 USDC
(worth $50) a liquidator would get back 100 DAI
(worth 100 dollars).
Without the bug, USDC would be priced $0.5, and it would mean that baseCollateral
would be equal to $0.5 * 200 USDC / $1 = 100 DAI
so to cover 200 USDC
debt (worth $100) the liquidator would correctly get 100 DAI
(worth 100 dollars) + liquidation bonus
Conclusion & Results about Scenario 2
The conclusion is that the liquidator, to liquidate 200 USDC
, worth $100 of debt, would get back 200 DAI
worth $200 (plus liquidation bonus). This means that liquidators would get far more than what they should deserve to liquidate the debt.
In this case, any liquidator would be willing to liquidate the debt, but the borrower would get its collateral liquidated by a factor much higher than it should be.
How to resolve the issue
Inside the LiquidationLogic._getConfigurationData
Aave should not assume that the e-mode of the debtAsset
is in the same e-mode as the user's e-mode and use the eModePriceSource
only if the debtReserve.configuration.getEModeCategory()
is equal to the user's e-mode category and if the eModePriceSource != address(0)
Here’s an example of a possible solution
function _getConfigurationData(
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.ReserveData storage collateralReserve,
DataTypes.ExecuteLiquidationCallParams memory params
)
internal
view
returns (
IAToken,
address,
address,
uint256
)
{
// ..
// OTHER CODE
// ..
if (params.userEModeCategory != 0) {
address eModePriceSource = eModeCategories[params.userEModeCategory].priceSource;
// ..
// CHECK COLLATERAL CODE
// ..
+ if (
+ EModeLogic.isInEModeCategory(
+ params.userEModeCategory,
+ debtReserve.configuration.getEModeCategory()
+ )
+ ) {
+
+ if (eModePriceSource != address(0)) {
+ debtPriceSource = eModePriceSource;
+ }
+ }
- // when in eMode, debt will always be in the same eMode category, can skip matching category check
- if (eModePriceSource != address(0)) {
- debtPriceSource = eModePriceSource;
- }
}
return (collateralAToken, collateralPriceSource, debtPriceSource, liquidationBonus);
}
Test case for scenario 1
import { expect } from 'chai'
import { BigNumber, utils } from 'ethers'
import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../helpers/constants'
import { ProtocolErrors, RateMode } from '../helpers/types'
import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'
import { makeSuite, TestEnv } from './helpers/make-suite'
import { getReserveData, getUserData } from './helpers/utils/helpers'
import './helpers/utils/wadraymath'
import { evmRevert, evmSnapshot, waitForTx } from '@aave/deploy-v3'
makeSuite(
'POC USDC depeg to $2, AAVE left with bad debts that liquidators wont liquidate',
(testEnv: TestEnv) => {
const { INVALID_HF } = ProtocolErrors
let snap: string
const CATEGORY = {
id: BigNumber.from('1'),
ltv: BigNumber.from('9800'),
lt: BigNumber.from('9800'),
lb: BigNumber.from('10100'),
oracle: ZERO_ADDRESS,
label: 'STABLECOINS',
}
before(async () => {
const { addressesProvider, oracle } = testEnv
await waitForTx(await addressesProvider.setPriceOracle(oracle.address))
snap = await evmSnapshot()
})
after(async () => {
const { aaveOracle, addressesProvider } = testEnv
await waitForTx(
await addressesProvider.setPriceOracle(aaveOracle.address)
)
})
it('POC EXECUTION', async () => {
await evmRevert(snap)
snap = await evmSnapshot()
// TEST configuration
const {
helpersContract,
oracle,
configurator,
pool,
poolAdmin,
dai,
usdc,
weth,
aDai,
users: [user1, user2],
} = testEnv
const EMODE_ORACLE_ADDRESS = user1.address
await oracle.setAssetPrice(EMODE_ORACLE_ADDRESS, utils.parseUnits('1', 8))
await oracle.setAssetPrice(dai.address, utils.parseUnits('0.99', 8))
await oracle.setAssetPrice(usdc.address, utils.parseUnits('1.01', 8))
expect(
await configurator
.connect(poolAdmin.signer)
.setEModeCategory(
1,
CATEGORY.ltv,
CATEGORY.lt,
CATEGORY.lb,
EMODE_ORACLE_ADDRESS,
CATEGORY.label
)
)
const categoryData = await pool.getEModeCategoryData(CATEGORY.id)
expect(categoryData.ltv).to.be.equal(
CATEGORY.ltv,
'invalid eMode category ltv'
)
expect(categoryData.liquidationThreshold).to.be.equal(
CATEGORY.lt,
'invalid eMode category liq threshold'
)
expect(categoryData.liquidationBonus).to.be.equal(
CATEGORY.lb,
'invalid eMode category liq bonus'
)
expect(categoryData.priceSource).to.be.equal(
EMODE_ORACLE_ADDRESS,
'invalid eMode category price source'
)
// Add Dai and USDC to category
await configurator
.connect(poolAdmin.signer)
.setAssetEModeCategory(dai.address, CATEGORY.id)
await configurator
.connect(poolAdmin.signer)
.setAssetEModeCategory(usdc.address, CATEGORY.id)
expect(
await helpersContract.getReserveEModeCategory(dai.address)
).to.be.eq(CATEGORY.id)
expect(
await helpersContract.getReserveEModeCategory(usdc.address)
).to.be.eq(CATEGORY.id)
// user1 supply 100 DAI
const daiSupplyAmount = utils.parseUnits('100', 18)
expect(await dai.connect(user1.signer)['mint(uint256)'](daiSupplyAmount))
expect(
await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT)
)
expect(
await pool
.connect(user1.signer)
.supply(dai.address, daiSupplyAmount, user1.address, 0)
)
// user2 supply 100 USDC
const usdcSupplyAmount = utils.parseUnits('100', 6)
expect(
await usdc
.connect(user2.signer)
['mint(uint256)'](usdcSupplyAmount.mul(10))
)
expect(
await usdc.connect(user2.signer).approve(pool.address, MAX_UINT_AMOUNT)
)
expect(
await pool
.connect(user2.signer)
.supply(usdc.address, usdcSupplyAmount, user2.address, 0)
)
// user1 activate e-mode
expect(await pool.connect(user1.signer).setUserEMode(CATEGORY.id))
// user1 borrow 98 USDC
const userData = await pool.getUserAccountData(user1.address)
const toBorrow = userData.availableBorrowsBase.div(100)
expect(
await pool
.connect(user1.signer)
.borrow(usdc.address, toBorrow, RateMode.Variable, 0, user1.address)
)
// Get user1 HF before
const userGlobalDataBefore = await pool.getUserAccountData(user1.address)
expect(userGlobalDataBefore.healthFactor).to.be.gte(
utils.parseUnits('1', 18)
)
console.log('userGlobalDataBefore', userGlobalDataBefore)
// USDC price goes to 2$
await oracle.setAssetPrice(
usdc.address,
(await oracle.getAssetPrice(usdc.address)).mul(2)
)
// Aave remove USDC from e-mode
expect(
await configurator
.connect(poolAdmin.signer)
.setAssetEModeCategory(usdc.address, 0)
)
expect(
await helpersContract.getReserveEModeCategory(usdc.address)
).to.be.eq(0)
// Get user1 HF after
const userGlobalDataAfter = await pool.getUserAccountData(user1.address)
expect(userGlobalDataAfter.healthFactor).to.be.lt(
utils.parseUnits('1', 18),
INVALID_HF
)
console.log('userGlobalDataAfter', userGlobalDataAfter)
// user2 liquidate user1
const balanceADAICollateralBefore = await aDai.balanceOf(user1.address)
const balanceUSDCBefore = await usdc.balanceOf(user2.address)
console.log(
'user1 aDAI collateral balance before liquidation',
utils.formatUnits(balanceADAICollateralBefore, 18)
)
console.log(
'user2 USDC balance before liquidation',
utils.formatUnits(balanceUSDCBefore, 6)
)
await pool
.connect(user2.signer)
.liquidationCall(
dai.address,
usdc.address,
user1.address,
toBorrow,
false
)
const balanceADAICollateralAfter = await aDai.balanceOf(user1.address)
const balanceUSDCAfter = await usdc.balanceOf(user2.address)
console.log(
'user1 aDAI collateral balance after liquidation',
utils.formatUnits(balanceADAICollateralAfter, 18)
)
console.log(
'user2 USDC balance after liquidation',
utils.formatUnits(balanceUSDCAfter, 6)
)
const collateralLiquidated = balanceADAICollateralBefore.sub(
balanceADAICollateralAfter
)
const usdcNeededToPerformLiquidation =
balanceUSDCBefore.sub(balanceUSDCAfter)
console.log(
'collateralLiquidated',
utils.formatUnits(collateralLiquidated, 18)
)
console.log(
'user2 USDC used to repay debt',
utils.formatUnits(usdcNeededToPerformLiquidation, 6)
)
})
}
)
Test case for scenario 2
import { expect } from 'chai'
import { BigNumber, utils } from 'ethers'
import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../helpers/constants'
import { ProtocolErrors, RateMode } from '../helpers/types'
import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'
import { makeSuite, TestEnv } from './helpers/make-suite'
import { getReserveData, getUserData } from './helpers/utils/helpers'
import './helpers/utils/wadraymath'
import { evmRevert, evmSnapshot, waitForTx } from '@aave/deploy-v3'
import { parseUnits } from 'ethers/lib/utils'
makeSuite(
'POC Scenario 2: USDC depeg to $0.5, liquidators would get more collateral than deserved to liquidate the debt',
(testEnv: TestEnv) => {
const {
INCONSISTENT_EMODE_CATEGORY,
HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD,
COLLATERAL_CANNOT_COVER_NEW_BORROW,
INVALID_EMODE_CATEGORY_PARAMS,
INVALID_HF,
} = ProtocolErrors
let snap: string
const CATEGORY = {
id: BigNumber.from('1'),
ltv: BigNumber.from('9800'),
lt: BigNumber.from('9800'),
lb: BigNumber.from('10100'),
oracle: ZERO_ADDRESS,
label: 'STABLECOINS',
}
before(async () => {
const { addressesProvider, oracle } = testEnv
await waitForTx(await addressesProvider.setPriceOracle(oracle.address))
snap = await evmSnapshot()
})
after(async () => {
const { aaveOracle, addressesProvider } = testEnv
await waitForTx(
await addressesProvider.setPriceOracle(aaveOracle.address)
)
})
it('POC EXECUTION', async () => {
await evmRevert(snap)
snap = await evmSnapshot()
// TEST configuration
const {
helpersContract,
oracle,
configurator,
pool,
poolAdmin,
dai,
usdc,
weth,
aDai,
users: [user1, user2],
} = testEnv
const EMODE_ORACLE_ADDRESS = user1.address
await oracle.setAssetPrice(EMODE_ORACLE_ADDRESS, utils.parseUnits('1', 8))
await oracle.setAssetPrice(dai.address, utils.parseUnits('0.99', 8))
await oracle.setAssetPrice(usdc.address, utils.parseUnits('1.01', 8))
await oracle.setAssetPrice(weth.address, utils.parseUnits('1000', 8))
expect(
await configurator
.connect(poolAdmin.signer)
.setEModeCategory(
1,
CATEGORY.ltv,
CATEGORY.lt,
CATEGORY.lb,
EMODE_ORACLE_ADDRESS,
CATEGORY.label
)
)
const categoryData = await pool.getEModeCategoryData(CATEGORY.id)
expect(categoryData.ltv).to.be.equal(
CATEGORY.ltv,
'invalid eMode category ltv'
)
expect(categoryData.liquidationThreshold).to.be.equal(
CATEGORY.lt,
'invalid eMode category liq threshold'
)
expect(categoryData.liquidationBonus).to.be.equal(
CATEGORY.lb,
'invalid eMode category liq bonus'
)
expect(categoryData.priceSource).to.be.equal(
EMODE_ORACLE_ADDRESS,
'invalid eMode category price source'
)
// Add Dai and USDC to category
await configurator
.connect(poolAdmin.signer)
.setAssetEModeCategory(dai.address, CATEGORY.id)
await configurator
.connect(poolAdmin.signer)
.setAssetEModeCategory(usdc.address, CATEGORY.id)
expect(
await helpersContract.getReserveEModeCategory(dai.address)
).to.be.eq(CATEGORY.id)
expect(
await helpersContract.getReserveEModeCategory(usdc.address)
).to.be.eq(CATEGORY.id)
// user1 supply 100 DAI
const daiSupplyAmount = utils.parseUnits('100', 18)
expect(await dai.connect(user1.signer)['mint(uint256)'](daiSupplyAmount))
expect(
await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT)
)
expect(
await pool
.connect(user1.signer)
.supply(dai.address, daiSupplyAmount, user1.address, 0)
)
// user1 supply 1 WETH
const wethSupplyAmount = utils.parseUnits('1', 18)
expect(
await weth.connect(user1.signer)['mint(uint256)'](wethSupplyAmount)
)
expect(
await weth.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT)
)
expect(
await pool
.connect(user1.signer)
.supply(weth.address, wethSupplyAmount, user1.address, 0)
)
// user2 supply 200 USDC
const usdcSupplyAmount = utils.parseUnits('200', 6)
expect(
await usdc
.connect(user2.signer)
['mint(uint256)'](usdcSupplyAmount.mul(10))
)
expect(
await usdc.connect(user2.signer).approve(pool.address, MAX_UINT_AMOUNT)
)
expect(
await pool
.connect(user2.signer)
.supply(usdc.address, usdcSupplyAmount, user2.address, 0)
)
// user1 activate e-mode
expect(await pool.connect(user1.signer).setUserEMode(CATEGORY.id))
// user1 borrow 200 USDC
const userData = await pool.getUserAccountData(user1.address)
const toBorrow = parseUnits('200', 6)
expect(
await pool
.connect(user1.signer)
.borrow(usdc.address, toBorrow, RateMode.Variable, 0, user1.address)
)
// Get user1 HF before
const userGlobalDataBefore = await pool.getUserAccountData(user1.address)
expect(userGlobalDataBefore.healthFactor).to.be.gte(
utils.parseUnits('1', 18)
)
console.log('userGlobalDataBefore', userGlobalDataBefore)
// USDC price goes to 0.5$
await oracle.setAssetPrice(
usdc.address,
(await oracle.getAssetPrice(usdc.address)).div(2)
)
// WETH prices goes down just to be able to liquidate
await oracle.setAssetPrice(weth.address, utils.parseUnits('1', 8))
// Aave remove USDC from e-mode
expect(
await configurator
.connect(poolAdmin.signer)
.setAssetEModeCategory(usdc.address, 0)
)
expect(
await helpersContract.getReserveEModeCategory(usdc.address)
).to.be.eq(0)
// Get user1 HF after
const userGlobalDataAfter = await pool.getUserAccountData(user1.address)
expect(userGlobalDataAfter.healthFactor).to.be.lt(
utils.parseUnits('1', 18),
INVALID_HF
)
console.log('userGlobalDataAfter', userGlobalDataAfter)
// user2 liquidate user1
const balanceADAICollateralBefore = await aDai.balanceOf(user1.address)
const balanceUSDCBefore = await usdc.balanceOf(user2.address)
console.log(
'user1 aDAI collateral balance before liquidation',
utils.formatUnits(balanceADAICollateralBefore, 18)
)
console.log(
'user2 USDC balance before liquidation',
utils.formatUnits(balanceUSDCBefore, 6)
)
await pool
.connect(user2.signer)
.liquidationCall(
dai.address,
usdc.address,
user1.address,
toBorrow,
false
)
const balanceADAICollateralAfter = await aDai.balanceOf(user1.address)
const balanceUSDCAfter = await usdc.balanceOf(user2.address)
console.log(
'user1 aDAI collateral balance after liquidation',
utils.formatUnits(balanceADAICollateralAfter, 18)
)
console.log(
'user2 USDC balance after liquidation',
utils.formatUnits(balanceUSDCAfter, 6)
)
const collateralLiquidated = balanceADAICollateralBefore.sub(
balanceADAICollateralAfter
)
const usdcNeededToPerformLiquidation =
balanceUSDCBefore.sub(balanceUSDCAfter)
console.log(
'collateralLiquidated',
utils.formatUnits(collateralLiquidated, 18)
)
console.log(
'user2 USDC used to repay debt',
utils.formatUnits(usdcNeededToPerformLiquidation, 6)
)
})
}
)