From 7e6565fed49802cf9e168772fc38e9d1ff934cc2 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Mon, 21 Jun 2021 08:13:20 +0200 Subject: [PATCH 1/6] test: multiple flashoborrow asset, test extensively fees to reserve, fees to protocol --- test-suites/test-aave/flashloan.spec.ts | 226 ++++++++++++++------ test-suites/test-aave/helpers/make-suite.ts | 4 + test-suites/test-amm/flashloan.spec.ts | 4 +- 3 files changed, 168 insertions(+), 66 deletions(-) diff --git a/test-suites/test-aave/flashloan.spec.ts b/test-suites/test-aave/flashloan.spec.ts index 91e162a5..4e2fcb23 100644 --- a/test-suites/test-aave/flashloan.spec.ts +++ b/test-suites/test-aave/flashloan.spec.ts @@ -27,12 +27,24 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { LP_BORROW_ALLOWANCE_NOT_ENOUGH, } = ProtocolErrors; + const TOTAL_PREMIUM = 9; + const PREMIUM_TO_PROTOCOL = 3; + const PREMIUM_TO_LP = TOTAL_PREMIUM - PREMIUM_TO_PROTOCOL; + before(async () => { _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); }); + it('Configurator sets total premium = 9 bps, premium to protocol = 3 bps', async () => { + const { configurator, pool } = testEnv; + await configurator.updateFlashloanPremiumTotal(TOTAL_PREMIUM); + await configurator.updateFlashloanPremiumToProtocol(PREMIUM_TO_PROTOCOL); + + expect(await pool.FLASHLOAN_PREMIUM_TOTAL()).to.be.equal(TOTAL_PREMIUM); + expect(await pool.FLASHLOAN_PREMIUM_TO_PROTOCOL()).to.be.equal(PREMIUM_TO_PROTOCOL); + }); it('Deposits WETH into the reserve', async () => { - const { pool, weth, aave } = testEnv; + const { pool, weth, aave, dai } = testEnv; const userAddress = await pool.signer.getAddress(); const amountToDeposit = ethers.utils.parseEther('1'); @@ -47,44 +59,102 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { await aave.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool.deposit(aave.address, amountToDeposit, userAddress, '0'); + await dai.mint(amountToDeposit); + + await dai.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool.deposit(dai.address, amountToDeposit, userAddress, '0'); }); - it('Takes WETH flash loan with mode = 0, returns the funds correctly', async () => { - const { pool, helpersContract, weth, aWETH } = testEnv; + it('Takes WETH + Dai flash loan with mode = 0, returns the funds correctly', async () => { + const { pool, helpersContract, weth, aWETH, dai, aDai } = testEnv; - const flashBorrowedAmount = ethers.utils.parseEther('0.8'); - const fees = new BigNumber(flashBorrowedAmount.mul(9).div(10000).toString()); + const wethFlashBorrowedAmount = ethers.utils.parseEther('0.8'); + const daiFlashBorrowedAmount = ethers.utils.parseEther('0.3'); + const wethTotalFees = new BigNumber( + wethFlashBorrowedAmount.mul(TOTAL_PREMIUM).div(10000).toString() + ); + const wethFeesToProtocol = wethFlashBorrowedAmount.mul(PREMIUM_TO_PROTOCOL).div(10000); + const wethFeesToLp = wethFlashBorrowedAmount.mul(PREMIUM_TO_LP).div(10000); + const daiTotalFees = new BigNumber( + daiFlashBorrowedAmount.mul(TOTAL_PREMIUM).div(10000).toString() + ); + const daiFeesToProtocol = daiFlashBorrowedAmount.mul(PREMIUM_TO_PROTOCOL).div(10000); + const daiFeesToLp = daiFlashBorrowedAmount.mul(PREMIUM_TO_LP).div(10000); - let reserveData = await helpersContract.getReserveData(weth.address); + const wethLiquidityIndexAdded = wethFeesToLp + .mul(ethers.BigNumber.from(10).pow(27)) + .div((await aWETH.totalSupply()).toString()); - const totalLiquidityBefore = new BigNumber(reserveData.availableLiquidity.toString()) - .plus(reserveData.totalStableDebt.toString()) - .plus(reserveData.totalVariableDebt.toString()); + const daiLiquidityIndexAdded = daiFeesToLp + .mul(ethers.BigNumber.from(10).pow(27)) + .div((await aDai.totalSupply()).toString()); + + let wethReserveData = await helpersContract.getReserveData(weth.address); + let daiReserveData = await helpersContract.getReserveData(dai.address); + + const wethLiquidityIndexBefore = wethReserveData.liquidityIndex; + const daiLiquidityIndexBefore = daiReserveData.liquidityIndex; + + const wethTotalLiquidityBefore = new BigNumber(wethReserveData.availableLiquidity.toString()) + .plus(wethReserveData.totalStableDebt.toString()) + .plus(wethReserveData.totalVariableDebt.toString()); + + const daiTotalLiquidityBefore = new BigNumber(daiReserveData.availableLiquidity.toString()) + .plus(daiReserveData.totalStableDebt.toString()) + .plus(daiReserveData.totalVariableDebt.toString()); + + const wethReservesBefore = await aWETH.balanceOf(await aWETH.RESERVE_TREASURY_ADDRESS()); + const daiReservesBefore = await aDai.balanceOf(await aDai.RESERVE_TREASURY_ADDRESS()); await pool.flashLoan( _mockFlashLoanReceiver.address, - [weth.address], - [flashBorrowedAmount], - [0], + [weth.address, dai.address], + [wethFlashBorrowedAmount, daiFlashBorrowedAmount], + [0, 0], _mockFlashLoanReceiver.address, '0x10', '0' ); - await pool.mintToTreasury([weth.address]); + await pool.mintToTreasury([weth.address, dai.address]); - reserveData = await helpersContract.getReserveData(weth.address); + wethReserveData = await helpersContract.getReserveData(weth.address); + daiReserveData = await helpersContract.getReserveData(dai.address); - const currentLiquidityRate = reserveData.liquidityRate; - const currentLiquidityIndex = reserveData.liquidityIndex; + const wethCurrentLiquidityRate = wethReserveData.liquidityRate; + const wethCurrentLiquidityIndex = wethReserveData.liquidityIndex; + const daiCurrentLiquidityRate = daiReserveData.liquidityRate; + const daiCurrentLiquidityIndex = daiReserveData.liquidityIndex; - const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) - .plus(reserveData.totalStableDebt.toString()) - .plus(reserveData.totalVariableDebt.toString()); + const wethTotalLiquidityAfter = new BigNumber(wethReserveData.availableLiquidity.toString()) + .plus(wethReserveData.totalStableDebt.toString()) + .plus(wethReserveData.totalVariableDebt.toString()); - expect(totalLiquidityBefore.plus(fees).toString()).to.be.equal(totalLiquidityAfter.toString()); - expect(currentLiquidityRate.toString()).to.be.equal('0'); - expect(currentLiquidityIndex.toString()).to.be.equal('1000720000000000000000000000'); + const daiTotalLiquidityAfter = new BigNumber(daiReserveData.availableLiquidity.toString()) + .plus(daiReserveData.totalStableDebt.toString()) + .plus(daiReserveData.totalVariableDebt.toString()); + + const wethReservesAfter = await aWETH.balanceOf(await aWETH.RESERVE_TREASURY_ADDRESS()); + const daiReservesAfter = await aDai.balanceOf(await aDai.RESERVE_TREASURY_ADDRESS()); + + expect(wethTotalLiquidityBefore.plus(wethTotalFees).toString()).to.be.equal( + wethTotalLiquidityAfter.toString() + ); + expect(wethCurrentLiquidityRate.toString()).to.be.equal('0'); + expect(wethCurrentLiquidityIndex.toString()).to.be.equal( + wethLiquidityIndexBefore.add(wethLiquidityIndexAdded.toString()).toString() + ); + expect(wethReservesAfter).to.be.equal(wethReservesBefore.add(wethFeesToProtocol)); + + expect(daiTotalLiquidityBefore.plus(daiTotalFees).toString()).to.be.equal( + daiTotalLiquidityAfter.toString() + ); + expect(daiCurrentLiquidityRate.toString()).to.be.equal('0'); + expect(daiCurrentLiquidityIndex.toString()).to.be.equal( + daiLiquidityIndexBefore.add(daiLiquidityIndexAdded.toString()).toString() + ); + expect(daiReservesAfter).to.be.equal(daiReservesBefore.add(daiFeesToProtocol)); }); it('Takes an authorized AAVE flash loan with mode = 0, returns the funds correctly', async () => { const { @@ -97,7 +167,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { await configurator.authorizeFlashBorrower(authorizedUser.address); const flashBorrowedAmount = ethers.utils.parseEther('0.8'); - const fees = new BigNumber(0); + const totalFees = new BigNumber(0); let reserveData = await helpersContract.getReserveData(aave.address); @@ -119,33 +189,43 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { await pool.mintToTreasury([aave.address]); - ethers.utils.parseUnits('10000'); - reserveData = await helpersContract.getReserveData(aave.address); const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); - expect(totalLiquidityBefore.plus(fees).toString()).to.be.equal(totalLiquidityAfter.toString()); + expect(totalLiquidityBefore.plus(totalFees).toString()).to.be.equal( + totalLiquidityAfter.toString() + ); }); it('Takes an ETH flashloan with mode = 0 as big as the available liquidity', async () => { - const { pool, helpersContract, weth } = testEnv; + const { pool, helpersContract, weth, aWETH } = testEnv; let reserveData = await helpersContract.getReserveData(weth.address); - const totalLiquidityBefore = new BigNumber(reserveData.availableLiquidity.toString()) - .plus(reserveData.totalStableDebt.toString()) - .plus(reserveData.totalVariableDebt.toString()); + const totalLiquidityBefore = reserveData.availableLiquidity + .add(reserveData.totalStableDebt) + .add(reserveData.totalVariableDebt); - const flashBorrowedAmount = totalLiquidityBefore.toString(); + const flashBorrowedAmount = totalLiquidityBefore; - const fees = new BigNumber(flashBorrowedAmount).multipliedBy(9).dividedBy(10000).toString(); + const totalFees = new BigNumber(flashBorrowedAmount.mul(TOTAL_PREMIUM).div(10000).toString()); + const feesToProtocol = flashBorrowedAmount.mul(PREMIUM_TO_PROTOCOL).div(10000); + const feesToLp = flashBorrowedAmount.mul(PREMIUM_TO_LP).div(10000); + const liquidityIndexBefore = reserveData.liquidityIndex; + const liquidityIndexAdded = feesToLp + .mul(ethers.BigNumber.from(10).pow(27)) + .div((await aWETH.totalSupply()).toString()) + .mul(liquidityIndexBefore) + .div(ethers.BigNumber.from(10).pow(27)); + + const reservesBefore = await aWETH.balanceOf(await aWETH.RESERVE_TREASURY_ADDRESS()); const txResult = await pool.flashLoan( _mockFlashLoanReceiver.address, [weth.address], - [totalLiquidityBefore.toString()], + [flashBorrowedAmount], [0], _mockFlashLoanReceiver.address, '0x10', @@ -156,22 +236,25 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { reserveData = await helpersContract.getReserveData(weth.address); + const currentLiquidityRate = reserveData.liquidityRate; + const currentLiquidityIndex = reserveData.liquidityIndex; + const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); - const currentLiqudityRate = reserveData.liquidityRate; - const currentLiquidityIndex = reserveData.liquidityIndex; - - const totalLiquidity = new BigNumber(reserveData.availableLiquidity.toString()) - .plus(reserveData.totalStableDebt.toString()) - .plus(reserveData.totalVariableDebt.toString()); - - expect(totalLiquidity.toString()).to.be.equal('1001620648000000000'); - expect(currentLiqudityRate.toString()).to.be.equal('0'); - expect(currentLiquidityIndex.toString()).to.be.equal('1001620648000000000000000000'); + const reservesAfter = await aWETH.balanceOf(await aWETH.RESERVE_TREASURY_ADDRESS()); + expect(new BigNumber(totalLiquidityBefore.toString()).plus(totalFees).toString()).to.be.equal( + totalLiquidityAfter.toString() + ); + expect(currentLiquidityRate.toString()).to.be.equal('0'); + expect(currentLiquidityIndex.toString()).to.be.equal( + liquidityIndexBefore.add(liquidityIndexAdded.toString()).toString() + ); + expect( + reservesAfter.sub(feesToProtocol).mul(liquidityIndexBefore).div(currentLiquidityIndex) + ).to.be.equal(reservesBefore); }); - it('Takes WETH flashloan, does not return the funds with mode = 0. (revert expected)', async () => { const { pool, weth, users } = testEnv; const caller = users[1]; @@ -337,46 +420,59 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { }); it('Takes out a 500 USDC flashloan, returns the funds correctly', async () => { - const { usdc, pool, helpersContract, deployer: depositor } = testEnv; + const { usdc, aUsdc, pool, helpersContract, deployer: depositor } = testEnv; await _mockFlashLoanReceiver.setFailExecutionTransfer(false); - const reserveDataBefore = await helpersContract.getReserveData(usdc.address); + const flashBorrowedAmount = await convertToCurrencyDecimals(usdc.address, '500'); + const totalFees = new BigNumber(flashBorrowedAmount.mul(TOTAL_PREMIUM).div(10000).toString()); + const feesToProtocol = flashBorrowedAmount.mul(PREMIUM_TO_PROTOCOL).div(10000); + const feesToLp = flashBorrowedAmount.mul(PREMIUM_TO_LP).div(10000); + const liquidityIndexAdded = feesToLp + .mul(ethers.BigNumber.from(10).pow(27)) + .div((await aUsdc.totalSupply()).toString()); - const flashloanAmount = await convertToCurrencyDecimals(usdc.address, '500'); + let reserveData = await helpersContract.getReserveData(usdc.address); + + const liquidityIndexBefore = reserveData.liquidityIndex; + + const totalLiquidityBefore = new BigNumber(reserveData.availableLiquidity.toString()) + .plus(reserveData.totalStableDebt.toString()) + .plus(reserveData.totalVariableDebt.toString()); + + const reservesBefore = await aUsdc.balanceOf(await aUsdc.RESERVE_TREASURY_ADDRESS()); await pool.flashLoan( _mockFlashLoanReceiver.address, [usdc.address], - [flashloanAmount], + [flashBorrowedAmount], [0], _mockFlashLoanReceiver.address, '0x10', '0' ); - const reserveDataAfter = helpersContract.getReserveData(usdc.address); + await pool.mintToTreasury([usdc.address]); - const reserveData = await helpersContract.getReserveData(usdc.address); - const userData = await helpersContract.getUserReserveData(usdc.address, depositor.address); + reserveData = await helpersContract.getReserveData(usdc.address); - const totalLiquidity = reserveData.availableLiquidity - .add(reserveData.totalStableDebt) - .add(reserveData.totalVariableDebt) - .toString(); - const currentLiqudityRate = reserveData.liquidityRate.toString(); - const currentLiquidityIndex = reserveData.liquidityIndex.toString(); - const currentUserBalance = userData.currentATokenBalance.toString(); + const currentLiquidityRate = reserveData.liquidityRate; + const currentLiquidityIndex = reserveData.liquidityIndex; - const expectedLiquidity = await convertToCurrencyDecimals(usdc.address, '1000.450'); + const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) + .plus(reserveData.totalStableDebt.toString()) + .plus(reserveData.totalVariableDebt.toString()); - expect(totalLiquidity).to.be.equal(expectedLiquidity, 'Invalid total liquidity'); - expect(currentLiqudityRate).to.be.equal('0', 'Invalid liquidity rate'); - expect(currentLiquidityIndex).to.be.equal( - new BigNumber('1.00045').multipliedBy(oneRay).toFixed(), - 'Invalid liquidity index' + const reservesAfter = await aUsdc.balanceOf(await aUsdc.RESERVE_TREASURY_ADDRESS()); + + expect(totalLiquidityBefore.plus(totalFees).toString()).to.be.equal( + totalLiquidityAfter.toString() ); - expect(currentUserBalance.toString()).to.be.equal(expectedLiquidity, 'Invalid user balance'); + expect(currentLiquidityRate.toString()).to.be.equal('0'); + expect(currentLiquidityIndex.toString()).to.be.equal( + liquidityIndexBefore.add(liquidityIndexAdded.toString()).toString() + ); + expect(reservesAfter).to.be.equal(reservesBefore.add(feesToProtocol)); }); it('Takes out a 500 USDC flashloan with mode = 0, does not return the funds. (revert expected)', async () => { diff --git a/test-suites/test-aave/helpers/make-suite.ts b/test-suites/test-aave/helpers/make-suite.ts index 490aeb0b..130635f1 100644 --- a/test-suites/test-aave/helpers/make-suite.ts +++ b/test-suites/test-aave/helpers/make-suite.ts @@ -63,6 +63,7 @@ export interface TestEnv { aWETH: AToken; dai: MintableERC20; aDai: AToken; + aUsdc: AToken; usdc: MintableERC20; aave: MintableERC20; addressesProvider: LendingPoolAddressesProvider; @@ -92,6 +93,7 @@ const testEnv: TestEnv = { aWETH: {} as AToken, dai: {} as MintableERC20, aDai: {} as AToken, + aUsdc: {} as AToken, usdc: {} as MintableERC20, aave: {} as MintableERC20, addressesProvider: {} as LendingPoolAddressesProvider, @@ -138,6 +140,7 @@ export async function initializeMakeSuite() { const allTokens = await testEnv.helpersContract.getAllATokens(); const aDaiAddress = allTokens.find((aToken) => aToken.symbol === 'aDAI')?.tokenAddress; + const aUsdcAddress = allTokens.find((aToken) => aToken.symbol === 'aUSDC')?.tokenAddress; const aWEthAddress = allTokens.find((aToken) => aToken.symbol === 'aWETH')?.tokenAddress; @@ -156,6 +159,7 @@ export async function initializeMakeSuite() { } testEnv.aDai = await getAToken(aDaiAddress); + testEnv.aUsdc = await getAToken(aUsdcAddress); testEnv.aWETH = await getAToken(aWEthAddress); testEnv.dai = await getMintableERC20(daiAddress); diff --git a/test-suites/test-amm/flashloan.spec.ts b/test-suites/test-amm/flashloan.spec.ts index 22e9ade2..c5281a0b 100644 --- a/test-suites/test-amm/flashloan.spec.ts +++ b/test-suites/test-amm/flashloan.spec.ts @@ -47,10 +47,12 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { it('Takes WETH flashloan with mode = 0, returns the funds correctly', async () => { const { pool, helpersContract, weth } = testEnv; + const borrowedAmount = ethers.utils.parseEther('0.8'); + await pool.flashLoan( _mockFlashLoanReceiver.address, [weth.address], - [ethers.utils.parseEther('0.8')], + [borrowedAmount], [0], _mockFlashLoanReceiver.address, '0x10', From 09233d09bf06b1f7b5662bca8d1714d28c48a568 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Mon, 21 Jun 2021 12:25:33 +0200 Subject: [PATCH 2/6] feat: added storage and check that repayer is not the last borrower of a reserve --- contracts/protocol/lendingpool/LendingPool.sol | 6 +++++- contracts/protocol/lendingpool/LendingPoolStorage.sol | 4 ++++ contracts/protocol/libraries/logic/ValidationLogic.sol | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index c88aecad..2396d8f5 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -240,7 +240,6 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ///@inheritdoc ILendingPool function rebalanceStableBorrowRate(address asset, address user) external override whenNotPaused { - DataTypes.ReserveData storage reserve = _reserves[asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); @@ -793,6 +792,9 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage vars.releaseUnderlying ? vars.amount : 0 ); + _lastBorrower = vars.user; + _lastBorrowTimestamp = uint40(block.timestamp); + if (vars.releaseUnderlying) { IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount); } @@ -908,6 +910,8 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage DataTypes.InterestRateMode interestRateMode = DataTypes.InterestRateMode(rateMode); ValidationLogic.validateRepay( + _lastBorrower, + _lastBorrowTimestamp, reserveCache, amount, interestRateMode, diff --git a/contracts/protocol/lendingpool/LendingPoolStorage.sol b/contracts/protocol/lendingpool/LendingPoolStorage.sol index b8516bbf..6289e008 100644 --- a/contracts/protocol/lendingpool/LendingPoolStorage.sol +++ b/contracts/protocol/lendingpool/LendingPoolStorage.sol @@ -33,4 +33,8 @@ contract LendingPoolStorage { mapping(address => bool) _authorizedFlashBorrowers; uint256 internal _flashLoanPremiumToProtocol; + + address internal _lastBorrower; + + uint40 internal _lastBorrowTimestamp; } diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 7f64887a..8cb6717e 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -255,6 +255,8 @@ library ValidationLogic { * @param variableDebt The borrow balance of the user */ function validateRepay( + address lastBorrower, + uint40 lastBorrowTimestamp, DataTypes.ReserveCache memory reserveCache, uint256 amountSent, DataTypes.InterestRateMode rateMode, @@ -268,6 +270,8 @@ library ValidationLogic { require(amountSent > 0, Errors.VL_INVALID_AMOUNT); + require(lastBorrower != onBehalfOf || lastBorrowTimestamp != uint40(block.timestamp)); + require( (stableDebt > 0 && DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE) || @@ -347,7 +351,6 @@ library ValidationLogic { IERC20 variableDebtToken, address aTokenAddress ) external view { - // to avoid potential abuses using flashloans, the rebalance stable rate must happen through an EOA require(!address(msg.sender).isContract(), Errors.LP_CALLER_NOT_EOA); From a0a185fbf6c8dd9b56f53884a5b52112d21dfe1b Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Mon, 21 Jun 2021 12:58:20 +0200 Subject: [PATCH 3/6] feat: added new Errors.VL_SAME_BLOCK_BORROW --- contracts/protocol/libraries/helpers/Errors.sol | 1 + contracts/protocol/libraries/logic/ValidationLogic.sol | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index e0fea7bb..66d8b8c0 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -113,6 +113,7 @@ library Errors { string public constant RL_STABLE_DEBT_NOT_ZERO = '89'; string public constant RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90'; string public constant LP_CALLER_NOT_EOA = '91'; + string public constant VL_SAME_BLOCK_BORROW = '94'; enum CollateralManagerErrors { NO_ERROR, diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 8cb6717e..56a3c777 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -270,7 +270,10 @@ library ValidationLogic { require(amountSent > 0, Errors.VL_INVALID_AMOUNT); - require(lastBorrower != onBehalfOf || lastBorrowTimestamp != uint40(block.timestamp)); + require( + lastBorrower != onBehalfOf || lastBorrowTimestamp != uint40(block.timestamp), + Errors.VL_SAME_BLOCK_BORROW + ); require( (stableDebt > 0 && From 22bbb962619c977c0e2f82b26109075f5ae16775 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Tue, 22 Jun 2021 12:25:13 +0200 Subject: [PATCH 4/6] test: added requirements in update premium functions of configurator --- .../lendingpool/LendingPoolConfigurator.sol | 16 ++++++++++++++++ contracts/protocol/libraries/helpers/Errors.sol | 2 ++ 2 files changed, 18 insertions(+) diff --git a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol index 3c699179..f8b730ee 100644 --- a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol +++ b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol @@ -514,6 +514,14 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur override onlyPoolAdmin { + require( + flashloanPremiumTotal < PercentageMath.PERCENTAGE_FACTOR, + Errors.LPC_FLASHLOAN_PREMIUM_INVALID + ); + require( + flashloanPremiumTotal >= _pool.FLASHLOAN_PREMIUM_TO_PROTOCOL(), + Errors.LPC_FLASHLOAN_PREMIUMS_MISMATCH + ); _pool.updateFlashloanPremiums(flashloanPremiumTotal, _pool.FLASHLOAN_PREMIUM_TO_PROTOCOL()); emit FlashloanPremiumTotalUpdated(flashloanPremiumTotal); } @@ -524,6 +532,14 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur override onlyPoolAdmin { + require( + flashloanPremiumToProtocol < PercentageMath.PERCENTAGE_FACTOR, + Errors.LPC_FLASHLOAN_PREMIUM_INVALID + ); + require( + flashloanPremiumToProtocol <= _pool.FLASHLOAN_PREMIUM_TOTAL(), + Errors.LPC_FLASHLOAN_PREMIUMS_MISMATCH + ); _pool.updateFlashloanPremiums(_pool.FLASHLOAN_PREMIUM_TOTAL(), flashloanPremiumToProtocol); emit FlashloanPremiumToProcolUpdated(flashloanPremiumToProtocol); } diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index e0fea7bb..8f576c40 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -113,6 +113,8 @@ library Errors { string public constant RL_STABLE_DEBT_NOT_ZERO = '89'; string public constant RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90'; string public constant LP_CALLER_NOT_EOA = '91'; + string public constant LPC_FLASHLOAN_PREMIUMS_MISMATCH = '95'; + string public constant LPC_FLASHLOAN_PREMIUM_INVALID = '96'; enum CollateralManagerErrors { NO_ERROR, From 694eb7731c07001debecd54502bc128a22188a9c Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Tue, 22 Jun 2021 12:40:12 +0200 Subject: [PATCH 5/6] test: configurator tested against update premium functions --- helpers/types.ts | 2 + test-suites/test-aave/configurator.spec.ts | 59 ++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/helpers/types.ts b/helpers/types.ts index 157f34ba..360050e5 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -187,6 +187,8 @@ export enum ProtocolErrors { RL_ATOKEN_SUPPLY_NOT_ZERO = '88', RL_STABLE_DEBT_NOT_ZERO = '89', RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90', + LPC_FLASHLOAN_PREMIUMS_MISMATCH = '95', + LPC_FLASHLOAN_PREMIUM_INVALID = '96', // old diff --git a/test-suites/test-aave/configurator.spec.ts b/test-suites/test-aave/configurator.spec.ts index 7a49e625..ecbcb0b0 100644 --- a/test-suites/test-aave/configurator.spec.ts +++ b/test-suites/test-aave/configurator.spec.ts @@ -26,6 +26,8 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { LPC_CALLER_NOT_EMERGENCY_OR_POOL_ADMIN, LPC_CALLER_NOT_RISK_OR_POOL_ADMIN, VL_RESERVE_PAUSED, + LPC_FLASHLOAN_PREMIUMS_MISMATCH, + LPC_FLASHLOAN_PREMIUM_INVALID, } = ProtocolErrors; it('Reverts trying to set an invalid reserve factor', async () => { @@ -1303,4 +1305,61 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { CALLER_NOT_POOL_ADMIN ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); }); + it('Update flash loan premiums: 10 toProtocol, 40 total', async () => { + const { dai, pool, configurator, users } = testEnv; + const newPremiumTotal = 40; + const newPremiumToProtocol = 10; + + await configurator.updateFlashloanPremiumTotal(newPremiumTotal); + await configurator.updateFlashloanPremiumToProtocol(newPremiumToProtocol); + + expect(await pool.FLASHLOAN_PREMIUM_TOTAL()).to.be.eq(newPremiumTotal); + expect(await pool.FLASHLOAN_PREMIUM_TO_PROTOCOL()).to.be.eq(newPremiumToProtocol); + }); + it('Fails to update flahloan premiums with toProtocol > total', async () => { + const { dai, pool, configurator, users } = testEnv; + const newPremiumTotal = 9; + const newPremiumToProtocol = 41; + + await expect(configurator.updateFlashloanPremiumTotal(newPremiumTotal)).to.be.revertedWith( + LPC_FLASHLOAN_PREMIUMS_MISMATCH + ); + await expect( + configurator.updateFlashloanPremiumToProtocol(newPremiumToProtocol) + ).to.be.revertedWith(LPC_FLASHLOAN_PREMIUMS_MISMATCH); + }); + it('Fails to update flahloan premiums > 100%', async () => { + const { dai, pool, configurator, users } = testEnv; + const newPremiumTotal = 10100; + const newPremiumToProtocol = 10100; + + await expect(configurator.updateFlashloanPremiumTotal(newPremiumTotal)).to.be.revertedWith( + LPC_FLASHLOAN_PREMIUM_INVALID + ); + await expect( + configurator.updateFlashloanPremiumToProtocol(newPremiumToProtocol) + ).to.be.revertedWith(LPC_FLASHLOAN_PREMIUM_INVALID); + }); + it('Checks only pool admin can update flashloan premiums', async () => { + const { dai, pool, configurator, users, riskAdmin, emergencyAdmin } = testEnv; + await expect( + configurator.connect(riskAdmin.signer).updateFlashloanPremiumToProtocol(50), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + + await expect( + configurator.connect(riskAdmin.signer).updateFlashloanPremiumTotal(50), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + + await expect( + configurator.connect(emergencyAdmin.signer).updateFlashloanPremiumToProtocol(50), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + + await expect( + configurator.connect(emergencyAdmin.signer).updateFlashloanPremiumTotal(50), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + }); }); From d52046dbbc5973992248c1f93348fe25265bf653 Mon Sep 17 00:00:00 2001 From: The3D Date: Tue, 22 Jun 2021 18:01:13 +0200 Subject: [PATCH 6/6] test: updated tests for the borrow/repay validation --- contracts/mocks/tests/BorrowRepayTestMock.sol | 51 ++++++++++++++++++ .../protocol/libraries/helpers/Errors.sol | 2 +- .../libraries/logic/ValidationLogic.sol | 2 +- helpers/types.ts | 2 +- test-suites/test-aave/borrow-repay-same-tx.ts | 52 +++++++++++++++++++ 5 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 contracts/mocks/tests/BorrowRepayTestMock.sol create mode 100644 test-suites/test-aave/borrow-repay-same-tx.ts diff --git a/contracts/mocks/tests/BorrowRepayTestMock.sol b/contracts/mocks/tests/BorrowRepayTestMock.sol new file mode 100644 index 00000000..dda8a428 --- /dev/null +++ b/contracts/mocks/tests/BorrowRepayTestMock.sol @@ -0,0 +1,51 @@ +pragma solidity 0.6.12; + +import {ILendingPool} from '../../interfaces/ILendingPool.sol'; +import {MintableERC20} from '../tokens/MintableERC20.sol'; + +contract BorrowRepayTestMock { + ILendingPool _pool; + address _weth; + address _dai; + + constructor(ILendingPool pool, address weth, address dai) public { + _pool = pool; + _weth = weth; + _dai = dai; + } + + function executeBorrowRepayVariable() external { + //mints 1 eth + MintableERC20(_weth).mint(1e18); + //deposits weth in the protocol + MintableERC20(_weth).approve(address(_pool),type(uint256).max); + _pool.deposit(_weth, 1e18, address(this),0); + //borrow 1 wei of weth at variable + _pool.borrow(_weth, 1, 2, 0, address(this)); + //repay 1 wei of weth (expected to fail) + _pool.repay(_weth, 1, 2, address(this)); + } + + function executeBorrowRepayStable() external { + //mints 1 eth + MintableERC20(_weth).mint(1e18); + //mints 1 dai + MintableERC20(_dai).mint(1e18); + //deposits weth in the protocol + MintableERC20(_weth).approve(address(_pool),type(uint256).max); + _pool.deposit(_weth, 1e18, address(this),0); + + //deposits dai in the protocol + MintableERC20(_dai).approve(address(_pool),type(uint256).max); + _pool.deposit(_dai, 1e18, address(this),0); + + //disabling dai as collateral so it can be borrowed at stable + _pool.setUserUseReserveAsCollateral(_dai, false); + //borrow 1 wei of dai at stable + _pool.borrow(_dai, 1, 1, 0, address(this)); + + //repay 1 wei of dai (expected to fail) + _pool.repay(_dai, 1, 1, address(this)); + } + +} diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 66d8b8c0..d9a11b7c 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -113,7 +113,7 @@ library Errors { string public constant RL_STABLE_DEBT_NOT_ZERO = '89'; string public constant RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90'; string public constant LP_CALLER_NOT_EOA = '91'; - string public constant VL_SAME_BLOCK_BORROW = '94'; + string public constant VL_SAME_BLOCK_BORROW_REPAY = '94'; enum CollateralManagerErrors { NO_ERROR, diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 56a3c777..9c7ea35a 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -272,7 +272,7 @@ library ValidationLogic { require( lastBorrower != onBehalfOf || lastBorrowTimestamp != uint40(block.timestamp), - Errors.VL_SAME_BLOCK_BORROW + Errors.VL_SAME_BLOCK_BORROW_REPAY ); require( diff --git a/helpers/types.ts b/helpers/types.ts index 157f34ba..3c7af8c3 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -187,7 +187,7 @@ export enum ProtocolErrors { RL_ATOKEN_SUPPLY_NOT_ZERO = '88', RL_STABLE_DEBT_NOT_ZERO = '89', RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90', - + VL_SAME_BLOCK_BORROW_REPAY = '94', // old INVALID_FROM_BALANCE_AFTER_TRANSFER = 'Invalid from balance after transfer', diff --git a/test-suites/test-aave/borrow-repay-same-tx.ts b/test-suites/test-aave/borrow-repay-same-tx.ts new file mode 100644 index 00000000..d1fed4f0 --- /dev/null +++ b/test-suites/test-aave/borrow-repay-same-tx.ts @@ -0,0 +1,52 @@ +import { TestEnv, makeSuite } from './helpers/make-suite'; +import { + APPROVAL_AMOUNT_LENDING_POOL, + MAX_UINT_AMOUNT, + RAY, + MAX_BORROW_CAP, + MAX_SUPPLY_CAP, +} from '../../helpers/constants'; +import { ProtocolErrors } from '../../helpers/types'; +import { + BorrowRepayTestMock, + BorrowRepayTestMockFactory, + MintableERC20, + WETH9, + WETH9Mocked, +} from '../../types'; +import { parseEther } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { waitForTx } from '../../helpers/misc-utils'; +import { getFirstSigner } from '../../helpers/contracts-getters'; + +const { expect } = require('chai'); + +makeSuite('Borrow/repay in the same tx', (testEnv: TestEnv) => { + const { VL_SAME_BLOCK_BORROW_REPAY } = ProtocolErrors; + const unitParse = async (token: WETH9Mocked | MintableERC20, nb: string) => + BigNumber.from(nb).mul(BigNumber.from('10').pow((await token.decimals()) - 3)); + + let testContract: BorrowRepayTestMock; + + it('Deploys the test contract', async () => { + const { weth, dai, pool } = testEnv; + + testContract = await ( + await new BorrowRepayTestMockFactory(await getFirstSigner()) + ).deploy(pool.address, weth.address, dai.address); + }); + + it('Executes a test borrow/repay in the same transaction at variable (revert expected)', async () => { + await expect(testContract.executeBorrowRepayVariable()).to.be.revertedWith( + VL_SAME_BLOCK_BORROW_REPAY, + 'Borrow/repay in the same transaction did not revert as expected' + ); + }); + + it('Executes a test borrow/repay in the same transaction at stabke (revert expected)', async () => { + await expect(testContract.executeBorrowRepayStable()).to.be.revertedWith( + VL_SAME_BLOCK_BORROW_REPAY, + 'Borrow/repay in the same transaction did not revert as expected' + ); + }); +});