From 29731286c06703ec9f992df6a739ebb3c941c7ff Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 12 Sep 2023 13:17:08 +0200 Subject: [PATCH] refactor(test): decouple integration test solidity pragma --- .github/workflows/foundry.yml | 56 +---- contracts/SupplyVault.sol | 2 +- package.json | 4 +- test/forge/BaseTest.sol | 126 ---------- test/forge/MarketTest.sol | 2 +- test/forge/RoleTest.sol | 2 +- test/forge/TimelockTest.sol | 3 +- test/forge/helpers/BaseTest.sol | 228 ++++++++++++++++++ .../{meta-morpho => }/SupplyVault.spec.ts | 2 +- 9 files changed, 239 insertions(+), 186 deletions(-) delete mode 100644 test/forge/BaseTest.sol create mode 100644 test/forge/helpers/BaseTest.sol rename test/hardhat/{meta-morpho => }/SupplyVault.spec.ts (98%) diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 65e92390..741fee08 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -67,10 +67,10 @@ jobs: out key: forge-${{ github.ref_name }} - test-local: + test: needs: build-no-ir - name: Local tests + name: Tests runs-on: ubuntu-latest strategy: @@ -105,59 +105,11 @@ jobs: - uses: ./.github/actions/install-cache - - name: Run local tests in ${{ matrix.type }} mode - run: yarn test:forge:local + - name: Run tests in ${{ matrix.type }} mode + run: yarn test:forge env: FOUNDRY_FUZZ_RUNS: ${{ matrix.fuzz-runs }} FOUNDRY_FUZZ_MAX_TEST_REJECTS: ${{ matrix.max-test-rejects }} FOUNDRY_INVARIANT_RUNS: ${{ matrix.invariant-runs }} FOUNDRY_INVARIANT_DEPTH: ${{ matrix.invariant-depth }} FOUNDRY_FUZZ_SEED: 0x${{ github.event.pull_request.base.sha || github.sha }} - - test-mainnet: - needs: build-no-ir - - name: Ethereum tests - runs-on: ubuntu-latest - - strategy: - fail-fast: true - matrix: - type: ["slow", "fast"] - include: - - type: "slow" - fuzz-runs: 1024 - max-test-rejects: 65536 - invariant-runs: 64 - invariant-depth: 1024 - - type: "fast" - fuzz-runs: 256 - max-test-rejects: 65536 - invariant-runs: 16 - invariant-depth: 256 - - steps: - - name: Generate a token - id: generate-token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 - with: - app_id: ${{ secrets.APP_ID }} - private_key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Checkout - uses: actions/checkout@v3 - with: - token: ${{ steps.generate-token.outputs.token }} - submodules: recursive - - - uses: ./.github/actions/install-cache - - - name: Run mainnet tests in ${{ matrix.type }} mode - run: yarn test:forge:mainnet - env: - ALCHEMY_KEY: ${{ secrets.ALCHEMY_KEY }} - FOUNDRY_FUZZ_RUNS: ${{ matrix.fuzz-runs }} - FOUNDRY_FUZZ_MAX_TEST_REJECTS: ${{ matrix.max-test-rejects }} - FOUNDRY_INVARIANT_RUNS: ${{ matrix.invariant-runs }} - FOUNDRY_INVARIANT_DEPTH: ${{ matrix.invariant-depth }} - FOUNDRY_FUZZ_SEED: 0x${{ github.event.pull_request.base.sha || github.sha }} diff --git a/contracts/SupplyVault.sol b/contracts/SupplyVault.sol index a0542e8a..ca004530 100644 --- a/contracts/SupplyVault.sol +++ b/contracts/SupplyVault.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.19; +pragma solidity 0.8.21; import {IMorphoMarketParams} from "./interfaces/IMorphoMarketParams.sol"; import {MarketAllocation, Pending, ISupplyVault} from "./interfaces/ISupplyVault.sol"; diff --git a/package.json b/package.json index f07333a6..921a254f 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,12 @@ "scripts": { "prepare": "husky install", "postinstall": "forge install", - "build:ts": "yarn build:hardhat && tsc --build ./tsconfig.build.json", + "build:ts": "tsc --build ./tsconfig.build.json", "build:forge": "FOUNDRY_PROFILE=build forge build", "build:hardhat": "npx hardhat compile", "build:blue": "cd lib/morpho-blue/ && yarn build:forge && cd ../..", "typecheck": "tsc --noEmit", "test:forge": "yarn build:blue && FOUNDRY_PROFILE=test forge test", - "test:forge:local": "FOUNDRY_MATCH_CONTRACT=LocalTest yarn test:forge", - "test:forge:mainnet": "FOUNDRY_MATCH_CONTRACT=EthereumTest yarn test:forge", "test:hardhat": "yarn build:blue && npx hardhat test", "lint": "yarn lint:forge && yarn lint:ts", "lint:ts": "prettier --check src test/hardhat", diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol deleted file mode 100644 index 1cf64c01..00000000 --- a/test/forge/BaseTest.sol +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.0; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -import {IrmMock as Irm} from "contracts/mocks/IrmMock.sol"; -import {ERC20Mock as ERC20} from "contracts/mocks/ERC20Mock.sol"; -import {OracleMock as Oracle} from "contracts/mocks/OracleMock.sol"; - -import {SupplyVault, IERC20, ErrorsLib, Pending, MarketAllocation} from "contracts/SupplyVault.sol"; -import {Morpho, MarketParamsLib, MarketParams, SharesMathLib, Id} from "@morpho-blue/Morpho.sol"; - -contract BaseTest is Test { - using MarketParamsLib for MarketParams; - - uint256 internal constant HIGH_COLLATERAL_AMOUNT = 1e35; - uint256 internal constant MIN_TEST_AMOUNT = 100; - uint256 internal constant MAX_TEST_AMOUNT = 1e28; - uint256 internal constant MIN_TEST_SHARES = MIN_TEST_AMOUNT * SharesMathLib.VIRTUAL_SHARES; - uint256 internal constant MAX_TEST_SHARES = MAX_TEST_AMOUNT * SharesMathLib.VIRTUAL_SHARES; - uint256 internal constant MIN_COLLATERAL_PRICE = 1e10; - uint256 internal constant MAX_COLLATERAL_PRICE = 1e40; - uint256 internal constant MAX_COLLATERAL_ASSETS = type(uint128).max; - uint256 internal constant NB_OF_MARKETS = 10; - - address internal SUPPLIER = _addrFromHashedString("Morpho Supplier"); - address internal BORROWER = _addrFromHashedString("Morpho Borrower"); - address internal REPAYER = _addrFromHashedString("Morpho Repayer"); - address internal ONBEHALF = _addrFromHashedString("Morpho On Behalf"); - address internal RECEIVER = _addrFromHashedString("Morpho Receiver"); - address internal LIQUIDATOR = _addrFromHashedString("Morpho Liquidator"); - address internal OWNER = _addrFromHashedString("Morpho Owner"); - address internal RISK_MANAGER = _addrFromHashedString("Morpho Risk Manager"); - address internal ALLOCATOR = _addrFromHashedString("Morpho Allocator"); - - uint256 internal constant LLTV = 0.8 ether; - uint256 internal constant TIMELOCK = 0; - uint128 internal constant CAP = type(uint128).max; - - Morpho internal morpho; - ERC20 internal borrowableToken; - ERC20 internal collateralToken; - Oracle internal oracle; - Irm internal irm; - MarketParams internal marketParams; - - SupplyVault internal vault; - - MarketParams[] internal allMarkets; - - function setUp() public virtual { - vm.label(OWNER, "Owner"); - vm.label(SUPPLIER, "Supplier"); - vm.label(BORROWER, "Borrower"); - vm.label(REPAYER, "Repayer"); - vm.label(ONBEHALF, "OnBehalf"); - vm.label(RECEIVER, "Receiver"); - vm.label(LIQUIDATOR, "Liquidator"); - - // Create Morpho. - morpho = new Morpho(OWNER); - vm.label(address(morpho), "Morpho"); - - // List a market. - borrowableToken = new ERC20("borrowable", "B"); - vm.label(address(borrowableToken), "Borrowable asset"); - - collateralToken = new ERC20("collateral", "C"); - vm.label(address(collateralToken), "Collateral asset"); - - oracle = new Oracle(); - vm.label(address(oracle), "Oracle"); - - oracle.setPrice(1e36); - - irm = new Irm(); - vm.label(address(irm), "IRM"); - - marketParams = - MarketParams(address(borrowableToken), address(collateralToken), address(oracle), address(irm), LLTV); - - vm.startPrank(OWNER); - - vault = new SupplyVault(address(morpho), TIMELOCK, IERC20(address(borrowableToken)), "MetaMorpho Vault", "MMV"); - - morpho.enableIrm(address(irm)); - - for (uint256 i; i < NB_OF_MARKETS; i++) { - morpho.enableLltv(LLTV / (i + 1)); - marketParams = MarketParams( - address(borrowableToken), address(collateralToken), address(oracle), address(irm), LLTV / (i + 1) - ); - morpho.createMarket(marketParams); - - allMarkets.push(marketParams); - } - - vault.setIsRiskManager(RISK_MANAGER, true); - vault.setIsAllocator(ALLOCATOR, true); - - vm.stopPrank(); - - vm.warp(block.timestamp + 1); - } - - function _addrFromHashedString(string memory str) internal pure returns (address) { - return address(uint160(uint256(keccak256(bytes(str))))); - } - - function _submitAndSetTimelock(uint128 timelock) internal { - vm.startPrank(OWNER); - vault.submitPendingTimelock(timelock); - vm.warp(block.timestamp + vault.timelock()); - vault.setTimelock(); - vm.stopPrank(); - } - - function _submitAndEnableMarket(MarketParams memory params, uint128 cap) internal { - vm.startPrank(RISK_MANAGER); - vault.submitPendingMarket(params, cap); - vm.warp(block.timestamp + vault.timelock()); - vault.enableMarket(params.id()); - vm.stopPrank(); - } -} diff --git a/test/forge/MarketTest.sol b/test/forge/MarketTest.sol index aa697e3c..ba9363ab 100644 --- a/test/forge/MarketTest.sol +++ b/test/forge/MarketTest.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import "./BaseTest.sol"; +import "./helpers/BaseTest.sol"; contract MarketTest is BaseTest { using MarketParamsLib for MarketParams; diff --git a/test/forge/RoleTest.sol b/test/forge/RoleTest.sol index a482b452..3c694bf2 100644 --- a/test/forge/RoleTest.sol +++ b/test/forge/RoleTest.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import "./BaseTest.sol"; +import "./helpers/BaseTest.sol"; contract RoleTest is BaseTest { using MarketParamsLib for MarketParams; diff --git a/test/forge/TimelockTest.sol b/test/forge/TimelockTest.sol index e7c4f039..215747bd 100644 --- a/test/forge/TimelockTest.sol +++ b/test/forge/TimelockTest.sol @@ -1,6 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import "./BaseTest.sol"; +import "./helpers/BaseTest.sol"; contract TimelockTest is BaseTest { function testSubmitPendingTimelock(uint256 timelock) public { diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol new file mode 100644 index 00000000..083c4aa4 --- /dev/null +++ b/test/forge/helpers/BaseTest.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@morpho-blue/interfaces/IMorpho.sol"; +import {IOracle} from "@morpho-blue/interfaces/IOracle.sol"; + +import {MarketParamsLib} from "@morpho-blue/libraries/MarketParamsLib.sol"; +import {MorphoLib} from "@morpho-blue/libraries/periphery/MorphoLib.sol"; +import {MorphoBalancesLib} from "@morpho-blue/libraries/periphery/MorphoBalancesLib.sol"; + +import {ORACLE_PRICE_SCALE} from "@morpho-blue/libraries/ConstantsLib.sol"; + +import {IrmMock} from "contracts/mocks/IrmMock.sol"; +import {ERC20Mock} from "contracts/mocks/ERC20Mock.sol"; +import {OracleMock} from "contracts/mocks/OracleMock.sol"; + +import {SupplyVault, IERC20, ErrorsLib, Pending, MarketAllocation} from "contracts/SupplyVault.sol"; + +import "forge-std/Test.sol"; +import "forge-std/console2.sol"; + +contract BaseTest is Test { + using MorphoLib for IMorpho; + using MorphoBalancesLib for IMorpho; + using MarketParamsLib for MarketParams; + using stdJson for string; + + uint256 internal constant BLOCK_TIME = 12; + uint256 internal constant MIN_TEST_AMOUNT = 100; + uint256 internal constant MAX_TEST_AMOUNT = 1e28; + uint256 internal constant MIN_TEST_LLTV = 0.01 ether; + uint256 internal constant MAX_TEST_LLTV = 0.99 ether; + uint256 internal constant NB_MARKETS = 10; + uint256 internal constant TIMELOCK = 0; + uint128 internal constant CAP = type(uint128).max; + + address internal OWNER; + address internal SUPPLIER; + address internal BORROWER; + address internal REPAYER; + address internal ONBEHALF; + address internal RECEIVER; + address internal ALLOCATOR; + address internal RISK_MANAGER; + address internal MORPHO_OWNER; + address internal MORPHO_FEE_RECIPIENT; + + IMorpho internal morpho; + ERC20Mock internal borrowableToken; + ERC20Mock internal collateralToken; + OracleMock internal oracle; + IrmMock internal irm; + + SupplyVault internal vault; + + MarketParams[] internal allMarkets; + + function setUp() public virtual { + OWNER = _addrFromHashedString("Owner"); + SUPPLIER = _addrFromHashedString("Supplier"); + BORROWER = _addrFromHashedString("Borrower"); + REPAYER = _addrFromHashedString("Repayer"); + ONBEHALF = _addrFromHashedString("OnBehalf"); + RECEIVER = _addrFromHashedString("Receiver"); + ALLOCATOR = _addrFromHashedString("Allocator"); + RISK_MANAGER = _addrFromHashedString("RiskManager"); + MORPHO_OWNER = _addrFromHashedString("MorphoOwner"); + MORPHO_FEE_RECIPIENT = _addrFromHashedString("MorphoFeeRecipient"); + + morpho = IMorpho(_deploy("lib/morpho-blue/out/Morpho.sol/Morpho.json", abi.encode(MORPHO_OWNER))); + vm.label(address(morpho), "Morpho"); + + borrowableToken = new ERC20Mock("borrowable", "B"); + vm.label(address(borrowableToken), "Borrowable"); + + collateralToken = new ERC20Mock("collateral", "C"); + vm.label(address(collateralToken), "Collateral"); + + oracle = new OracleMock(); + + oracle.setPrice(ORACLE_PRICE_SCALE); + + irm = new IrmMock(); + + vm.startPrank(MORPHO_OWNER); + morpho.enableIrm(address(irm)); + morpho.setFeeRecipient(MORPHO_FEE_RECIPIENT); + vm.stopPrank(); + + vault = new SupplyVault(address(morpho), TIMELOCK, IERC20(address(borrowableToken)), "MetaMorpho Vault", "MMV"); + + borrowableToken.approve(address(vault), type(uint256).max); + collateralToken.approve(address(vault), type(uint256).max); + vm.startPrank(SUPPLIER); + borrowableToken.approve(address(vault), type(uint256).max); + collateralToken.approve(address(vault), type(uint256).max); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + vm.stopPrank(); + vm.startPrank(BORROWER); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + vm.stopPrank(); + vm.startPrank(REPAYER); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + vm.stopPrank(); + vm.startPrank(ONBEHALF); + borrowableToken.approve(address(vault), type(uint256).max); + collateralToken.approve(address(vault), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(OWNER); + vault = new SupplyVault(address(morpho), TIMELOCK, IERC20(address(borrowableToken)), "MetaMorpho Vault", "MMV"); + + vault.setIsRiskManager(RISK_MANAGER, true); + vault.setIsAllocator(ALLOCATOR, true); + vm.stopPrank(); + + for (uint256 i; i < NB_MARKETS; ++i) { + uint256 lltv = 0.8 ether / (i + 1); + + MarketParams memory marketParams = + MarketParams(address(borrowableToken), address(collateralToken), address(oracle), address(irm), lltv); + + vm.startPrank(MORPHO_OWNER); + morpho.enableLltv(lltv); + morpho.createMarket(marketParams); + vm.stopPrank(); + + allMarkets.push(marketParams); + } + } + + function _addrFromHashedString(string memory name) internal returns (address addr) { + addr = address(uint160(uint256(keccak256(bytes(name))))); + vm.label(addr, name); + } + + /// @dev Rolls & warps the given number of blocks forward the blockchain. + function _forward(uint256 blocks) internal { + vm.roll(block.number + blocks); + vm.warp(block.timestamp + blocks * BLOCK_TIME); // Block speed should depend on test network. + } + + /// @dev Bounds the fuzzing input to a realistic number of blocks. + function _boundBlocks(uint256 blocks) internal view returns (uint256) { + return bound(blocks, 1, type(uint24).max); + } + + /// @dev Bounds the fuzzing input to a non-zero address. + /// @dev This function should be used in place of `vm.assume` in invariant test handler functions: + /// https://github.com/foundry-rs/foundry/issues/4190. + function _boundAddressNotZero(address input) internal view virtual returns (address) { + return address(uint160(bound(uint256(uint160(input)), 1, type(uint160).max))); + } + + function _boundTestLltv(uint256 lltv) internal view returns (uint256) { + return bound(lltv, MIN_TEST_LLTV, MAX_TEST_LLTV); + } + + function _accrueInterest(MarketParams memory market) internal { + collateralToken.setBalance(address(this), 1); + morpho.supplyCollateral(market, 1, address(this), hex""); + morpho.withdrawCollateral(market, 1, address(this), address(10)); + } + + function neq(MarketParams memory a, MarketParams memory b) internal pure returns (bool) { + return (Id.unwrap(a.id()) != Id.unwrap(b.id())); + } + + function _randomCandidate(address[] memory candidates, uint256 seed) internal pure returns (address) { + if (candidates.length == 0) return address(0); + + return candidates[seed % candidates.length]; + } + + function _removeAll(address[] memory inputs, address removed) internal pure returns (address[] memory result) { + result = new address[](inputs.length); + + uint256 nbAddresses; + for (uint256 i; i < inputs.length; ++i) { + address input = inputs[i]; + + if (input != removed) { + result[nbAddresses] = input; + ++nbAddresses; + } + } + + assembly { + mstore(result, nbAddresses) + } + } + + function _randomNonZero(address[] memory users, uint256 seed) internal pure returns (address) { + users = _removeAll(users, address(0)); + + return _randomCandidate(users, seed); + } + + function _deploy(string memory artifactPath, bytes memory constructorArgs) internal returns (address deployed) { + string memory artifact = vm.readFile(artifactPath); + bytes memory bytecode = bytes.concat(artifact.readBytes("$.bytecode.object"), constructorArgs); + + assembly { + deployed := create(0, add(bytecode, 0x20), mload(bytecode)) + } + + require(deployed != address(0), string.concat("could not deploy `", artifactPath, "`")); + } + + function _submitAndSetTimelock(uint128 timelock) internal { + vm.startPrank(OWNER); + vault.submitPendingTimelock(timelock); + vm.warp(block.timestamp + vault.timelock()); + vault.setTimelock(); + vm.stopPrank(); + } + + function _submitAndEnableMarket(MarketParams memory params, uint128 cap) internal { + vm.startPrank(RISK_MANAGER); + vault.submitPendingMarket(params, cap); + vm.warp(block.timestamp + vault.timelock()); + vault.enableMarket(params.id()); + vm.stopPrank(); + } +} diff --git a/test/hardhat/meta-morpho/SupplyVault.spec.ts b/test/hardhat/SupplyVault.spec.ts similarity index 98% rename from test/hardhat/meta-morpho/SupplyVault.spec.ts rename to test/hardhat/SupplyVault.spec.ts index f953439b..5f0d299b 100644 --- a/test/hardhat/meta-morpho/SupplyVault.spec.ts +++ b/test/hardhat/SupplyVault.spec.ts @@ -8,7 +8,7 @@ import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { setNextBlockTimestamp } from "@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time"; // Must use relative import path. -import MorphoArtifact from "../../../lib/morpho-blue/out/Morpho.sol/Morpho.json"; +import MorphoArtifact from "../../lib/morpho-blue/out/Morpho.sol/Morpho.json"; // Without the division it overflows. const initBalance = MaxUint256 / 10000000000000000n;