From 9e91a955dc59520015c3b6953e48f2a08d8d8344 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 6 Feb 2023 17:16:20 +0100 Subject: [PATCH 01/11] test(integration): improve integration test setup --- test/helpers/ForkTest.sol | 1 + test/helpers/IntegrationTest.sol | 40 +++++++++++++++++++------------- test/helpers/TestMarketLib.sol | 24 +++++++++++++++---- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/test/helpers/ForkTest.sol b/test/helpers/ForkTest.sol index 861c45651..43874ede0 100644 --- a/test/helpers/ForkTest.sol +++ b/test/helpers/ForkTest.sol @@ -15,6 +15,7 @@ import {Events} from "src/libraries/Events.sol"; import {Errors} from "src/libraries/Errors.sol"; import {TestConfig, TestConfigLib} from "test/helpers/TestConfigLib.sol"; import {DataTypes} from "@aave-v3-core/protocol/libraries/types/DataTypes.sol"; +import {Errors as AaveErrors} from "@aave-v3-core/protocol/libraries/helpers/Errors.sol"; import {AaveOracleMock} from "test/mocks/AaveOracleMock.sol"; import {PoolAdminMock} from "test/mocks/PoolAdminMock.sol"; diff --git a/test/helpers/IntegrationTest.sol b/test/helpers/IntegrationTest.sol index 5e9e5cdfd..72ea9ff30 100644 --- a/test/helpers/IntegrationTest.sol +++ b/test/helpers/IntegrationTest.sol @@ -22,9 +22,12 @@ contract IntegrationTest is ForkTest { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using TestMarketLib for TestMarket; + uint8 internal constant E_MODE = 0; uint256 internal constant INITIAL_BALANCE = 10_000_000_000 ether; - uint256 internal constant MIN_USD_AMOUNT = 0.0001e8; // AaveV3 base currency is USD, 8 decimals on all L2s. - uint256 internal constant MAX_USD_AMOUNT = 500_000_000e8; // AaveV3 base currency is USD, 8 decimals on all L2s. + + // AaveV3 base currency is USD, 8 decimals on all L2s. + uint256 internal constant MIN_USD_AMOUNT = 0.01e8; // 0.01$ + uint256 internal constant MAX_USD_AMOUNT = 500_000_000e8; // 500m$ IMorpho internal morpho; IPositionsManager internal positionsManager; @@ -42,6 +45,7 @@ contract IntegrationTest is ForkTest { mapping(address => TestMarket) internal testMarkets; address[] internal underlyings; + address[] internal collateralUnderlyings; address[] internal borrowableUnderlyings; function setUp() public virtual override { @@ -80,8 +84,8 @@ contract IntegrationTest is ForkTest { } function _deploy() internal { - positionsManager = new PositionsManager(address(addressesProvider), 0); - morphoImpl = new Morpho(address(addressesProvider), 0); + positionsManager = new PositionsManager(address(addressesProvider), E_MODE); + morphoImpl = new Morpho(address(addressesProvider), E_MODE); proxyAdmin = new ProxyAdmin(); morphoProxy = new TransparentUpgradeableProxy(payable(address(morphoImpl)), address(proxyAdmin), ""); @@ -124,6 +128,8 @@ contract IntegrationTest is ForkTest { market.price = oracle.getAssetPrice(underlying); // Price is constant, equal to price at fork block number. (market.ltv, market.lt, market.liquidationBonus, market.decimals,,) = reserve.configuration.getParams(); + market.isBorrowable = reserve.configuration.getBorrowingEnabled() && !reserve.configuration.getSiloedBorrowing() + && !reserve.configuration.getBorrowableInIsolation() && market.borrowGap() > 0; market.minAmount = (MIN_USD_AMOUNT * 10 ** market.decimals) / market.price; market.maxAmount = (MAX_USD_AMOUNT * 10 ** market.decimals) / market.price; @@ -135,7 +141,8 @@ contract IntegrationTest is ForkTest { market.borrowCap = type(uint256).max; vm.label(reserve.aTokenAddress, string.concat("a", market.symbol)); - vm.label(reserve.variableDebtTokenAddress, string.concat("d", market.symbol)); + vm.label(reserve.variableDebtTokenAddress, string.concat("vd", market.symbol)); + vm.label(reserve.stableDebtTokenAddress, string.concat("sd", market.symbol)); } function _createMarket(address underlying, uint16 reserveFactor, uint16 p2pIndexCursor) internal { @@ -143,10 +150,10 @@ contract IntegrationTest is ForkTest { _initMarket(underlying, reserveFactor, p2pIndexCursor); underlyings.push(underlying); - if ( - market.ltv > 0 && reserve.configuration.getBorrowingEnabled() && !reserve.configuration.getSiloedBorrowing() - && !reserve.configuration.getBorrowableInIsolation() && market.borrowGap() > 0 - ) borrowableUnderlyings.push(underlying); + if (market.ltv > 0 && reserve.configuration.getEModeCategory() == E_MODE) { + collateralUnderlyings.push(underlying); + } + if (market.isBorrowable) borrowableUnderlyings.push(underlying); morpho.createMarket(market.underlying, market.reserveFactor, market.p2pIndexCursor); } @@ -179,7 +186,8 @@ contract IntegrationTest is ForkTest { return bound(amount, market.minAmount, Math.min(market.maxAmount, market.supplyGap())); } - /// @dev Bounds the input between 0 and the maximum borrowable quantity, without exceeding the market's liquidity nor its borrow cap. + /// @dev Bounds the input between the minimum USD amount expected in tests + /// and the maximum borrowable quantity, without exceeding the market's liquidity nor its borrow cap. function _boundBorrow(TestMarket storage market, uint256 amount) internal view returns (uint256) { return bound( amount, market.minAmount, Math.min(market.maxAmount, Math.min(market.liquidity(), market.borrowGap())) @@ -200,7 +208,7 @@ contract IntegrationTest is ForkTest { vm.prank(borrower); borrowed = morpho.borrow(market.underlying, amount, onBehalf, receiver, maxIterations); - _deposit(dai, testMarkets[dai].minCollateral(market, borrowed), address(morpho)); // Make Morpho solvent with default collateral market, having no supply cap. + _deposit(dai, testMarkets[dai].minBorrowCollateral(market, borrowed), address(morpho)); // Make Morpho able to borrow again with dai collateral. oracle.setAssetPrice(market.underlying, market.price); } @@ -216,7 +224,7 @@ contract IntegrationTest is ForkTest { try promoter.borrow(market.underlying, borrowed) { market.resetPreviousIndex(address(morpho)); // Enable borrow/repay in same block. - _deposit(dai, testMarkets[dai].minCollateral(market, borrowed), address(morpho)); // Make Morpho solvent with default collateral market, having no supply cap. + _deposit(dai, testMarkets[dai].minBorrowCollateral(market, borrowed), address(morpho)); // Make Morpho able to borrow again with dai collateral. } catch { borrowed = 0; } @@ -250,8 +258,8 @@ contract IntegrationTest is ForkTest { // Set the supply cap as exceeded. _setSupplyCap(market, market.totalSupply()); - user.approve(market.underlying, amount); - user.repay(market.underlying, amount, onBehalf); + hacker.approve(market.underlying, amount); + hacker.repay(market.underlying, amount, onBehalf); return amount; } @@ -271,8 +279,8 @@ contract IntegrationTest is ForkTest { // Set the max iterations to 0 upon repay to skip demotion and fallback to supply delta. morpho.setDefaultMaxIterations(Types.MaxIterations({repay: 0, withdraw: 10})); - user.approve(market.underlying, amount); - user.repay(market.underlying, amount, onBehalf); + hacker.approve(market.underlying, amount); + hacker.repay(market.underlying, amount, onBehalf); return amount; } diff --git a/test/helpers/TestMarketLib.sol b/test/helpers/TestMarketLib.sol index 273960ecc..f33e611e5 100644 --- a/test/helpers/TestMarketLib.sol +++ b/test/helpers/TestMarketLib.sol @@ -27,6 +27,8 @@ struct TestMarket { uint256 price; uint256 minAmount; uint256 maxAmount; + // + bool isBorrowable; } library TestMarketLib { @@ -101,26 +103,40 @@ library TestMarketLib { return market.borrowCap.zeroFloorSub(totalBorrow(market)); } - /// @dev Calculates the maximum borrowable quantity collateralized by the given quantity of collateral. + /// @dev Calculates a lower bound of the maximum borrowable quantity collateralized by the given quantity of collateral. function borrowable(TestMarket storage borrowedMarket, TestMarket storage collateralMarket, uint256 collateral) internal view returns (uint256) { return ( - (collateral * collateralMarket.price * 10 ** borrowedMarket.decimals).percentMul(collateralMarket.ltv - 1) + (collateral * collateralMarket.price * 10 ** borrowedMarket.decimals).percentMul(collateralMarket.ltv) / (borrowedMarket.price * 10 ** collateralMarket.decimals) ); } - /// @dev Calculates the minimum collateral quantity necessary to collateralize the given quantity of debt. + /// @dev Calculates an upper bound of the minimum collateral quantity necessary + /// to collateralize the given quantity of debt and still be able to borrow. + function minBorrowCollateral(TestMarket storage collateralMarket, TestMarket storage borrowedMarket, uint256 amount) + internal + view + returns (uint256) + { + return ( + (amount * borrowedMarket.price * 10 ** collateralMarket.decimals).percentDiv(collateralMarket.ltv) + / (collateralMarket.price * 10 ** borrowedMarket.decimals) + ); + } + + /// @dev Calculates the minimum collateral quantity necessary to collateralize the given quantity of debt, + /// without necessarily being able to borrow more. function minCollateral(TestMarket storage collateralMarket, TestMarket storage borrowedMarket, uint256 amount) internal view returns (uint256) { return ( - (amount * borrowedMarket.price * 10 ** collateralMarket.decimals).percentDiv(collateralMarket.ltv - 1) + (amount * borrowedMarket.price * 10 ** collateralMarket.decimals).percentDiv(collateralMarket.lt) / (collateralMarket.price * 10 ** borrowedMarket.decimals) ); } From ec26fdb543b8f239b025d6fda8bea0797f9f67da Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 6 Feb 2023 17:16:39 +0100 Subject: [PATCH 02/11] ci: remove redundant caching --- .github/actions/forge-test/action.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/actions/forge-test/action.yml b/.github/actions/forge-test/action.yml index e2c81e860..afcaf7f69 100644 --- a/.github/actions/forge-test/action.yml +++ b/.github/actions/forge-test/action.yml @@ -30,12 +30,6 @@ runs: with: version: nightly - - name: Foundry fork cache - uses: actions/cache@v3 - with: - path: ~/.foundry/cache - key: foundry-${{ hashFiles(format('config/{0}.json', inputs.network)) }} # where fork block numbers & RPC are stored - - name: Foundry compilation cache uses: actions/cache@v3 with: From b60f765a51fa5d051d0bcd80df3cfdcef8efeb9c Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 6 Feb 2023 17:25:38 +0100 Subject: [PATCH 03/11] test(getters): remove unnecessary unit test --- test/internal/TestMorphoGetters.sol | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 test/internal/TestMorphoGetters.sol diff --git a/test/internal/TestMorphoGetters.sol b/test/internal/TestMorphoGetters.sol deleted file mode 100644 index 93e8f80b4..000000000 --- a/test/internal/TestMorphoGetters.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.0; - -import {MorphoStorage} from "src/MorphoStorage.sol"; -import {MorphoGetters} from "src/MorphoGetters.sol"; - -import "test/helpers/InternalTest.sol"; - -contract TestInternalMorphoGetters is InternalTest, MorphoGetters { - function testIsManaging(address owner, address manager, bool isAllowed) public { - _approveManager(owner, manager, isAllowed); - assertEq(this.isManaging(owner, manager), isAllowed); - } -} From 5e8e3b60e42a88a538ab142fbd7b9ccd74650135 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 6 Feb 2023 17:38:58 +0100 Subject: [PATCH 04/11] ci(test): decrease fuzz runs --- .github/actions/forge-test/action.yml | 4 ++-- Makefile | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/forge-test/action.yml b/.github/actions/forge-test/action.yml index afcaf7f69..4b0149502 100644 --- a/.github/actions/forge-test/action.yml +++ b/.github/actions/forge-test/action.yml @@ -43,7 +43,7 @@ runs: shell: bash env: NETWORK: ${{ inputs.network }} - FOUNDRY_FUZZ_RUNS: 32768 + FOUNDRY_FUZZ_RUNS: 16384 - name: Run internal tests run: make test-internal @@ -57,7 +57,7 @@ runs: shell: bash env: NETWORK: ${{ inputs.network }} - FOUNDRY_FUZZ_RUNS: 128 + FOUNDRY_FUZZ_RUNS: 64 - name: Generate lcov coverage report if: ${{ inputs.codecovToken != '' }} diff --git a/Makefile b/Makefile index 96cc1b9f9..397c5ffb1 100644 --- a/Makefile +++ b/Makefile @@ -28,16 +28,16 @@ test-integration: test-%: - @FOUNDRY_MATCH_TEST=$* make test + @FOUNDRY_MATCH_TEST=$* FOUNDRY_FUZZ_RUNS=32 make test test-unit-%: - @FOUNDRY_MATCH_TEST=$* make test-unit + @FOUNDRY_MATCH_TEST=$* FOUNDRY_FUZZ_RUNS=32 make test-unit test-internal-%: - @FOUNDRY_MATCH_TEST=$* make test-internal + @FOUNDRY_MATCH_TEST=$* FOUNDRY_FUZZ_RUNS=32 make test-internal test-integration-%: - @FOUNDRY_MATCH_TEST=$* make test-integration + @FOUNDRY_MATCH_TEST=$* FOUNDRY_FUZZ_RUNS=32 make test-integration coverage: From eb50de6462e9a2dde123ef233e92510eddc5e6ae Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 6 Feb 2023 17:50:26 +0100 Subject: [PATCH 05/11] chore(makefile): remove overwhelming fuzz runs --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 397c5ffb1..96cc1b9f9 100644 --- a/Makefile +++ b/Makefile @@ -28,16 +28,16 @@ test-integration: test-%: - @FOUNDRY_MATCH_TEST=$* FOUNDRY_FUZZ_RUNS=32 make test + @FOUNDRY_MATCH_TEST=$* make test test-unit-%: - @FOUNDRY_MATCH_TEST=$* FOUNDRY_FUZZ_RUNS=32 make test-unit + @FOUNDRY_MATCH_TEST=$* make test-unit test-internal-%: - @FOUNDRY_MATCH_TEST=$* FOUNDRY_FUZZ_RUNS=32 make test-internal + @FOUNDRY_MATCH_TEST=$* make test-internal test-integration-%: - @FOUNDRY_MATCH_TEST=$* FOUNDRY_FUZZ_RUNS=32 make test-integration + @FOUNDRY_MATCH_TEST=$* make test-integration coverage: From 0a18c8e05de2caa0b358cb9deec4bedb12270682 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 7 Feb 2023 10:32:00 +0100 Subject: [PATCH 06/11] build(script): add output files --- Makefile | 2 +- foundry.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 96cc1b9f9..4dc764561 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ install: forge install contracts: - FOUNDRY_TEST=/dev/null forge build --via-ir --sizes --force + FOUNDRY_TEST=/dev/null forge build --via-ir --sizes test: diff --git a/foundry.toml b/foundry.toml index 4d973208f..d77dafa7f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,7 @@ names = true sizes = true libs = ["node_modules", "lib"] fs_permissions = [{ access = "read", path = "./config/"}] +extra_output_files = ["ir", "irOptimized"] [fuzz] runs = 8 From ca125930a01366615f33b1c86fb802945d194561 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 7 Feb 2023 10:49:51 +0100 Subject: [PATCH 07/11] test(internal): add oracle price sentinel to internal tests --- Makefile | 2 +- foundry.toml | 1 - test/helpers/ForkTest.sol | 31 ++++++++++++------- test/helpers/TestMarketLib.sol | 19 ++++++------ .../internal/TestPositionsManagerInternal.sol | 15 +-------- 5 files changed, 31 insertions(+), 37 deletions(-) diff --git a/Makefile b/Makefile index 4dc764561..ee2568922 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ install: forge install contracts: - FOUNDRY_TEST=/dev/null forge build --via-ir --sizes + FOUNDRY_TEST=/dev/null forge build --via-ir --extra-output-files irOptimized --sizes --force test: diff --git a/foundry.toml b/foundry.toml index d77dafa7f..4d973208f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,6 @@ names = true sizes = true libs = ["node_modules", "lib"] fs_permissions = [{ access = "read", path = "./config/"}] -extra_output_files = ["ir", "irOptimized"] [fuzz] runs = 8 diff --git a/test/helpers/ForkTest.sol b/test/helpers/ForkTest.sol index 43874ede0..8b05a7018 100644 --- a/test/helpers/ForkTest.sol +++ b/test/helpers/ForkTest.sol @@ -17,6 +17,7 @@ import {TestConfig, TestConfigLib} from "test/helpers/TestConfigLib.sol"; import {DataTypes} from "@aave-v3-core/protocol/libraries/types/DataTypes.sol"; import {Errors as AaveErrors} from "@aave-v3-core/protocol/libraries/helpers/Errors.sol"; +import {PriceOracleSentinelMock} from "test/mocks/PriceOracleSentinelMock.sol"; import {AaveOracleMock} from "test/mocks/AaveOracleMock.sol"; import {PoolAdminMock} from "test/mocks/PoolAdminMock.sol"; import "./BaseTest.sol"; @@ -54,6 +55,7 @@ contract ForkTest is BaseTest { address internal aclAdmin; AaveOracleMock internal oracle; PoolAdminMock internal poolAdmin; + PriceOracleSentinelMock oracleSentinel; uint256 snapshotId = type(uint256).max; @@ -61,8 +63,9 @@ contract ForkTest is BaseTest { _initConfig(); _loadConfig(); - _mockOracle(); _mockPoolAdmin(); + _mockOracle(); + _mockOracleSentinel(); _setBalances(address(this), type(uint256).max); } @@ -138,26 +141,30 @@ contract ForkTest is BaseTest { vm.label(wNative, "wNative"); } - function _mockOracle() internal { - oracle = new AaveOracleMock(IAaveOracle(addressesProvider.getPriceOracle()), pool.getReservesList()); - - vm.store( - address(addressesProvider), - keccak256(abi.encode(bytes32("PRICE_ORACLE"), 2)), - bytes32(uint256(uint160(address(oracle)))) - ); - } - function _mockPoolAdmin() internal { poolAdmin = new PoolAdminMock(poolConfigurator); vm.startPrank(aclAdmin); aclManager.addPoolAdmin(address(poolAdmin)); - aclManager.addEmergencyAdmin(address(poolAdmin)); aclManager.addRiskAdmin(address(poolAdmin)); + aclManager.addEmergencyAdmin(address(poolAdmin)); vm.stopPrank(); } + function _mockOracle() internal { + oracle = new AaveOracleMock(IAaveOracle(addressesProvider.getPriceOracle()), pool.getReservesList()); + + vm.prank(aclAdmin); + addressesProvider.setPriceOracle(address(oracle)); + } + + function _mockOracleSentinel() internal { + oracleSentinel = new PriceOracleSentinelMock(address(addressesProvider)); + + vm.prank(aclAdmin); + addressesProvider.setPriceOracleSentinel(address(oracleSentinel)); + } + function _setBalances(address user, uint256 balance) internal { deal(dai, user, balance); deal(frax, user, balance); diff --git a/test/helpers/TestMarketLib.sol b/test/helpers/TestMarketLib.sol index f33e611e5..ba7738a36 100644 --- a/test/helpers/TestMarketLib.sol +++ b/test/helpers/TestMarketLib.sol @@ -103,29 +103,28 @@ library TestMarketLib { return market.borrowCap.zeroFloorSub(totalBorrow(market)); } - /// @dev Calculates a lower bound of the maximum borrowable quantity collateralized by the given quantity of collateral. + /// @dev Calculates the maximum borrowable quantity collateralized by the given quantity of collateral. function borrowable(TestMarket storage borrowedMarket, TestMarket storage collateralMarket, uint256 collateral) internal view returns (uint256) { return ( - (collateral * collateralMarket.price * 10 ** borrowedMarket.decimals).percentMul(collateralMarket.ltv) + (collateral * collateralMarket.price * 10 ** borrowedMarket.decimals) / (borrowedMarket.price * 10 ** collateralMarket.decimals) - ); + ).percentMul(collateralMarket.ltv); } - /// @dev Calculates an upper bound of the minimum collateral quantity necessary - /// to collateralize the given quantity of debt and still be able to borrow. + /// @dev Calculates the minimum collateral quantity necessary to collateralize the given quantity of debt and still be able to borrow. function minBorrowCollateral(TestMarket storage collateralMarket, TestMarket storage borrowedMarket, uint256 amount) internal view returns (uint256) { return ( - (amount * borrowedMarket.price * 10 ** collateralMarket.decimals).percentDiv(collateralMarket.ltv) + (amount * borrowedMarket.price * 10 ** collateralMarket.decimals) / (collateralMarket.price * 10 ** borrowedMarket.decimals) - ); + ).percentDiv(collateralMarket.ltv); } /// @dev Calculates the minimum collateral quantity necessary to collateralize the given quantity of debt, @@ -136,8 +135,10 @@ library TestMarketLib { returns (uint256) { return ( - (amount * borrowedMarket.price * 10 ** collateralMarket.decimals).percentDiv(collateralMarket.lt) - / (collateralMarket.price * 10 ** borrowedMarket.decimals) + ( + (amount * borrowedMarket.price * 10 ** collateralMarket.decimals) + / (collateralMarket.price * 10 ** borrowedMarket.decimals) + ).percentDiv(collateralMarket.lt) ); } } diff --git a/test/internal/TestPositionsManagerInternal.sol b/test/internal/TestPositionsManagerInternal.sol index e90c11192..e8eb6eb9d 100644 --- a/test/internal/TestPositionsManagerInternal.sol +++ b/test/internal/TestPositionsManagerInternal.sol @@ -12,11 +12,8 @@ import {TestConfigLib, TestConfig} from "../helpers/TestConfigLib.sol"; import {PoolLib} from "src/libraries/PoolLib.sol"; import {MarketLib} from "src/libraries/MarketLib.sol"; -import {PriceOracleSentinelMock} from "../mocks/PriceOracleSentinelMock.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {IPriceOracleGetter} from "@aave-v3-core/interfaces/IPriceOracleGetter.sol"; -import {IPriceOracleSentinel} from "@aave-v3-core/interfaces/IPriceOracleSentinel.sol"; import {IPool, IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPool.sol"; import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol"; @@ -45,12 +42,7 @@ contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerI uint256 constant MIN_AMOUNT = 1 ether; uint256 constant MAX_AMOUNT = type(uint96).max / 2; - IPriceOracleGetter internal priceOracle; - address internal poolOwner; - function setUp() public virtual override { - poolOwner = Ownable(address(addressesProvider)).owner(); - _defaultMaxIterations = Types.MaxIterations(10, 10); _createMarket(dai, 0, 3_333); @@ -66,8 +58,6 @@ contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerI _POOL.supplyToPool(usdc, 1e8); _POOL.supplyToPool(usdt, 1e8); _POOL.supplyToPool(wNative, 1 ether); - - priceOracle = IPriceOracleGetter(_ADDRESSES_PROVIDER.getPriceOracle()); } function testValidatePermission(address owner, address manager) public { @@ -276,10 +266,7 @@ contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerI dai, address(this), amount.rayDiv(indexes.borrow.poolIndex).percentMulUp(lt * 101 / 100), 0, true ); - PriceOracleSentinelMock priceOracleSentinel = new PriceOracleSentinelMock(address(_ADDRESSES_PROVIDER)); - priceOracleSentinel.setLiquidationAllowed(false); - vm.prank(poolOwner); - _ADDRESSES_PROVIDER.setPriceOracleSentinel(address(priceOracleSentinel)); + oracleSentinel.setLiquidationAllowed(false); vm.expectRevert(abi.encodeWithSelector(Errors.UnauthorizedLiquidate.selector)); this.authorizeLiquidate(dai, dai, address(this)); From fe7dfd4c7784049e4b4f95e0131c73fa01601d38 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 7 Feb 2023 10:58:22 +0100 Subject: [PATCH 08/11] chore(imports): remove unnecessary imports --- test/helpers/BaseTest.sol | 1 + test/helpers/ForkTest.sol | 4 +--- .../internal/TestPositionsManagerInternal.sol | 21 +------------------ 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/test/helpers/BaseTest.sol b/test/helpers/BaseTest.sol index 73f1a6715..aeadeb4e5 100644 --- a/test/helpers/BaseTest.sol +++ b/test/helpers/BaseTest.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import {Types} from "src/libraries/Types.sol"; import {Events} from "src/libraries/Events.sol"; import {Errors} from "src/libraries/Errors.sol"; +import {Constants} from "src/libraries/Constants.sol"; import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol"; import {Math} from "@morpho-utils/math/Math.sol"; diff --git a/test/helpers/ForkTest.sol b/test/helpers/ForkTest.sol index 8b05a7018..cd9bbaf63 100644 --- a/test/helpers/ForkTest.sol +++ b/test/helpers/ForkTest.sol @@ -10,12 +10,10 @@ import {IPool, IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPool.sol" import {IVariableDebtToken} from "@aave-v3-core/interfaces/IVariableDebtToken.sol"; import {DataTypes} from "@aave-v3-core/protocol/libraries/types/DataTypes.sol"; -import {Types} from "src/libraries/Types.sol"; -import {Events} from "src/libraries/Events.sol"; -import {Errors} from "src/libraries/Errors.sol"; import {TestConfig, TestConfigLib} from "test/helpers/TestConfigLib.sol"; import {DataTypes} from "@aave-v3-core/protocol/libraries/types/DataTypes.sol"; import {Errors as AaveErrors} from "@aave-v3-core/protocol/libraries/helpers/Errors.sol"; +import {ReserveConfiguration} from "@aave-v3-core/protocol/libraries/configuration/ReserveConfiguration.sol"; import {PriceOracleSentinelMock} from "test/mocks/PriceOracleSentinelMock.sol"; import {AaveOracleMock} from "test/mocks/AaveOracleMock.sol"; diff --git a/test/internal/TestPositionsManagerInternal.sol b/test/internal/TestPositionsManagerInternal.sol index e8eb6eb9d..46111795a 100644 --- a/test/internal/TestPositionsManagerInternal.sol +++ b/test/internal/TestPositionsManagerInternal.sol @@ -1,31 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import {Errors} from "src/libraries/Errors.sol"; -import {MorphoStorage} from "src/MorphoStorage.sol"; -import {PositionsManagerInternal} from "src/PositionsManagerInternal.sol"; - -import {Types} from "src/libraries/Types.sol"; -import {Constants} from "src/libraries/Constants.sol"; - -import {TestConfigLib, TestConfig} from "../helpers/TestConfigLib.sol"; import {PoolLib} from "src/libraries/PoolLib.sol"; import {MarketLib} from "src/libraries/MarketLib.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {IPool, IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPool.sol"; - -import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol"; - -import {DataTypes} from "@aave-v3-core/protocol/libraries/types/DataTypes.sol"; -import {ReserveConfiguration} from "@aave-v3-core/protocol/libraries/configuration/ReserveConfiguration.sol"; - -import {WadRayMath} from "@morpho-utils/math/WadRayMath.sol"; -import {PercentageMath} from "@morpho-utils/math/PercentageMath.sol"; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - +import {PositionsManagerInternal} from "src/PositionsManagerInternal.sol"; import "test/helpers/InternalTest.sol"; contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerInternal { From 4a5e68bf2063f745c43bf297554876b23490547c Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 7 Feb 2023 11:28:38 +0100 Subject: [PATCH 09/11] test(config): fix json abs paths --- config/avalanche-mainnet.json | 6 ----- test/helpers/ForkTest.sol | 43 +++++++++++++++++++++------------- test/helpers/InternalTest.sol | 2 +- test/helpers/TestConfigLib.sol | 29 +++++++++++------------ 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/config/avalanche-mainnet.json b/config/avalanche-mainnet.json index 5a7d05b56..89e746fc0 100644 --- a/config/avalanche-mainnet.json +++ b/config/avalanche-mainnet.json @@ -3,12 +3,6 @@ "rpc": "https://rpc.ankr.com/avalanche", "chainId": 43114, "testBlock": 24800000, - "testMarkets": [ - "DAI", - "USDC", - "WBTC", - "WETH" - ], "addressesProvider": "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb", "DAI": "0xd586E7F844cEa2F87f50152665BCbc2C279D8d70", "FRAX": "0xD24C2Ad096400B6FBcd2ad8B24E7acBc21A1da64", diff --git a/test/helpers/ForkTest.sol b/test/helpers/ForkTest.sol index cd9bbaf63..a2974c309 100644 --- a/test/helpers/ForkTest.sol +++ b/test/helpers/ForkTest.sol @@ -89,7 +89,7 @@ contract ForkTest is BaseTest { function _loadConfig() internal { forkId = config.createFork(); - addressesProvider = IPoolAddressesProvider(config.getAddress("addressesProvider")); + addressesProvider = IPoolAddressesProvider(config.getAddressesProvider()); pool = IPool(addressesProvider.getPool()); aclAdmin = addressesProvider.getACLAdmin(); @@ -97,21 +97,32 @@ contract ForkTest is BaseTest { poolConfigurator = IPoolConfigurator(addressesProvider.getPoolConfigurator()); poolDataProvider = IPoolDataProvider(addressesProvider.getPoolDataProvider()); - dai = config.getAddress("DAI"); - frax = config.getAddress("FRAX"); - mai = config.getAddress("MAI"); - usdc = config.getAddress("USDC"); - usdt = config.getAddress("USDT"); - aave = config.getAddress("AAVE"); - btcb = config.getAddress("BTCb"); - link = config.getAddress("LINK"); - sAvax = config.getAddress("sAVAX"); - wavax = config.getAddress("WAVAX"); - wbtc = config.getAddress("WBTC"); - weth = config.getAddress("WETH"); - wNative = config.getAddress("wrappedNative"); - - allUnderlyings = config.getTestMarkets(); + dai = config.getAddress("$.DAI"); + frax = config.getAddress("$.FRAX"); + mai = config.getAddress("$.MAI"); + usdc = config.getAddress("$.USDC"); + usdt = config.getAddress("$.USDT"); + aave = config.getAddress("$.AAVE"); + btcb = config.getAddress("$.BTCb"); + link = config.getAddress("$.LINK"); + sAvax = config.getAddress("$.sAVAX"); + wavax = config.getAddress("$.WAVAX"); + wbtc = config.getAddress("$.WBTC"); + weth = config.getAddress("$.WETH"); + wNative = config.getAddress("$.wrappedNative"); + + allUnderlyings.push(dai); + allUnderlyings.push(frax); + allUnderlyings.push(mai); + allUnderlyings.push(usdc); + allUnderlyings.push(usdt); + allUnderlyings.push(aave); + allUnderlyings.push(btcb); + allUnderlyings.push(link); + allUnderlyings.push(sAvax); + allUnderlyings.push(wavax); + allUnderlyings.push(wbtc); + allUnderlyings.push(weth); } function _label() internal virtual { diff --git a/test/helpers/InternalTest.sol b/test/helpers/InternalTest.sol index f3b3a2360..ea2c57e98 100644 --- a/test/helpers/InternalTest.sol +++ b/test/helpers/InternalTest.sol @@ -8,5 +8,5 @@ import "./ForkTest.sol"; contract InternalTest is ForkTest, MorphoStorage { using TestConfigLib for TestConfig; - constructor() MorphoStorage(_initConfig().getAddress("addressesProvider"), 0) {} + constructor() MorphoStorage(_initConfig().getAddressesProvider(), 0) {} } diff --git a/test/helpers/TestConfigLib.sol b/test/helpers/TestConfigLib.sol index 57b5b0d26..2a5030b62 100644 --- a/test/helpers/TestConfigLib.sol +++ b/test/helpers/TestConfigLib.sol @@ -13,9 +13,15 @@ library TestConfigLib { Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + string public constant RPC_PATH = "$.rpc"; + string public constant CHAIN_ID_PATH = "$.chainId"; + string public constant TEST_BLOCK_PATH = "$.testBlock"; + string public constant USES_RPC_PREFIX_PATH = "$.usesRpcPrefix"; + string public constant ADDRESSES_PROVIDER_PATH = "$.addressesProvider"; + function load(TestConfig storage config, string memory network) internal returns (TestConfig storage) { string memory root = vm.projectRoot(); - string memory path = string(abi.encodePacked(root, "/config/", network, ".json")); + string memory path = string.concat(root, "/config/", network, ".json"); config.json = vm.readFile(path); @@ -26,24 +32,17 @@ library TestConfigLib { return config.json.readAddress(string(abi.encodePacked(key))); } - function getTestMarkets(TestConfig storage config) internal view returns (address[] memory) { - string[] memory marketNames = config.json.readStringArray(string(abi.encodePacked("testMarkets"))); - address[] memory markets = new address[](marketNames.length); - - for (uint256 i; i < markets.length; i++) { - markets[i] = getAddress(config, marketNames[i]); - } - - return markets; + function getAddressesProvider(TestConfig storage config) internal view returns (address) { + return getAddress(config, ADDRESSES_PROVIDER_PATH); } function createFork(TestConfig storage config) internal returns (uint256 forkId) { - bool rpcPrefixed = stdJson.readBool(config.json, string(abi.encodePacked("usesRpcPrefix"))); + bool rpcPrefixed = stdJson.readBool(config.json, USES_RPC_PREFIX_PATH); string memory endpoint = rpcPrefixed - ? string(abi.encodePacked(config.json.readString(string(abi.encodePacked("rpc"))), vm.envString("ALCHEMY_KEY"))) - : config.json.readString(string(abi.encodePacked("rpc"))); + ? string.concat(config.json.readString(RPC_PATH), vm.envString("ALCHEMY_KEY")) + : config.json.readString(RPC_PATH); - forkId = vm.createSelectFork(endpoint, config.json.readUint(string(abi.encodePacked("testBlock")))); - vm.chainId(config.json.readUint(string(abi.encodePacked("chainId")))); + forkId = vm.createSelectFork(endpoint, config.json.readUint(TEST_BLOCK_PATH)); + vm.chainId(config.json.readUint(CHAIN_ID_PATH)); } } From d79fb2b4a44b91470e3a9b453f88f10b73fe0db1 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 7 Feb 2023 15:58:46 +0100 Subject: [PATCH 10/11] test(integration): add util functions --- test/helpers/IntegrationTest.sol | 33 ++++++++++++++++++++++++-------- test/helpers/TestMarketLib.sol | 4 ++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/test/helpers/IntegrationTest.sol b/test/helpers/IntegrationTest.sol index 72ea9ff30..dca297aeb 100644 --- a/test/helpers/IntegrationTest.sol +++ b/test/helpers/IntegrationTest.sol @@ -22,7 +22,7 @@ contract IntegrationTest is ForkTest { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using TestMarketLib for TestMarket; - uint8 internal constant E_MODE = 0; + uint8 internal constant E_MODE_CATEGORY_ID = 0; uint256 internal constant INITIAL_BALANCE = 10_000_000_000 ether; // AaveV3 base currency is USD, 8 decimals on all L2s. @@ -84,8 +84,8 @@ contract IntegrationTest is ForkTest { } function _deploy() internal { - positionsManager = new PositionsManager(address(addressesProvider), E_MODE); - morphoImpl = new Morpho(address(addressesProvider), E_MODE); + positionsManager = new PositionsManager(address(addressesProvider), E_MODE_CATEGORY_ID); + morphoImpl = new Morpho(address(addressesProvider), E_MODE_CATEGORY_ID); proxyAdmin = new ProxyAdmin(); morphoProxy = new TransparentUpgradeableProxy(payable(address(morphoImpl)), address(proxyAdmin), ""); @@ -128,8 +128,6 @@ contract IntegrationTest is ForkTest { market.price = oracle.getAssetPrice(underlying); // Price is constant, equal to price at fork block number. (market.ltv, market.lt, market.liquidationBonus, market.decimals,,) = reserve.configuration.getParams(); - market.isBorrowable = reserve.configuration.getBorrowingEnabled() && !reserve.configuration.getSiloedBorrowing() - && !reserve.configuration.getBorrowableInIsolation() && market.borrowGap() > 0; market.minAmount = (MIN_USD_AMOUNT * 10 ** market.decimals) / market.price; market.maxAmount = (MAX_USD_AMOUNT * 10 ** market.decimals) / market.price; @@ -140,6 +138,10 @@ contract IntegrationTest is ForkTest { market.supplyCap = type(uint256).max; market.borrowCap = type(uint256).max; + market.isBorrowable = reserve.configuration.getBorrowingEnabled() && !reserve.configuration.getSiloedBorrowing() + && !reserve.configuration.getBorrowableInIsolation() + && (E_MODE_CATEGORY_ID == 0 || E_MODE_CATEGORY_ID == config.getEModeCategory()); + vm.label(reserve.aTokenAddress, string.concat("a", market.symbol)); vm.label(reserve.variableDebtTokenAddress, string.concat("vd", market.symbol)); vm.label(reserve.stableDebtTokenAddress, string.concat("sd", market.symbol)); @@ -150,9 +152,7 @@ contract IntegrationTest is ForkTest { _initMarket(underlying, reserveFactor, p2pIndexCursor); underlyings.push(underlying); - if (market.ltv > 0 && reserve.configuration.getEModeCategory() == E_MODE) { - collateralUnderlyings.push(underlying); - } + if (market.ltv > 0) collateralUnderlyings.push(underlying); if (market.isBorrowable) borrowableUnderlyings.push(underlying); morpho.createMarket(market.underlying, market.reserveFactor, market.p2pIndexCursor); @@ -186,6 +186,23 @@ contract IntegrationTest is ForkTest { return bound(amount, market.minAmount, Math.min(market.maxAmount, market.supplyGap())); } + /// @dev Bounds the input so that the amount returned can collateralize a debt between + /// the minimum & the maximum USD amount expected in tests, without exceeding the market's supply cap. + function _boundCollateral(TestMarket storage collateralMarket, uint256 amount, TestMarket storage borrowedMarket) + internal + view + returns (uint256) + { + return bound( + amount, + collateralMarket.minBorrowCollateral(borrowedMarket, borrowedMarket.minAmount), + Math.min( + collateralMarket.minBorrowCollateral(borrowedMarket, borrowedMarket.maxAmount), + collateralMarket.supplyGap() + ) + ); + } + /// @dev Bounds the input between the minimum USD amount expected in tests /// and the maximum borrowable quantity, without exceeding the market's liquidity nor its borrow cap. function _boundBorrow(TestMarket storage market, uint256 amount) internal view returns (uint256) { diff --git a/test/helpers/TestMarketLib.sol b/test/helpers/TestMarketLib.sol index ba7738a36..bfd8f7dbc 100644 --- a/test/helpers/TestMarketLib.sol +++ b/test/helpers/TestMarketLib.sol @@ -110,9 +110,9 @@ library TestMarketLib { returns (uint256) { return ( - (collateral * collateralMarket.price * 10 ** borrowedMarket.decimals) + (collateral.percentMul(collateralMarket.ltv - 1) * collateralMarket.price * 10 ** borrowedMarket.decimals) / (borrowedMarket.price * 10 ** collateralMarket.decimals) - ).percentMul(collateralMarket.ltv); + ); } /// @dev Calculates the minimum collateral quantity necessary to collateralize the given quantity of debt and still be able to borrow. From dee8a8ac1e3a6cd28e7ae78c9d11a812a753500c Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 8 Feb 2023 10:50:13 +0100 Subject: [PATCH 11/11] test(config): fix uninitialized variable --- test/helpers/IntegrationTest.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/IntegrationTest.sol b/test/helpers/IntegrationTest.sol index dca297aeb..8d9e74747 100644 --- a/test/helpers/IntegrationTest.sol +++ b/test/helpers/IntegrationTest.sol @@ -140,7 +140,7 @@ contract IntegrationTest is ForkTest { market.isBorrowable = reserve.configuration.getBorrowingEnabled() && !reserve.configuration.getSiloedBorrowing() && !reserve.configuration.getBorrowableInIsolation() - && (E_MODE_CATEGORY_ID == 0 || E_MODE_CATEGORY_ID == config.getEModeCategory()); + && (E_MODE_CATEGORY_ID == 0 || E_MODE_CATEGORY_ID == reserve.configuration.getEModeCategory()); vm.label(reserve.aTokenAddress, string.concat("a", market.symbol)); vm.label(reserve.variableDebtTokenAddress, string.concat("vd", market.symbol));