From ea4867086d39f094303916e72e180f99d8149fd5 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 23 Mar 2023 12:04:34 +0100 Subject: [PATCH] fix: Fix collateral behavior of zero-ltv assets (#820) * fix: patch zero ltv transfer * fix: revert undesired changes * fix: change implementation * fix: add docs * fix: rename error * fix: improved isolation mode enter mechanic * fix: improve tests * fix: remove check from bridge as anotehr role is used already * fix: use acl manager * fix: flashloan receiver.executeOperation params * fix: fix comment * fix: move role to constant * fix: commit broken test * fix: add additional test * fix: add flashloan test * fix: add additional test * fix: fix tests * fix: prune fl fix * Update package.json * fix: resolve conflicts * fix: add comments * fix: also add automatic isolation logic to mintUnbacked fix: update docs * fix: update test * fix: add some more ltv0 tests * feat: fix tests & add another one * fix: Fetch addressesProvider from AToken --------- Co-authored-by: miguelmtz <36620902+miguelmtzinf@users.noreply.github.com> Co-authored-by: miguelmtzinf --- .../protocol/libraries/helpers/Errors.sol | 2 +- .../protocol/libraries/logic/BridgeLogic.sol | 5 +- .../libraries/logic/LiquidationLogic.sol | 5 +- .../protocol/libraries/logic/SupplyLogic.sol | 12 +- .../libraries/logic/ValidationLogic.sol | 52 ++++++- .../protocol/libraries/types/DataTypes.sol | 6 +- helpers/types.ts | 2 +- test-suites/isolation-mode.spec.ts | 125 ++++++++++++++--- test-suites/ltv-validation.spec.ts | 130 +++++++++++++++++- 9 files changed, 295 insertions(+), 44 deletions(-) diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 1dacaf392..990069e0f 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -67,7 +67,7 @@ library Errors { string public constant PRICE_ORACLE_SENTINEL_CHECK_FAILED = '59'; // 'Price oracle sentinel validation failed' string public constant ASSET_NOT_BORROWABLE_IN_ISOLATION = '60'; // 'Asset is not borrowable in isolation mode' string public constant RESERVE_ALREADY_INITIALIZED = '61'; // 'Reserve has already been initialized' - string public constant USER_IN_ISOLATION_MODE = '62'; // 'User is in isolation mode' + string public constant USER_IN_ISOLATION_MODE_OR_LTV_ZERO = '62'; // 'User is in isolation mode or ltv is zero' string public constant INVALID_LTV = '63'; // 'Invalid ltv parameter for the reserve' string public constant INVALID_LIQ_THRESHOLD = '64'; // 'Invalid liquidity threshold parameter for the reserve' string public constant INVALID_LIQ_BONUS = '65'; // 'Invalid liquidity bonus parameter for the reserve' diff --git a/contracts/protocol/libraries/logic/BridgeLogic.sol b/contracts/protocol/libraries/logic/BridgeLogic.sol index 8662afecb..bc37b57a9 100644 --- a/contracts/protocol/libraries/logic/BridgeLogic.sol +++ b/contracts/protocol/libraries/logic/BridgeLogic.sol @@ -86,11 +86,12 @@ library BridgeLogic { if (isFirstSupply) { if ( - ValidationLogic.validateUseAsCollateral( + ValidationLogic.validateAutomaticUseAsCollateral( reservesData, reservesList, userConfig, - reserveCache.reserveConfiguration + reserveCache.reserveConfiguration, + reserveCache.aTokenAddress ) ) { userConfig.setUsingAsCollateral(reserve.id, true); diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index a1db290bc..8c2f4bbc3 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -300,11 +300,12 @@ library LiquidationLogic { if (liquidatorPreviousATokenBalance == 0) { DataTypes.UserConfigurationMap storage liquidatorConfig = usersConfig[msg.sender]; if ( - ValidationLogic.validateUseAsCollateral( + ValidationLogic.validateAutomaticUseAsCollateral( reservesData, reservesList, liquidatorConfig, - collateralReserve.configuration + collateralReserve.configuration, + collateralReserve.aTokenAddress ) ) { liquidatorConfig.setUsingAsCollateral(collateralReserve.id, true); diff --git a/contracts/protocol/libraries/logic/SupplyLogic.sol b/contracts/protocol/libraries/logic/SupplyLogic.sol index d8310ec55..6398bde63 100644 --- a/contracts/protocol/libraries/logic/SupplyLogic.sol +++ b/contracts/protocol/libraries/logic/SupplyLogic.sol @@ -75,11 +75,12 @@ library SupplyLogic { if (isFirstSupply) { if ( - ValidationLogic.validateUseAsCollateral( + ValidationLogic.validateAutomaticUseAsCollateral( reservesData, reservesList, userConfig, - reserveCache.reserveConfiguration + reserveCache.reserveConfiguration, + reserveCache.aTokenAddress ) ) { userConfig.setUsingAsCollateral(reserve.id, true); @@ -212,11 +213,12 @@ library SupplyLogic { if (params.balanceToBefore == 0) { DataTypes.UserConfigurationMap storage toConfig = usersConfig[params.to]; if ( - ValidationLogic.validateUseAsCollateral( + ValidationLogic.validateAutomaticUseAsCollateral( reservesData, reservesList, toConfig, - reserve.configuration + reserve.configuration, + reserve.aTokenAddress ) ) { toConfig.setUsingAsCollateral(reserveId, true); @@ -270,7 +272,7 @@ library SupplyLogic { userConfig, reserveCache.reserveConfiguration ), - Errors.USER_IN_ISOLATION_MODE + Errors.USER_IN_ISOLATION_MODE_OR_LTV_ZERO ); userConfig.setUsingAsCollateral(reserve.id, true); diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 15994a875..747d3a26f 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -10,6 +10,8 @@ import {IScaledBalanceToken} from '../../../interfaces/IScaledBalanceToken.sol'; import {IPriceOracleGetter} from '../../../interfaces/IPriceOracleGetter.sol'; import {IAToken} from '../../../interfaces/IAToken.sol'; import {IPriceOracleSentinel} from '../../../interfaces/IPriceOracleSentinel.sol'; +import {IPoolAddressesProvider} from '../../../interfaces/IPoolAddressesProvider.sol'; +import {IAccessControl} from '../../../dependencies/openzeppelin/contracts/IAccessControl.sol'; import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol'; import {UserConfiguration} from '../configuration/UserConfiguration.sol'; import {Errors} from '../helpers/Errors.sol'; @@ -19,6 +21,7 @@ import {DataTypes} from '../types/DataTypes.sol'; import {ReserveLogic} from './ReserveLogic.sol'; import {GenericLogic} from './GenericLogic.sol'; import {SafeCast} from '../../../dependencies/openzeppelin/contracts/SafeCast.sol'; +import {IncentivizedERC20} from '../../tokenization/base/IncentivizedERC20.sol'; /** * @title ReserveLogic library @@ -49,6 +52,12 @@ library ValidationLogic { */ uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; + /** + * @dev Role identifier for the role allowed to supply isolated reserves as collateral + */ + bytes32 public constant ISOLATED_COLLATERAL_SUPPLIER_ROLE = + keccak256('ISOLATED_COLLATERAL_SUPPLIER'); + /** * @notice Validates a supply action. * @param reserveCache The cached data of the reserve @@ -664,7 +673,7 @@ library ValidationLogic { Errors.INCONSISTENT_EMODE_CATEGORY ); - //eMode can always be enabled if the user hasn't supplied anything + // eMode can always be enabled if the user hasn't supplied anything if (userConfig.isEmpty()) { return; } @@ -688,10 +697,8 @@ library ValidationLogic { } /** - * @notice Validates if an asset can be activated as collateral in the following actions: supply, transfer, - * set as collateral, mint unbacked, and liquidate - * @dev This is used to ensure that the constraints for isolated assets are respected by all the actions that - * generate transfers of aTokens + * @notice Validates the action of activating the asset as collateral. + * @dev Only possible if the asset has non-zero LTV and the user is not in isolation mode * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param userConfig the user configuration @@ -704,6 +711,9 @@ library ValidationLogic { DataTypes.UserConfigurationMap storage userConfig, DataTypes.ReserveConfigurationMap memory reserveConfig ) internal view returns (bool) { + if (reserveConfig.getLtv() == 0) { + return false; + } if (!userConfig.isUsingAsCollateralAny()) { return true; } @@ -711,4 +721,36 @@ library ValidationLogic { return (!isolationModeActive && reserveConfig.getDebtCeiling() == 0); } + + /** + * @notice Validates if an asset should be automatically activated as collateral in the following actions: supply, + * transfer, mint unbacked, and liquidate + * @dev This is used to ensure that isolated assets are not enabled as collateral automatically + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param userConfig the user configuration + * @param reserveConfig The reserve configuration + * @return True if the asset can be activated as collateral, false otherwise + */ + function validateAutomaticUseAsCollateral( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveConfigurationMap memory reserveConfig, + address aTokenAddress + ) internal view returns (bool) { + if (reserveConfig.getDebtCeiling() != 0) { + // ensures only the ISOLATED_COLLATERAL_SUPPLIER_ROLE can enable collateral as side-effect of an action + IPoolAddressesProvider addressesProvider = IncentivizedERC20(aTokenAddress) + .POOL() + .ADDRESSES_PROVIDER(); + if ( + !IAccessControl(addressesProvider.getACLManager()).hasRole( + ISOLATED_COLLATERAL_SUPPLIER_ROLE, + msg.sender + ) + ) return false; + } + return validateUseAsCollateral(reservesData, reservesList, userConfig, reserveConfig); + } } diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index c40d732f4..8589dc0af 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -78,11 +78,7 @@ library DataTypes { string label; } - enum InterestRateMode { - NONE, - STABLE, - VARIABLE - } + enum InterestRateMode {NONE, STABLE, VARIABLE} struct ReserveCache { uint256 currScaledVariableDebt; diff --git a/helpers/types.ts b/helpers/types.ts index 8828af16f..a8961434e 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -131,7 +131,7 @@ export enum ProtocolErrors { PRICE_ORACLE_SENTINEL_CHECK_FAILED = '59', // 'Price oracle sentinel validation failed' ASSET_NOT_BORROWABLE_IN_ISOLATION = '60', // 'Asset is not borrowable in isolation mode' RESERVE_ALREADY_INITIALIZED = '61', // 'Reserve has already been initialized' - USER_IN_ISOLATION_MODE = '62', // 'User is in isolation mode' + USER_IN_ISOLATION_MODE_OR_LTV_ZERO = '62', // 'User is in isolation mode or ltv is zero' INVALID_LTV = '63', // 'Invalid ltv parameter for the reserve' INVALID_LIQ_THRESHOLD = '64', // 'Invalid liquidity threshold parameter for the reserve' INVALID_LIQ_BONUS = '65', // 'Invalid liquidity bonus parameter for the reserve' diff --git a/test-suites/isolation-mode.spec.ts b/test-suites/isolation-mode.spec.ts index a36167ffd..c83a0f66d 100644 --- a/test-suites/isolation-mode.spec.ts +++ b/test-suites/isolation-mode.spec.ts @@ -30,6 +30,10 @@ const expectEqual = ( }; makeSuite('Isolation mode', (testEnv: TestEnv) => { + const ISOLATED_COLLATERAL_SUPPLIER_ROLE = utils.keccak256( + utils.toUtf8Bytes('ISOLATED_COLLATERAL_SUPPLIER') + ); + const depositAmount = utils.parseEther('1000'); const borrowAmount = utils.parseEther('200'); const ceilingAmount = '10000'; @@ -40,8 +44,11 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { const mintAmount = withdrawAmount.mul(denominatorBP.sub(feeBps)).div(denominatorBP); const bridgeProtocolFeeBps = BigNumber.from(2000); - const { ASSET_NOT_BORROWABLE_IN_ISOLATION, DEBT_CEILING_EXCEEDED, USER_IN_ISOLATION_MODE } = - ProtocolErrors; + const { + ASSET_NOT_BORROWABLE_IN_ISOLATION, + DEBT_CEILING_EXCEEDED, + USER_IN_ISOLATION_MODE_OR_LTV_ZERO, + } = ProtocolErrors; let aclManager; let oracleBaseDecimals; @@ -81,14 +88,51 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { await pool.connect(users[0].signer).supply(dai.address, depositAmount, users[0].address, 0); }); - it('User 1 supply 2 aave. Checks that aave is activated as collateral ', async () => { + it('User 1 supply 2 aave. Checks that aave is not activated as collateral.', async () => { + const snap = await evmSnapshot(); const { users, pool, aave, helpersContract } = testEnv; await aave.connect(users[1].signer)['mint(uint256)'](utils.parseEther('2')); await aave.connect(users[1].signer).approve(pool.address, MAX_UINT_AMOUNT); await pool .connect(users[1].signer) .supply(aave.address, utils.parseEther('2'), users[1].address, 0); + const userData = await helpersContract.getUserReserveData(aave.address, users[1].address); + + expect(userData.usageAsCollateralEnabled).to.be.eq(false); + await evmRevert(snap); + }); + + it('User 1 as ISOLATED_COLLATERAL_SUPPLIER_ROLE supply 2 aave to user 2. Checks that aave is activated as isolated collateral.', async () => { + const snap = await evmSnapshot(); + const { users, pool, aave, helpersContract, deployer } = testEnv; + + await aave.connect(users[1].signer)['mint(uint256)'](utils.parseEther('2')); + await aave.connect(users[1].signer).approve(pool.address, MAX_UINT_AMOUNT); + await aclManager + .connect(deployer.signer) + .grantRole(ISOLATED_COLLATERAL_SUPPLIER_ROLE, users[1].address); + const hasRole = await aclManager + .connect(users[1].address) + .hasRole(ISOLATED_COLLATERAL_SUPPLIER_ROLE, users[1].address); + expect(hasRole).to.be.eq(true); + + await pool + .connect(users[1].signer) + .supply(aave.address, utils.parseEther('2'), users[2].address, 0); + const userData = await helpersContract.getUserReserveData(aave.address, users[2].address); + expect(userData.usageAsCollateralEnabled).to.be.eq(true); + await evmRevert(snap); + }); + + it('User 1 supply 2 aave. Enables collateral. Checks that aave is activated as isolated collateral.', async () => { + const { users, pool, aave, helpersContract } = testEnv; + await aave.connect(users[1].signer)['mint(uint256)'](utils.parseEther('2')); + await aave.connect(users[1].signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(users[1].signer) + .supply(aave.address, utils.parseEther('2'), users[1].address, 0); + await pool.connect(users[1].signer).setUserUseReserveAsCollateral(aave.address, true); const userData = await helpersContract.getUserReserveData(aave.address, users[1].address); expect(userData.usageAsCollateralEnabled).to.be.eq(true); @@ -115,7 +159,7 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { await expect( pool.connect(users[1].signer).setUserUseReserveAsCollateral(weth.address, true) - ).to.be.revertedWith(USER_IN_ISOLATION_MODE); + ).to.be.revertedWith(USER_IN_ISOLATION_MODE_OR_LTV_ZERO); const userDataAfter = await helpersContract.getUserReserveData(weth.address, users[1].address); expect(userDataAfter.usageAsCollateralEnabled).to.be.eq(false); @@ -150,7 +194,7 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { await expect( pool.connect(user2.signer).setUserUseReserveAsCollateral(aave.address, true) - ).to.be.revertedWith(USER_IN_ISOLATION_MODE); + ).to.be.revertedWith(USER_IN_ISOLATION_MODE_OR_LTV_ZERO); const userDataAfter = await helpersContract.getUserReserveData(aave.address, user2.address); expect(userDataAfter.usageAsCollateralEnabled).to.be.eq(false); @@ -186,6 +230,36 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { expect(userData.usageAsCollateralEnabled).to.be.eq(false); }); + it('User 2 (as bridge) mint 100 unbacked aave (isolated) to user 3. Checks that aave is NOT activated as collateral', async () => { + const { users, riskAdmin, pool, configurator, aave, helpersContract } = testEnv; + + // configure unbacked cap for dai + expect(await configurator.connect(riskAdmin.signer).setUnbackedMintCap(aave.address, '10')); + expect( + await configurator + .connect(riskAdmin.signer) + .setUnbackedMintCap(aave.address, MAX_UNBACKED_MINT_CAP) + ); + + const reserveDataBefore = await getReserveData(helpersContract, aave.address); + const tx = await waitForTx( + await pool + .connect(users[2].signer) + .mintUnbacked(aave.address, mintAmount, users[3].address, 0) + ); + const { txTimestamp } = await getTxCostAndTimestamp(tx); + const expectedDataAfter = calcExpectedReserveDataAfterMintUnbacked( + mintAmount.toString(), + reserveDataBefore, + txTimestamp + ); + const reserveDataAfter = await getReserveData(helpersContract, aave.address); + expectEqual(reserveDataAfter, expectedDataAfter); + + const userData = await helpersContract.getUserReserveData(aave.address, users[3].address); + expect(userData.usageAsCollateralEnabled).to.be.eq(false); + }); + it('User 2 supply 100 DAI, transfers to user 1. Checks that DAI is NOT activated as collateral for user 1', async () => { const { dai, aDai, users, pool, helpersContract } = testEnv; @@ -262,6 +336,7 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { await aave.connect(users[1].signer)['mint(uint256)'](aaveAmount); await aave.connect(users[1].signer).approve(pool.address, MAX_UINT_AMOUNT); await pool.connect(users[1].signer).supply(aave.address, aaveAmount, users[1].address, 0); + await pool.connect(users[1].signer).setUserUseReserveAsCollateral(aave.address, true); await expect( pool @@ -308,6 +383,7 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { await aave.connect(users[3].signer)['mint(uint256)'](aaveAmount); await aave.connect(users[3].signer).approve(pool.address, MAX_UINT_AMOUNT); await pool.connect(users[3].signer).supply(aave.address, aaveAmount, users[3].address, 0); + await pool.connect(users[3].signer).setUserUseReserveAsCollateral(aave.address, true); const borrowAmount = utils.parseEther('10'); await expect( @@ -327,6 +403,7 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { await aave.connect(users[3].signer)['mint(uint256)'](aaveAmount); await aave.connect(users[3].signer).approve(pool.address, MAX_UINT_AMOUNT); await pool.connect(users[3].signer).supply(aave.address, aaveAmount, users[3].address, 0); + await pool.connect(users[3].signer).setUserUseReserveAsCollateral(aave.address, true); const borrowAmount = utils.parseEther('100'); await expect( @@ -385,6 +462,7 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { await aave.connect(borrower.signer)['mint(uint256)'](depositAmount); await aave.connect(borrower.signer).approve(pool.address, MAX_UINT_AMOUNT); await pool.connect(borrower.signer).supply(aave.address, depositAmount, borrower.address, 0); + await pool.connect(borrower.signer).setUserUseReserveAsCollateral(aave.address, true); const userData = await helpersContract.getUserReserveData(aave.address, borrower.address); expect(userData.usageAsCollateralEnabled).to.be.eq(true); @@ -448,32 +526,33 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { expect(aaveUserData.usageAsCollateralEnabled).to.be.eq(false); }); - it('User 5s isolation mode asset is liquidated by User 6', async () => { - const { weth, dai, aave, aAave, users, pool, helpersContract, oracle } = testEnv; + it('User 5 supplies isolation mode asset is liquidated by User 6', async () => { + const { dai, aave, users, pool, helpersContract, oracle } = testEnv; await evmRevert(snapshot); snapshot = await evmSnapshot(); - + // supply dai as user 6, so user 5 can borrow const daiAmount = utils.parseEther('700'); - await dai.connect(users[5].signer)['mint(uint256)'](daiAmount); - await dai.connect(users[5].signer).approve(pool.address, MAX_UINT_AMOUNT); - await pool.connect(users[5].signer).supply(dai.address, daiAmount, users[5].address, 0); + await dai.connect(users[6].signer)['mint(uint256)'](daiAmount); + await dai.connect(users[6].signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool.connect(users[6].signer).supply(dai.address, daiAmount, users[6].address, 0); const aaveAmount = utils.parseEther('.3'); - await aave.connect(users[6].signer)['mint(uint256)'](aaveAmount); - await aave.connect(users[6].signer).approve(pool.address, MAX_UINT_AMOUNT); - await pool.connect(users[6].signer).supply(aave.address, aaveAmount, users[6].address, 0); + await aave.connect(users[5].signer)['mint(uint256)'](aaveAmount); + await aave.connect(users[5].signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool.connect(users[5].signer).supply(aave.address, aaveAmount, users[5].address, 0); + await pool.connect(users[5].signer).setUserUseReserveAsCollateral(aave.address, true); // borrow with health factor just above 1 - const userGlobalData = await pool.getUserAccountData(users[6].address); + const userGlobalData = await pool.getUserAccountData(users[5].address); const daiPrice = await oracle.getAssetPrice(dai.address); let amountDAIToBorrow = await convertToCurrencyDecimals( dai.address, userGlobalData.availableBorrowsBase.div(daiPrice.toString()).percentMul(9999).toString() ); await pool - .connect(users[6].signer) - .borrow(dai.address, amountDAIToBorrow, RateMode.Variable, 0, users[6].address); + .connect(users[5].signer) + .borrow(dai.address, amountDAIToBorrow, RateMode.Variable, 0, users[5].address); // advance time so health factor is less than one and liquidate await advanceTimeAndBlock(86400 * 365 * 100); @@ -481,17 +560,18 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { pool, helpersContract, dai.address, - users[6].address + users[5].address ); const amountToLiquidate = userDaiReserveDataBefore.currentVariableDebt.div(2); - await dai.connect(users[5].signer)['mint(uint256)'](daiAmount); + await dai.connect(users[6].signer)['mint(uint256)'](daiAmount); + await dai.connect(users[6].signer).approve(pool.address, MAX_UINT_AMOUNT); const tx = await pool - .connect(users[5].signer) - .liquidationCall(aave.address, dai.address, users[6].address, amountToLiquidate, true); + .connect(users[6].signer) + .liquidationCall(aave.address, dai.address, users[5].address, amountToLiquidate, true); await tx.wait(); // confirm the newly received aave tokens (in isolation mode) cannot be used as collateral - const userData = await helpersContract.getUserReserveData(aave.address, users[5].address); + const userData = await helpersContract.getUserReserveData(aave.address, users[6].address); expect(userData.usageAsCollateralEnabled).to.be.eq(false); }); @@ -512,6 +592,7 @@ makeSuite('Isolation mode', (testEnv: TestEnv) => { await pool .connect(users[1].signer) .supply(aave.address, aaveAmountToSupply, users[1].address, 0); + await pool.connect(users[1].signer).setUserUseReserveAsCollateral(aave.address, true); // User 1 borrows DAI against isolated AAVE const { isolationModeTotalDebt: isolationModeTotalDebtBeforeBorrow } = diff --git a/test-suites/ltv-validation.spec.ts b/test-suites/ltv-validation.spec.ts index d0fea27ed..d9006cb58 100644 --- a/test-suites/ltv-validation.spec.ts +++ b/test-suites/ltv-validation.spec.ts @@ -7,7 +7,7 @@ import { evmRevert, evmSnapshot } from '@aave/deploy-v3'; import { parseUnits } from 'ethers/lib/utils'; makeSuite('LTV validation', (testEnv: TestEnv) => { - const { LTV_VALIDATION_FAILED } = ProtocolErrors; + const { LTV_VALIDATION_FAILED, USER_IN_ISOLATION_MODE_OR_LTV_ZERO } = ProtocolErrors; let snap: string; before(async () => { @@ -147,4 +147,132 @@ makeSuite('LTV validation', (testEnv: TestEnv) => { expect(userData.totalCollateralBase).to.be.eq(parseUnits('10', 8)); expect(userData.totalDebtBase).to.be.eq(0); }); + + it('User 1 deposit dai as collateral, ltv drops to 0, tries to enable as collateral (nothing should happen)', async () => { + await evmRevert(snap); + const { + pool, + dai, + users: [user1], + configurator, + helpersContract, + } = testEnv; + + const daiAmount = await convertToCurrencyDecimals(dai.address, '10'); + + await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + + await dai.connect(user1.signer)['mint(uint256)'](daiAmount); + + await pool.connect(user1.signer).supply(dai.address, daiAmount, user1.address, 0); + + // Set DAI LTV = 0 + expect(await configurator.configureReserveAsCollateral(dai.address, 0, 8000, 10500)) + .to.emit(configurator, 'CollateralConfigurationChanged') + .withArgs(dai.address, 0, 8000, 10500); + const ltv = (await helpersContract.getReserveConfigurationData(dai.address)).ltv; + expect(ltv).to.be.equal(0); + + const userDataBefore = await helpersContract.getUserReserveData(dai.address, user1.address); + expect(userDataBefore.usageAsCollateralEnabled).to.be.eq(true); + + await pool.connect(user1.signer).setUserUseReserveAsCollateral(dai.address, true); + + const userDataAfter = await helpersContract.getUserReserveData(dai.address, user1.address); + expect(userDataAfter.usageAsCollateralEnabled).to.be.eq(true); + }); + + it('User 1 deposit zero ltv dai, tries to enable as collateral (revert expected)', async () => { + await evmRevert(snap); + const { + pool, + dai, + users: [user1], + configurator, + helpersContract, + } = testEnv; + + // Clean user's state by withdrawing all aDAI + await pool.connect(user1.signer).withdraw(dai.address, MAX_UINT_AMOUNT, user1.address); + + // Set DAI LTV = 0 + expect(await configurator.configureReserveAsCollateral(dai.address, 0, 8000, 10500)) + .to.emit(configurator, 'CollateralConfigurationChanged') + .withArgs(dai.address, 0, 8000, 10500); + const ltv = (await helpersContract.getReserveConfigurationData(dai.address)).ltv; + expect(ltv).to.be.equal(0); + + const daiAmount = await convertToCurrencyDecimals(dai.address, '10'); + + await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + + await dai.connect(user1.signer)['mint(uint256)'](daiAmount); + + await pool.connect(user1.signer).supply(dai.address, daiAmount, user1.address, 0); + + await expect( + pool.connect(user1.signer).setUserUseReserveAsCollateral(dai.address, true) + ).to.be.revertedWith(USER_IN_ISOLATION_MODE_OR_LTV_ZERO); + }); + + it('User 1 deposit zero ltv dai, dai should not be enabled as collateral', async () => { + await evmRevert(snap); + const { + pool, + dai, + users: [user1], + configurator, + helpersContract, + } = testEnv; + + // Set DAI LTV = 0 + expect(await configurator.configureReserveAsCollateral(dai.address, 0, 8000, 10500)) + .to.emit(configurator, 'CollateralConfigurationChanged') + .withArgs(dai.address, 0, 8000, 10500); + const ltv = (await helpersContract.getReserveConfigurationData(dai.address)).ltv; + expect(ltv).to.be.equal(0); + + const daiAmount = await convertToCurrencyDecimals(dai.address, '10'); + + await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + + await dai.connect(user1.signer)['mint(uint256)'](daiAmount); + + await pool.connect(user1.signer).supply(dai.address, daiAmount, user1.address, 0); + + const userData = await helpersContract.getUserReserveData(dai.address, user1.address); + expect(userData.usageAsCollateralEnabled).to.be.eq(false); + }); + + it('User 1 deposit dai, DAI ltv drops to 0, transfers dai, dai should not be enabled as collateral for receiver', async () => { + await evmRevert(snap); + const { + pool, + dai, + aDai, + users: [user1, user2], + configurator, + helpersContract, + } = testEnv; + + const daiAmount = await convertToCurrencyDecimals(dai.address, '10'); + + await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + + await dai.connect(user1.signer)['mint(uint256)'](daiAmount); + + await pool.connect(user1.signer).supply(dai.address, daiAmount, user1.address, 0); + + // Set DAI LTV = 0 + expect(await configurator.configureReserveAsCollateral(dai.address, 0, 8000, 10500)) + .to.emit(configurator, 'CollateralConfigurationChanged') + .withArgs(dai.address, 0, 8000, 10500); + const ltv = (await helpersContract.getReserveConfigurationData(dai.address)).ltv; + expect(ltv).to.be.equal(0); + + // Transfer 0 LTV DAI to user2 + await aDai.connect(user1.signer).transfer(user2.address, 1); + const userData = await helpersContract.getUserReserveData(dai.address, user2.address); + expect(userData.usageAsCollateralEnabled).to.be.eq(false); + }); });