diff --git a/contracts/interfaces/IACLManager.sol b/contracts/interfaces/IACLManager.sol index b71f76086..c481ce94d 100644 --- a/contracts/interfaces/IACLManager.sol +++ b/contracts/interfaces/IACLManager.sol @@ -123,7 +123,7 @@ interface IACLManager { function addFlashBorrower(address borrower) external; /** - * @notice Removes an admin as FlashBorrower + * @notice Removes an address as FlashBorrower * @param borrower The address of the FlashBorrower to remove */ function removeFlashBorrower(address borrower) external; diff --git a/contracts/interfaces/IPool.sol b/contracts/interfaces/IPool.sol index 0bea9aad0..bff8638d6 100644 --- a/contracts/interfaces/IPool.sol +++ b/contracts/interfaces/IPool.sol @@ -211,7 +211,7 @@ interface IPool { event MintedToTreasury(address indexed reserve, uint256 amountMinted); /** - * @dev Mints an `amount` of aTokens to the `onBehalfOf` + * @notice Mints an `amount` of aTokens to the `onBehalfOf` * @param asset The address of the underlying asset to mint * @param amount The amount to mint * @param onBehalfOf The address that will receive the aTokens @@ -226,16 +226,17 @@ interface IPool { ) external; /** - * @dev Back the current unbacked underlying with `amount` and pay `fee`. + * @notice Back the current unbacked underlying with `amount` and pay `fee`. * @param asset The address of the underlying asset to back * @param amount The amount to back * @param fee The amount paid in fees + * @return The backed amount **/ function backUnbacked( address asset, uint256 amount, uint256 fee - ) external; + ) external returns (uint256); /** * @notice Supplies an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 640e46322..d022f65ee 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -54,7 +54,6 @@ library Errors { string public constant HEALTH_FACTOR_NOT_BELOW_THRESHOLD = '45'; // 'Health factor is not below the threshold' string public constant COLLATERAL_CANNOT_BE_LIQUIDATED = '46'; // 'The collateral chosen cannot be liquidated' string public constant SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER = '47'; // 'User did not borrow the specified currency' - string public constant SAME_BLOCK_BORROW_REPAY = '48'; // 'Borrow and repay in same block is not allowed' string public constant INCONSISTENT_FLASHLOAN_PARAMS = '49'; // 'Inconsistent flashloan parameters' string public constant BORROW_CAP_EXCEEDED = '50'; // 'Borrow cap is exceeded' string public constant SUPPLY_CAP_EXCEEDED = '51'; // 'Supply cap is exceeded' diff --git a/contracts/protocol/libraries/logic/BridgeLogic.sol b/contracts/protocol/libraries/logic/BridgeLogic.sol index d5f1ce9e2..fc2b6fc6e 100644 --- a/contracts/protocol/libraries/logic/BridgeLogic.sol +++ b/contracts/protocol/libraries/logic/BridgeLogic.sol @@ -100,12 +100,14 @@ library BridgeLogic { /** * @notice Back the current unbacked with `amount` and pay `fee`. + * @dev It is not possible to back more than the existing unbacked amount of the reserve * @dev Emits the `BackUnbacked` event * @param reserve The reserve to back unbacked for * @param asset The address of the underlying asset to repay * @param amount The amount to back * @param fee The amount paid in fees * @param protocolFeeBps The fraction of fees in basis points paid to the protocol + * @return The backed amount **/ function executeBackUnbacked( DataTypes.ReserveData storage reserve, @@ -113,7 +115,7 @@ library BridgeLogic { uint256 amount, uint256 fee, uint256 protocolFeeBps - ) external { + ) external returns (uint256) { DataTypes.ReserveCache memory reserveCache = reserve.cache(); reserve.updateState(reserveCache); @@ -137,5 +139,7 @@ library BridgeLogic { IERC20(asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, added); emit BackUnbacked(asset, msg.sender, backingAmount, fee); + + return backingAmount; } } diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index 7d91d3365..0742cedbc 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -166,6 +166,13 @@ library LiquidationLogic { userConfig.setBorrowing(debtReserve.id, false); } + // If the collateral being liquidated is equal to the user balance, + // we set the currency as not being used as collateral anymore + if (vars.actualCollateralToLiquidate == vars.userCollateralBalance) { + userConfig.setUsingAsCollateral(collateralReserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.collateralAsset, params.user); + } + _burnDebtTokens(params, vars); debtReserve.updateInterestRates( @@ -197,14 +204,7 @@ library LiquidationLogic { 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.actualCollateralToLiquidate == vars.userCollateralBalance) { - userConfig.setUsingAsCollateral(collateralReserve.id, false); - emit ReserveUsedAsCollateralDisabled(params.collateralAsset, params.user); - } - + // Transfers the debt asset being repaid to the aToken, where the liquidity is kept IERC20(params.debtAsset).safeTransferFrom( msg.sender, diff --git a/contracts/protocol/libraries/logic/ReserveLogic.sol b/contracts/protocol/libraries/logic/ReserveLogic.sol index 4ca939199..a7b563c95 100644 --- a/contracts/protocol/libraries/logic/ReserveLogic.sol +++ b/contracts/protocol/libraries/logic/ReserveLogic.sol @@ -292,7 +292,9 @@ library ReserveLogic { DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache ) internal { - //only cumulating if there is any income being produced + // Only cumulating on the supply side if there is any income being produced + // The case of Reserve Factor 100% is not a problem (currentLiquidityRate == 0), + // as liquidity index should not be updated if (reserveCache.currLiquidityRate != 0) { uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest( reserveCache.currLiquidityRate, @@ -302,19 +304,21 @@ library ReserveLogic { reserveCache.currLiquidityIndex ); reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128(); + } - //as the liquidity rate might come only from stable rate loans, we need to ensure - //that there is actual variable debt before accumulating - if (reserveCache.currScaledVariableDebt != 0) { - uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest( - reserveCache.currVariableBorrowRate, - reserveCache.reserveLastUpdateTimestamp - ); - reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul( - reserveCache.currVariableBorrowIndex - ); - reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); - } + // Variable borrow index only gets updated if there is any variable debt. + // reserveCache.currVariableBorrowRate != 0 is not a correct validation, + // because a positive base variable rate can be stored on + // reserveCache.currVariableBorrowRate, but the index should not increase + if (reserveCache.currScaledVariableDebt != 0) { + uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest( + reserveCache.currVariableBorrowRate, + reserveCache.reserveLastUpdateTimestamp + ); + reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul( + reserveCache.currVariableBorrowIndex + ); + reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); } } diff --git a/contracts/protocol/libraries/logic/SupplyLogic.sol b/contracts/protocol/libraries/logic/SupplyLogic.sol index 98ad8e8f9..1c7d2d259 100644 --- a/contracts/protocol/libraries/logic/SupplyLogic.sol +++ b/contracts/protocol/libraries/logic/SupplyLogic.sol @@ -128,6 +128,13 @@ library SupplyLogic { reserve.updateInterestRates(reserveCache, params.asset, 0, amountToWithdraw); + bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); + + if (isCollateral && amountToWithdraw == userBalance) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); + } + IAToken(reserveCache.aTokenAddress).burn( msg.sender, params.to, @@ -135,25 +142,18 @@ library SupplyLogic { reserveCache.nextLiquidityIndex ); - if (userConfig.isUsingAsCollateral(reserve.id)) { - if (userConfig.isBorrowingAny()) { - ValidationLogic.validateHFAndLtv( - reservesData, - reservesList, - eModeCategories, - userConfig, - params.asset, - msg.sender, - params.reservesCount, - params.oracle, - params.userEModeCategory - ); - } - - if (amountToWithdraw == userBalance) { - userConfig.setUsingAsCollateral(reserve.id, false); - emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); - } + if (isCollateral && userConfig.isBorrowingAny()) { + ValidationLogic.validateHFAndLtv( + reservesData, + reservesList, + eModeCategories, + userConfig, + params.asset, + msg.sender, + params.reservesCount, + params.oracle, + params.userEModeCategory + ); } emit Withdraw(params.asset, msg.sender, params.to, amountToWithdraw); @@ -264,7 +264,12 @@ library SupplyLogic { if (useAsCollateral) { require( - ValidationLogic.validateUseAsCollateral(reservesData, reservesList, userConfig, reserveCache.reserveConfiguration), + ValidationLogic.validateUseAsCollateral( + reservesData, + reservesList, + userConfig, + reserveCache.reserveConfiguration + ), Errors.USER_IN_ISOLATION_MODE ); diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index fb2eda0c1..784fc728d 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -327,20 +327,6 @@ library ValidationLogic { require(isActive, Errors.RESERVE_INACTIVE); require(!isPaused, Errors.RESERVE_PAUSED); - uint256 variableDebtPreviousIndex = IScaledBalanceToken(reserveCache.variableDebtTokenAddress) - .getPreviousIndex(onBehalfOf); - - uint40 stableRatePreviousTimestamp = IStableDebtToken(reserveCache.stableDebtTokenAddress) - .getUserLastUpdated(onBehalfOf); - - require( - (stableRatePreviousTimestamp < uint40(block.timestamp) && - interestRateMode == DataTypes.InterestRateMode.STABLE) || - (variableDebtPreviousIndex < reserveCache.nextVariableBorrowIndex && - interestRateMode == DataTypes.InterestRateMode.VARIABLE), - Errors.SAME_BLOCK_BORROW_REPAY - ); - require( (stableDebt != 0 && interestRateMode == DataTypes.InterestRateMode.STABLE) || (variableDebt != 0 && interestRateMode == DataTypes.InterestRateMode.VARIABLE), diff --git a/contracts/protocol/pool/Pool.sol b/contracts/protocol/pool/Pool.sol index b89b3b60a..8c3fcacda 100644 --- a/contracts/protocol/pool/Pool.sol +++ b/contracts/protocol/pool/Pool.sol @@ -136,8 +136,9 @@ contract Pool is VersionedInitializable, PoolStorage, IPool { address asset, uint256 amount, uint256 fee - ) external virtual override onlyBridge { - BridgeLogic.executeBackUnbacked(_reserves[asset], asset, amount, fee, _bridgeProtocolFee); + ) external virtual override onlyBridge returns (uint256) { + return + BridgeLogic.executeBackUnbacked(_reserves[asset], asset, amount, fee, _bridgeProtocolFee); } /// @inheritdoc IPool diff --git a/contracts/protocol/tokenization/AToken.sol b/contracts/protocol/tokenization/AToken.sol index 2bd122e6a..1ee28e8d5 100644 --- a/contracts/protocol/tokenization/AToken.sol +++ b/contracts/protocol/tokenization/AToken.sol @@ -107,7 +107,7 @@ contract AToken is VersionedInitializable, ScaledBalanceTokenBase, EIP712Base, I } /// @inheritdoc IAToken - function mintToTreasury(uint256 amount, uint256 index) external override onlyPool { + function mintToTreasury(uint256 amount, uint256 index) external virtual override onlyPool { if (amount == 0) { return; } @@ -119,7 +119,7 @@ contract AToken is VersionedInitializable, ScaledBalanceTokenBase, EIP712Base, I address from, address to, uint256 value - ) external override onlyPool { + ) external virtual override onlyPool { // Being a normal transfer, the Transfer() and BalanceTransfer() are emitted // so no need to emit a specific event here _transfer(from, to, value, false); diff --git a/helpers/types.ts b/helpers/types.ts index 79cd5f3d6..5ad641560 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -118,7 +118,6 @@ export enum ProtocolErrors { HEALTH_FACTOR_NOT_BELOW_THRESHOLD = '45', // 'Health factor is not below the threshold' COLLATERAL_CANNOT_BE_LIQUIDATED = '46', // 'The collateral chosen cannot be liquidated' SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER = '47', // 'User did not borrow the specified currency' - SAME_BLOCK_BORROW_REPAY = '48', // 'Borrow and repay in same block is not allowed' INCONSISTENT_FLASHLOAN_PARAMS = '49', // 'Inconsistent flashloan parameters' BORROW_CAP_EXCEEDED = '50', // 'Borrow cap is exceeded' SUPPLY_CAP_EXCEEDED = '51', // 'Supply cap is exceeded' diff --git a/test-suites/pool-edge.spec.ts b/test-suites/pool-edge.spec.ts index d2c18be5e..a85f5ac3f 100644 --- a/test-suites/pool-edge.spec.ts +++ b/test-suites/pool-edge.spec.ts @@ -1,14 +1,14 @@ -import { expect } from 'chai'; -import { BigNumber, BigNumberish, utils } from 'ethers'; -import { impersonateAccountsHardhat } from '../helpers/misc-utils'; -import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../helpers/constants'; -import { deployMintableERC20 } from '@aave/deploy-v3/dist/helpers/contract-deployments'; -import { ProtocolErrors } from '../helpers/types'; -import { getFirstSigner } from '@aave/deploy-v3/dist/helpers/utilities/signer'; -import { topUpNonPayableWithEther } from './helpers/utils/funds'; -import { makeSuite, TestEnv } from './helpers/make-suite'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { evmSnapshot, evmRevert, getPoolLibraries } from '@aave/deploy-v3'; +import {expect} from 'chai'; +import {BigNumber, BigNumberish, utils} from 'ethers'; +import {impersonateAccountsHardhat} from '../helpers/misc-utils'; +import {MAX_UINT_AMOUNT, ZERO_ADDRESS} from '../helpers/constants'; +import {deployMintableERC20} from '@aave/deploy-v3/dist/helpers/contract-deployments'; +import {ProtocolErrors, RateMode} from '../helpers/types'; +import {getFirstSigner} from '@aave/deploy-v3/dist/helpers/utilities/signer'; +import {topUpNonPayableWithEther} from './helpers/utils/funds'; +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {evmSnapshot, evmRevert, getPoolLibraries, advanceTimeAndBlock} from '@aave/deploy-v3'; import { MockPoolInherited__factory, MockReserveInterestRateStrategy__factory, @@ -19,10 +19,64 @@ import { InitializableImmutableAdminUpgradeabilityProxy, ERC20__factory, } from '../types'; -import { getProxyImplementation } from '../helpers/contracts-helpers'; +import {convertToCurrencyDecimals, getProxyImplementation} from '../helpers/contracts-helpers'; declare var hre: HardhatRuntimeEnvironment; +// Setup function to have 1 user with DAI deposits, and another user with WETH collateral +// and DAI borrowings at an indicated borrowing mode +const setupPositions = async (testEnv: TestEnv, borrowingMode: RateMode) => { + const { + pool, + dai, + weth, + oracle, + users: [depositor, borrower], + } = testEnv; + + // mints DAI to depositor + await dai + .connect(depositor.signer) + ['mint(uint256)'](await convertToCurrencyDecimals(dai.address, '20000')); + + // 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, '10000'); + + 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(uint256)'](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).mul(5000).div(10000).toString() + ); + await pool + .connect(borrower.signer) + .borrow(dai.address, amountDAIToBorrow, borrowingMode, '0', borrower.address); +}; + makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { const { NO_MORE_RESERVES_ALLOWED, @@ -59,7 +113,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { dai, users: [user0], } = testEnv; - const { deployer: deployerName } = await hre.getNamedAccounts(); + const {deployer: deployerName} = await hre.getNamedAccounts(); // Deploy the mock Pool with a `dropReserve` skipping the checks const NEW_POOL_IMPL_ARTIFACT = await hre.deployments.deploy('MockPoolInheritedDropper', { @@ -129,7 +183,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { addressesProvider, users: [deployer], } = testEnv; - const { deployer: deployerName } = await hre.getNamedAccounts(); + const {deployer: deployerName} = await hre.getNamedAccounts(); const NEW_POOL_IMPL_ARTIFACT = await hre.deployments.deploy('Pool', { contract: 'Pool', @@ -155,7 +209,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Check initialization', async () => { - const { pool } = testEnv; + const {pool} = testEnv; expect(await pool.MAX_STABLE_RATE_BORROW_SIZE_PERCENT()).to.be.eq( MAX_STABLE_RATE_BORROW_SIZE_PERCENT @@ -164,7 +218,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Tries to initialize a reserve as non PoolConfigurator (revert expected)', async () => { - const { pool, users, dai, helpersContract } = testEnv; + const {pool, users, dai, helpersContract} = testEnv; const config = await helpersContract.getReserveTokensAddresses(dai.address); @@ -249,7 +303,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Call `mintToTreasury()` on a pool with an inactive reserve', async () => { - const { pool, poolAdmin, dai, users, configurator } = testEnv; + const {pool, poolAdmin, dai, users, configurator} = testEnv; // Deactivate reserve expect(await configurator.connect(poolAdmin.signer).setReserveActive(dai.address, false)); @@ -259,7 +313,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Tries to call `finalizeTransfer()` by a non-aToken address (revert expected)', async () => { - const { pool, dai, users } = testEnv; + const {pool, dai, users} = testEnv; await expect( pool @@ -269,7 +323,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Tries to call `initReserve()` with an EOA as reserve (revert expected)', async () => { - const { pool, deployer, users, configurator } = testEnv; + const {pool, deployer, users, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -284,7 +338,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('PoolConfigurator updates the ReserveInterestRateStrategy address', async () => { - const { pool, deployer, dai, configurator } = testEnv; + const {pool, deployer, dai, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -302,7 +356,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('PoolConfigurator updates the ReserveInterestRateStrategy address for asset 0', async () => { - const { pool, deployer, dai, configurator } = testEnv; + const {pool, deployer, dai, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -315,7 +369,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('PoolConfigurator updates the ReserveInterestRateStrategy address for an unlisted asset (revert expected)', async () => { - const { pool, deployer, dai, configurator, users } = testEnv; + const {pool, deployer, dai, configurator, users} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -330,14 +384,14 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Activates the zero address reserve for borrowing via pool admin (expect revert)', async () => { - const { configurator } = testEnv; + const {configurator} = testEnv; await expect(configurator.setReserveBorrowing(ZERO_ADDRESS, true)).to.be.revertedWith( ZERO_ADDRESS_NOT_VALID ); }); it('Initialize an already initialized reserve. ReserveLogic `init` where aTokenAddress != ZERO_ADDRESS (revert expected)', async () => { - const { pool, dai, deployer, configurator } = testEnv; + const {pool, dai, deployer, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -363,7 +417,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { * `_addReserveToList()` is called from `initReserve`. However, in `initReserve` we run `init` before the `_addReserveToList()`, * and in `init` we are checking if `aTokenAddress == address(0)`, so to bypass that we need this odd init. */ - const { pool, dai, deployer, configurator } = testEnv; + const {pool, dai, deployer, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -406,8 +460,8 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { it('Initialize reserves until max, then add one more (revert expected)', async () => { // Upgrade the Pool to update the maximum number of reserves - const { addressesProvider, poolAdmin, pool, dai, deployer, configurator } = testEnv; - const { deployer: deployerName } = await hre.getNamedAccounts(); + const {addressesProvider, poolAdmin, pool, dai, deployer, configurator} = testEnv; + const {deployer: deployerName} = await hre.getNamedAccounts(); // Impersonate the PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -475,7 +529,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { * 3. Init a new asset. * Intended behaviour new asset is inserted into one of the available spots in */ - const { configurator, pool, poolAdmin, addressesProvider } = testEnv; + const {configurator, pool, poolAdmin, addressesProvider} = testEnv; const reservesListBefore = await pool.connect(configurator.signer).getReservesList(); @@ -574,8 +628,8 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { */ // Upgrade the Pool to update the maximum number of reserves - const { addressesProvider, poolAdmin, pool, dai, deployer, configurator } = testEnv; - const { deployer: deployerName } = await hre.getNamedAccounts(); + const {addressesProvider, poolAdmin, pool, dai, deployer, configurator} = testEnv; + const {deployer: deployerName} = await hre.getNamedAccounts(); // Impersonate the PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -707,7 +761,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Tries to initialize a reserve with an AToken, StableDebtToken, and VariableDebt each deployed with the wrong pool address (revert expected)', async () => { - const { pool, deployer, configurator, addressesProvider } = testEnv; + const {pool, deployer, configurator, addressesProvider} = testEnv; const NEW_POOL_IMPL_ARTIFACT = await hre.deployments.deploy('DummyPool', { contract: 'Pool', @@ -793,4 +847,110 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { initInputParams[0].variableDebtTokenImpl = variableDebtTokenImp.address; expect(await configurator.initReserves(initInputParams)); }); + + it('LendingPool Reserve Factor 100%. Only variable borrowings. Validates that variable borrow index accrue, liquidity index not, and the Collector receives accruedToTreasury allocation after interest accrues', async () => { + const { + configurator, + pool, + aDai, + dai, + users: [depositor], + } = testEnv; + + await setupPositions(testEnv, RateMode.Variable); + + // Set the RF to 100% + await configurator.setReserveFactor(dai.address, '10000'); + + const reserveDataBefore = await pool.getReserveData(dai.address); + + await advanceTimeAndBlock(10000); + + // Deposit to "settle" the liquidity index accrual from pre-RF increase to 100% + await pool + .connect(depositor.signer) + .deposit( + dai.address, + await convertToCurrencyDecimals(dai.address, '1'), + depositor.address, + '0' + ); + + const reserveDataAfter1 = await pool.getReserveData(dai.address); + + expect(reserveDataAfter1.variableBorrowIndex).to.be.gt(reserveDataBefore.variableBorrowIndex); + expect(reserveDataAfter1.accruedToTreasury).to.be.gt(reserveDataBefore.accruedToTreasury); + expect(reserveDataAfter1.liquidityIndex).to.be.gt(reserveDataBefore.liquidityIndex); + + await advanceTimeAndBlock(10000); + + // "Clean" update, that should not increase the liquidity index, only variable borrow + await pool + .connect(depositor.signer) + .deposit( + dai.address, + await convertToCurrencyDecimals(dai.address, '1'), + depositor.address, + '0' + ); + + const reserveDataAfter2 = await pool.getReserveData(dai.address); + + expect(reserveDataAfter2.variableBorrowIndex).to.be.gt(reserveDataAfter1.variableBorrowIndex); + expect(reserveDataAfter2.accruedToTreasury).to.be.gt(reserveDataAfter1.accruedToTreasury); + expect(reserveDataAfter2.liquidityIndex).to.be.eq(reserveDataAfter1.liquidityIndex); + }); + + it('LendingPool Reserve Factor 100%. Only stable borrowings. Validates that neither variable borrow index nor liquidity index increase, but the Collector receives accruedToTreasury allocation after interest accrues', async () => { + const { + configurator, + pool, + aDai, + dai, + users: [depositor], + } = testEnv; + + await setupPositions(testEnv, RateMode.Stable); + + // Set the RF to 100% + await configurator.setReserveFactor(dai.address, '10000'); + + const reserveDataBefore = await pool.getReserveData(dai.address); + + await advanceTimeAndBlock(10000); + + // Deposit to "settle" the liquidity index accrual from pre-RF increase to 100% + await pool + .connect(depositor.signer) + .deposit( + dai.address, + await convertToCurrencyDecimals(dai.address, '1'), + depositor.address, + '0' + ); + + const reserveDataAfter1 = await pool.getReserveData(dai.address); + + expect(reserveDataAfter1.variableBorrowIndex).to.be.eq(reserveDataBefore.variableBorrowIndex); + expect(reserveDataAfter1.accruedToTreasury).to.be.gt(reserveDataBefore.accruedToTreasury); + expect(reserveDataAfter1.liquidityIndex).to.be.gt(reserveDataBefore.liquidityIndex); + + await advanceTimeAndBlock(10000); + + // "Clean" update, that should not increase the liquidity index, only stable borrow + await pool + .connect(depositor.signer) + .deposit( + dai.address, + await convertToCurrencyDecimals(dai.address, '1'), + depositor.address, + '0' + ); + + const reserveDataAfter2 = await pool.getReserveData(dai.address); + + expect(reserveDataAfter2.variableBorrowIndex).to.be.eq(reserveDataAfter1.variableBorrowIndex); + expect(reserveDataAfter2.accruedToTreasury).to.be.gt(reserveDataAfter1.accruedToTreasury); + expect(reserveDataAfter2.liquidityIndex).to.be.eq(reserveDataAfter1.liquidityIndex); + }); }); diff --git a/test-suites/stable-debt-token.spec.ts b/test-suites/stable-debt-token.spec.ts index 584eed351..fa34952a5 100644 --- a/test-suites/stable-debt-token.spec.ts +++ b/test-suites/stable-debt-token.spec.ts @@ -1,26 +1,30 @@ -import { expect } from 'chai'; -import { BigNumber, utils } from 'ethers'; -import { ProtocolErrors, RateMode } from '../helpers/types'; -import { MAX_UINT_AMOUNT, RAY, ZERO_ADDRESS } from '../helpers/constants'; -import { impersonateAccountsHardhat, setAutomine } from '../helpers/misc-utils'; -import { makeSuite, TestEnv } from './helpers/make-suite'; -import { topUpNonPayableWithEther } from './helpers/utils/funds'; -import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { evmRevert, evmSnapshot, increaseTime, waitForTx } from '@aave/deploy-v3'; -import { StableDebtToken__factory } from '../types'; +import {expect} from 'chai'; +import {BigNumber, utils} from 'ethers'; +import {ProtocolErrors, RateMode} from '../helpers/types'; +import {MAX_UINT_AMOUNT, RAY, ZERO_ADDRESS} from '../helpers/constants'; +import {impersonateAccountsHardhat, setAutomine, setAutomineEvm} from '../helpers/misc-utils'; +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {topUpNonPayableWithEther} from './helpers/utils/funds'; +import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {evmRevert, evmSnapshot, getStableDebtToken, increaseTime, waitForTx} from '@aave/deploy-v3'; +import {StableDebtToken__factory} from '../types'; declare var hre: HardhatRuntimeEnvironment; makeSuite('StableDebtToken', (testEnv: TestEnv) => { - const { CALLER_MUST_BE_POOL, CALLER_NOT_POOL_ADMIN } = ProtocolErrors; + const {CALLER_MUST_BE_POOL, CALLER_NOT_POOL_ADMIN} = ProtocolErrors; + let snap: string; - before(async () => { + beforeEach(async () => { snap = await evmSnapshot(); }); + afterEach(async () => { + await evmRevert(snap); + }); it('Check initialization', async () => { - const { pool, weth, dai, helpersContract, users } = testEnv; + const {pool, weth, dai, helpersContract, users} = testEnv; const daiStableDebtTokenAddress = (await helpersContract.getReserveTokensAddresses(dai.address)) .stableDebtTokenAddress; const stableDebtContract = StableDebtToken__factory.connect( @@ -70,7 +74,7 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { }); it('Tries to mint not being the Pool (revert expected)', async () => { - const { deployer, dai, helpersContract } = testEnv; + const {deployer, dai, helpersContract} = testEnv; const daiStableDebtTokenAddress = (await helpersContract.getReserveTokensAddresses(dai.address)) .stableDebtTokenAddress; @@ -86,7 +90,7 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { }); it('Tries to burn not being the Pool (revert expected)', async () => { - const { deployer, dai, helpersContract } = testEnv; + const {deployer, dai, helpersContract} = testEnv; const daiStableDebtTokenAddress = (await helpersContract.getReserveTokensAddresses(dai.address)) .stableDebtTokenAddress; @@ -105,7 +109,7 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { }); it('Tries to transfer debt tokens (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiStableDebtTokenAddress = (await helpersContract.getReserveTokensAddresses(dai.address)) .stableDebtTokenAddress; const stableDebtContract = StableDebtToken__factory.connect( @@ -119,7 +123,7 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { }); it('Check Mint and Transfer events when borrowing on behalf', async () => { - const snapId = await evmSnapshot(); + // const snapId = await evmSnapshot(); const { pool, weth, @@ -191,14 +195,14 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { ); const rawTransferEvents = tx.logs.filter( - ({ topics, address }) => topics[0] === transferEventSig && address == stableDebtToken.address + ({topics, address}) => topics[0] === transferEventSig && address == stableDebtToken.address ); const transferAmount = stableDebtToken.interface.parseLog(rawTransferEvents[0]).args.value; const rawMintEvents = tx.logs.filter( - ({ topics, address }) => topics[0] === mintEventSig && address == stableDebtToken.address + ({topics, address}) => topics[0] === mintEventSig && address == stableDebtToken.address ); - const { amount: mintAmount, balanceIncrease } = stableDebtToken.interface.parseLog( + const {amount: mintAmount, balanceIncrease} = stableDebtToken.interface.parseLog( rawMintEvents[0] ).args; @@ -207,11 +211,11 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { expect(expectedDebtIncreaseUser1).to.be.eq(balanceIncrease); expect(afterDebtBalanceUser2).to.be.eq(beforeDebtBalanceUser2); - await evmRevert(snapId); + // await evmRevert(snapId); }); it('Tries to approve debt tokens (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiStableDebtTokenAddress = (await helpersContract.getReserveTokensAddresses(dai.address)) .stableDebtTokenAddress; const stableDebtContract = StableDebtToken__factory.connect( @@ -228,7 +232,7 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { }); it('Tries to increase allowance of debt tokens (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiStableDebtTokenAddress = (await helpersContract.getReserveTokensAddresses(dai.address)) .stableDebtTokenAddress; const stableDebtContract = StableDebtToken__factory.connect( @@ -242,7 +246,7 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { }); it('Tries to decrease allowance of debt tokens (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiStableDebtTokenAddress = (await helpersContract.getReserveTokensAddresses(dai.address)) .stableDebtTokenAddress; const stableDebtContract = StableDebtToken__factory.connect( @@ -256,7 +260,7 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { }); it('Tries to transferFrom (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiStableDebtTokenAddress = (await helpersContract.getReserveTokensAddresses(dai.address)) .stableDebtTokenAddress; const stableDebtContract = StableDebtToken__factory.connect( @@ -280,13 +284,13 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { // progress time by a year, to accrue significant debt. // then let user 2 withdraw sufficient funds such that secondTerm (userStableRate * burnAmount) >= averageRate * supply // if we do not have user 1 deposit as well, we will have issues getting past previousSupply <= amount, as amount > supply for secondTerm to be > firstTerm. - await evmRevert(snap); + // await evmRevert(snap); const rateGuess1 = BigNumber.from(RAY); const rateGuess2 = BigNumber.from(10).pow(30); const amount1 = BigNumber.from(2); const amount2 = BigNumber.from(1); - const { deployer, pool, dai, helpersContract, users } = testEnv; + const {deployer, pool, dai, helpersContract, users} = testEnv; // Impersonate the Pool await topUpNonPayableWithEther(deployer.signer, [pool.address], utils.parseEther('1')); @@ -316,8 +320,8 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { }); it('setIncentivesController() ', async () => { - const snapshot = await evmSnapshot(); - const { dai, helpersContract, poolAdmin, aclManager, deployer } = testEnv; + // const snapshot = await evmSnapshot(); + const {dai, helpersContract, poolAdmin, aclManager, deployer} = testEnv; const config = await helpersContract.getReserveTokensAddresses(dai.address); const stableDebt = StableDebtToken__factory.connect( config.stableDebtTokenAddress, @@ -330,7 +334,7 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { expect(await stableDebt.connect(poolAdmin.signer).setIncentivesController(ZERO_ADDRESS)); expect(await stableDebt.getIncentivesController()).to.be.eq(ZERO_ADDRESS); - await evmRevert(snapshot); + // await evmRevert(snapshot); }); it('setIncentivesController() from not pool admin (revert expected)', async () => { @@ -348,4 +352,111 @@ makeSuite('StableDebtToken', (testEnv: TestEnv) => { stableDebt.connect(user.signer).setIncentivesController(ZERO_ADDRESS) ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); }); + + it('User borrows and repays in same block with zero fees', async () => { + const {pool, users, dai, aDai, usdc, stableDebtDai} = testEnv; + const user = users[0]; + + // We need some debt. + await usdc.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); + await usdc.connect(user.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(user.signer) + .deposit(usdc.address, utils.parseEther('2000'), user.address, 0); + await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); + await dai.connect(user.signer).transfer(aDai.address, utils.parseEther('2000')); + await dai.connect(user.signer).approve(pool.address, MAX_UINT_AMOUNT); + + const userDataBefore = await pool.getUserAccountData(user.address); + expect(await stableDebtDai.balanceOf(user.address)).to.be.eq(0); + + // Turn off automining - pretty sure that coverage is getting messed up here. + await setAutomine(false); + // Borrow 500 dai + await pool + .connect(user.signer) + .borrow(dai.address, utils.parseEther('500'), RateMode.Stable, 0, user.address); + + // Turn on automining, but not mine a new block until next tx + await setAutomineEvm(true); + expect( + await pool + .connect(user.signer) + .repay(dai.address, utils.parseEther('500'), RateMode.Stable, user.address) + ); + + expect(await stableDebtDai.balanceOf(user.address)).to.be.eq(0); + expect(await dai.balanceOf(user.address)).to.be.eq(0); + expect(await dai.balanceOf(aDai.address)).to.be.eq(utils.parseEther('2000')); + + const userDataAfter = await pool.getUserAccountData(user.address); + expect(userDataBefore.totalCollateralBase).to.be.lte(userDataAfter.totalCollateralBase); + expect(userDataBefore.healthFactor).to.be.lte(userDataAfter.healthFactor); + expect(userDataBefore.totalDebtBase).to.be.eq(userDataAfter.totalDebtBase); + }); + + it('User borrows and repays in same block using credit delegation with zero fees', async () => { + const { + pool, + dai, + aDai, + weth, + users: [user1, user2, user3], + } = testEnv; + + // Add liquidity + await dai.connect(user3.signer)['mint(uint256)'](utils.parseUnits('1000', 18)); + await dai.connect(user3.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(user3.signer) + .supply(dai.address, utils.parseUnits('1000', 18), user3.address, 0); + + // User1 supplies 10 WETH + await dai.connect(user1.signer)['mint(uint256)'](utils.parseUnits('100', 18)); + await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + await weth.connect(user1.signer)['mint(uint256)'](utils.parseUnits('10', 18)); + await weth.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(user1.signer) + .supply(weth.address, utils.parseUnits('10', 18), user1.address, 0); + + const daiData = await pool.getReserveData(dai.address); + const stableDebtToken = await getStableDebtToken(daiData.stableDebtTokenAddress); + + // User1 approves User2 to borrow 1000 DAI + expect( + await stableDebtToken + .connect(user1.signer) + .approveDelegation(user2.address, utils.parseUnits('1000', 18)) + ); + const userDataBefore = await pool.getUserAccountData(user1.address); + + // Turn off automining to simulate actions in same block + await setAutomine(false); + + // User2 borrows 2 DAI on behalf of User1 + expect( + await pool + .connect(user2.signer) + .borrow(dai.address, utils.parseEther('2'), RateMode.Stable, 0, user1.address) + ); + + // Turn on automining, but not mine a new block until next tx + await setAutomineEvm(true); + + expect( + await pool + .connect(user1.signer) + .repay(dai.address, utils.parseEther('2'), RateMode.Stable, user1.address) + ); + + expect(await stableDebtToken.balanceOf(user1.address)).to.be.eq(0); + expect(await dai.balanceOf(user2.address)).to.be.eq(utils.parseEther('2')); + expect(await dai.balanceOf(aDai.address)).to.be.eq(utils.parseEther('1000')); + + const userDataAfter = await pool.getUserAccountData(user1.address); + expect(userDataBefore.totalCollateralBase).to.be.lte(userDataAfter.totalCollateralBase); + expect(userDataBefore.healthFactor).to.be.lte(userDataAfter.healthFactor); + expect(userDataBefore.totalDebtBase).to.be.eq(userDataAfter.totalDebtBase); + }); }); diff --git a/test-suites/validation-logic.spec.ts b/test-suites/validation-logic.spec.ts index 3bcbffc12..017e0c5ce 100644 --- a/test-suites/validation-logic.spec.ts +++ b/test-suites/validation-logic.spec.ts @@ -1,14 +1,14 @@ -import { expect } from 'chai'; -import { utils, constants } from 'ethers'; -import { parseUnits } from '@ethersproject/units'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { MAX_UINT_AMOUNT } from '../helpers/constants'; -import { RateMode, ProtocolErrors } from '../helpers/types'; -import { impersonateAccountsHardhat, setAutomine, setAutomineEvm } from '../helpers/misc-utils'; -import { makeSuite, TestEnv } from './helpers/make-suite'; -import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'; -import { waitForTx, evmSnapshot, evmRevert, getVariableDebtToken } from '@aave/deploy-v3'; -import { topUpNonPayableWithEther } from './helpers/utils/funds'; +import {expect} from 'chai'; +import {utils, constants} from 'ethers'; +import {parseUnits} from '@ethersproject/units'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {MAX_UINT_AMOUNT} from '../helpers/constants'; +import {RateMode, ProtocolErrors} from '../helpers/types'; +import {impersonateAccountsHardhat} from '../helpers/misc-utils'; +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; +import {waitForTx, evmSnapshot, evmRevert} from '@aave/deploy-v3'; +import {topUpNonPayableWithEther} from './helpers/utils/funds'; declare var hre: HardhatRuntimeEnvironment; @@ -23,7 +23,6 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { COLLATERAL_SAME_AS_BORROWING_CURRENCY, AMOUNT_BIGGER_THAN_MAX_LOAN_SIZE_STABLE, NO_DEBT_OF_SELECTED_TYPE, - SAME_BLOCK_BORROW_REPAY, HEALTH_FACTOR_NOT_BELOW_THRESHOLD, INVALID_INTEREST_RATE_MODE_SELECTED, UNDERLYING_BALANCE_ZERO, @@ -35,13 +34,13 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { let snap: string; before(async () => { - const { addressesProvider, oracle } = testEnv; + const {addressesProvider, oracle} = testEnv; await waitForTx(await addressesProvider.setPriceOracle(oracle.address)); }); after(async () => { - const { aaveOracle, addressesProvider } = testEnv; + const {aaveOracle, addressesProvider} = testEnv; await waitForTx(await addressesProvider.setPriceOracle(aaveOracle.address)); }); @@ -53,7 +52,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateDeposit() when reserve is not active (revert expected)', async () => { - const { pool, poolAdmin, configurator, helpersContract, users, dai } = testEnv; + const {pool, poolAdmin, configurator, helpersContract, users, dai} = testEnv; const user = users[0]; const configBefore = await helpersContract.getReserveConfigurationData(dai.address); @@ -74,7 +73,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateDeposit() when reserve is frozen (revert expected)', async () => { - const { pool, poolAdmin, configurator, helpersContract, users, dai } = testEnv; + const {pool, poolAdmin, configurator, helpersContract, users, dai} = testEnv; const user = users[0]; const configBefore = await helpersContract.getReserveConfigurationData(dai.address); @@ -101,7 +100,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { * If deposited normally it is not possible for us deactivate. */ - const { pool, poolAdmin, configurator, helpersContract, users, dai, aDai, usdc } = testEnv; + const {pool, poolAdmin, configurator, helpersContract, users, dai, aDai, usdc} = testEnv; const user = users[0]; await usdc.connect(user.signer)['mint(uint256)'](utils.parseEther('10000')); @@ -132,7 +131,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateBorrow() when reserve is frozen (revert expected)', async () => { - const { pool, poolAdmin, configurator, helpersContract, users, dai, usdc } = testEnv; + const {pool, poolAdmin, configurator, helpersContract, users, dai, usdc} = testEnv; const user = users[0]; await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('1000')); @@ -163,7 +162,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateBorrow() when amount == 0 (revert expected)', async () => { - const { pool, users, dai } = testEnv; + const {pool, users, dai} = testEnv; const user = users[0]; await expect( @@ -172,7 +171,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateBorrow() when borrowing is not enabled (revert expected)', async () => { - const { pool, poolAdmin, configurator, helpersContract, users, dai, usdc } = testEnv; + const {pool, poolAdmin, configurator, helpersContract, users, dai, usdc} = testEnv; const user = users[0]; await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('1000')); @@ -203,7 +202,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateBorrow() when stableRateBorrowing is not enabled', async () => { - const { pool, poolAdmin, configurator, helpersContract, users, dai, aDai, usdc } = testEnv; + const {pool, poolAdmin, configurator, helpersContract, users, dai, aDai, usdc} = testEnv; const user = users[0]; await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('1000')); @@ -227,7 +226,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateBorrow() borrowing when user has already a HF < threshold', async () => { - const { pool, users, dai, usdc, oracle } = testEnv; + const {pool, users, dai, usdc, oracle} = testEnv; const user = users[0]; const depositor = users[1]; @@ -290,7 +289,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { // ltv != 0 // amount < aToken Balance - const { pool, users, dai, aDai, usdc } = testEnv; + const {pool, users, dai, aDai, usdc} = testEnv; const user = users[0]; await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); @@ -312,7 +311,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateBorrow() stable borrowing when amount > maxLoanSizeStable (revert expected)', async () => { - const { pool, users, dai, aDai, usdc } = testEnv; + const {pool, users, dai, aDai, usdc} = testEnv; const user = users[0]; await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); @@ -335,7 +334,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { it('validateLiquidationCall() when healthFactor > threshold (revert expected)', async () => { // Liquidation something that is not liquidatable - const { pool, users, dai, usdc } = testEnv; + const {pool, users, dai, usdc} = testEnv; const depositor = users[0]; const borrower = users[1]; @@ -384,7 +383,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { it('validateRepay() when reserve is not active (revert expected)', async () => { // Unsure how we can end in this scenario. Would require that it could be deactivated after someone have borrowed - const { pool, users, dai, helpersContract, configurator, poolAdmin } = testEnv; + const {pool, users, dai, helpersContract, configurator, poolAdmin} = testEnv; const user = users[0]; const configBefore = await helpersContract.getReserveConfigurationData(dai.address); @@ -404,137 +403,11 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { ).to.be.revertedWith(RESERVE_INACTIVE); }); - it('validateRepay() when variable borrowing and repaying in same block (revert expected)', async () => { - // Same block repay - - const { pool, users, dai, aDai, usdc } = testEnv; - const user = users[0]; - - // We need some debt. - await usdc.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); - await usdc.connect(user.signer).approve(pool.address, MAX_UINT_AMOUNT); - await pool - .connect(user.signer) - .deposit(usdc.address, utils.parseEther('2000'), user.address, 0); - await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); - await dai.connect(user.signer).transfer(aDai.address, utils.parseEther('2000')); - - // Turn off automining - pretty sure that coverage is getting messed up here. - await setAutomine(false); - - // Borrow 500 dai - await pool - .connect(user.signer) - .borrow(dai.address, utils.parseEther('500'), RateMode.Variable, 0, user.address); - - // Turn on automining, but not mine a new block until next tx - await setAutomineEvm(true); - - await expect( - pool - .connect(user.signer) - .repay(dai.address, utils.parseEther('500'), RateMode.Variable, user.address) - ).to.be.revertedWith(SAME_BLOCK_BORROW_REPAY); - }); - - it('validateRepay() when variable borrowing and repaying in same block using credit delegation (revert expected)', async () => { - const { - pool, - dai, - weth, - users: [user1, user2, user3], - } = testEnv; - - // Add liquidity - await dai.connect(user3.signer)['mint(uint256)'](utils.parseUnits('1000', 18)); - await dai.connect(user3.signer).approve(pool.address, MAX_UINT_AMOUNT); - await pool - .connect(user3.signer) - .supply(dai.address, utils.parseUnits('1000', 18), user3.address, 0); - - // User1 supplies 10 WETH - await dai.connect(user1.signer)['mint(uint256)'](utils.parseUnits('100', 18)); - await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); - await weth.connect(user1.signer)['mint(uint256)'](utils.parseUnits('10', 18)); - await weth.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); - await pool - .connect(user1.signer) - .supply(weth.address, utils.parseUnits('10', 18), user1.address, 0); - - const daiData = await pool.getReserveData(dai.address); - const variableDebtToken = await getVariableDebtToken(daiData.variableDebtTokenAddress); - - // User1 approves User2 to borrow 1000 DAI - expect( - await variableDebtToken - .connect(user1.signer) - .approveDelegation(user2.address, utils.parseUnits('1000', 18)) - ); - - // User2 borrows on behalf of User1 - const borrowOnBehalfAmount = utils.parseUnits('100', 18); - expect( - await pool - .connect(user2.signer) - .borrow(dai.address, borrowOnBehalfAmount, RateMode.Variable, 0, user1.address) - ); - - // Turn off automining to simulate actions in same block - await setAutomine(false); - - // User2 borrows 2 DAI on behalf of User1 - await pool - .connect(user2.signer) - .borrow(dai.address, utils.parseEther('2'), RateMode.Variable, 0, user1.address); - - // Turn on automining, but not mine a new block until next tx - await setAutomineEvm(true); - - await expect( - pool - .connect(user1.signer) - .repay(dai.address, utils.parseEther('2'), RateMode.Variable, user1.address) - ).to.be.revertedWith(SAME_BLOCK_BORROW_REPAY); - }); - - it('validateRepay() when stable borrowing and repaying in same block (revert expected)', async () => { - // Same block repay - - const { pool, users, dai, aDai, usdc } = testEnv; - const user = users[0]; - - // We need some debt. - await usdc.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); - await usdc.connect(user.signer).approve(pool.address, MAX_UINT_AMOUNT); - await pool - .connect(user.signer) - .deposit(usdc.address, utils.parseEther('2000'), user.address, 0); - await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); - await dai.connect(user.signer).transfer(aDai.address, utils.parseEther('2000')); - - // Turn off automining - pretty sure that coverage is getting messed up here. - await setAutomine(false); - - // Borrow 500 dai - await pool - .connect(user.signer) - .borrow(dai.address, utils.parseEther('500'), RateMode.Stable, 0, user.address); - - // Turn on automining, but not mine a new block until next tx - await setAutomineEvm(true); - - await expect( - pool - .connect(user.signer) - .repay(dai.address, utils.parseEther('500'), RateMode.Stable, user.address) - ).to.be.revertedWith(SAME_BLOCK_BORROW_REPAY); - }); - it('validateRepay() the variable debt when is 0 (stableDebt > 0) (revert expected)', async () => { // (stableDebt > 0 && DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE) || // (variableDebt > 0 && DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.VARIABLE), - const { pool, users, dai, aDai, usdc } = testEnv; + const {pool, users, dai, aDai, usdc} = testEnv; const user = users[0]; // We need some debt @@ -561,7 +434,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { // (stableDebt > 0 && DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE) || // (variableDebt > 0 && DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.VARIABLE), - const { pool, users, dai } = testEnv; + const {pool, users, dai} = testEnv; const user = users[0]; // We need some debt @@ -582,7 +455,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { it('validateSwapRateMode() when reserve is not active', async () => { // Not clear when this would be useful in practice, as you should not be able to have debt if it is deactivated - const { pool, poolAdmin, configurator, helpersContract, users, dai, aDai } = testEnv; + const {pool, poolAdmin, configurator, helpersContract, users, dai, aDai} = testEnv; const user = users[0]; const configBefore = await helpersContract.getReserveConfigurationData(dai.address); @@ -608,7 +481,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { it('validateSwapRateMode() when reserve is frozen', async () => { // Not clear when this would be useful in practice, as you should not be able to have debt if it is deactivated - const { pool, poolAdmin, configurator, helpersContract, users, dai } = testEnv; + const {pool, poolAdmin, configurator, helpersContract, users, dai} = testEnv; const user = users[0]; const configBefore = await helpersContract.getReserveConfigurationData(dai.address); @@ -633,7 +506,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateSwapRateMode() with currentRateMode not equal to stable or variable, (revert expected)', async () => { - const { pool, helpersContract, users, dai } = testEnv; + const {pool, helpersContract, users, dai} = testEnv; const user = users[0]; const configBefore = await helpersContract.getReserveConfigurationData(dai.address); @@ -646,7 +519,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateSwapRateMode() from variable to stable with stableBorrowing disabled (revert expected)', async () => { - const { pool, poolAdmin, configurator, helpersContract, users, dai } = testEnv; + const {pool, poolAdmin, configurator, helpersContract, users, dai} = testEnv; const user = users[0]; await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('1000')); @@ -693,7 +566,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { // ltv != 0 // stableDebt + variableDebt < aToken - const { pool, users, dai, aDai, usdc } = testEnv; + const {pool, users, dai, aDai, usdc} = testEnv; const user = users[0]; await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); @@ -717,7 +590,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateRebalanceStableBorrowRate() when reserve is not active (revert expected)', async () => { - const { pool, configurator, helpersContract, poolAdmin, users, dai } = testEnv; + const {pool, configurator, helpersContract, poolAdmin, users, dai} = testEnv; const user = users[0]; const configBefore = await helpersContract.getReserveConfigurationData(dai.address); @@ -741,7 +614,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { * aToken balance (aDAI) its not technically possible to end up in this situation. * However, we impersonate the Pool to get some aDAI and make the test possible */ - const { pool, configurator, helpersContract, poolAdmin, users, dai, aDai } = testEnv; + const {pool, configurator, helpersContract, poolAdmin, users, dai, aDai} = testEnv; const user = users[0]; const configBefore = await helpersContract.getReserveConfigurationData(dai.address); @@ -769,7 +642,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateSetUseReserveAsCollateral() with userBalance == 0 (revert expected)', async () => { - const { pool, users, dai } = testEnv; + const {pool, users, dai} = testEnv; const user = users[0]; await expect( @@ -782,7 +655,7 @@ makeSuite('ValidationLogic: Edge cases', (testEnv: TestEnv) => { }); it('validateFlashloan() with inconsistent params (revert expected)', async () => { - const { pool, users, dai, aDai, usdc } = testEnv; + const {pool, users, dai, aDai, usdc} = testEnv; const user = users[0]; await expect( diff --git a/test-suites/variable-debt-token.spec.ts b/test-suites/variable-debt-token.spec.ts index 444a0de92..9f9029a36 100644 --- a/test-suites/variable-debt-token.spec.ts +++ b/test-suites/variable-debt-token.spec.ts @@ -1,24 +1,39 @@ -import { expect } from 'chai'; -import { utils } from 'ethers'; -import { impersonateAccountsHardhat } from '../helpers/misc-utils'; -import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../helpers/constants'; -import { ProtocolErrors, RateMode } from '../helpers/types'; -import { makeSuite, TestEnv } from './helpers/make-suite'; -import { topUpNonPayableWithEther } from './helpers/utils/funds'; -import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { evmRevert, evmSnapshot, increaseTime, waitForTx } from '@aave/deploy-v3'; -import { VariableDebtToken__factory } from '../types'; +import {expect} from 'chai'; +import {utils} from 'ethers'; +import {impersonateAccountsHardhat, setAutomine, setAutomineEvm} from '../helpers/misc-utils'; +import {MAX_UINT_AMOUNT, ZERO_ADDRESS} from '../helpers/constants'; +import {ProtocolErrors, RateMode} from '../helpers/types'; +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {topUpNonPayableWithEther} from './helpers/utils/funds'; +import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import { + evmRevert, + evmSnapshot, + getVariableDebtToken, + increaseTime, + waitForTx, +} from '@aave/deploy-v3'; +import {VariableDebtToken__factory} from '../types'; import './helpers/utils/wadraymath'; declare var hre: HardhatRuntimeEnvironment; makeSuite('VariableDebtToken', (testEnv: TestEnv) => { - const { CALLER_MUST_BE_POOL, INVALID_MINT_AMOUNT, INVALID_BURN_AMOUNT, CALLER_NOT_POOL_ADMIN } = + const {CALLER_MUST_BE_POOL, INVALID_MINT_AMOUNT, INVALID_BURN_AMOUNT, CALLER_NOT_POOL_ADMIN} = ProtocolErrors; + let snap: string; + + beforeEach(async () => { + snap = await evmSnapshot(); + }); + afterEach(async () => { + await evmRevert(snap); + }); + it('Check initialization', async () => { - const { pool, weth, dai, helpersContract, users } = testEnv; + const {pool, weth, dai, helpersContract, users} = testEnv; const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) ).variableDebtTokenAddress; @@ -81,7 +96,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('Tries to mint not being the Pool (revert expected)', async () => { - const { deployer, dai, helpersContract } = testEnv; + const {deployer, dai, helpersContract} = testEnv; const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) @@ -98,7 +113,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('Tries to burn not being the Pool (revert expected)', async () => { - const { deployer, dai, helpersContract } = testEnv; + const {deployer, dai, helpersContract} = testEnv; const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) @@ -115,7 +130,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('Tries to mint with amountScaled == 0 (revert expected)', async () => { - const { deployer, pool, dai, helpersContract, users } = testEnv; + const {deployer, pool, dai, helpersContract, users} = testEnv; // Impersonate the Pool await topUpNonPayableWithEther(deployer.signer, [pool.address], utils.parseEther('1')); @@ -139,7 +154,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('Tries to burn with amountScaled == 0 (revert expected)', async () => { - const { deployer, pool, dai, helpersContract, users } = testEnv; + const {deployer, pool, dai, helpersContract, users} = testEnv; // Impersonate the Pool await topUpNonPayableWithEther(deployer.signer, [pool.address], utils.parseEther('1')); @@ -161,7 +176,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('Tries to transfer debt tokens (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) ).variableDebtTokenAddress; @@ -176,7 +191,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('Tries to approve debt tokens (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) ).variableDebtTokenAddress; @@ -194,7 +209,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('Tries to increaseAllowance (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) ).variableDebtTokenAddress; @@ -209,7 +224,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('Tries to decreaseAllowance (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) ).variableDebtTokenAddress; @@ -224,7 +239,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('Tries to transferFrom debt tokens (revert expected)', async () => { - const { users, dai, helpersContract } = testEnv; + const {users, dai, helpersContract} = testEnv; const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) ).variableDebtTokenAddress; @@ -241,8 +256,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { }); it('setIncentivesController() ', async () => { - const snapshot = await evmSnapshot(); - const { dai, helpersContract, poolAdmin, aclManager, deployer } = testEnv; + const {dai, helpersContract, poolAdmin, aclManager, deployer} = testEnv; const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) ).variableDebtTokenAddress; @@ -258,8 +272,6 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { await variableDebtContract.connect(poolAdmin.signer).setIncentivesController(ZERO_ADDRESS) ); expect(await variableDebtContract.getIncentivesController()).to.be.eq(ZERO_ADDRESS); - - await evmRevert(snapshot); }); it('setIncentivesController() from not pool admin (revert expected)', async () => { @@ -357,8 +369,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { ); const rawTransferEvents = tx.logs.filter( - ({ topics, address }) => - topics[0] === transferEventSig && address == variableDebtToken.address + ({topics, address}) => topics[0] === transferEventSig && address == variableDebtToken.address ); const parsedTransferEvent = variableDebtToken.interface.parseLog(rawTransferEvents[0]); const transferAmount = parsedTransferEvent.args.value; @@ -369,7 +380,7 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { utils.toUtf8Bytes('Mint(address,address,uint256,uint256,uint256)') ); const rawMintEvents = tx.logs.filter( - ({ topics, address }) => topics[0] === mintEventSig && address == variableDebtToken.address + ({topics, address}) => topics[0] === mintEventSig && address == variableDebtToken.address ); const parsedMintEvent = variableDebtToken.interface.parseLog(rawMintEvents[0]); @@ -377,4 +388,174 @@ makeSuite('VariableDebtToken', (testEnv: TestEnv) => { expect(parsedMintEvent.args.value).to.be.closeTo(borrowOnBehalfAmount.add(interest), 2); expect(parsedMintEvent.args.balanceIncrease).to.be.closeTo(interest, 2); }); + + it('User borrows and repays in same block with zero fees', async () => { + const {pool, users, dai, aDai, usdc, variableDebtDai} = testEnv; + const user = users[0]; + + // We need some debt. + await usdc.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); + await usdc.connect(user.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(user.signer) + .deposit(usdc.address, utils.parseEther('2000'), user.address, 0); + await dai.connect(user.signer)['mint(uint256)'](utils.parseEther('2000')); + await dai.connect(user.signer).transfer(aDai.address, utils.parseEther('2000')); + await dai.connect(user.signer).approve(pool.address, MAX_UINT_AMOUNT); + + const userDataBefore = await pool.getUserAccountData(user.address); + expect(await variableDebtDai.balanceOf(user.address)).to.be.eq(0); + + // Turn off automining - pretty sure that coverage is getting messed up here. + await setAutomine(false); + // Borrow 500 dai + await pool + .connect(user.signer) + .borrow(dai.address, utils.parseEther('500'), RateMode.Variable, 0, user.address); + + // Turn on automining, but not mine a new block until next tx + await setAutomineEvm(true); + expect( + await pool + .connect(user.signer) + .repay(dai.address, utils.parseEther('500'), RateMode.Variable, user.address) + ); + + expect(await variableDebtDai.balanceOf(user.address)).to.be.eq(0); + expect(await dai.balanceOf(user.address)).to.be.eq(0); + expect(await dai.balanceOf(aDai.address)).to.be.eq(utils.parseEther('2000')); + + const userDataAfter = await pool.getUserAccountData(user.address); + expect(userDataBefore.totalCollateralBase).to.be.lte(userDataAfter.totalCollateralBase); + expect(userDataBefore.healthFactor).to.be.lte(userDataAfter.healthFactor); + expect(userDataBefore.totalDebtBase).to.be.eq(userDataAfter.totalDebtBase); + }); + + it('User borrows and repays in same block using credit delegation with zero fees', async () => { + const { + pool, + dai, + aDai, + weth, + users: [user1, user2, user3], + } = testEnv; + + // Add liquidity + await dai.connect(user3.signer)['mint(uint256)'](utils.parseUnits('1000', 18)); + await dai.connect(user3.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(user3.signer) + .supply(dai.address, utils.parseUnits('1000', 18), user3.address, 0); + + // User1 supplies 10 WETH + await dai.connect(user1.signer)['mint(uint256)'](utils.parseUnits('100', 18)); + await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + await weth.connect(user1.signer)['mint(uint256)'](utils.parseUnits('10', 18)); + await weth.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(user1.signer) + .supply(weth.address, utils.parseUnits('10', 18), user1.address, 0); + + const daiData = await pool.getReserveData(dai.address); + const variableDebtToken = await getVariableDebtToken(daiData.variableDebtTokenAddress); + + // User1 approves User2 to borrow 1000 DAI + expect( + await variableDebtToken + .connect(user1.signer) + .approveDelegation(user2.address, utils.parseUnits('1000', 18)) + ); + + const userDataBefore = await pool.getUserAccountData(user1.address); + + // Turn off automining to simulate actions in same block + await setAutomine(false); + + // User2 borrows 2 DAI on behalf of User1 + await pool + .connect(user2.signer) + .borrow(dai.address, utils.parseEther('2'), RateMode.Variable, 0, user1.address); + + // Turn on automining, but not mine a new block until next tx + await setAutomineEvm(true); + + expect( + await pool + .connect(user1.signer) + .repay(dai.address, utils.parseEther('2'), RateMode.Variable, user1.address) + ); + + expect(await variableDebtToken.balanceOf(user1.address)).to.be.eq(0); + expect(await dai.balanceOf(user2.address)).to.be.eq(utils.parseEther('2')); + expect(await dai.balanceOf(aDai.address)).to.be.eq(utils.parseEther('1000')); + + const userDataAfter = await pool.getUserAccountData(user1.address); + expect(userDataBefore.totalCollateralBase).to.be.lte(userDataAfter.totalCollateralBase); + expect(userDataBefore.healthFactor).to.be.lte(userDataAfter.healthFactor); + expect(userDataBefore.totalDebtBase).to.be.eq(userDataAfter.totalDebtBase); + }); + + it('User borrows and repays in same block using credit delegation with zero fees', async () => { + const { + pool, + dai, + aDai, + weth, + users: [user1, user2, user3], + } = testEnv; + + // Add liquidity + await dai.connect(user3.signer)['mint(uint256)'](utils.parseUnits('1000', 18)); + await dai.connect(user3.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(user3.signer) + .supply(dai.address, utils.parseUnits('1000', 18), user3.address, 0); + + // User1 supplies 10 WETH + await dai.connect(user1.signer)['mint(uint256)'](utils.parseUnits('100', 18)); + await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + await weth.connect(user1.signer)['mint(uint256)'](utils.parseUnits('10', 18)); + await weth.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(user1.signer) + .supply(weth.address, utils.parseUnits('10', 18), user1.address, 0); + + const daiData = await pool.getReserveData(dai.address); + const variableDebtToken = await getVariableDebtToken(daiData.variableDebtTokenAddress); + + // User1 approves User2 to borrow 1000 DAI + expect( + await variableDebtToken + .connect(user1.signer) + .approveDelegation(user2.address, utils.parseUnits('1000', 18)) + ); + + const userDataBefore = await pool.getUserAccountData(user1.address); + + // Turn off automining to simulate actions in same block + await setAutomine(false); + + // User2 borrows 2 DAI on behalf of User1 + await pool + .connect(user2.signer) + .borrow(dai.address, utils.parseEther('2'), RateMode.Variable, 0, user1.address); + + // Turn on automining, but not mine a new block until next tx + await setAutomineEvm(true); + + expect( + await pool + .connect(user1.signer) + .repay(dai.address, utils.parseEther('2'), RateMode.Variable, user1.address) + ); + + expect(await variableDebtToken.balanceOf(user1.address)).to.be.eq(0); + expect(await dai.balanceOf(user2.address)).to.be.eq(utils.parseEther('2')); + expect(await dai.balanceOf(aDai.address)).to.be.eq(utils.parseEther('1000')); + + const userDataAfter = await pool.getUserAccountData(user1.address); + expect(userDataBefore.totalCollateralBase).to.be.lte(userDataAfter.totalCollateralBase); + expect(userDataBefore.healthFactor).to.be.lte(userDataAfter.healthFactor); + expect(userDataBefore.totalDebtBase).to.be.eq(userDataAfter.totalDebtBase); + }); });