diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index d022f65ee..dcde03ae9 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -59,7 +59,7 @@ library Errors { string public constant SUPPLY_CAP_EXCEEDED = '51'; // 'Supply cap is exceeded' string public constant UNBACKED_MINT_CAP_EXCEEDED = '52'; // 'Unbacked mint cap is exceeded' string public constant DEBT_CEILING_EXCEEDED = '53'; // 'Debt ceiling is exceeded' - string public constant ATOKEN_SUPPLY_NOT_ZERO = '54'; // 'AToken supply is not zero' + string public constant UNDERLYING_CLAIMABLE_RIGHTS_NOT_ZERO = '54'; // 'Claimable rights over underlying not zero (aToken supply or accruedToTreasury)' string public constant STABLE_DEBT_NOT_ZERO = '55'; // 'Stable debt supply is not zero' string public constant VARIABLE_DEBT_SUPPLY_NOT_ZERO = '56'; // 'Variable debt supply is not zero' string public constant LTV_VALIDATION_FAILED = '57'; // 'Ltv validation failed' diff --git a/contracts/protocol/libraries/logic/BridgeLogic.sol b/contracts/protocol/libraries/logic/BridgeLogic.sol index fc2b6fc6e..a6ec0da2a 100644 --- a/contracts/protocol/libraries/logic/BridgeLogic.sol +++ b/contracts/protocol/libraries/logic/BridgeLogic.sol @@ -63,7 +63,7 @@ library BridgeLogic { reserve.updateState(reserveCache); - ValidationLogic.validateSupply(reserveCache, amount); + ValidationLogic.validateSupply(reserveCache, reserve, amount); uint256 unbackedMintCap = reserveCache.reserveConfiguration.getUnbackedMintCap(); uint256 reserveDecimals = reserveCache.reserveConfiguration.getDecimals(); @@ -127,7 +127,8 @@ library BridgeLogic { uint256 added = backingAmount + fee; reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex( - IERC20(reserveCache.aTokenAddress).totalSupply(), + IERC20(reserveCache.aTokenAddress).totalSupply() + + uint256(reserve.accruedToTreasury).rayMul(reserveCache.nextLiquidityIndex), feeToLP ); diff --git a/contracts/protocol/libraries/logic/FlashLoanLogic.sol b/contracts/protocol/libraries/logic/FlashLoanLogic.sol index fa800e321..521ed212c 100644 --- a/contracts/protocol/libraries/logic/FlashLoanLogic.sol +++ b/contracts/protocol/libraries/logic/FlashLoanLogic.sol @@ -231,7 +231,8 @@ library FlashLoanLogic { DataTypes.ReserveCache memory reserveCache = reserve.cache(); reserve.updateState(reserveCache); reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex( - IERC20(reserveCache.aTokenAddress).totalSupply(), + IERC20(reserveCache.aTokenAddress).totalSupply() + + uint256(reserve.accruedToTreasury).rayMul(reserveCache.nextLiquidityIndex), premiumToLP ); diff --git a/contracts/protocol/libraries/logic/SupplyLogic.sol b/contracts/protocol/libraries/logic/SupplyLogic.sol index 1c7d2d259..d8310ec55 100644 --- a/contracts/protocol/libraries/logic/SupplyLogic.sol +++ b/contracts/protocol/libraries/logic/SupplyLogic.sol @@ -60,7 +60,7 @@ library SupplyLogic { reserve.updateState(reserveCache); - ValidationLogic.validateSupply(reserveCache, params.amount); + ValidationLogic.validateSupply(reserveCache, reserve, params.amount); reserve.updateInterestRates(reserveCache, params.asset, params.amount, 0); diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 784fc728d..8aeee65cc 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -54,10 +54,11 @@ library ValidationLogic { * @param reserveCache The cached data of the reserve * @param amount The amount to be supplied */ - function validateSupply(DataTypes.ReserveCache memory reserveCache, uint256 amount) - internal - view - { + function validateSupply( + DataTypes.ReserveCache memory reserveCache, + DataTypes.ReserveData storage reserve, + uint256 amount + ) internal view { require(amount != 0, Errors.INVALID_AMOUNT); (bool isActive, bool isFrozen, , , bool isPaused) = reserveCache @@ -70,9 +71,8 @@ library ValidationLogic { uint256 supplyCap = reserveCache.reserveConfiguration.getSupplyCap(); require( supplyCap == 0 || - (IAToken(reserveCache.aTokenAddress).scaledTotalSupply().rayMul( - reserveCache.nextLiquidityIndex - ) + amount) <= + ((IAToken(reserveCache.aTokenAddress).scaledTotalSupply() + + uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex) + amount) <= supplyCap * (10**reserveCache.reserveConfiguration.getDecimals()), Errors.SUPPLY_CAP_EXCEEDED ); @@ -636,7 +636,10 @@ library ValidationLogic { IERC20(reserve.variableDebtTokenAddress).totalSupply() == 0, Errors.VARIABLE_DEBT_SUPPLY_NOT_ZERO ); - require(IERC20(reserve.aTokenAddress).totalSupply() == 0, Errors.ATOKEN_SUPPLY_NOT_ZERO); + require( + IERC20(reserve.aTokenAddress).totalSupply() == 0 && reserve.accruedToTreasury == 0, + Errors.UNDERLYING_CLAIMABLE_RIGHTS_NOT_ZERO + ); } /** diff --git a/contracts/protocol/pool/PoolConfigurator.sol b/contracts/protocol/pool/PoolConfigurator.sol index b99610c2e..43cfddd19 100644 --- a/contracts/protocol/pool/PoolConfigurator.sol +++ b/contracts/protocol/pool/PoolConfigurator.sol @@ -480,9 +480,11 @@ contract PoolConfigurator is VersionedInitializable, IPoolConfigurator { } function _checkNoSuppliers(address asset) internal view { - uint256 totalATokens = IPoolDataProvider(_addressesProvider.getPoolDataProvider()) - .getATokenTotalSupply(asset); - require(totalATokens == 0, Errors.RESERVE_LIQUIDITY_NOT_ZERO); + (, uint256 accruedToTreasury, uint256 totalATokens, , , , , , , , , ) = IPoolDataProvider( + _addressesProvider.getPoolDataProvider() + ).getReserveData(asset); + + require(totalATokens == 0 && accruedToTreasury == 0, Errors.RESERVE_LIQUIDITY_NOT_ZERO); } function _checkNoBorrowers(address asset) internal view { diff --git a/helpers/types.ts b/helpers/types.ts index 5ad641560..2238c8a8c 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -1,4 +1,4 @@ -import { BigNumber } from 'ethers'; +import {BigNumber} from 'ethers'; export interface SymbolMap { [symbol: string]: T; @@ -123,7 +123,7 @@ export enum ProtocolErrors { SUPPLY_CAP_EXCEEDED = '51', // 'Supply cap is exceeded' UNBACKED_MINT_CAP_EXCEEDED = '52', // 'Unbacked mint cap is exceeded' DEBT_CEILING_EXCEEDED = '53', // 'Debt ceiling is exceeded' - ATOKEN_SUPPLY_NOT_ZERO = '54', // 'AToken supply is not zero' + UNDERLYING_CLAIMABLE_RIGHTS_NOT_ZERO = '54', // 'Claimable rights over underlying not zero (aToken supply or accruedToTreasury)' STABLE_DEBT_NOT_ZERO = '55', // 'Stable debt supply is not zero' VARIABLE_DEBT_SUPPLY_NOT_ZERO = '56', // 'Variable debt supply is not zero' LTV_VALIDATION_FAILED = '57', // 'Ltv validation failed' diff --git a/test-suites/helpers/utils/calculations.ts b/test-suites/helpers/utils/calculations.ts index 9484c54ad..f28db4f29 100644 --- a/test-suites/helpers/utils/calculations.ts +++ b/test-suites/helpers/utils/calculations.ts @@ -269,7 +269,9 @@ export const calcExpectedReserveDataAfterBackUnbacked = ( expectedReserveData.liquidityIndex = cumulateToLiquidityIndex( expectedReserveData.liquidityIndex, - totalSupply, + totalSupply.add( + expectedReserveData.accruedToTreasuryScaled.rayMul(expectedReserveData.liquidityIndex) + ), premiumToLP ); diff --git a/test-suites/liquidity-indexes.spec.ts b/test-suites/liquidity-indexes.spec.ts new file mode 100644 index 000000000..a481d0bbc --- /dev/null +++ b/test-suites/liquidity-indexes.spec.ts @@ -0,0 +1,123 @@ +import {expect} from 'chai'; +import {BigNumber, ethers} from 'ethers'; +import {MAX_UINT_AMOUNT} from '../helpers/constants'; +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import { + evmSnapshot, + evmRevert, + MockFlashLoanReceiver, + getMockFlashLoanReceiver, +} from '@aave/deploy-v3'; +import './helpers/utils/wadraymath'; + +declare var hre: HardhatRuntimeEnvironment; + +makeSuite('Pool: liquidity indexes misc tests', (testEnv: TestEnv) => { + const TOTAL_PREMIUM = 9; + const PREMIUM_TO_PROTOCOL = 3000; + + let _mockFlashLoanReceiver = {} as MockFlashLoanReceiver; + + let snap: string; + + const setupForFlashloan = async (testEnv: TestEnv) => { + const { + configurator, + pool, + weth, + aave, + dai, + users: [user0], + } = testEnv; + + _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); + + await configurator.updateFlashloanPremiumTotal(TOTAL_PREMIUM); + await configurator.updateFlashloanPremiumToProtocol(PREMIUM_TO_PROTOCOL); + + const userAddress = user0.address; + const amountToDeposit = ethers.utils.parseEther('1'); + + await weth['mint(uint256)'](amountToDeposit); + + await weth.approve(pool.address, MAX_UINT_AMOUNT); + + await pool.deposit(weth.address, amountToDeposit, userAddress, '0'); + + await aave['mint(uint256)'](amountToDeposit); + + await aave.approve(pool.address, MAX_UINT_AMOUNT); + + await pool.deposit(aave.address, amountToDeposit, userAddress, '0'); + await dai['mint(uint256)'](amountToDeposit); + + await dai.approve(pool.address, MAX_UINT_AMOUNT); + + await pool.deposit(dai.address, amountToDeposit, userAddress, '0'); + }; + + before(async () => { + await setupForFlashloan(testEnv); + }); + + beforeEach(async () => { + snap = await evmSnapshot(); + }); + + afterEach(async () => { + await evmRevert(snap); + }); + + it('Validates that the flash loan fee properly takes into account both aToken supply and accruedToTreasury', async () => { + const { + pool, + helpersContract, + weth, + aWETH, + users: [depositorWeth], + } = testEnv; + + /** + * 1. Flashes 0.8 WETH + * 2. Flashes again 0.8 ETH (to have accruedToTreasury) + * 3. Validates that liquidity index took into account both aToken supply and accruedToTreasury + */ + + const wethFlashBorrowedAmount = ethers.utils.parseEther('0.8'); + + await pool.flashLoan( + _mockFlashLoanReceiver.address, + [weth.address], + [wethFlashBorrowedAmount], + [0], + _mockFlashLoanReceiver.address, + '0x10', + '0' + ); + + await pool.flashLoan( + _mockFlashLoanReceiver.address, + [weth.address], + [wethFlashBorrowedAmount], + [0], + _mockFlashLoanReceiver.address, + '0x10', + '0' + ); + + const wethReserveDataAfterSecondFlash = await helpersContract.getReserveData(weth.address); + + const totalScaledWithTreasuryAfterSecondFlash = ( + await aWETH.scaledBalanceOf(depositorWeth.address) + ).add(wethReserveDataAfterSecondFlash.accruedToTreasuryScaled.toString()); + + expect(await weth.balanceOf(aWETH.address)).to.be.closeTo( + BigNumber.from(totalScaledWithTreasuryAfterSecondFlash.toString()).rayMul( + wethReserveDataAfterSecondFlash.liquidityIndex + ), + 1, + 'Scaled total supply not (+/- 1) equal to WETH balance of aWETH' + ); + }); +}); diff --git a/test-suites/pool-drop-reserve.spec.ts b/test-suites/pool-drop-reserve.spec.ts index e14ffa92e..a0f484fef 100644 --- a/test-suites/pool-drop-reserve.spec.ts +++ b/test-suites/pool-drop-reserve.spec.ts @@ -10,7 +10,7 @@ makeSuite('Pool: Drop Reserve', (testEnv: TestEnv) => { let _mockFlashLoanReceiver = {} as MockFlashLoanReceiver; const { - ATOKEN_SUPPLY_NOT_ZERO, + UNDERLYING_CLAIMABLE_RIGHTS_NOT_ZERO, STABLE_DEBT_NOT_ZERO, VARIABLE_DEBT_SUPPLY_NOT_ZERO, ASSET_NOT_LISTED, @@ -46,7 +46,7 @@ makeSuite('Pool: Drop Reserve', (testEnv: TestEnv) => { await pool.deposit(dai.address, depositedAmount, deployer.address, 0); - await expect(configurator.dropReserve(dai.address)).to.be.revertedWith(ATOKEN_SUPPLY_NOT_ZERO); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith(UNDERLYING_CLAIMABLE_RIGHTS_NOT_ZERO); await pool.connect(user1.signer).deposit(weth.address, depositedAmount, user1.address, 0); @@ -71,7 +71,7 @@ makeSuite('Pool: Drop Reserve', (testEnv: TestEnv) => { ); expect(await pool.connect(user1.signer).repay(dai.address, MAX_UINT_AMOUNT, 2, user1.address)); - await expect(configurator.dropReserve(dai.address)).to.be.revertedWith(ATOKEN_SUPPLY_NOT_ZERO); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith(UNDERLYING_CLAIMABLE_RIGHTS_NOT_ZERO); }); it('User 1 withdraw DAI, drop DAI reserve should succeed', async () => { diff --git a/test-suites/pool-edge.spec.ts b/test-suites/pool-edge.spec.ts index a85f5ac3f..27bd7be25 100644 --- a/test-suites/pool-edge.spec.ts +++ b/test-suites/pool-edge.spec.ts @@ -8,7 +8,14 @@ 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 { + evmSnapshot, + evmRevert, + getPoolLibraries, + MockFlashLoanReceiver, + getMockFlashLoanReceiver, + advanceTimeAndBlock, +} from '@aave/deploy-v3'; import { MockPoolInherited__factory, MockReserveInterestRateStrategy__factory, @@ -16,10 +23,10 @@ import { VariableDebtToken__factory, AToken__factory, Pool__factory, - InitializableImmutableAdminUpgradeabilityProxy, ERC20__factory, } from '../types'; import {convertToCurrencyDecimals, getProxyImplementation} from '../helpers/contracts-helpers'; +import {ethers} from 'hardhat'; declare var hre: HardhatRuntimeEnvironment; @@ -89,15 +96,22 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { DEBT_CEILING_NOT_ZERO, ASSET_NOT_LISTED, ZERO_ADDRESS_NOT_VALID, + UNDERLYING_CLAIMABLE_RIGHTS_NOT_ZERO, + SUPPLY_CAP_EXCEEDED, + RESERVE_LIQUIDITY_NOT_ZERO, } = ProtocolErrors; const MAX_STABLE_RATE_BORROW_SIZE_PERCENT = 2500; const MAX_NUMBER_RESERVES = 128; + const TOTAL_PREMIUM = 9; + const PREMIUM_TO_PROTOCOL = 3000; const POOL_ID = utils.formatBytes32String('POOL'); let snap: string; + let _mockFlashLoanReceiver = {} as MockFlashLoanReceiver; + beforeEach(async () => { snap = await evmSnapshot(); }); @@ -848,6 +862,164 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { expect(await configurator.initReserves(initInputParams)); }); + it('dropReserve(). Only allows to drop a reserve if both the aToken supply and accruedToTreasury are 0', async () => { + const { + configurator, + pool, + weth, + aWETH, + dai, + users: [user0], + } = testEnv; + + _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); + + await configurator.updateFlashloanPremiumTotal(TOTAL_PREMIUM); + await configurator.updateFlashloanPremiumToProtocol(PREMIUM_TO_PROTOCOL); + + const userAddress = user0.address; + const amountToDeposit = ethers.utils.parseEther('1'); + + await weth['mint(uint256)'](amountToDeposit); + + await weth.approve(pool.address, MAX_UINT_AMOUNT); + + await pool.deposit(weth.address, amountToDeposit, userAddress, '0'); + + const wethFlashBorrowedAmount = ethers.utils.parseEther('0.8'); + + await pool.flashLoan( + _mockFlashLoanReceiver.address, + [weth.address], + [wethFlashBorrowedAmount], + [0], + _mockFlashLoanReceiver.address, + '0x10', + '0' + ); + + await pool.connect(user0.signer).withdraw(weth.address, MAX_UINT_AMOUNT, userAddress); + + await expect( + configurator.dropReserve(weth.address), + 'dropReserve() should not be possible as there are funds' + ).to.be.revertedWith(UNDERLYING_CLAIMABLE_RIGHTS_NOT_ZERO); + + await pool.mintToTreasury([weth.address]); + + // Impersonate Collector + const collectorAddress = await aWETH.RESERVE_TREASURY_ADDRESS(); + await topUpNonPayableWithEther(user0.signer, [collectorAddress], utils.parseEther('1')); + await impersonateAccountsHardhat([collectorAddress]); + const collectorSigner = await hre.ethers.getSigner(collectorAddress); + await pool.connect(collectorSigner).withdraw(weth.address, MAX_UINT_AMOUNT, collectorAddress); + + await configurator.dropReserve(weth.address); + }); + + it('validateSupply(). Only allows to supply if amount + (scaled aToken supply + accruedToTreasury) <= supplyCap', async () => { + const { + configurator, + pool, + weth, + aWETH, + users: [user0], + } = testEnv; + + _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); + + await configurator.updateFlashloanPremiumTotal(TOTAL_PREMIUM); + await configurator.updateFlashloanPremiumToProtocol(PREMIUM_TO_PROTOCOL); + + const userAddress = user0.address; + const amountToDeposit = ethers.utils.parseEther('100000'); + + await weth['mint(uint256)'](amountToDeposit.add(ethers.utils.parseEther('30'))); + + await weth.approve(pool.address, MAX_UINT_AMOUNT); + + await pool.deposit(weth.address, amountToDeposit, userAddress, '0'); + + const wethFlashBorrowedAmount = ethers.utils.parseEther('100000'); + + await pool.flashLoan( + _mockFlashLoanReceiver.address, + [weth.address], + [wethFlashBorrowedAmount], + [0], + _mockFlashLoanReceiver.address, + '0x10', + '0' + ); + + // At this point the totalSupply + accruedToTreasury is ~100090 WETH, with 100063 from supply and ~27 from accruedToTreasury + // so to properly test the supply cap condition: + // - Set supply cap above that, at 110 WETH + // - Try to supply 30 WETH more . Should work if not taken into account accruedToTreasury, but will not + // - Try to supply 5 WETH more. Should work + + await configurator.setSupplyCap(weth.address, BigNumber.from('100000').add('110')); + + await expect( + pool.deposit(weth.address, ethers.utils.parseEther('30'), userAddress, '0') + ).to.be.revertedWith(SUPPLY_CAP_EXCEEDED); + + await pool.deposit(weth.address, ethers.utils.parseEther('5'), userAddress, '0'); + }); + + it('_checkNoSuppliers() (PoolConfigurator). Properly disables actions if aToken supply == 0, but accruedToTreasury != 0', async () => { + const { + configurator, + pool, + weth, + aWETH, + users: [user0], + } = testEnv; + + _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); + + await configurator.updateFlashloanPremiumTotal(TOTAL_PREMIUM); + await configurator.updateFlashloanPremiumToProtocol(PREMIUM_TO_PROTOCOL); + + const userAddress = user0.address; + const amountToDeposit = ethers.utils.parseEther('100000'); + + await weth['mint(uint256)'](amountToDeposit.add(ethers.utils.parseEther('30'))); + + await weth.approve(pool.address, MAX_UINT_AMOUNT); + + await pool.deposit(weth.address, amountToDeposit, userAddress, '0'); + + const wethFlashBorrowedAmount = ethers.utils.parseEther('100000'); + + await pool.flashLoan( + _mockFlashLoanReceiver.address, + [weth.address], + [wethFlashBorrowedAmount], + [0], + _mockFlashLoanReceiver.address, + '0x10', + '0' + ); + + await pool.connect(user0.signer).withdraw(weth.address, MAX_UINT_AMOUNT, userAddress); + + await expect(configurator.setReserveActive(weth.address, false)).to.be.revertedWith( + RESERVE_LIQUIDITY_NOT_ZERO + ); + + await pool.mintToTreasury([weth.address]); + + // Impersonate Collector + const collectorAddress = await aWETH.RESERVE_TREASURY_ADDRESS(); + await topUpNonPayableWithEther(user0.signer, [collectorAddress], utils.parseEther('1')); + await impersonateAccountsHardhat([collectorAddress]); + const collectorSigner = await hre.ethers.getSigner(collectorAddress); + await pool.connect(collectorSigner).withdraw(weth.address, MAX_UINT_AMOUNT, collectorAddress); + + await configurator.setReserveActive(weth.address, false); + }); + 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,