From 983e275e24c844978463d47f991e5b6a607d1ab0 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Wed, 8 Oct 2025 14:34:55 +0200 Subject: [PATCH 001/106] chore: add full invariant suite (handlers, hooks, specs, replays, README) --- .gitignore | 5 + Makefile | 19 + medusa.json | 97 ++++ .../HandlerAggregator.t.sol | 25 + tests/enigma-dark-invariants/Invariants.t.sol | 60 +++ tests/enigma-dark-invariants/README.md | 61 +++ tests/enigma-dark-invariants/Setup.t.sol | 271 +++++++++++ .../SpecAggregator.t.sol | 42 ++ tests/enigma-dark-invariants/Tester.t.sol | 21 + .../TesterFoundry.t.sol | 44 ++ .../_config/echidna_config.yaml | 61 +++ .../base/BaseHandler.t.sol | 81 ++++ .../base/BaseHooks.t.sol | 18 + .../base/BaseStorage.t.sol | 133 ++++++ .../base/BaseTest.t.sol | 150 ++++++ .../base/ProtocolAssertions.t.sol | 10 + .../handlers/hub/HubConfiguratorHandler.t.sol | 53 +++ .../handlers/hub/HubHandler.t.sol | 50 ++ .../interfaces/IHubConfiguratorHandler.sol | 6 + .../handlers/interfaces/IHubHandler.sol | 6 + .../interfaces/ISpokeConfiguratorHandler.sol | 6 + .../handlers/interfaces/ISpokeHandler.sol | 15 + .../interfaces/ITreasurySpokeHandler.sol | 6 + .../handlers/simulators/.gitkeep | 0 .../simulators/DonationAttackHandler.t.sol | 39 ++ .../simulators/PriceFeeSimulatorHandler.t.sol | 32 ++ .../spoke/SpokeConfiguratorHandler.t.sol | 59 +++ .../handlers/spoke/SpokeHandler.t.sol | 290 ++++++++++++ .../handlers/spoke/TreasurySpokeHandler.t.sol | 77 +++ .../helpers/extended/.gitkeep | 0 .../helpers/fixtures/.gitkeep | 0 .../hooks/DefaultBeforeAfterHooks.t.sol | 216 +++++++++ .../hooks/HookAggregator.t.sol | 141 ++++++ .../invariants/HubInvariants.t.sol | 124 +++++ .../invariants/SpokeInvariants.t.sol | 72 +++ .../enigma-dark-invariants/remote/DOCKERFILE | 17 + .../replays/ReplayTest_1.t.sol | 95 ++++ .../replays/ReplayTest_2.t.sol | 157 +++++++ .../specs/InvariantsSpec.t.sol | 84 ++++ .../specs/PostconditionsSpec.t.sol | 73 +++ tests/enigma-dark-invariants/utils/Actor.sol | 60 +++ .../enigma-dark-invariants/utils/CREATE3.sol | 128 +++++ .../utils/DeployPermit2.sol | 22 + .../utils/PropertiesAsserts.sol | 410 ++++++++++++++++ .../utils/PropertiesConstants.sol | 16 + .../utils/StdAsserts.sol | 443 ++++++++++++++++++ .../utils/mocks/MockPriceFeedSimulator.sol | 46 ++ 47 files changed, 3841 insertions(+) create mode 100644 medusa.json create mode 100644 tests/enigma-dark-invariants/HandlerAggregator.t.sol create mode 100644 tests/enigma-dark-invariants/Invariants.t.sol create mode 100644 tests/enigma-dark-invariants/README.md create mode 100644 tests/enigma-dark-invariants/Setup.t.sol create mode 100644 tests/enigma-dark-invariants/SpecAggregator.t.sol create mode 100644 tests/enigma-dark-invariants/Tester.t.sol create mode 100644 tests/enigma-dark-invariants/TesterFoundry.t.sol create mode 100644 tests/enigma-dark-invariants/_config/echidna_config.yaml create mode 100644 tests/enigma-dark-invariants/base/BaseHandler.t.sol create mode 100644 tests/enigma-dark-invariants/base/BaseHooks.t.sol create mode 100644 tests/enigma-dark-invariants/base/BaseStorage.t.sol create mode 100644 tests/enigma-dark-invariants/base/BaseTest.t.sol create mode 100644 tests/enigma-dark-invariants/base/ProtocolAssertions.t.sol create mode 100644 tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol create mode 100644 tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol create mode 100644 tests/enigma-dark-invariants/handlers/interfaces/IHubConfiguratorHandler.sol create mode 100644 tests/enigma-dark-invariants/handlers/interfaces/IHubHandler.sol create mode 100644 tests/enigma-dark-invariants/handlers/interfaces/ISpokeConfiguratorHandler.sol create mode 100644 tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol create mode 100644 tests/enigma-dark-invariants/handlers/interfaces/ITreasurySpokeHandler.sol create mode 100644 tests/enigma-dark-invariants/handlers/simulators/.gitkeep create mode 100644 tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol create mode 100644 tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol create mode 100644 tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol create mode 100644 tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol create mode 100644 tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol create mode 100644 tests/enigma-dark-invariants/helpers/extended/.gitkeep create mode 100644 tests/enigma-dark-invariants/helpers/fixtures/.gitkeep create mode 100644 tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol create mode 100644 tests/enigma-dark-invariants/hooks/HookAggregator.t.sol create mode 100644 tests/enigma-dark-invariants/invariants/HubInvariants.t.sol create mode 100644 tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol create mode 100644 tests/enigma-dark-invariants/remote/DOCKERFILE create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol create mode 100644 tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol create mode 100644 tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol create mode 100644 tests/enigma-dark-invariants/utils/Actor.sol create mode 100644 tests/enigma-dark-invariants/utils/CREATE3.sol create mode 100644 tests/enigma-dark-invariants/utils/DeployPermit2.sol create mode 100644 tests/enigma-dark-invariants/utils/PropertiesAsserts.sol create mode 100644 tests/enigma-dark-invariants/utils/PropertiesConstants.sol create mode 100644 tests/enigma-dark-invariants/utils/StdAsserts.sol create mode 100644 tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol diff --git a/.gitignore b/.gitignore index 8c400184a..1e0ec8dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ lcov* report/ .DS_Store + +# Invariant testing artifacts +_corpus/ +crytic-export/ +slither_results.json diff --git a/Makefile b/Makefile index fc12dd32a..26bb07e67 100644 --- a/Makefile +++ b/Makefile @@ -29,3 +29,22 @@ coverage : make coverage-clean make coverage-report make coverage-badge + +# Echidna +echidna: + echidna tests/enigma-dark-invariants/Tester.t.sol --contract Tester --config ./tests/enigma-dark-invariants/_config/echidna_config.yaml +echidna-assert: + echidna tests/enigma-dark-invariants/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/enigma-dark-invariants/_config/echidna_config.yaml +echidna-explore: + echidna tests/enigma-dark-invariants/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/enigma-dark-invariants/_config/echidna_config.yaml + +# Medusa +medusa: + medusa fuzz --config ./medusa.json + +foundry-invariants: + forge test --mc TesterFoundry -vvv + +# Echidna Results +runes: + runes convert ./tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/enigma-dark-invariants/replays \ No newline at end of file diff --git a/medusa.json b/medusa.json new file mode 100644 index 000000000..a784f3667 --- /dev/null +++ b/medusa.json @@ -0,0 +1,97 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "callSequenceLength": 100, + "corpusDirectory": "tests/enigma-dark-invariants/_corpus/medusa", + "coverageEnabled": true, + "deploymentOrder": [ + "Tester" + ], + "targetContracts": [ + "Tester" + ], + "targetContractsBalances": [ + "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" + ], + "predeployedContracts": { + "LiquidationLogic": "0xf01" + }, + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 12500000000, + "transactionGasLimit": 1250000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "assertionModes": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "fuzz_", + "invariant_" + ] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": [ + "optimize_" + ] + }, + "excludeFunctionSignatures": [ + "Tester.checkPostConditions()" + ] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": "tests/enigma-dark-invariants/Tester.t.sol", + "solcVersion": "", + "exportDirectory": "", + "args": [ + "--solc-remaps", + "forge-std/=../../../lib/forge-std/src/", + "--compile-libraries=(LiquidationLogic,0xf01)" + ] + } + }, + "logging": { + "level": "info", + "logDirectory": "" + } +} \ No newline at end of file diff --git a/tests/enigma-dark-invariants/HandlerAggregator.t.sol b/tests/enigma-dark-invariants/HandlerAggregator.t.sol new file mode 100644 index 000000000..acd1ec48b --- /dev/null +++ b/tests/enigma-dark-invariants/HandlerAggregator.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Handler contracts +import {HubHandler} from "./handlers/hub/HubHandler.t.sol"; +import {SpokeHandler} from "./handlers/spoke/SpokeHandler.t.sol"; +import {HubConfiguratorHandler} from "./handlers/hub/HubConfiguratorHandler.t.sol"; +import {SpokeConfiguratorHandler} from "./handlers/spoke/SpokeConfiguratorHandler.t.sol"; + +// Simulator contracts +import {PriceFeeSimulatorHandler} from "./handlers/simulators/PriceFeeSimulatorHandler.t.sol"; +import {DonationAttackHandler} from "./handlers/simulators/DonationAttackHandler.t.sol"; + +/// @notice Helper contract to aggregate all handler contracts, inherited in BaseInvariants +abstract contract HandlerAggregator is + HubHandler, // Main handlers + SpokeHandler, + HubConfiguratorHandler, // Configurators + SpokeConfiguratorHandler, + PriceFeeSimulatorHandler, // Simulators + DonationAttackHandler +{ + /// @notice Helper function in case any handler requires additional setup + function _setUpHandlers() internal {} +} diff --git a/tests/enigma-dark-invariants/Invariants.t.sol b/tests/enigma-dark-invariants/Invariants.t.sol new file mode 100644 index 000000000..72a0d11b4 --- /dev/null +++ b/tests/enigma-dark-invariants/Invariants.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Invariant Contracts +import {HubInvariants} from "./invariants/HubInvariants.t.sol"; +import {SpokeInvariants} from "./invariants/SpokeInvariants.t.sol"; + +/// @title Invariants +/// @notice Wrappers for the protocol invariants implemented in each invariants contract +/// @dev recognised by Echidna when property mode is activated +/// @dev Inherits HubInvariants, SpokeInvariants +abstract contract Invariants is HubInvariants, SpokeInvariants { + /////////////////////////////////////////////////////////////////////////////////////////////// + // BASE INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function invariant_INV_HUB() public returns (bool) { + // Applied per assetId registered in the hub + for (uint256 i; i < baseAssets.length; i++) { + uint256 assetId = baseAssets[i].assetId; + + // Hub invariants + assert_INV_HUB_A(assetId); + assert_INV_HUB_B(assetId); + assert_INV_HUB_C(assetId); + assert_INV_HUB_EF(assetId); + assert_INV_HUB_GH(assetId); + assert_INV_HUB_I(assetId, baseAssets[i].underlying); + assert_INV_HUB_K(assetId); + assert_INV_HUB_L(assetId); + } + + return true; + } + + function invariant_INV_SP() public returns (bool) { + // Applied per spoke + for (uint256 i; i < spokesAddresses.length; i++) { + address spoke = spokesAddresses[i]; + + // Applied per actor per spoke + for (uint256 j; j < actorAddresses.length; j++) { + assert_INV_SP_D(spoke, actorAddresses[j]); + } + + // Applied per reserve of the spoke + for (uint256 j; j < spokeReserveIds[spoke].length; j++) { + uint256 reserveId = _getReserveId(spoke, j); + assert_INV_SP_A(spoke, reserveId); + assert_INV_SP_C(spoke, reserveId); + + // Applied per actor per reserve of the spoke + for (uint256 k; k < actorAddresses.length; k++) { + assert_INV_SP_B(spoke, reserveId, actorAddresses[k]); + } + } + } + return true; + } +} diff --git a/tests/enigma-dark-invariants/README.md b/tests/enigma-dark-invariants/README.md new file mode 100644 index 000000000..015fd6b94 --- /dev/null +++ b/tests/enigma-dark-invariants/README.md @@ -0,0 +1,61 @@ +# Enigma Dark – Invariant & Fuzzing Suite + +This folder contains a Handler-based invariant testing suite for the Aave v4 protocol. It performs stateful fuzzing (supply/withdraw/borrow/repay, liquidations, config updates) through handler contracts and checks system-level postconditions after every call and for every state. + +This suite helps identify invariants violations and protocol misconfigurations via stateful fuzzing. + +## Tooling + +This suite is compatible with Echidna, Medusa, and Foundry. + +## Project Layout + +- Setup + - `Setup.t.sol` – deploys core protocol (Hub, Spokes, Oracle, IR strategy), assets, and actors + - `base/` – shared storage, hooks plumbing, assertions, and helpers +- Execution + - `HandlerAggregator.t.sol` – collects handlers used by the fuzzer + - `handlers/` – user- and admin-facing action drivers (Spoke, TreasurySpoke, Hub/Spoke configurators) + - `hooks/` – before/after hooks, state snapshots, and postcondition checks + - `specs/` – property strings + - `invariants/` – invariant entrypoints (`invariant_*`) and campaign wiring + - `replays/` – minimal repro tests for failures +- Entrypoint + - `Tester.t.sol` – top-level test contract that wires the suite together + +## Workflow + +- The fuzzer calls into handlers with fuzzed inputs. +- Each call is wrapped by hooks that snapshot state and then assert postconditions. +- Handlers use actor proxies to simulate realistic multi-user flows and respect protocol roles. +- Admin handlers exercise configuration updates (reserve config, dynamic reserve config, liquidation settings). + +## Running tests + +- Echidna: + - Property mode: + ```bash + make echidna + ``` + - Assertion mode: + ```bash + make echidna-assert + ``` + - Exploration mode: + ```bash + make echidna-explore + ``` + - Generate Replay Tests: + ```bash + make runes + ``` +- Medusa: + - Property & Assertion mode: + ```bash + make medusa + ``` +- Foundry Invariants: + ```bash + make foundry-invariants + ``` + diff --git a/tests/enigma-dark-invariants/Setup.t.sol b/tests/enigma-dark-invariants/Setup.t.sol new file mode 100644 index 000000000..4ccc4f9de --- /dev/null +++ b/tests/enigma-dark-invariants/Setup.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Libraries +import {CREATE3} from "./utils/CREATE3.sol"; +import {Constants} from "tests/Constants.sol"; + +// Interfaces +import {IAaveOracle} from "src/spoke/interfaces/IAaveOracle.sol"; +import {ISpoke} from "src/spoke/Spoke.sol"; +import {ITreasurySpoke} from "src/spoke/TreasurySpoke.sol"; +import {IHub} from "src/hub/Hub.sol"; + +// Test Contracts +import {Actor} from "./utils/Actor.sol"; +import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; +import {MockPriceFeedSimulator} from "tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol"; + +// Contracts +import {BaseTest} from "./base/BaseTest.t.sol"; +import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; +import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; +import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; +import {Hub} from "src/hub/Hub.sol"; +import {TreasurySpoke} from "src/spoke/TreasurySpoke.sol"; +import {SpokeInstance} from "src/spoke/instances/SpokeInstance.sol"; +import {Spoke} from "src/spoke/Spoke.sol"; +import {TransparentUpgradeableProxy} from "src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol"; +import {AaveOracle} from "src/spoke/AaveOracle.sol"; + +/// @notice Setup contract for the invariant test Suite, inherited by Tester +contract Setup is BaseTest { + /// @notice Number of actors to deploy + function _setUp() internal { + // Deploy the suite assets + _deployAssets(); + + // Deploy protocol contracts and protocol actors + _deployProtocolCore(); + + // Configure the token list on the protocol + _configureTokenList(); + + // Deploy actors + _setUpActors(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSETS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy the suite assets + function _deployAssets() internal { + usdc = new TestnetERC20("USDC", "USDC", 6); + weth = new TestnetERC20("WETH", "WETH", 18); + usdcAssetId = 0; + wethAssetId = 1; + + baseAssets.push(AssetInfo({assetId: usdcAssetId, underlying: address(usdc), decimals: 6})); + baseAssets.push(AssetInfo({assetId: wethAssetId, underlying: address(weth), decimals: 18})); + + vm.label(address(usdc), "usdc"); + vm.label(address(weth), "weth"); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CORE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy protocol core contracts + function _deployProtocolCore() internal { + accessManager = new AccessManager(admin); + hub = new Hub(address(accessManager)); + + irStrategy = new AssetInterestRateStrategy(address(hub)); + (spoke1, oracle1) = _deploySpokeWithOracle(admin, address(accessManager), "Spoke 1 (USD)"); + (spoke2, oracle2) = _deploySpokeWithOracle(admin, address(accessManager), "Spoke 2 (USD)"); + spokesAddresses.push(address(spoke1)); // TODO integrate spoke 2 and treasury spoke + treasurySpoke = ITreasurySpoke(new TreasurySpoke(admin, address(hub))); + + vm.label(address(spoke1), "spoke1"); + vm.label(address(spoke2), "spoke2"); + vm.label(address(treasurySpoke), "treasurySpoke"); + vm.label(address(irStrategy), "irStrategy"); + vm.label(address(accessManager), "accessManager"); + vm.label(address(hub), "hub"); + } + + /// @notice Deploy a spoke with an oracle using CREATE3 + function _deploySpokeWithOracle(address proxyAdminOwner, address _accessManager, string memory _oracleDesc) + internal + returns (ISpoke, IAaveOracle) + { + bytes32 salt = keccak256(abi.encodePacked(_oracleDesc)); + address predictedOracle = CREATE3.predictDeterministicAddress(salt, admin); + address spokeImpl = address(new SpokeInstance(predictedOracle)); + + ISpoke spoke = ISpoke(_proxify(spokeImpl, proxyAdminOwner, abi.encodeCall(Spoke.initialize, (_accessManager)))); + IAaveOracle oracle = IAaveOracle( + CREATE3.deployDeterministic( + abi.encodePacked(type(AaveOracle).creationCode, abi.encode(address(spoke), uint8(8), _oracleDesc)), salt + ) + ); + assertEq(address(oracle), predictedOracle, "predictedOracle mismatch"); + assertEq(spoke.ORACLE(), address(oracle), "spoke.ORACLE() mismatch"); + assertEq(oracle.SPOKE(), address(spoke), "oracle.SPOKE() mismatch"); + return (spoke, oracle); + } + + /// @notice Proxify an implementation contract using TransparentUpgradeableProxy + function _proxify(address impl, address proxyAdminOwner, bytes memory initData) internal returns (address) { + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(impl, proxyAdminOwner, initData); + return address(proxy); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CONFIGS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _configureTokenList() internal { + IHub.SpokeConfig memory spokeConfig = IHub.SpokeConfig({ + active: true, + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP + }); + + bytes memory encodedIrData = abi.encode( + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 90_00, // 90.00% + baseVariableBorrowRate: 5_00, // 5.00% + variableRateSlope1: 5_00, // 5.00% + variableRateSlope2: 5_00 // 5.00% + }) + ); + + // Add USDC + hub.addAsset(address(usdc), usdc.decimals(), address(treasurySpoke), address(irStrategy), encodedIrData); + hub.updateAssetConfig( + usdcAssetId, + IHub.AssetConfig({ + liquidityFee: 5_00, + feeReceiver: address(treasurySpoke), + irStrategy: address(irStrategy), + reinvestmentController: address(0) // TODO should this be integrated? + }), + new bytes(0) + ); + + // Add WETH + hub.addAsset(address(weth), weth.decimals(), address(treasurySpoke), address(irStrategy), encodedIrData); + hub.updateAssetConfig( + wethAssetId, + IHub.AssetConfig({ + liquidityFee: 10_00, + feeReceiver: address(treasurySpoke), + irStrategy: address(irStrategy), + reinvestmentController: address(0) + }), + new bytes(0) + ); + + // Spoke 1 reserve configs + spokeInfo[spoke1].weth.reserveConfig = + ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 15_00}); + spokeInfo[spoke1].weth.dynReserveConfig = + ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); + + spokeInfo[spoke1].usdc.reserveConfig = + ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 50_00}); + spokeInfo[spoke1].usdc.dynReserveConfig = + ISpoke.DynamicReserveConfig({collateralFactor: 78_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); + + priceFeeds.push(_deployMockPriceFeed(spoke1, 1e8)); + priceFeeds.push(_deployMockPriceFeed(spoke1, 2000e8)); + + spokeInfo[spoke1].usdc.reserveId = spoke1.addReserve( + address(hub), + usdcAssetId, + priceFeeds[0], + spokeInfo[spoke1].usdc.reserveConfig, + spokeInfo[spoke1].usdc.dynReserveConfig + ); + + spokeInfo[spoke1].weth.reserveId = spoke1.addReserve( + address(hub), + wethAssetId, + priceFeeds[1], + spokeInfo[spoke1].weth.reserveConfig, + spokeInfo[spoke1].weth.dynReserveConfig + ); + + assetIdToReserveId[address(spoke1)][usdcAssetId] = spokeInfo[spoke1].usdc.reserveId; + assetIdToReserveId[address(spoke1)][wethAssetId] = spokeInfo[spoke1].weth.reserveId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].usdc.reserveId] = usdcAssetId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].weth.reserveId] = wethAssetId; + + spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].usdc.reserveId); + spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].weth.reserveId); + + hub.addSpoke(wethAssetId, address(spoke1), spokeConfig); + hub.addSpoke(usdcAssetId, address(spoke1), spokeConfig); + + // TODO integrate spoke 2 + } + + function _deployMockPriceFeed(ISpoke spoke, uint256 price) internal returns (address) { + AaveOracle oracle = AaveOracle(spoke.ORACLE()); + return address(new MockPriceFeedSimulator(oracle.DECIMALS(), oracle.DESCRIPTION(), price)); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy protocol actors and initialize their balances + function _setUpActors() internal { + // Initialize the three actors of the fuzzers + address[] memory addresses = new address[](3); + addresses[0] = USER1; + addresses[1] = USER2; + addresses[2] = USER3; + + // Initialize the tokens array + address[] memory tokens = new address[](2); + tokens[0] = address(usdc); + tokens[1] = address(weth); + + address[] memory contracts = new address[](3); + contracts[0] = address(spoke1); + contracts[1] = address(spoke2); + contracts[2] = address(hub); + + for (uint256 i; i < NUMBER_OF_ACTORS; i++) { + // Deploy actor proxies and approve system contracts + address _actor = _setUpActor(addresses[i], tokens, contracts); + //vm.label(address(_actor), string.concat("actor", vm.toString(i + 1))); + + // Mint initial balances to actors + for (uint256 j = 0; j < tokens.length; j++) { + TestnetERC20 _token = TestnetERC20(tokens[j]); + _token.mint(_actor, INITIAL_BALANCE); + } + actorAddresses.push(_actor); + } + } + + /// @notice Deploy an actor proxy contract for a user address + /// @param userAddress Address of the user + /// @param tokens Array of token addresses + /// @param contracts Array of contract addresses to aprove tokens to + /// @return actorAddress Address of the deployed actor + function _setUpActor(address userAddress, address[] memory tokens, address[] memory contracts) + internal + returns (address actorAddress) + { + bool success; + Actor _actor = new Actor(tokens, contracts); + actors[userAddress] = _actor; + (success,) = address(_actor).call{value: INITIAL_ETH_BALANCE}(""); + assert(success); + actorAddress = address(_actor); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // LOGGING // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _logSetup() internal { + // Log the setup + } +} diff --git a/tests/enigma-dark-invariants/SpecAggregator.t.sol b/tests/enigma-dark-invariants/SpecAggregator.t.sol new file mode 100644 index 000000000..ebbf14686 --- /dev/null +++ b/tests/enigma-dark-invariants/SpecAggregator.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Test Contracts +import {InvariantsSpec} from "./specs/InvariantsSpec.t.sol"; +import {PostconditionsSpec} from "./specs/PostconditionsSpec.t.sol"; + +/// @title SpecAggregator +/// @notice Helper contract to aggregate all spec contracts, inherited in BaseHooks +/// @dev inherits InvariantsSpec, PostconditionsSpec +abstract contract SpecAggregator is + InvariantsSpec, + PostconditionsSpec +{ +/////////////////////////////////////////////////////////////////////////////////////////////// +// PROPERTY TYPES // +/////////////////////////////////////////////////////////////////////////////////////////////// + +/// In this invariant testing framework, there are two types of properties: + +/// - INVARIANTS (INV): +/// - Properties that should always hold true in the system. +/// - Implemented in the /invariants folder. + +/// - POSTCONDITIONS: +/// - Properties that should hold true after an action is executed. +/// - Implemented in the /hooks and /handlers folders. + +/// - There are two types of POSTCONDITIONS: + +/// - GLOBAL POSTCONDITIONS (GPOST): +/// - Properties that should always hold true after any action is executed. +/// - Checked in the `_checkPostConditions` function within the HookAggregator contract. + +/// - HANDLER-SPECIFIC POSTCONDITIONS (HSPOST): +/// - Properties that should hold true after a specific action is executed in a specific context. +/// - Implemented within each handler function, under the HANDLER-SPECIFIC POSTCONDITIONS section. + +/// - ERC4626 PROPERTIES: +/// - Properties that should always hold true in the system, which check compliance with the ERC4626 standard. +/// - Implemented across the testing suite as invariants, postconditions and specific custom handlers. +} diff --git a/tests/enigma-dark-invariants/Tester.t.sol b/tests/enigma-dark-invariants/Tester.t.sol new file mode 100644 index 000000000..53a7493ec --- /dev/null +++ b/tests/enigma-dark-invariants/Tester.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Invariants} from "./Invariants.t.sol"; +import {Setup} from "./Setup.t.sol"; + +/// @title Tester +/// @notice Entry point for invariant testing, inherits all contracts, invariants & handler +/// @dev Mono contract that contains all the testing logic +contract Tester is Invariants, Setup { + constructor() payable { + // Deploy protocol contracts and protocol actors + setUp(); + } + + /// @dev Foundry compatibility faster setup debugging + function setUp() internal { + // Deploy protocol contracts and protocol actors + _setUp(); + } +} diff --git a/tests/enigma-dark-invariants/TesterFoundry.t.sol b/tests/enigma-dark-invariants/TesterFoundry.t.sol new file mode 100644 index 000000000..0ffe52372 --- /dev/null +++ b/tests/enigma-dark-invariants/TesterFoundry.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Contracts +import {StdInvariant} from "forge-std/StdInvariant.sol"; +import {Invariants} from "./Invariants.t.sol"; +import {Setup} from "./Setup.t.sol"; + +/// @title TesterFoundry +/// @notice Entry point for invariant testing, inherits all contracts, invariants & handler +/// @dev Mono contract that contains all the testing logic +contract TesterFoundry is Invariants, Setup, StdInvariant { + /// forge-config: default.invariant.fail-on-revert = false + /// forge-config: default.invariant.runs = 1000 + /// forge-config: default.invariant.depth = 100 + /// forge-config: default.invariant.corpus-dir = "tests/enigma-dark-invariants/_corpus/foundry" + /// forge-config: default.invariant.show-solidity = true + /// forge-config: default.invariant.show-metrics = true + /// forge-config: default.fuzz.seed = '0x1' + /// forge-config: default.fuzz.include-storage = true + /// forge-config: default.fuzz.include-push-bytes = true + /// forge-config: default.fuzz.call-override = false + /// forge-config: default.fuzz.dictionary-weight = 80 + /// forge-config: default.fuzz.shrink-sequence = true + /// @dev Foundry compatibility faster setup debugging + function setUp() public { + // Deploy protocol contracts and protocol actors + _setUp(); + + // Set the target contract + targetContract(address(this)); + + // Exclude target selectors + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = bytes4(keccak256("checkPostConditions()")); + + excludeSelector(FuzzSelector({addr: address(this), selectors: selectors})); + + // Set the target senders + targetSender(USER1); + targetSender(USER2); + targetSender(USER3); + } +} diff --git a/tests/enigma-dark-invariants/_config/echidna_config.yaml b/tests/enigma-dark-invariants/_config/echidna_config.yaml new file mode 100644 index 000000000..3d194a734 --- /dev/null +++ b/tests/enigma-dark-invariants/_config/echidna_config.yaml @@ -0,0 +1,61 @@ +#codeSize max code size for deployed contratcs (default 24576, per EIP-170) +codeSize: 224576 + +#whether ot not to use the multi-abi mode of testing +#it’s not working for us, see: https://github.com/crytic/echidna/issues/547 +#multi-abi: true + +#balanceAddr is default balance for addresses +balanceAddr: 0x1000000000000000000000000 +#balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) +balanceContract: 0x1000000000000000000000000000000000000000000000000 + +#testLimit is the number of test sequences to run +testLimit: 20000000 + +#seqLen defines how many transactions are in a test sequence +seqLen: 300 + +#shrinkLimit determines how much effort is spent shrinking failing sequences +shrinkLimit: 2500 + +#propMaxGas defines gas cost at which a property fails +propMaxGas: 1000000000 + +#testMaxGas is a gas limit; does not cause failure, but terminates sequence +testMaxGas: 1000000000 + +# list of methods to filter +filterFunctions: [ "Tester.checkPostConditions()" ] +# by default, blacklist methods in filterFunctions +#filterBlacklist: false + +prefix: "invariant_" + +#stopOnFail makes echidna terminate as soon as any property fails and has been shrunk +stopOnFail: false + +#coverage controls coverage guided testing +coverage: true + +# list of file formats to save coverage reports in; default is all possible formats +coverageFormats: [ "html" ] + +#directory to save the corpus; by default is disabled +corpusDir: "tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus" +# constants for corpus mutations (for experimentation only) +#mutConsts: [100, 1, 1] + +#remappings +cryticArgs: [ "--solc-remaps", "forge-std/=lib/forge-std/src/", "--compile-libraries=(LiquidationLogic,0xf01)" ] + +deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] + +# maximum value to send to payable functions +maxValue: 1e+30 # 100000000000 eth + +#quiet produces (much) less verbose output +quiet: false + +# concurrent workers +workers: 10 diff --git a/tests/enigma-dark-invariants/base/BaseHandler.t.sol b/tests/enigma-dark-invariants/base/BaseHandler.t.sol new file mode 100644 index 000000000..f7da42eb0 --- /dev/null +++ b/tests/enigma-dark-invariants/base/BaseHandler.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +// Libraries +import {MockERC20} from 'tests/mocks/MockERC20.sol'; + +// Contracts +import {Actor} from "../utils/Actor.sol"; +import {HookAggregator} from "../hooks/HookAggregator.t.sol"; + +/// @title BaseHandler +/// @notice Contains common logic for all handlers +/// @dev inherits all suite assertions since per action assertions are implmenteds in the handlers +contract BaseHandler is HookAggregator { + /////////////////////////////////////////////////////////////////////////////////////////////// + // MODIFIERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SHARED VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Helper function to randomize a uint256 seed with a string salt + function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(seed, salt))); + } + + /// @notice Helper function to get a random value + function _getRandomValue(uint256 modulus) internal view returns (uint256) { + uint256 randomNumber = uint256(keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender))); + return randomNumber % modulus; // Adjust the modulus to the desired range + } + + /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor + function _approve(address token, Actor actor_, address spender, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor_.proxy(token, abi.encodeWithSelector(0x095ea7b3, spender, amount)); + require(success, string(returnData)); + } + + /// @notice Helper function to safely approve an amount of tokens to a spender + + function _approve(address token, address owner, address spender, uint256 amount) internal { + vm.prank(owner); + _safeApprove(token, spender, 0); + vm.prank(owner); + _safeApprove(token, spender, amount); + } + + /// @notice Helper function to safely approve an amount of tokens to a spender + /// @dev This function is used to revert on failed approvals + function _safeApprove(address token, address spender, uint256 amount) internal { + (bool success, bytes memory retdata) = + token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); + assert(success); + if (retdata.length > 0) assert(abi.decode(retdata, (bool))); + } + + /// @notice Helper function to mint an amount of tokens to an address + function _mint(address token, address receiver, uint256 amount) internal { + MockERC20(token).mint(receiver, amount); + } + + /// @notice Helper function to mint an amount of tokens to an address and approve them to a spender + /// @param token Address of the token to mint + /// @param owner Address of the new owner of the tokens + /// @param spender Address of the spender to approve the tokens to + /// @param amount Amount of tokens to mint and approve + function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { + _mint(token, owner, amount); + _approve(token, owner, spender, amount); + } +} diff --git a/tests/enigma-dark-invariants/base/BaseHooks.t.sol b/tests/enigma-dark-invariants/base/BaseHooks.t.sol new file mode 100644 index 000000000..68e4ef887 --- /dev/null +++ b/tests/enigma-dark-invariants/base/BaseHooks.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Contracts +import {ProtocolAssertions} from "./ProtocolAssertions.t.sol"; + +// Test Contracts +import {SpecAggregator} from "../SpecAggregator.t.sol"; + +/// @title BaseHooks +/// @notice Contains common logic for all hooks +/// @dev inherits all suite assertions since per-action assertions are implemented in the handlers +/// @dev inherits SpecAggregator +contract BaseHooks is ProtocolAssertions, SpecAggregator { +/////////////////////////////////////////////////////////////////////////////////////////////// +// HELPERS // +/////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/base/BaseStorage.t.sol b/tests/enigma-dark-invariants/base/BaseStorage.t.sol new file mode 100644 index 000000000..31b9d1ca4 --- /dev/null +++ b/tests/enigma-dark-invariants/base/BaseStorage.t.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Contracts +import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; +import {IHub} from "src/hub/Hub.sol"; +import {ITreasurySpoke} from "src/spoke/TreasurySpoke.sol"; +import {ISpoke} from "src/spoke/Spoke.sol"; +import {IAaveOracle} from "src/spoke/interfaces/IAaveOracle.sol"; +import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; +import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; + +// Mock Contracts + +// Test Contracts + +// Utils +import {Actor} from "../utils/Actor.sol"; + +/// @notice BaseStorage contract for all test contracts, works in tandem with BaseTest +abstract contract BaseStorage { + /////////////////////////////////////////////////////////////////////////////////////////////// + // CONSTANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + uint256 constant MAX_TOKEN_AMOUNT = 1e29; + + uint256 constant ONE_DAY = 1 days; + uint256 constant ONE_MONTH = ONE_YEAR / 12; + uint256 constant ONE_YEAR = 365 days; + + uint256 internal constant NUMBER_OF_ACTORS = 3; + uint256 internal constant INITIAL_ETH_BALANCE = 1e26; + uint256 internal constant INITIAL_COLL_BALANCE = 1e21; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice The address of the admin, the tester itself + address internal admin = address(this); + + /// @notice Stores the actor during a handler call + Actor internal actor; + + /// @notice Mapping of fuzzer user addresses to actors + mapping(address => Actor) internal actors; + + /// @notice Array of all actor addresses + address[] internal actorAddresses; + + /// @notice The address that is targeted when executing an action (OPTIONAL) + address internal targetActor; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSETS STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice The USDC token + TestnetERC20 internal usdc; + /// @notice The WETH token + TestnetERC20 internal weth; + + /// @notice The asset IDs of the USDC and WETH tokens + uint256 internal usdcAssetId; + uint256 internal wethAssetId; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SUITE STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // PROTOCOL CONTRACTS + IHub internal hub; + + ITreasurySpoke internal treasurySpoke; + ISpoke internal spoke1; + ISpoke internal spoke2; + + // ORACLES + IAaveOracle internal oracle1; + IAaveOracle internal oracle2; + + // CONFIGURATION + AssetInterestRateStrategy internal irStrategy; + AccessManager internal accessManager; + + // MOCKS + + /////////////////////////////////////////////////////////////////////////////////////////////// + // EXTRA VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // ASSETS + /// @notice Array of base assets for the suite + AssetInfo[] internal baseAssets; + + // SPOKES + /// @notice Array of spokes addresses for the suite + address[] internal spokesAddresses; + /// @notice Spoke configurations + mapping(ISpoke => SpokeInfo) internal spokeInfo; + /// @notice Spoke reserveIds + mapping(address => uint256[]) internal spokeReserveIds; + /// @notice Spoke reserveIds to global assetIds + mapping(address => mapping(uint256 => uint256)) internal reserveIdToAssetId; + /// @notice Spoke assetIds to reserveIds info + mapping(address => mapping(uint256 => uint256)) internal assetIdToReserveId; + + // PRICE FEEDS + address[] internal priceFeeds; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct SpokeInfo { + ReserveInfo weth; + ReserveInfo usdc; + uint256 MAX_ALLOWED_ASSET_ID; + } + + struct ReserveInfo { + uint256 reserveId; + ISpoke.ReserveConfig reserveConfig; + ISpoke.DynamicReserveConfig dynReserveConfig; + } + + struct AssetInfo { + uint256 assetId; + address underlying; + uint8 decimals; + } +} diff --git a/tests/enigma-dark-invariants/base/BaseTest.t.sol b/tests/enigma-dark-invariants/base/BaseTest.t.sol new file mode 100644 index 000000000..9f4d2df0e --- /dev/null +++ b/tests/enigma-dark-invariants/base/BaseTest.t.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Libraries +import {Vm} from "forge-std/Base.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; +import "forge-std/console.sol"; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; +import {PropertiesConstants} from "../utils/PropertiesConstants.sol"; +import {StdAsserts} from "../utils/StdAsserts.sol"; + +// Base +import {BaseStorage} from "./BaseStorage.t.sol"; + +/// @notice Base contract for all test contracts extends BaseStorage +/// @dev Provides setup modifier and cheat code setup +/// @dev inherits Storage, Testing constants assertions and utils needed for testing +abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdUtils { + bool internal IS_TEST = true; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTOR PROXY MECHANISM // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Actor proxy mechanism + modifier setup() virtual { + console.log("setup", msg.sender); + actor = actors[msg.sender]; + console.log("actor", address(actor)); + _; + delete actor; + } + + /// @dev Solves medusa backward time warp issue + modifier monotonicTimestamp() virtual { + /// @dev: Implement monotonic timestamp if needed + _; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CALLBACKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + receive() external payable {} + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CHEAT CODE SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. + address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + + /// @dev Virtual machine instance + Vm internal constant vm = Vm(VM_ADDRESS); + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS: RANDOM GETTERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Get a random actor proxy address + function _getRandomActor(uint256 _i) internal view returns (address) { + uint256 _actorIndex = _i % NUMBER_OF_ACTORS; + return actorAddresses[_actorIndex]; + } + + /// @notice Helper function to get a random base asset + function _getRandomBaseAsset(uint256 i) internal view returns (address) { + uint256 _assetIndex = i % baseAssets.length; + return baseAssets[_assetIndex].underlying; + } + + /// @notice Helper function to get a random base asset id + function _getRandomBaseAssetId(uint256 i) internal view returns (uint256) { + uint256 _assetIndex = i % baseAssets.length; + return baseAssets[_assetIndex].assetId; + } + + /// @notice Helper function to get random base asset full info + function _getRandomBaseAssetFullInfo(uint256 i) internal view returns (AssetInfo memory) { + uint256 _assetIndex = i % baseAssets.length; + return baseAssets[_assetIndex]; + } + + /// @notice Helper function to get a random spoke address + function _getRandomSpoke(uint256 i) internal view returns (address) { + uint256 _spokeIndex = i % spokesAddresses.length; + return spokesAddresses[_spokeIndex]; + } + + /// @notice Helper function to get a random reserve id from a spoke + function _getRandomReserveId(address spoke, uint256 i) internal view returns (uint256) { + uint256 _reserveIndex = i % spokeReserveIds[spoke].length; + return spokeReserveIds[spoke][_reserveIndex]; + } + + /// @notice Helper function to get a random price feed address + function _getRandomPriceFeed(uint256 i) internal view returns (address) { + uint256 _priceFeedIndex = i % priceFeeds.length; + return priceFeeds[_priceFeedIndex]; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS: GETTERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _getAssetId(address spoke, uint256 reserveId) internal view returns (uint256) { + return reserveIdToAssetId[spoke][reserveId]; + } + + function _getReserveId(address spoke, uint256 assetId) internal view returns (uint256) { + return assetIdToReserveId[spoke][assetId]; + } + + function _isHealthy(address spoke, address user) internal view returns (bool) { + return + ISpoke(spoke).getUserAccountData(user).healthFactor >= ISpoke(spoke).HEALTH_FACTOR_LIQUIDATION_THRESHOLD(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Get a random address + function _makeAddr(string memory name) internal pure returns (address addr) { + uint256 privateKey = uint256(keccak256(abi.encodePacked(name))); + addr = vm.addr(privateKey); + } + + /// @notice Helper function to transfer tokens by actor + function _transferByActor(address token, address to, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor.proxy(token, abi.encodeWithSelector(IERC20.transfer.selector, to, amount)); + require(success, string(returnData)); + } + + /// @notice Helper function to approve tokens by actor + function _approveByActor(address token, address spender, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor.proxy(token, abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); + require(success, string(returnData)); + } +} diff --git a/tests/enigma-dark-invariants/base/ProtocolAssertions.t.sol b/tests/enigma-dark-invariants/base/ProtocolAssertions.t.sol new file mode 100644 index 000000000..355bdfe56 --- /dev/null +++ b/tests/enigma-dark-invariants/base/ProtocolAssertions.t.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Base +import {BaseTest} from "./BaseTest.t.sol"; +import {StdAsserts} from "../utils/StdAsserts.sol"; + +/// @title ProtocolAssertions +/// @notice Helper contract for protocol specific assertions +contract ProtocolAssertions is StdAsserts, BaseTest {} diff --git a/tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol b/tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol new file mode 100644 index 000000000..fc82f5ca1 --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IHubConfiguratorHandler} from "../interfaces/IHubConfiguratorHandler.sol"; + +// Libraries +import "forge-std/console.sol"; + +// Test Contracts +import {Actor} from "../../utils/Actor.sol"; +import {BaseHandler} from "../../base/BaseHandler.t.sol"; + +/// @title HubConfiguratorHandler +/// @notice Handler test contract for a set of actions +contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// +/* + + E.g. num of active pools + uint256 public activePools; + + */ + +/////////////////////////////////////////////////////////////////////////////////////////////// +// ACTIONS // +/////////////////////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////////////////////// +// OWNER ACTIONS // +/////////////////////////////////////////////////////////////////////////////////////////////// + +// TODO: +// updateLiquidityFee +// updateFeeConfig +// updateInterestRateStrategy +// freezeAsset +// pauseAsset +// updateSpokeActive +// updateSpokeSupplyCap +// updateSpokeDrawCap +// updateSpokeCaps +// pauseSpoke +// freezeSpoke +// updateInterestRateData + +/////////////////////////////////////////////////////////////////////////////////////////////// +// HELPERS // +/////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol b/tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol new file mode 100644 index 000000000..bb5c60217 --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IHubHandler} from "../interfaces/IHubHandler.sol"; + +// Libraries +import "forge-std/console.sol"; + +// Test Contracts +import {Actor} from "../../utils/Actor.sol"; +import {BaseHandler} from "../../base/BaseHandler.t.sol"; + +/// @title HubHandler +/// @notice Handler test contract for a set of actions +contract HubHandler is BaseHandler, IHubHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /* + + E.g. num of active pools + uint256 public activePools; + + */ + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // TODO: + // Configurator: + // updateAssetConfig + // updateSpokeConfig + // setInterestRateData + + // Reinvestment controller: + // sweep + // reclaim + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/handlers/interfaces/IHubConfiguratorHandler.sol b/tests/enigma-dark-invariants/handlers/interfaces/IHubConfiguratorHandler.sol new file mode 100644 index 000000000..d12003b5e --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/interfaces/IHubConfiguratorHandler.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title IHubConfiguratorHandler +/// @notice Interface for the HubConfiguratorHandler +interface IHubConfiguratorHandler {} diff --git a/tests/enigma-dark-invariants/handlers/interfaces/IHubHandler.sol b/tests/enigma-dark-invariants/handlers/interfaces/IHubHandler.sol new file mode 100644 index 000000000..c86de9be2 --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/interfaces/IHubHandler.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title IHubHandler +/// @notice Interface for the HubHandler +interface IHubHandler {} diff --git a/tests/enigma-dark-invariants/handlers/interfaces/ISpokeConfiguratorHandler.sol b/tests/enigma-dark-invariants/handlers/interfaces/ISpokeConfiguratorHandler.sol new file mode 100644 index 000000000..a5d1ce267 --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/interfaces/ISpokeConfiguratorHandler.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title ISpokeConfiguratorHandler +/// @notice Interface for the SpokeConfiguratorHandler +interface ISpokeConfiguratorHandler {} diff --git a/tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol b/tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol new file mode 100644 index 000000000..9cc454d37 --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title ISpokeHandler +/// @notice Interface for the SpokeHandler +interface ISpokeHandler { + function supply(uint256 amount, uint8 i, uint8 j, uint8 k) external; + function withdraw(uint256 amount, uint8 i, uint8 j, uint8 k) external; + function borrow(uint256 amount, uint8 i, uint8 j, uint8 k) external; + function repay(uint256 amount, uint8 i, uint8 j, uint8 k) external; + function liquidationCall(uint256 debtToCover, uint8 i, uint8 j, uint8 k, uint8 l) external; + function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external; + function updateUserRiskPremium(uint8 i) external; + function updateUserDynamicConfig(uint8 i) external; +} diff --git a/tests/enigma-dark-invariants/handlers/interfaces/ITreasurySpokeHandler.sol b/tests/enigma-dark-invariants/handlers/interfaces/ITreasurySpokeHandler.sol new file mode 100644 index 000000000..07108f442 --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/interfaces/ITreasurySpokeHandler.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title ITreasurySpokeHandler +/// @notice Interface for the TreasurySpokeHandler +interface ITreasurySpokeHandler {} diff --git a/tests/enigma-dark-invariants/handlers/simulators/.gitkeep b/tests/enigma-dark-invariants/handlers/simulators/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol b/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol new file mode 100644 index 000000000..c8a0353b0 --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Test Contracts +import {BaseHandler} from "../../base/BaseHandler.t.sol"; +import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; + +/// @title DonationAttackHandler +/// @notice Handler test contract for a set of actions +contract DonationAttackHandler is BaseHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function donateUnderlyingToHub(uint256 amount, uint8 i) external { + address underlying = _getRandomBaseAsset(i); + + TestnetERC20(underlying).mint(address(hub), amount); + } + + function donateUnderlyingToSpoke(uint256 amount, uint8 i, uint8 j) external { + address spoke = _getRandomSpoke(j); + address underlying = _getRandomBaseAsset(i); + + TestnetERC20(underlying).mint(address(spoke), amount); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol b/tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol new file mode 100644 index 000000000..56ea6c24f --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Test Contracts +import {BaseHandler} from "../../base/BaseHandler.t.sol"; +import {MockPriceFeedSimulator} from "../../utils/mocks/MockPriceFeedSimulator.sol"; + +/// @title PriceFeeSimulatorHandler +/// @notice Handler test contract for a set of actions +contract PriceFeeSimulatorHandler is BaseHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function setPrice(uint256 price, uint8 i) public { + address priceFeed = _getRandomPriceFeed(i); + + MockPriceFeedSimulator(priceFeed).setPrice(price); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol b/tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol new file mode 100644 index 000000000..c8399e47b --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ISpokeConfiguratorHandler} from "../interfaces/ISpokeConfiguratorHandler.sol"; + +// Libraries +import "forge-std/console.sol"; + +// Test Contracts +import {Actor} from "../../utils/Actor.sol"; +import {BaseHandler} from "../../base/BaseHandler.t.sol"; + +/// @title SpokeConfiguratorHandler +/// @notice Handler test contract for a set of actions +contract SpokeConfiguratorHandler is BaseHandler, ISpokeConfiguratorHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// +/* + + E.g. num of active pools + uint256 public activePools; + + */ + +/////////////////////////////////////////////////////////////////////////////////////////////// +// ACTIONS // +/////////////////////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////////////////////// +// OWNER ACTIONS // +/////////////////////////////////////////////////////////////////////////////////////////////// + +// TODO +// updateLiquidationTargetHealthFactor +// updateHealthFactorForMaxBonus +// updateLiquidationBonusFactor +// updateLiquidationConfig +// updatePaused +// updateFrozen +// updateBorrowable +// updateCollateralRisk +// addCollateralFactor +// updateCollateralFactor +// addLiquidationBonus +// updateMaxLiquidationBonus +// addLiquidationFee +// updateLiquidationFee +// addDynamicReserveConfig +// updateDynamicReserveConfig +// pauseAllReserves +// freezeAllReserves + +/////////////////////////////////////////////////////////////////////////////////////////////// +// HELPERS // +/////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol b/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol new file mode 100644 index 000000000..fe085adbb --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; +import {ISpokeHandler} from "../interfaces/ISpokeHandler.sol"; + +// Libraries +import "forge-std/console.sol"; + +// Test Contracts +import {Actor} from "../../utils/Actor.sol"; +import {BaseHandler} from "../../base/BaseHandler.t.sol"; + +// Contracts +import {Spoke} from "src/spoke/Spoke.sol"; + +/// @title SpokeHandler +/// @notice Handler test contract for a set of actions +contract SpokeHandler is BaseHandler, ISpokeHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Used on the liquidation handler to store the collateral and debt reserve IDs and avoid stack to deep errors + /// @dev should be zeroed after each liquidation call + uint256 internal collateralReserveId; + uint256 internal debtReserveId; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function supply(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { + bool success; + bytes memory returnData; + + // Get one of the three actors randomly + address onBehalfOf = _getRandomActor(i); + + address spoke = _getRandomSpoke(j); + + // Get one of the reserves IDs randomly + uint256 reserveId = _getReserveId(spoke, k); + + // Register user to check postconditions + _registerUserToCheck(spoke, reserveId, onBehalfOf); + + _before(); + (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.supply, (reserveId, amount, onBehalfOf))); + + if (success) { + _after(); + } else { + revert("DefaultHandler: supply failed"); + } + } + + function withdraw(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { + bool success; + bytes memory returnData; + + // Get one of the three actors randomly + address onBehalfOf = _getRandomActor(i); + + address spoke = _getRandomSpoke(j); + + // Get one of the reserves IDs randomly + uint256 reserveId = _getReserveId(spoke, k); + + // Register user to check postconditions + _registerUserToCheck(spoke, reserveId, address(actor)); + + _before(); + (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.withdraw, (reserveId, amount, onBehalfOf))); + + if (success) { + _after(); + } else { + revert("DefaultHandler: withdraw failed"); + } + } + + function borrow(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { + bool success; + bytes memory returnData; + + // Get one of the three actors randomly + address onBehalfOf = _getRandomActor(i); + + address spoke = _getRandomSpoke(j); + + // Get one of the reserves IDs randomly + uint256 reserveId = _getReserveId(spoke, k); + + // Register user to check postconditions + _registerUserToCheck(spoke, reserveId, address(actor)); + + // Check if user is healthy + bool isHealthy = _isHealthy(spoke, onBehalfOf); + + _before(); + (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.borrow, (reserveId, amount, onBehalfOf))); + + if (success) { + _after(); + + ///// HSPOST ///// + + assertTrue(isHealthy, HSPOST_SP_D); + } else { + revert("DefaultHandler: borrow failed"); + } + } + + function repay(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { + bool success; + bytes memory returnData; + + // Get one of the three actors randomly + address onBehalfOf = _getRandomActor(i); + + address spoke = _getRandomSpoke(j); + + // Get one of the reserves IDs randomly + uint256 reserveId = _getReserveId(spoke, k); + + // Register user to check postconditions + _registerUserToCheck(spoke, reserveId, onBehalfOf); + + _before(); + (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.repay, (reserveId, amount, onBehalfOf))); + + if (success) { + _after(); + + ///// HSPOST ///// + + assertLt( + defaultVarsAfter.userVars[spoke][reserveId][onBehalfOf].totalDebt, + defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt, + HSPOST_SP_C + ); + } else { + revert("DefaultHandler: repay failed"); + } + } + + function liquidationCall(uint256 debtToCover, uint8 i, uint8 j, uint8 k, uint8 l) external setup { + bool success; + bytes memory returnData; + + // Get one of the three actors randomly + address spoke = _getRandomSpoke(j); + + // Get one of the reserves IDs randomly + collateralReserveId = _getReserveId(spoke, k); + debtReserveId = _getReserveId(spoke, l); + + uint256 debtValueInBaseCurrency = ISpoke(spoke).getUserAccountData(_getRandomActor(i)).totalDebtInBaseCurrency; + + // Register users to check postconditions: liquidated user and liquidator for both reserves + _registerUserToCheck(spoke, debtReserveId, _getRandomActor(i)); + _registerUserToCheck(spoke, collateralReserveId, _getRandomActor(i)); + _registerUserToCheck(spoke, debtReserveId, address(actor)); + _registerUserToCheck(spoke, collateralReserveId, address(actor)); + + _before(); + (success, returnData) = actor.proxy( + spoke, + abi.encodeCall(Spoke.liquidationCall, (collateralReserveId, debtReserveId, _getRandomActor(i), debtToCover)) + ); + + if (success) { + _after(); + + ///// HSPOST ///// + + uint256 userTotalDebt = ISpoke(spoke).getUserTotalDebt(debtReserveId, _getRandomActor(i)); + + assertLt(debtToCover, userTotalDebt, HSPOST_SP_LIQ_A); + + if (debtValueInBaseCurrency > ISpoke(spoke).DUST_DEBT_LIQUIDATION_THRESHOLD()) { + assertEq(userTotalDebt, 0, HSPOST_SP_LIQ_C); + } + } else { + revert("DefaultHandler: liquidationCall failed"); + } + + delete collateralReserveId; + delete debtReserveId; + } + + function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external setup { + bool success; + bytes memory returnData; + + address onBehalfOf = address(actor); + + address spoke = _getRandomSpoke(i); + + uint256 reserveId = _getReserveId(spoke, j); + + // Register user to check postconditions + _registerUserToCheck(spoke, reserveId, onBehalfOf); + + _before(); + (success, returnData) = + actor.proxy(spoke, abi.encodeCall(Spoke.setUsingAsCollateral, (reserveId, usingAsCollateral, onBehalfOf))); + + if (success) { + _after(); + } else { + revert("DefaultHandler: setUsingAsCollateral failed"); + } + } + + function updateUserRiskPremium(uint8 i) external setup { + bool success; + bytes memory returnData; + + address onBehalfOf = address(actor); + + address spoke = _getRandomSpoke(i); + + uint256 totalDebt = _getTotalDebt(spoke, onBehalfOf); + + // Register user to check postconditions + _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); + + _before(); + (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.updateUserRiskPremium, (onBehalfOf))); + + if (success) { + _after(); + + ///// HSPOST ///// + + assertApproxEqAbs( + totalDebt, totalDebt, 2, HSPOST_SP_F + ); + } else { + revert("DefaultHandler: updateUserRiskPremium failed"); + } + } + + function updateUserDynamicConfig(uint8 i) external setup { + bool success; + bytes memory returnData; + + address onBehalfOf = address(actor); + + address spoke = _getRandomSpoke(i); + + _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); + + _before(); + (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.updateUserDynamicConfig, (onBehalfOf))); + + if (success) { + _after(); + } else { + revert("DefaultHandler: updateUserDynamicConfig failed"); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // TODO + // Configurator: + // updateLiquidationConfig + // updateReserveConfig + // addDynamicReserveConfig + // updateDynamicReserveConfig + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _getTotalDebt(address spoke, address user) internal view returns (uint256) { + uint256 totalDebt; + uint256 reserveCount = spokeReserveIds[spoke].length; + for (uint256 i; i < reserveCount; i++) { + totalDebt += ISpoke(spoke).getUserTotalDebt(spokeReserveIds[spoke][i], user); + } + return totalDebt; + } +} diff --git a/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol b/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol new file mode 100644 index 000000000..df31b7cab --- /dev/null +++ b/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {ITreasurySpokeHandler} from "../interfaces/ITreasurySpokeHandler.sol"; + +// Libraries +import "forge-std/console.sol"; + +// Test Contracts +import {BaseHandler} from "../../base/BaseHandler.t.sol"; + +/// @title TreasurySpokeHandler +/// @notice Handler test contract for a set of actions +contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /* + + E.g. num of active pools + uint256 public activePools; + + */ + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function supply(uint256 amount, uint8 i) external setup { + // Get one of the reserves IDs randomly + uint256 reserveId = _getRandomReserveId(address(treasurySpoke), i); + + _before(); + try treasurySpoke.supply(reserveId, amount, msg.sender) { + _after(); + } catch { + revert("DefaultHandler: supply failed"); + } + } + + function withdraw(uint256 amount, uint8 i) external setup { + // Get one of the reserves IDs randomly + uint256 reserveId = _getRandomReserveId(address(treasurySpoke), i); + + _before(); + try treasurySpoke.withdraw(reserveId, amount, msg.sender) { + _after(); + } catch { + revert("DefaultHandler: withdraw failed"); + } + } + + function transfer(uint256 amount, uint8 i, uint8 j) external setup { + // Get one of the actors randomly + address to = _getRandomActor(i); + + // Get one of the assets IDs randomly + address asset = _getRandomBaseAsset(j); + + _before(); + try treasurySpoke.transfer(asset, to, amount) { + _after(); + } catch { + revert("DefaultHandler: transfer failed"); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/helpers/extended/.gitkeep b/tests/enigma-dark-invariants/helpers/extended/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/enigma-dark-invariants/helpers/fixtures/.gitkeep b/tests/enigma-dark-invariants/helpers/fixtures/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol new file mode 100644 index 000000000..03e94d59c --- /dev/null +++ b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Libraries +import {MathUtils} from "src/libraries/math/MathUtils.sol"; +import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; +import {PropertiesConstants} from "../utils/PropertiesConstants.sol"; +import {StdAsserts} from "../utils/StdAsserts.sol"; + +// Interfaces +import {ISpokeHandler} from "../handlers/interfaces/ISpokeHandler.sol"; +import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; +import {IHub} from "src/hub/interfaces/IHub.sol"; + +// Contracts +import {BaseHooks} from "../base/BaseHooks.t.sol"; + +/// @title DefaultBeforeAfterHooks +/// @notice Helper contract for before and after hooks, state variable caching and postconditions +/// @dev This contract is inherited by handlers +abstract contract DefaultBeforeAfterHooks is BaseHooks { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct AssetVars { + uint256 drawnIndex; + uint256 totalAssets; + uint256 totalShares; + uint256 drawn; + uint256 premium; + } + + struct UserVars { + uint256 premiumDebt; + uint256 totalDebt; + } + + struct DefaultVars { + mapping(uint256 assetId => AssetVars) assetVars; + mapping(address spoke => mapping(uint256 reserveId => mapping(address user => UserVars))) userVars; + } + + struct UserInfo { + address spoke; + uint256 reserveId; + address user; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Default variables before and after + DefaultVars defaultVarsBefore; + DefaultVars defaultVarsAfter; + + // Temp array of users to check postconditions for, reset after each handler on _resetState + UserInfo[] usersToCheck; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Default hooks setup + function _setUpDefaultHooks() internal {} + + /// @notice Helper to initialize storage arrays of default vars + function _setUpDefaultVars(DefaultVars storage _defaultVars) internal {} + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _defaultHooksBefore() internal { + // Asset values + _setAssetValues(defaultVarsBefore); + // User values + _setUserValues(defaultVarsBefore); + } + + function _defaultHooksAfter() internal { + // Asset values + _setAssetValues(defaultVarsAfter); + // User values + _setUserValues(defaultVarsAfter); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _setAssetValues(DefaultVars storage _defaultVars) internal { + // Iterate through all assets IDs registered in the hub + for (uint256 i; i < baseAssets.length; i++) { + _defaultVars.assetVars[i].drawnIndex = hub.getAssetDrawnIndex(baseAssets[i].assetId); + _defaultVars.assetVars[i].totalAssets = hub.getAddedAssets(baseAssets[i].assetId); + _defaultVars.assetVars[i].totalShares = hub.getAddedShares(baseAssets[i].assetId); + (_defaultVars.assetVars[i].drawn, _defaultVars.assetVars[i].premium) = + hub.getAssetOwed(baseAssets[i].assetId); + } + } + + function _setUserValues(DefaultVars storage _defaultVars) internal { + // Iterate through all users to check + for (uint256 i; i < usersToCheck.length; i++) { + UserInfo memory userInfo = usersToCheck[i]; + + // Cache values for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig + if (userInfo.reserveId == CHECK_ALL_RESERVES) { + // Iterate through all reserves of the spoke + for (uint256 j; j < spokeReserveIds[userInfo.spoke].length; j++) { + ( + , + _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + .premiumDebt + ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); + _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].totalDebt = + ISpoke(userInfo.spoke).getUserTotalDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); + } + } else { + // Cache values for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral + (, _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].premiumDebt) = + ISpoke(userInfo.spoke).getUserDebt(userInfo.reserveId, userInfo.user); + _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].totalDebt = + ISpoke(userInfo.spoke).getUserTotalDebt(userInfo.reserveId, userInfo.user); + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POST CONDITIONS: HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_GPOST_HUB_A(uint256 assetId) internal { + assertGe( + defaultVarsAfter.assetVars[assetId].drawnIndex, defaultVarsBefore.assetVars[assetId].drawnIndex, GPOST_HUB_A + ); + } + + function assert_GPOST_HUB_B(uint256 assetId) internal { + uint256 totalSharesBefore = defaultVarsBefore.assetVars[assetId].totalShares; + uint256 totalSharesAfter = defaultVarsAfter.assetVars[assetId].totalShares; + + uint256 rateWadRawBefore = totalSharesBefore == 0 + ? 0 + : MathUtils.mulDivDown(defaultVarsBefore.assetVars[assetId].totalAssets, 1e18, totalSharesBefore); + + uint256 rateWadRawAfter = totalSharesAfter == 0 + ? 0 + : MathUtils.mulDivDown(defaultVarsAfter.assetVars[assetId].totalAssets, 1e18, totalSharesAfter); + + assertGe(rateWadRawAfter, rateWadRawBefore, GPOST_HUB_B); + } + + function assert_GPOST_HUB_C(uint256 assetId) internal { + assertEq( + hub.getAssetDrawnRate(assetId), + irStrategy.calculateInterestRate( + assetId, + hub.getLiquidity(assetId), + defaultVarsAfter.assetVars[assetId].drawn, + hub.getDeficit(assetId), + hub.getSwept(assetId) + ), + GPOST_HUB_C + ); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POST CONDITIONS: SPOKE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_GPOST_SP_A(address spoke, uint256 reserveId, address user) internal { + ISpoke.UserPosition memory userPosition = ISpoke(spoke).getUserPosition(reserveId, user); + uint256 userRiskPremium = ISpoke(spoke).getUserAccountData(user).userRiskPremium; + + uint256 expected = PercentageMath.percentMulUp(userPosition.drawnShares, userRiskPremium); + assertEq(userPosition.premiumShares, expected, GPOST_SP_A); + } + + function assert_GPOST_SP_B(address spoke, uint256 reserveId, address user) internal { + if ( + defaultVarsAfter.userVars[spoke][reserveId][user].premiumDebt + < defaultVarsBefore.userVars[spoke][reserveId][user].premiumDebt + ) { + assertTrue(msg.sig == ISpokeHandler.repay.selector, GPOST_SP_B); + } + } + + function assert_GPOST_SP_E(address spoke, uint256 reserveId, address user) internal { + // latest reserve key + uint16 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; + // user-stored key + uint16 userKey = ISpoke(spoke).getUserPosition(reserveId, user).configKey; + + if ( + msg.sig == ISpokeHandler.borrow.selector || msg.sig == ISpokeHandler.withdraw.selector + || msg.sig == ISpokeHandler.setUsingAsCollateral.selector + || msg.sig == ISpokeHandler.updateUserDynamicConfig.selector + ) { + assertEq(latestKey, userKey, GPOST_SP_E); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _registerUserToCheck(address spoke, uint256 reserveId, address user) internal { + usersToCheck.push(UserInfo(spoke, reserveId, user)); + } +} diff --git a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol new file mode 100644 index 000000000..739c62230 --- /dev/null +++ b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Hook Contracts +import {DefaultBeforeAfterHooks} from "./DefaultBeforeAfterHooks.t.sol"; + +/// @title HookAggregator +/// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler +abstract contract HookAggregator is DefaultBeforeAfterHooks { + /// @dev Selector for Panic(uint256) as defined by Solidity + bytes4 internal constant _PANIC_SELECTOR = 0x4e487b71; + /// @dev Panic code for assertion failed (0x01) + uint256 internal constant _PANIC_ASSERTION_FAILED = 0x01; + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Initializer for the hooks + function _setUpHooks() internal { + _setUpDefaultHooks(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Before hook for the handlers + function _before() internal { + _defaultHooksBefore(); + } + + /// @notice After hook for the handlers + function _after() internal { + _defaultHooksAfter(); + + // POST-CONDITIONS + _checkPostConditions(); + + // Reset the state + _resetState(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POSTCONDITION CHECKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Postconditions for the handlers + function _checkPostConditions() internal { + try this.checkPostConditions() {} + catch (bytes memory returnData) { + _handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); + } + } + + /// @dev postconditions checks entrypoint, should vbe self-called + function checkPostConditions() external { + // Reset the state + _resetState(); + + // Hub postconditions + _hubPostConditions(); + // Spoke postconditions + _spokePostConditions(); + } + + function _hubPostConditions() internal { + // Iterate through all assets IDs registered in the hub + for (uint256 i; i < baseAssets.length; i++) { + uint256 assetId = baseAssets[i].assetId; + assert_GPOST_HUB_A(assetId); + assert_GPOST_HUB_B(assetId); + assert_GPOST_HUB_C(assetId); + } + } + + function _spokePostConditions() internal { + // Iterate through all users to check + for (uint256 i; i < usersToCheck.length; i++) { + uint256 reserveId = usersToCheck[i].reserveId; + address spoke = usersToCheck[i].spoke; + address user = usersToCheck[i].user; + + // Check properties for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig + if (reserveId == CHECK_ALL_RESERVES) { + // Iterate through all reserves of the spoke + for (uint256 j; j < spokeReserveIds[spoke].length; i++) { + assert_GPOST_SP_A(spoke, spokeReserveIds[spoke][i], user); + assert_GPOST_SP_B(spoke, spokeReserveIds[spoke][i], user); + assert_GPOST_SP_E(spoke, spokeReserveIds[spoke][i], user); + } + } else { + // Check properties for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral + assert_GPOST_SP_A(spoke, reserveId, user); + assert_GPOST_SP_B(spoke, reserveId, user); + assert_GPOST_SP_E(spoke, reserveId, user); + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Resets the state of the handlers + function _resetState() internal { + delete usersToCheck; + } + + /// @notice Checks if a call failed due to an assertion error and propagates the error if found. + /// @param success Indicates whether the call was successful. + /// @param returnData The data returned from the call. + function _handleAssertionError( + bool success, + bytes memory returnData, + bool detectNonAssertionErrors, + string memory errorMessage + ) internal { + // Case 1: do nothing if success is true + if (success) return; + + // Case 2: detect Panic(0x01) "Assertion" errors + // Decode potential Panic(uint256) (selector + uint256 = 36 bytes) + if (returnData.length == 36) { + bytes4 selector; + uint256 code; + assembly { + selector := mload(add(returnData, 0x20)) + code := mload(add(returnData, 0x24)) + } + // Case 3: if Panic(0x01) "Assertion" -> assert(false), this propagates the assertion error to the Tester context + if (selector == _PANIC_SELECTOR && code == _PANIC_ASSERTION_FAILED) { + assert(false); + } + } + + // Case 3: detect non-assertion errors and assert with the error message + if (detectNonAssertionErrors) { + assertWithMsg(false, errorMessage); + } + } +} diff --git a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol new file mode 100644 index 000000000..dd952d9d0 --- /dev/null +++ b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {IHub} from "src/hub/interfaces/IHub.sol"; +import {IERC20} from "src/dependencies/openzeppelin/IERC20.sol"; + +// Contracts +import {HandlerAggregator} from "../HandlerAggregator.t.sol"; + +import "forge-std/console.sol"; + +/// @title HubInvariants +/// @notice Implements Hub Invariants for the protocol +/// @dev Inherits HandlerAggregator to check actions in assertion testing mode +abstract contract HubInvariants is HandlerAggregator { + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_INV_HUB_A(uint256 assetId) internal { + uint256 assets = hub.getAddedAssets(assetId); + + if (assets == 0) { + assertEq(hub.getAddedShares(assetId), 0, INV_HUB_A2); + } + } + + function assert_INV_HUB_B(uint256 assetId) internal { + // Sum per-spoke values + uint256 spokeCount = spokesAddresses.length; + uint256 sumDebt; + + for (uint256 i; i < spokeCount; i++) { + (uint256 d, uint256 p) = hub.getSpokeOwed(assetId, spokesAddresses[i]); + sumDebt += d + p; + } + + uint256 assetTotal = hub.getAssetTotalOwed(assetId); // drawn + premium + assertGe(sumDebt, assetTotal, INV_HUB_B); + } + + function assert_INV_HUB_C(uint256 assetId) internal { + // Sum per-spoke values + uint256 spokeCount = spokesAddresses.length; + + uint256 sumDrawnShares; + uint256 sumPremDrawnShares; + uint256 sumPremOffset; + uint256 sumPremRealized; + + for (uint256 i; i < spokeCount; i++) { + address spoke = spokesAddresses[i]; + sumDrawnShares += hub.getSpokeDrawnShares(assetId, spoke); + (uint256 premiumDrawnShares, uint256 premiumOffset, uint256 realizedPremium) = + hub.getSpokePremiumData(assetId, spoke); + sumPremDrawnShares += premiumDrawnShares; + sumPremOffset += premiumOffset; + sumPremRealized += realizedPremium; + } + + // Asset totals + IHub.Asset memory a = hub.getAsset(assetId); + + // Checks + assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); + assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); + assertEq(sumPremOffset, a.premiumOffset, INV_HUB_C); + assertEq(sumPremRealized, a.realizedPremium, INV_HUB_C); + } + + function assert_INV_HUB_EF(uint256 assetId) internal { + // Total amounts + uint256 totalSuppliedAssets = hub.getAddedAssets(assetId); + uint256 convertedAssets = hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)); + + IHub.Asset memory asset = hub.getAsset(assetId); + uint256 totalDebt = hub.getAssetTotalOwed(assetId); + + // Checks + //assertEq(totalSuppliedAssets, convertedAssets, INV_HUB_E); TODO review this invariant test_replay_invariant_INV_HUB_E + assertEq(totalSuppliedAssets, asset.liquidity + totalDebt + asset.deficit + asset.swept, INV_HUB_F); + } + + function assert_INV_HUB_GH(uint256 assetId) internal { + uint256 spokeCount = spokesAddresses.length; + + // Sum per-spoke values + uint256 totalAddedAssets; + uint256 totalAddedShares; + for (uint256 i; i < spokeCount; i++) { + totalAddedAssets += hub.getSpokeAddedAssets(assetId, spokesAddresses[i]); + totalAddedShares += hub.getSpokeAddedShares(assetId, spokesAddresses[i]); + } + + // Checks + assertEq(totalAddedAssets, hub.getAddedAssets(assetId), INV_HUB_G); + assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); + } + + function assert_INV_HUB_I(uint256 assetId, address underlying) internal { + // Query values + uint256 liquidity = hub.getLiquidity(assetId); + uint256 swept = hub.getSwept(assetId); + uint256 underlyingBalance = IERC20(underlying).balanceOf(address(hub)); + + // Checks + assertEq(underlyingBalance + swept, liquidity, INV_HUB_I); + } + + function assert_INV_HUB_K(uint256 assetId) internal { + // TODO for this check to be meaningful, strategy configuration operations have to be integrated + IHub.AssetConfig memory assetConfig = hub.getAssetConfig(assetId); + + // Checks + assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); + } + + function assert_INV_HUB_L(uint256 assetId) internal { + (uint256 premiumShares, uint256 premiumOffset,) = hub.getAssetPremiumData(assetId); + + assertGe(hub.previewRestoreByShares(assetId, premiumShares), premiumOffset, INV_HUB_L); + } +} diff --git a/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol b/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol new file mode 100644 index 000000000..f7b2895e0 --- /dev/null +++ b/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {ISpokeBase} from "src/spoke/interfaces/ISpokeBase.sol"; +import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; + +// Contracts +import {HandlerAggregator} from "../HandlerAggregator.t.sol"; + +/// @title SpokeInvariants +/// @notice Implements Spoke Invariants for the protocol +/// @dev Inherits HandlerAggregator to check actions in assertion testing mode +abstract contract SpokeInvariants is HandlerAggregator { + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_INV_SP_A(address spoke, uint256 reserveId) internal { + // Get the assetId related to the reserveId of the spoke + uint256 assetId = _getAssetId(spoke, reserveId); + + // supply + assertEq( + ISpokeBase(spoke).getReserveSuppliedShares(reserveId), hub.getSpokeAddedShares(assetId, spoke), INV_SP_A + ); + assertEq( + ISpokeBase(spoke).getReserveSuppliedAssets(reserveId), hub.getSpokeAddedAssets(assetId, spoke), INV_SP_A + ); + + // debt + (uint256 d1, uint256 p1) = hub.getSpokeOwed(assetId, spoke); + (uint256 d2, uint256 p2) = ISpokeBase(spoke).getReserveDebt(reserveId); + assertEq(d2, d1, INV_SP_A); + assertEq(p2, p1, INV_SP_A); + } + + function assert_INV_SP_B(address spoke, uint256 reserveId, address user) internal { + // reserve supply + if (ISpokeBase(spoke).getReserveSuppliedAssets(reserveId) > 0) { + assertGt(ISpokeBase(spoke).getReserveSuppliedShares(reserveId), 0, INV_SP_B); + } + // reserve debt + if (ISpokeBase(spoke).getReserveTotalDebt(reserveId) > 0) { + assertGt(hub.getSpokeDrawnShares(_getAssetId(spoke, reserveId), spoke), 0, INV_SP_B); + } + // user supply + if (ISpokeBase(spoke).getUserSuppliedAssets(reserveId, user) > 0) { + assertGt(ISpokeBase(spoke).getUserSuppliedShares(reserveId, user), 0, INV_SP_B); + } + // user debt + if (ISpokeBase(spoke).getUserTotalDebt(reserveId, user) > 0) { + ISpoke.UserPosition memory up = ISpoke(spoke).getUserPosition(reserveId, user); + assertTrue(up.drawnShares > 0 || up.premiumShares > 0, INV_SP_B); + } + } + + function assert_INV_SP_C(address spoke, uint256 reserveId) internal { + uint256 sumSpokeDebts; + for (uint256 i; i < actorAddresses.length; i++) { + sumSpokeDebts += ISpokeBase(spoke).getUserTotalDebt(reserveId, actorAddresses[i]); + } + assertGe(sumSpokeDebts, ISpokeBase(spoke).getReserveTotalDebt(reserveId), INV_SP_C); + } + + function assert_INV_SP_D(address spoke, address user) internal { + ISpoke.UserAccountData memory d = ISpoke(spoke).getUserAccountData(user); + if (d.totalCollateralInBaseCurrency == 0) { + assertEq(d.totalDebtInBaseCurrency, 0, INV_SP_D); + } + } +} diff --git a/tests/enigma-dark-invariants/remote/DOCKERFILE b/tests/enigma-dark-invariants/remote/DOCKERFILE new file mode 100644 index 000000000..62a120340 --- /dev/null +++ b/tests/enigma-dark-invariants/remote/DOCKERFILE @@ -0,0 +1,17 @@ +#################### +## FOUNDRY INSTALL +#################### +FROM ghcr.io/foundry-rs/foundry:v0.3.0 AS builder-foundry + +RUN forge --version + +################## +## RUNNER +################## +FROM trailofbits/echidna:v2.2.7 AS run + +RUN apt-get update && apt-get install -y make && rm -rf /var/lib/apt/lists/* + +COPY --from=builder-foundry /usr/local/bin/forge /usr/local/bin/forge + +WORKDIR /app diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol new file mode 100644 index 000000000..e444a5b41 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest1 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest1 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_1_updateUserRiskPremium() public { + _setUpActor(USER3); + Tester.setUsingAsCollateral(true, 1, 26); + Tester.supply(8548, 251, 14, 14); + Tester.borrow(716, 227, 46, 18); + _setUpActor(USER1); + _delay(483724); + Tester.updateUserRiskPremium(0); + // Invalid: 54653460198616960432589820!=5465941435645494nah 5997582564, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block + } + + function test_replay_1_invariant_INV_HUB_E() public { + _setUpActor(USER1); + _delay(7335); + Tester.setUsingAsCollateral(true, 0, 0); + _delay(33390); + Tester.supply(203156, 36, 0, 86); + _delay(895); + Tester.borrow(736, 0, 0, 38); + _delay(12354444); + invariant_INV_HUB(); // Invalid: 203179!=203159, reason: INV_HUB_E: hub.getTotalSuppliedAssets and hub.getAssetSuppliedAmount should match at any time + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol new file mode 100644 index 000000000..082127b12 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest2 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest2 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + + function test_replay_2_withdraw() public { + _setUpActor(USER3); + Tester.supply(1, 23, 0, 2); + Tester.withdraw(3105157079507136411131272320472170162160557515874316681599825263298447597, 2, 0, 0); + + } + + function test_replay_2_supply() public { + _setUpActor(USER2); + Tester.setUsingAsCollateral(true, 25, 3); + Tester.supply(855, 7, 0, 3); + Tester.borrow(1, 1, 11, 0); + _setUpActor(USER1); + _delay(5032074); + Tester.supply(1768, 0, 0, 0); + + } + + function test_replay_2_setUsingAsCollateral() public { + _setUpActor(USER2); + Tester.supply(119335, 1, 0, 0); + Tester.setUsingAsCollateral(true, 0, 0); + Tester.borrow(1, 1, 1, 0); + _setUpActor(USER1); + _delay(2); + Tester.setUsingAsCollateral(false, 0, 0); + + } + + function test_replay_2_liquidationCall() public { + _setUpActor(USER1); + _delay(716); + Tester.supply(431999, 84, 27, 125); + _setUpActor(USER2); + _delay(577108); + Tester.supply(80962052803112294330194839042, 16, 45, 125); + _setUpActor(USER3); + _delay(7578); + Tester.setUsingAsCollateral(true, 1, 254); + Tester.supply(10092, 38, 37, 223); + Tester.borrow(7800, 227, 62, 51); + _setUpActor(USER1); + _delay(2138596); + _delay(194640); + Tester.withdraw(115792089237316195423570985008687907853269984665640564039457584007913129585001, 15, 131, 247); + _delay(322334); + Tester.setUsingAsCollateral(true, 31, 244); + _setUpActor(USER2); + _delay(827249); + _setUpActor(USER1); + _delay(405856); + Tester.liquidationCall(61422963954080211324642765950298638470789887452796784425666156856462101843613, 53, 251, 56, 20); + + } + + function test_replay_2_updateUserDynamicConfig() public { + _setUpActor(USER3); + Tester.supply(7123984353, 2, 126, 4); + Tester.setUsingAsCollateral(true, 6, 8); + Tester.borrow(1468, 2, 0, 0); + _setUpActor(USER1); + _delay(12809); + Tester.updateUserDynamicConfig(0); + + } + + function test_replay_2_repay() public { + _setUpActor(USER2); + Tester.supply(16, 25, 2, 7); + Tester.setUsingAsCollateral(true, 81, 2); + Tester.borrow(2, 16, 5, 4); + _delay(61); + Tester.repay(1, 19, 0, 248); + + } + + function test_replay_2_updateUserRiskPremium() public { + _setUpActor(USER3); + Tester.setUsingAsCollateral(true, 0, 22); + Tester.supply(2786, 251, 0, 14); + Tester.borrow(71, 77, 5, 3); + _setUpActor(USER1); + _delay(53617); + Tester.updateUserRiskPremium(0); + + } + + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} \ No newline at end of file diff --git a/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol b/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol new file mode 100644 index 000000000..ba2b511fb --- /dev/null +++ b/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title InvariantsSpec +/// @notice Invariants specification for the protocol +/// @dev Contains pseudo code and description for the invariant properties in the protocol +abstract contract InvariantsSpec { + // TODO: check invariant overlap + /*///////////////////////////////////////////////////////////////////////////////////////////// + // PROPERTY TYPES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// - INVARIANTS (INV): + /// - Properties that should always hold true in the system. + /// - Implemented in the /invariants folder. + + /////////////////////////////////////////////////////////////////////////////////////////////*/ + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant INV_HUB_A = + "INV_HUB_A: Sum of spoke added assets on a single asset must be greater or equal than the total amount of added assets of the asset"; // TODO this overlaps with INV_HUB_G + + string constant INV_HUB_A2 = "INV_HUB_A2: If hub assets = 0 => shares 0"; + + string constant INV_HUB_B = + "INV_HUB_A: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset"; + + string constant INV_HUB_C = + "INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffset/realized] of individual (spoke/user) should match the corresponding value of the asset on the Hub"; + + string constant INV_HUB_E = + "INV_HUB_E: hub.getTotalSuppliedAssets and hub.getAssetSuppliedAmount should match at any time, should not be off by more than 1 share worth of assets due to division precision loss"; + + string constant INV_HUB_F = + "INV_HUB_E2: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + totalDebt + deficit + swept"; + + string constant INV_HUB_G = + "INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke)"; + + string constant INV_HUB_H = "INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes"; + + string constant INV_HUB_I = "INV_HUB_I: asset.underlying.balanceOf(hub) >= asset.liquidity - asset.swept"; + + string constant INV_HUB_J = + "INV_HUB_J: totalDrawn{Shares,Assets} >= any spoke totalDrawn{Shares,Assets} (same for premium debt)"; // TODO + + string constant INV_HUB_K = + "INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset"; + + string constant INV_HUB_L = "INV_HUB_L: Asset.premiumShares.toDrawnAssetsUp() >= Asset.premiumOffset"; + + string constant INV_HUB_M = + "INV_HUB_M: Liquidity growth (ie accrued interest) >= AccruedFees (even with 100.00% liquidity fee)"; // TODO + + string constant INV_HUB_N = + "INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares"; // TODO + + string constant INV_HUB_O = "INV_HUB_O: sum of deficit across spokes for a given asset == total asset deficit"; // TODO explore how to track deficit for each spoke + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant INV_SP_A = + "INV_SP_A: Spoke reserve accounting should always match hub's spoke accounting on the corresponding registered asset."; + + string constant INV_SP_B = + "INV_SP_B: User/Reserve cannot have non-zero assets and zero shares in supply or debt sides."; + + string constant INV_SP_C = + "INV_SP_C: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset"; + + string constant INV_SP_D = "INV_SP_D: Users without collateral also have no debt."; + + //// + /////////////////////////////////////////////////////////////////////////////////////////////// + // TODO + // - Spoke/Asset cannot have non-zero assets and zero shares in add or draw sides."; + // - 4626 roundtrip on preview methods + // - Asset.feeReceiver should always +} diff --git a/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol b/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol new file mode 100644 index 000000000..9588849e5 --- /dev/null +++ b/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title PostconditionsSpec +/// @notice Postcoditions specification for the protocol +/// @dev Contains pseudo code and description for the postcondition properties in the protocol +abstract contract PostconditionsSpec { + /*///////////////////////////////////////////////////////////////////////////////////////////// + // PROPERTY TYPES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// - POSTCONDITIONS: + /// - Properties that should hold true after an action is executed. + /// - Implemented in the /hooks and /handlers folders. + /// - There are two types of POSTCONDITIONS: + /// - GLOBAL POSTCONDITIONS (GPOST): + /// - Properties that should always hold true after any action is executed. + /// - Checked in the `_checkPostConditions` function within the HookAggregator contract. + /// - HANDLER-SPECIFIC POSTCONDITIONS (HSPOST): + /// - Properties that should hold true after a specific action is executed in a specific context. + /// - Implemented within each handler function, under the HANDLER-SPECIFIC POSTCONDITIONS section. + + /////////////////////////////////////////////////////////////////////////////////////////////*/ + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + ///////////////////////////////////////////////////////////////////////////////////////////////// + + string constant GPOST_HUB_A = + "GPOST_HUB_A: Drawn index cannot decrease (remains constant or increases). If no time passes, it stays constant. only increases due to interest accumulation"; + + string constant GPOST_HUB_B = + "GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding)."; + + string constant GPOST_HUB_C = + "GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block."; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant GPOST_SP_A = + "GPOST_SP_A: Stored (user.premiumDrawnShares/user.baseDrawnShares) & calculated user risk premium (calculation based on user's position, via spoke.calculateUserAccountData) need to be the same right after an operation"; + + string constant GPOST_SP_B = + "GPOST_SP_B: Premium debt of an individual user, spoke can only decrease by calling repay when premium debt is not zero"; + + string constant HSPOST_SP_C = "HSPOST_SP_C: User liability should decrease after repayment"; + + string constant HSPOST_SP_D = "HSPOST_SP_D: Unhealthy users cannot borrow more"; + + string constant GPOST_SP_E = + "GPOST_SP_E: DynamicRiskConfiguration for a user position is updated to latest reserve state whenever an action can potentially make their position less healthy"; + // - Updates on: borrow, withdraw, disableAsCollateral. + // - Unchanged on: supply, repay, liquidate, updateUserRiskPremium, setUserPositionManager. + // - Enabling collateral updates only the relevant reserve's dynamic config. + // - Exception: updateUserDynamicConfig explicitly refreshes the user's dynamic config. + + string constant HSPOST_SP_F = "HSPOST_SP_F: Total debt of a user should not change after updateUserRiskPremium"; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE: LIQUIDATION // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant HSPOST_SP_LIQ_A = + "HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position"; + + string constant HSPOST_SP_LIQ_B = + "HSPOST_SP_LIQ_B: Liquidation cannot result in an amount of seized collateral (sold collateral + liquidation bonus) > user's collateral position"; // TODO + + string constant HSPOST_SP_LIQ_C = + "HSPOST_SP_LIQ_C: Liquidator is always forced to repay all the debt of a user if debt value is below DUST_DEBT_LIQUIDATION_THRESHOLD"; +} diff --git a/tests/enigma-dark-invariants/utils/Actor.sol b/tests/enigma-dark-invariants/utils/Actor.sol new file mode 100644 index 000000000..5b24cb8bf --- /dev/null +++ b/tests/enigma-dark-invariants/utils/Actor.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +/// @notice Proxy contract for invariant suite actors to avoid aTester calling contracts +contract Actor { + /// @notice list of tokens to approve + address[] internal tokens; + /// @notice list of contracts to approve tokens to + address[] internal contracts; + + constructor(address[] memory _tokens, address[] memory _contracts) payable { + tokens = _tokens; + contracts = _contracts; + for (uint256 i = 0; i < tokens.length; i++) { + for (uint256 j = 0; j < contracts.length; j++) { + IERC20(tokens[i]).approve(contracts[j], type(uint256).max); + } + } + } + + /// @notice Helper function to proxy a call to a target contract, used to avoid Tester calling contracts + function proxy(address _target, bytes memory _calldata) public returns (bool success, bytes memory returnData) { + (success, returnData) = address(_target).call(_calldata); + + handleAssertionError(success, returnData); + } + + /// @notice Helper function to proxy a call and value to a target contract, used to avoid Tester calling contracts + function proxy(address _target, bytes memory _calldata, uint256 value) + public + returns (bool success, bytes memory returnData) + { + (success, returnData) = address(_target).call{value: value}(_calldata); + + handleAssertionError(success, returnData); + } + + /// @notice Checks if a call failed due to an assertion error and propagates the error if found. + /// @param success Indicates whether the call was successful. + /// @param returnData The data returned from the call. + function handleAssertionError(bool success, bytes memory returnData) internal pure { + if (!success && returnData.length == 36) { + bytes4 selector; + uint256 code; + assembly { + selector := mload(add(returnData, 0x20)) + code := mload(add(returnData, 0x24)) + } + + if (selector == bytes4(0x4e487b71) && code == 1) { + assert(false); + } + } + } + + receive() external payable {} +} diff --git a/tests/enigma-dark-invariants/utils/CREATE3.sol b/tests/enigma-dark-invariants/utils/CREATE3.sol new file mode 100644 index 000000000..5b3beab01 --- /dev/null +++ b/tests/enigma-dark-invariants/utils/CREATE3.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Deterministic deployments agnostic to the initialization code. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/CREATE3.sol) +/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/CREATE3.sol) +/// @author Modified from 0xSequence (https://github.com/0xSequence/create3/blob/master/contracts/Create3.sol) +library CREATE3 { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Unable to deploy the contract. + error DeploymentFailed(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* BYTECODE CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * -------------------------------------------------------------------+ + * Opcode | Mnemonic | Stack | Memory | + * -------------------------------------------------------------------| + * 36 | CALLDATASIZE | cds | | + * 3d | RETURNDATASIZE | 0 cds | | + * 3d | RETURNDATASIZE | 0 0 cds | | + * 37 | CALLDATACOPY | | [0..cds): calldata | + * 36 | CALLDATASIZE | cds | [0..cds): calldata | + * 3d | RETURNDATASIZE | 0 cds | [0..cds): calldata | + * 34 | CALLVALUE | value 0 cds | [0..cds): calldata | + * f0 | CREATE | newContract | [0..cds): calldata | + * -------------------------------------------------------------------| + * Opcode | Mnemonic | Stack | Memory | + * -------------------------------------------------------------------| + * 67 bytecode | PUSH8 bytecode | bytecode | | + * 3d | RETURNDATASIZE | 0 bytecode | | + * 52 | MSTORE | | [0..8): bytecode | + * 60 0x08 | PUSH1 0x08 | 0x08 | [0..8): bytecode | + * 60 0x18 | PUSH1 0x18 | 0x18 0x08 | [0..8): bytecode | + * f3 | RETURN | | [0..8): bytecode | + * -------------------------------------------------------------------+ + */ + + /// @dev The proxy initialization code. + uint256 private constant _PROXY_INITCODE = 0x67363d3d37363d34f03d5260086018f3; + + /// @dev Hash of the `_PROXY_INITCODE`. + /// Equivalent to `keccak256(abi.encodePacked(hex"67363d3d37363d34f03d5260086018f3"))`. + bytes32 internal constant PROXY_INITCODE_HASH = + 0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CREATE3 OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Deploys `initCode` deterministically with a `salt`. + /// Returns the deterministic address of the deployed contract, + /// which solely depends on `salt`. + function deployDeterministic(bytes memory initCode, bytes32 salt) + internal + returns (address deployed) + { + deployed = deployDeterministic(0, initCode, salt); + } + + /// @dev Deploys `initCode` deterministically with a `salt`. + /// The deployed contract is funded with `value` (in wei) ETH. + /// Returns the deterministic address of the deployed contract, + /// which solely depends on `salt`. + function deployDeterministic(uint256 value, bytes memory initCode, bytes32 salt) + internal + returns (address deployed) + { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, _PROXY_INITCODE) // Store the `_PROXY_INITCODE`. + let proxy := create2(0, 0x10, 0x10, salt) + if iszero(proxy) { + mstore(0x00, 0x30116425) // `DeploymentFailed()`. + revert(0x1c, 0x04) + } + mstore(0x14, proxy) // Store the proxy's address. + // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01). + // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex). + mstore(0x00, 0xd694) + mstore8(0x34, 0x01) // Nonce of the proxy contract (1). + deployed := keccak256(0x1e, 0x17) + if iszero( + mul( // The arguments of `mul` are evaluated last to first. + extcodesize(deployed), + call(gas(), proxy, value, add(initCode, 0x20), mload(initCode), 0x00, 0x00) + ) + ) { + mstore(0x00, 0x30116425) // `DeploymentFailed()`. + revert(0x1c, 0x04) + } + } + } + + /// @dev Returns the deterministic address for `salt`. + function predictDeterministicAddress(bytes32 salt) internal view returns (address deployed) { + deployed = predictDeterministicAddress(salt, address(this)); + } + + /// @dev Returns the deterministic address for `salt` with `deployer`. + function predictDeterministicAddress(bytes32 salt, address deployer) + internal + pure + returns (address deployed) + { + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Cache the free memory pointer. + mstore(0x00, deployer) // Store `deployer`. + mstore8(0x0b, 0xff) // Store the prefix. + mstore(0x20, salt) // Store the salt. + mstore(0x40, PROXY_INITCODE_HASH) // Store the bytecode hash. + + mstore(0x14, keccak256(0x0b, 0x55)) // Store the proxy's address. + mstore(0x40, m) // Restore the free memory pointer. + // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01). + // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex). + mstore(0x00, 0xd694) + mstore8(0x34, 0x01) // Nonce of the proxy contract (1). + deployed := keccak256(0x1e, 0x17) + } + } +} \ No newline at end of file diff --git a/tests/enigma-dark-invariants/utils/DeployPermit2.sol b/tests/enigma-dark-invariants/utils/DeployPermit2.sol new file mode 100644 index 000000000..3fe83dcd3 --- /dev/null +++ b/tests/enigma-dark-invariants/utils/DeployPermit2.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/// @notice helper to deploy permit2 from precompiled bytecode +/// @dev useful if testing externally against permit2 and want to avoid +/// recompiling entirely and requiring viaIR compilation +library DeployPermit2 { + /// @notice deploy permit2 + function deployPermit2() internal returns (address) { + bytes memory bytecode = + hex"60c0346100bb574660a052602081017f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a60408301524660608301523060808301526080825260a082019180831060018060401b038411176100a557826040525190206080526123c090816100c1823960805181611b47015260a05181611b210152f35b634e487b7160e01b600052604160045260246000fd5b600080fdfe6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000003611b69577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a"; + + return deployFromBytecode(bytecode); + } + + /// @notice helper function to deploy bytecode + function deployFromBytecode(bytes memory bytecode) internal returns (address child) { + assembly { + child := create(0, add(bytecode, 0x20), mload(bytecode)) + } + } +} diff --git a/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol b/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol new file mode 100644 index 000000000..6d0fcc1b7 --- /dev/null +++ b/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol @@ -0,0 +1,410 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice PropertiesAsserts is a library that provides assertions for properties of Solidity contracts. +/// @dev Added more assertions to the original PropertiesAsserts library. +abstract contract PropertiesAsserts { + event LogUint256(string, uint256); + event LogAddress(string, address); + event LogString(string); + + event AssertFail(string); + event AssertEqFail(string); + event AssertNeqFail(string); + event AssertGeFail(string); + event AssertGtFail(string); + event AssertLeFail(string); + event AssertLtFail(string); + + function assertWithMsg(bool b, string memory reason) internal { + if (!b) { + emit AssertFail(reason); + assert(false); + } + } + + /// @notice asserts that a is equal to b. + function assertEq(uint256 a, uint256 b) internal pure { + if (a != b) { + assert(false); + } + } + + function assertEq(int256 a, int256 b) internal pure { + if (a != b) { + assert(false); + } + } + + //write the function below for address + function assertEq(address a, address b) internal pure { + if (a != b) { + assert(false); + } + } + + // now with a reason parameter + function assertEq(address a, address b, string memory reason) internal { + if (a != b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "!=", bStr, ", reason: ", reason); + emit AssertEqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is equal to b. Violations are logged using reason. + function assertEq(uint256 a, uint256 b, string memory reason) internal { + if (a != b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "!=", bStr, ", reason: ", reason); + emit AssertEqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertEq + function assertEq(int256 a, int256 b, string memory reason) internal { + if (a != b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "!=", bStr, ", reason: ", reason); + emit AssertEqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is not equal to b. Violations are logged using reason. + function assertNeq(uint256 a, uint256 b, string memory reason) internal { + if (a == b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "==", bStr, ", reason: ", reason); + emit AssertNeqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertNeq + function assertNeq(int256 a, int256 b, string memory reason) internal { + if (a == b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "==", bStr, ", reason: ", reason); + emit AssertNeqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is greater than or equal to b. Violations are logged using reason. + function assertGe(uint256 a, uint256 b, string memory reason) internal { + if (!(a >= b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "<", bStr, " failed, reason: ", reason); + emit AssertGeFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertGe + function assertGe(int256 a, int256 b, string memory reason) internal { + if (!(a >= b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "<", bStr, " failed, reason: ", reason); + emit AssertGeFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is greater than b. Violations are logged using reason. + function assertGt(uint256 a, uint256 b, string memory reason) internal { + if (!(a > b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "<=", bStr, " failed, reason: ", reason); + emit AssertGtFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertGt + function assertGt(int256 a, int256 b, string memory reason) internal { + if (!(a > b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "<=", bStr, " failed, reason: ", reason); + emit AssertGtFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is less than or equal to b. Violations are logged using reason. + function assertLe(uint256 a, uint256 b, string memory reason) internal { + if (!(a <= b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, ">", bStr, " failed, reason: ", reason); + emit AssertLeFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertLe + function assertLe(int256 a, int256 b, string memory reason) internal { + if (!(a <= b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, ">", bStr, " failed, reason: ", reason); + emit AssertLeFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is less than b. Violations are logged using reason. + function assertLt(uint256 a, uint256 b, string memory reason) internal { + if (!(a < b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, ">=", bStr, " failed, reason: ", reason); + emit AssertLtFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertLt + function assertLt(int256 a, int256 b, string memory reason) internal { + if (!(a < b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, ">=", bStr, " failed, reason: ", reason); + emit AssertLtFail(string(assertMsg)); + assert(false); + } + } + + /// @notice Clamps value to be between low and high, both inclusive + function clampBetween(uint256 value, uint256 low, uint256 high) internal returns (uint256) { + if (value < low || value > high) { + uint256 ans = low + (value % (high - low + 1)); + string memory valueStr = PropertiesLibString.toString(value); + string memory ansStr = PropertiesLibString.toString(ans); + bytes memory message = abi.encodePacked("Clamping value ", valueStr, " to ", ansStr); + emit LogString(string(message)); + return ans; + } + return value; + } + + /// @notice int256 version of clampBetween + function clampBetween(int256 value, int256 low, int256 high) internal returns (int256) { + if (value < low || value > high) { + int256 range = high - low + 1; + int256 clamped = (value - low) % (range); + if (clamped < 0) clamped += range; + int256 ans = low + clamped; + string memory valueStr = PropertiesLibString.toString(value); + string memory ansStr = PropertiesLibString.toString(ans); + bytes memory message = abi.encodePacked("Clamping value ", valueStr, " to ", ansStr); + emit LogString(string(message)); + return ans; + } + return value; + } + + /// @notice clamps a to be less than b + function clampLt(uint256 a, uint256 b) internal returns (uint256) { + if (!(a < b)) { + assertNeq(b, 0, "clampLt cannot clamp value a to be less than zero. Check your inputs/assumptions."); + uint256 value = a % b; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice int256 version of clampLt + function clampLt(int256 a, int256 b) internal returns (int256) { + if (!(a < b)) { + int256 value = b - 1; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice clamps a to be less than or equal to b + function clampLe(uint256 a, uint256 b) internal returns (uint256) { + if (!(a <= b)) { + uint256 value = a % (b + 1); + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice int256 version of clampLe + function clampLe(int256 a, int256 b) internal returns (int256) { + if (!(a <= b)) { + int256 value = b; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice clamps a to be greater than b + function clampGt(uint256 a, uint256 b) internal returns (uint256) { + if (!(a > b)) { + assertNeq( + b, + type(uint256).max, + "clampGt cannot clamp value a to be larger than uint256.max. Check your inputs/assumptions." + ); + uint256 value = b + 1; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); + emit LogString(string(message)); + return value; + } else { + return a; + } + } + + /// @notice int256 version of clampGt + function clampGt(int256 a, int256 b) internal returns (int256) { + if (!(a > b)) { + int256 value = b + 1; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); + emit LogString(string(message)); + return value; + } else { + return a; + } + } + + /// @notice clamps a to be greater than or equal to b + function clampGe(uint256 a, uint256 b) internal returns (uint256) { + if (!(a > b)) { + uint256 value = b; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice int256 version of clampGe + function clampGe(int256 a, int256 b) internal returns (int256) { + if (!(a > b)) { + int256 value = b; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); + emit LogString(string(message)); + return value; + } + return a; + } +} + +/// @notice Efficient library for creating string representations of integers. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/LibString.sol) +/// @author Modified from Solady (https://github.com/Vectorized/solady/blob/main/src/utils/LibString.sol) +/// @dev Name of the library is modified to prevent collisions with contract-under-test uses of LibString +library PropertiesLibString { + function toString(int256 value) internal pure returns (string memory str) { + uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); + str = toString(absValue); + + if (value < 0) { + str = string(abi.encodePacked("-", str)); + } + } + + function toString(uint256 value) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes + // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the + // trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. + let newFreeMemoryPointer := add(mload(0x40), 160) + + // Update the free memory pointer to avoid overriding our string. + mstore(0x40, newFreeMemoryPointer) + + // Assign str to the end of the zone of newly allocated memory. + str := sub(newFreeMemoryPointer, 32) + + // Clean the last word of memory it may not be overwritten. + mstore(str, 0) + + // Cache the end of the memory to calculate the length later. + let end := str + + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + // prettier-ignore + for { let temp := value } 1 {} { + // Move the pointer 1 byte to the left. + str := sub(str, 1) + + // Write the character to the pointer. + // The ASCII index of the '0' character is 48. + mstore8(str, add(48, mod(temp, 10))) + + // Keep dividing temp until zero. + temp := div(temp, 10) + + // prettier-ignore + if iszero(temp) { break } + } + + // Compute and cache the final total length of the string. + let length := sub(end, str) + + // Move the pointer 32 bytes leftwards to make room for the length. + str := sub(str, 32) + + // Store the string's length at the start of memory allocated for our string. + mstore(str, length) + } + } + + function toString(address value) internal pure returns (string memory str) { + bytes memory s = new bytes(40); + for (uint256 i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint256(uint160(value)) / (2 ** (8 * (19 - i))))); + bytes1 hi = bytes1(uint8(b) / 16); + bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); + s[2 * i] = char(hi); + s[2 * i + 1] = char(lo); + } + return string(s); + } + + function char(bytes1 b) internal pure returns (bytes1 c) { + if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); + else return bytes1(uint8(b) + 0x57); + } +} diff --git a/tests/enigma-dark-invariants/utils/PropertiesConstants.sol b/tests/enigma-dark-invariants/utils/PropertiesConstants.sol new file mode 100644 index 000000000..db2c0ac99 --- /dev/null +++ b/tests/enigma-dark-invariants/utils/PropertiesConstants.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +abstract contract PropertiesConstants { + // Echidna constants + address constant USER1 = address(0x10000); + address constant USER2 = address(0x20000); + address constant USER3 = address(0x30000); + uint256 constant INITIAL_BALANCE = 1000e30; + + // Suite constants + uint256 constant CHECK_ALL_RESERVES = type(uint256).max; + string constant GPOST_CHECK_FAILED = "GPOST_CHECK_FAILED: checkPostConditions reverted"; + + // Protocol constants +} diff --git a/tests/enigma-dark-invariants/utils/StdAsserts.sol b/tests/enigma-dark-invariants/utils/StdAsserts.sol new file mode 100644 index 000000000..e43d10e86 --- /dev/null +++ b/tests/enigma-dark-invariants/utils/StdAsserts.sol @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2 <0.9.0; + +import {PropertiesAsserts} from "./PropertiesAsserts.sol"; +import {stdMath} from "forge-std/StdMath.sol"; + +/// @notice Standardized assertions for use in Invariant tests, inherits PropertiesAsserts +/// @dev Adapted from forge to work with echidna & medusa +abstract contract StdAsserts is PropertiesAsserts { + event log(string); + event logs(bytes); + + event log_address(address); + event log_bytes32(bytes32); + event log_int(int256); + event log_uint(uint256); + event log_bytes(bytes); + event log_string(string); + + event log_named_address(string key, address val); + event log_named_bytes32(string key, bytes32 val); + event log_named_decimal_int(string key, int256 val, uint256 decimals); + event log_named_decimal_uint(string key, uint256 val, uint256 decimals); + event log_named_int(string key, int256 val); + event log_named_uint(string key, uint256 val); + event log_named_bytes(string key, bytes val); + event log_named_string(string key, string val); + event log_array(uint256[] val); + event log_array(int256[] val); + event log_array(address[] val); + event log_named_array(string key, uint256[] val); + event log_named_array(string key, int256[] val); + event log_named_array(string key, address[] val); + + function fail(string memory err) internal virtual { + emit log_named_string("Error", err); + fail(); + } + + function fail() internal virtual { + assert(false); + } + + function assertTrue(bool condition) internal { + if (!condition) { + emit log("Error: Assertion Failed"); + fail(); + } + } + + function assertTrue(bool condition, string memory err) internal { + if (!condition) { + emit log_named_string("Error", err); + assertTrue(condition); + } + } + + function assertFalse(bool data) internal virtual { + assertTrue(!data); + } + + function assertFalse(bool data, string memory err) internal virtual { + assertTrue(!data, err); + } + + function checkEq0(bytes memory a, bytes memory b) internal pure returns (bool ok) { + ok = true; + if (a.length == b.length) { + for (uint256 i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + ok = false; + } + } + } else { + ok = false; + } + } + + function assertEq0(bytes memory a, bytes memory b) internal { + if (!checkEq0(a, b)) { + emit log("Error: a == b not satisfied [bytes]"); + emit log_named_bytes(" Expected", a); + emit log_named_bytes(" Actual", b); + fail(); + } + } + + function assertEq0(bytes memory a, bytes memory b, string memory err) internal { + if (!checkEq0(a, b)) { + emit log_named_string("Error", err); + assertEq0(a, b); + } + } + + function assertEq(bool a, bool b) internal virtual { + if (a != b) { + emit log("Error: a == b not satisfied [bool]"); + emit log_named_string(" Left", a ? "true" : "false"); + emit log_named_string(" Right", b ? "true" : "false"); + fail(); + } + } + + function assertEq(bool a, bool b, string memory err) internal virtual { + if (a != b) { + emit log_named_string("Error", err); + assertEq(a, b); + } + } + + function assertEq(bytes memory a, bytes memory b) internal virtual { + assertEq0(a, b); + } + + function assertEq(bytes memory a, bytes memory b, string memory err) internal virtual { + assertEq0(a, b, err); + } + + function assertEq(uint256[] memory a, uint256[] memory b) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log("Error: a == b not satisfied [uint[]]"); + emit log_named_array(" Left", a); + emit log_named_array(" Right", b); + fail(); + } + } + + function assertEq(int256[] memory a, int256[] memory b) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log("Error: a == b not satisfied [int[]]"); + emit log_named_array(" Left", a); + emit log_named_array(" Right", b); + fail(); + } + } + + function assertEq(address[] memory a, address[] memory b) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log("Error: a == b not satisfied [address[]]"); + emit log_named_array(" Left", a); + emit log_named_array(" Right", b); + fail(); + } + } + + function assertEq(uint256[] memory a, uint256[] memory b, string memory err) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log_named_string("Error", err); + assertEq(a, b); + } + } + + function assertEq(int256[] memory a, int256[] memory b, string memory err) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log_named_string("Error", err); + assertEq(a, b); + } + } + + function assertEq(address[] memory a, address[] memory b, string memory err) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log_named_string("Error", err); + assertEq(a, b); + } + } + + // Legacy helper + function assertEqUint(uint256 a, uint256 b) internal virtual { + assertEq(uint256(a), uint256(b)); + } + + function assertApproxEqAbs(uint256 a, uint256 b, uint256 maxDelta) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log("Error: a ~= b not satisfied [uint]"); + emit log_named_uint(" Left", a); + emit log_named_uint(" Right", b); + emit log_named_uint(" Max Delta", maxDelta); + emit log_named_uint(" Delta", delta); + fail(); + } + } + + function assertApproxEqAbs(uint256 a, uint256 b, uint256 maxDelta, string memory err) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log_named_string("Error", err); + assertApproxEqAbs(a, b, maxDelta); + } + } + + function assertApproxEqAbsDecimal(uint256 a, uint256 b, uint256 maxDelta, uint256 decimals) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log("Error: a ~= b not satisfied [uint]"); + emit log_named_decimal_uint(" Left", a, decimals); + emit log_named_decimal_uint(" Right", b, decimals); + emit log_named_decimal_uint(" Max Delta", maxDelta, decimals); + emit log_named_decimal_uint(" Delta", delta, decimals); + fail(); + } + } + + function assertApproxEqAbsDecimal(uint256 a, uint256 b, uint256 maxDelta, uint256 decimals, string memory err) + internal + virtual + { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log_named_string("Error", err); + assertApproxEqAbsDecimal(a, b, maxDelta, decimals); + } + } + + function assertApproxEqAbs(int256 a, int256 b, uint256 maxDelta) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log("Error: a ~= b not satisfied [int]"); + emit log_named_int(" Left", a); + emit log_named_int(" Right", b); + emit log_named_uint(" Max Delta", maxDelta); + emit log_named_uint(" Delta", delta); + fail(); + } + } + + function assertApproxEqAbs(int256 a, int256 b, uint256 maxDelta, string memory err) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log_named_string("Error", err); + assertApproxEqAbs(a, b, maxDelta); + } + } + + function assertApproxEqAbsDecimal(int256 a, int256 b, uint256 maxDelta, uint256 decimals) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log("Error: a ~= b not satisfied [int]"); + emit log_named_decimal_int(" Left", a, decimals); + emit log_named_decimal_int(" Right", b, decimals); + emit log_named_decimal_uint(" Max Delta", maxDelta, decimals); + emit log_named_decimal_uint(" Delta", delta, decimals); + fail(); + } + } + + function assertApproxEqAbsDecimal(int256 a, int256 b, uint256 maxDelta, uint256 decimals, string memory err) + internal + virtual + { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log_named_string("Error", err); + assertApproxEqAbsDecimal(a, b, maxDelta, decimals); + } + } + + function assertApproxEqRel( + uint256 a, + uint256 b, + uint256 maxPercentDelta // An 18 decimal fixed point number, where 1e18 == 100% + ) internal virtual { + if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log("Error: a ~= b not satisfied [uint]"); + emit log_named_uint(" Left", a); + emit log_named_uint(" Right", b); + emit log_named_decimal_uint(" Max % Delta", maxPercentDelta * 100, 18); + emit log_named_decimal_uint(" % Delta", percentDelta * 100, 18); + fail(); + } + } + + function assertApproxEqRel( + uint256 a, + uint256 b, + uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% + string memory err + ) internal virtual { + if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log_named_string("Error", err); + assertApproxEqRel(a, b, maxPercentDelta); + } + } + + function assertApproxEqRelDecimal( + uint256 a, + uint256 b, + uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% + uint256 decimals + ) internal virtual { + if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log("Error: a ~= b not satisfied [uint]"); + emit log_named_decimal_uint(" Left", a, decimals); + emit log_named_decimal_uint(" Right", b, decimals); + emit log_named_decimal_uint(" Max % Delta", maxPercentDelta * 100, 18); + emit log_named_decimal_uint(" % Delta", percentDelta * 100, 18); + fail(); + } + } + + function assertApproxEqRelDecimal( + uint256 a, + uint256 b, + uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% + uint256 decimals, + string memory err + ) internal virtual { + if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log_named_string("Error", err); + assertApproxEqRelDecimal(a, b, maxPercentDelta, decimals); + } + } + + function assertApproxEqRel(int256 a, int256 b, uint256 maxPercentDelta) internal virtual { + if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log("Error: a ~= b not satisfied [int]"); + emit log_named_int(" Left", a); + emit log_named_int(" Right", b); + emit log_named_decimal_uint(" Max % Delta", maxPercentDelta * 100, 18); + emit log_named_decimal_uint(" % Delta", percentDelta * 100, 18); + fail(); + } + } + + function assertApproxEqRel(int256 a, int256 b, uint256 maxPercentDelta, string memory err) internal virtual { + if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log_named_string("Error", err); + assertApproxEqRel(a, b, maxPercentDelta); + } + } + + function assertApproxEqRelDecimal(int256 a, int256 b, uint256 maxPercentDelta, uint256 decimals) internal virtual { + if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log("Error: a ~= b not satisfied [int]"); + emit log_named_decimal_int(" Left", a, decimals); + emit log_named_decimal_int(" Right", b, decimals); + emit log_named_decimal_uint(" Max % Delta", maxPercentDelta * 100, 18); + emit log_named_decimal_uint(" % Delta", percentDelta * 100, 18); + fail(); + } + } + + function assertApproxEqRelDecimal(int256 a, int256 b, uint256 maxPercentDelta, uint256 decimals, string memory err) + internal + virtual + { + if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log_named_string("Error", err); + assertApproxEqRelDecimal(a, b, maxPercentDelta, decimals); + } + } + + function assertEqCall(address target, bytes memory callDataA, bytes memory callDataB) internal virtual { + assertEqCall(target, callDataA, target, callDataB, true); + } + + function assertEqCall(address targetA, bytes memory callDataA, address targetB, bytes memory callDataB) + internal + virtual + { + assertEqCall(targetA, callDataA, targetB, callDataB, true); + } + + function assertEqCall(address target, bytes memory callDataA, bytes memory callDataB, bool strictRevertData) + internal + virtual + { + assertEqCall(target, callDataA, target, callDataB, strictRevertData); + } + + function assertEqCall( + address targetA, + bytes memory callDataA, + address targetB, + bytes memory callDataB, + bool strictRevertData + ) internal virtual { + (bool successA, bytes memory returnDataA) = address(targetA).call(callDataA); + (bool successB, bytes memory returnDataB) = address(targetB).call(callDataB); + + if (successA && successB) { + assertEq(returnDataA, returnDataB, "Call return data does not match"); + } + + if (!successA && !successB && strictRevertData) { + assertEq(returnDataA, returnDataB, "Call revert data does not match"); + } + + if (!successA && successB) { + emit log("Error: Calls were not equal"); + emit log_named_bytes(" Left call revert data", returnDataA); + emit log_named_bytes(" Right call return data", returnDataB); + fail(); + } + + if (successA && !successB) { + emit log("Error: Calls were not equal"); + emit log_named_bytes(" Left call return data", returnDataA); + emit log_named_bytes(" Right call revert data", returnDataB); + fail(); + } + } +} diff --git a/tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol b/tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol new file mode 100644 index 000000000..e966c19ac --- /dev/null +++ b/tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AggregatorV3Interface} from "src/dependencies/chainlink/AggregatorV3Interface.sol"; + +contract MockPriceFeedSimulator is AggregatorV3Interface { + uint8 public immutable override decimals; + string public override description; + + int256 private _price; + + error OperationNotSupported(); + + constructor(uint8 decimals_, string memory description_, uint256 price_) { + decimals = decimals_; + description = description_; + _price = int256(price_); + } + + function version() external pure override returns (uint256) { + return 1; + } + + function getRoundData(uint80) external pure override returns (uint80, int256, uint256, uint256, uint80) { + revert OperationNotSupported(); + } + + function latestRoundData() + external + view + virtual + override + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + roundId = uint80(block.timestamp); + answer = _price; + startedAt = block.timestamp; + updatedAt = block.timestamp; + answeredInRound = roundId; + } + + function setPrice(uint256 price) external { + _price = int256(price); + } +} From 25693d435046c52804c1c5885eaa3aa0b20084cf Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 16:04:41 +0200 Subject: [PATCH 002/106] chore: failing replays, adnvanced setup, ci workflow, coverage --- .github/workflows/enigma-dark-invariants.yml | 58 +++ Makefile | 6 +- .../HandlerAggregator.t.sol | 2 + tests/enigma-dark-invariants/Invariants.t.sol | 30 +- tests/enigma-dark-invariants/Setup.t.sol | 336 +++++++++++++++--- .../_config/echidna_config_ci.yaml | 61 ++++ .../base/BaseStorage.t.sol | 41 ++- .../base/BaseTest.t.sol | 16 +- .../simulators/DonationAttackHandler.t.sol | 11 +- .../simulators/PriceFeeSimulatorHandler.t.sol | 6 +- .../handlers/spoke/SpokeHandler.t.sol | 10 +- .../handlers/spoke/TreasurySpokeHandler.t.sol | 32 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 84 +++-- .../hooks/HookAggregator.t.sol | 18 +- .../invariants/HubInvariants.t.sol | 66 ++-- .../invariants/SpokeInvariants.t.sol | 9 +- .../replays/ReplayTest_10.t.sol | 86 +++++ .../replays/ReplayTest_11.t.sol | 82 +++++ .../replays/ReplayTest_12.t.sol | 87 +++++ .../replays/ReplayTest_13.t.sol | 133 +++++++ .../replays/ReplayTest_3.t.sol | 199 +++++++++++ .../replays/ReplayTest_4.t.sol | 199 +++++++++++ .../replays/ReplayTest_5.t.sol | 84 +++++ .../replays/ReplayTest_6.t.sol | 199 +++++++++++ .../replays/ReplayTest_7.t.sol | 146 ++++++++ .../replays/ReplayTest_8.t.sol | 79 ++++ .../replays/ReplayTest_9.t.sol | 80 +++++ .../utils/PropertiesAsserts.sol | 31 ++ .../utils/PropertiesConstants.sol | 17 +- .../utils/mocks/MockPriceFeedSimulator.sol | 4 +- 30 files changed, 2026 insertions(+), 186 deletions(-) create mode 100644 .github/workflows/enigma-dark-invariants.yml create mode 100644 tests/enigma-dark-invariants/_config/echidna_config_ci.yaml create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_10.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_11.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_12.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_5.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_8.t.sol create mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_9.t.sol diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml new file mode 100644 index 000000000..bbd3249ae --- /dev/null +++ b/.github/workflows/enigma-dark-invariants.yml @@ -0,0 +1,58 @@ +name: Echidna Enigma Dark Invariants + +on: + push: + branches: + - main + pull_request: + +env: + FOUNDRY_PROFILE: ci + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + echidna: + name: Echidna Enigma Dark Invariants + runs-on: aave-latest + + strategy: + matrix: + mode: [property, assertion] # Define the modes here + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H github.com >> ~/.ssh/known_hosts + + - name: Clone the private repo into corpus folder + run: | + git clone git@github.com:aave/aave-v4-invariants-corpus.git corpus + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Compile contracts + run: | + forge build --build-info + + - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode + uses: crytic/echidna-action@v2 + with: + files: . + contract: Tester + config: tests/enigma-dark-invariants/_config/echidna_config_ci.yaml + crytic-args: --ignore-compile + test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} \ No newline at end of file diff --git a/Makefile b/Makefile index 26bb07e67..4fcfc6e5f 100644 --- a/Makefile +++ b/Makefile @@ -46,5 +46,7 @@ foundry-invariants: forge test --mc TesterFoundry -vvv # Echidna Results -runes: - runes convert ./tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/enigma-dark-invariants/replays \ No newline at end of file +runes-echidna: + runes convert ./tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/enigma-dark-invariants/replays +runes-medusa: + runes convert ./tests/enigma-dark-invariants/_corpus/medusa/ --output ./tests/enigma-dark-invariants/replays \ No newline at end of file diff --git a/tests/enigma-dark-invariants/HandlerAggregator.t.sol b/tests/enigma-dark-invariants/HandlerAggregator.t.sol index acd1ec48b..673ca4d06 100644 --- a/tests/enigma-dark-invariants/HandlerAggregator.t.sol +++ b/tests/enigma-dark-invariants/HandlerAggregator.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; // Handler contracts import {HubHandler} from "./handlers/hub/HubHandler.t.sol"; import {SpokeHandler} from "./handlers/spoke/SpokeHandler.t.sol"; +import {TreasurySpokeHandler} from "./handlers/spoke/TreasurySpokeHandler.t.sol"; import {HubConfiguratorHandler} from "./handlers/hub/HubConfiguratorHandler.t.sol"; import {SpokeConfiguratorHandler} from "./handlers/spoke/SpokeConfiguratorHandler.t.sol"; @@ -15,6 +16,7 @@ import {DonationAttackHandler} from "./handlers/simulators/DonationAttackHandler abstract contract HandlerAggregator is HubHandler, // Main handlers SpokeHandler, + TreasurySpokeHandler, HubConfiguratorHandler, // Configurators SpokeConfiguratorHandler, PriceFeeSimulatorHandler, // Simulators diff --git a/tests/enigma-dark-invariants/Invariants.t.sol b/tests/enigma-dark-invariants/Invariants.t.sol index 72a0d11b4..eec6a0ad2 100644 --- a/tests/enigma-dark-invariants/Invariants.t.sol +++ b/tests/enigma-dark-invariants/Invariants.t.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +// Interfaces +import {IHub} from "src/hub/interfaces/IHub.sol"; + // Invariant Contracts import {HubInvariants} from "./invariants/HubInvariants.t.sol"; import {SpokeInvariants} from "./invariants/SpokeInvariants.t.sol"; @@ -15,19 +18,20 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { /////////////////////////////////////////////////////////////////////////////////////////////// function invariant_INV_HUB() public returns (bool) { - // Applied per assetId registered in the hub - for (uint256 i; i < baseAssets.length; i++) { - uint256 assetId = baseAssets[i].assetId; - - // Hub invariants - assert_INV_HUB_A(assetId); - assert_INV_HUB_B(assetId); - assert_INV_HUB_C(assetId); - assert_INV_HUB_EF(assetId); - assert_INV_HUB_GH(assetId); - assert_INV_HUB_I(assetId, baseAssets[i].underlying); - assert_INV_HUB_K(assetId); - assert_INV_HUB_L(assetId); + for (uint256 i; i < hubAddresses.length; i++) { + address hubAddress = hubAddresses[i]; + + uint256 assetCount = IHub(hubAddress).getAssetCount(); + for (uint256 j; j < assetCount; j++) { + assert_INV_HUB_A(hubAddress, j); + assert_INV_HUB_B(hubAddress, j); + assert_INV_HUB_C(hubAddress, j); + assert_INV_HUB_EF(hubAddress, j); + assert_INV_HUB_GH(hubAddress, j); + assert_INV_HUB_I(hubAddress, j, baseAssets[i].underlying); + assert_INV_HUB_K(hubAddress, j); + assert_INV_HUB_L(hubAddress, j); + } } return true; diff --git a/tests/enigma-dark-invariants/Setup.t.sol b/tests/enigma-dark-invariants/Setup.t.sol index 4ccc4f9de..f9bc64a02 100644 --- a/tests/enigma-dark-invariants/Setup.t.sol +++ b/tests/enigma-dark-invariants/Setup.t.sol @@ -53,11 +53,9 @@ contract Setup is BaseTest { function _deployAssets() internal { usdc = new TestnetERC20("USDC", "USDC", 6); weth = new TestnetERC20("WETH", "WETH", 18); - usdcAssetId = 0; - wethAssetId = 1; - baseAssets.push(AssetInfo({assetId: usdcAssetId, underlying: address(usdc), decimals: 6})); - baseAssets.push(AssetInfo({assetId: wethAssetId, underlying: address(weth), decimals: 18})); + baseAssets.push(AssetInfo({underlying: address(usdc), decimals: 6})); + baseAssets.push(AssetInfo({underlying: address(weth), decimals: 18})); vm.label(address(usdc), "usdc"); vm.label(address(weth), "weth"); @@ -69,21 +67,40 @@ contract Setup is BaseTest { /// @notice Deploy protocol core contracts function _deployProtocolCore() internal { + // Access manager accessManager = new AccessManager(admin); - hub = new Hub(address(accessManager)); - irStrategy = new AssetInterestRateStrategy(address(hub)); + // Hub 1 + hub1 = new Hub(address(accessManager)); + irStrategy1 = new AssetInterestRateStrategy(address(hub1)); + hubInfo[address(hub1)] = HubInfo({treasureSpoke: address(treasurySpoke1), irStrategy: address(irStrategy1)}); + hubAddresses.push(address(hub1)); + + // Hub 2 + hub2 = new Hub(address(accessManager)); + irStrategy2 = new AssetInterestRateStrategy(address(hub2)); + hubInfo[address(hub2)] = HubInfo({treasureSpoke: address(treasurySpoke2), irStrategy: address(irStrategy2)}); + hubAddresses.push(address(hub2)); + + // Spokes (spoke1, oracle1) = _deploySpokeWithOracle(admin, address(accessManager), "Spoke 1 (USD)"); (spoke2, oracle2) = _deploySpokeWithOracle(admin, address(accessManager), "Spoke 2 (USD)"); - spokesAddresses.push(address(spoke1)); // TODO integrate spoke 2 and treasury spoke - treasurySpoke = ITreasurySpoke(new TreasurySpoke(admin, address(hub))); + treasurySpoke1 = ITreasurySpoke(new TreasurySpoke(admin, address(hub1))); + treasurySpoke2 = ITreasurySpoke(new TreasurySpoke(admin, address(hub2))); + allSpokes.push(address(treasurySpoke1)); + allSpokes.push(address(treasurySpoke2)); + vm.label(address(accessManager), "accessManager"); + vm.label(address(hub1), "hub1"); + vm.label(address(hub2), "hub2"); + vm.label(address(irStrategy1), "irStrategy1"); + vm.label(address(irStrategy2), "irStrategy2"); vm.label(address(spoke1), "spoke1"); vm.label(address(spoke2), "spoke2"); - vm.label(address(treasurySpoke), "treasurySpoke"); - vm.label(address(irStrategy), "irStrategy"); - vm.label(address(accessManager), "accessManager"); - vm.label(address(hub), "hub"); + vm.label(address(treasurySpoke1), "treasurySpoke1"); + vm.label(address(treasurySpoke2), "treasurySpoke2"); + vm.label(address(oracle1), "oracle1"); + vm.label(address(oracle2), "oracle2"); } /// @notice Deploy a spoke with an oracle using CREATE3 @@ -104,6 +121,10 @@ contract Setup is BaseTest { assertEq(address(oracle), predictedOracle, "predictedOracle mismatch"); assertEq(spoke.ORACLE(), address(oracle), "spoke.ORACLE() mismatch"); assertEq(oracle.SPOKE(), address(spoke), "oracle.SPOKE() mismatch"); + + spokesAddresses.push(address(spoke)); + allSpokes.push(address(spoke)); + return (spoke, oracle); } @@ -118,89 +139,294 @@ contract Setup is BaseTest { /////////////////////////////////////////////////////////////////////////////////////////////// function _configureTokenList() internal { - IHub.SpokeConfig memory spokeConfig = IHub.SpokeConfig({ - active: true, - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP - }); + // Configure hubs + _configureHubs(); + + // Configure spokes + _configureSpokes(); + } - bytes memory encodedIrData = abi.encode( + /// @notice Configure the hubs + function _configureHubs() internal { + // HUB 1 + bytes memory encodedIrData1 = abi.encode( IAssetInterestRateStrategy.InterestRateData({ - optimalUsageRatio: 90_00, // 90.00% - baseVariableBorrowRate: 5_00, // 5.00% - variableRateSlope1: 5_00, // 5.00% - variableRateSlope2: 5_00 // 5.00% + optimalUsageRatio: OPTIMAL_USAGE_RATIO_IR1, + baseVariableBorrowRate: BASE_VARIABLE_BORROW_RATE_IR1, + variableRateSlope1: VARIABLE_RATE_SLOPE_1_IR1, + variableRateSlope2: VARIABLE_RATE_SLOPE_2_IR1 }) ); // Add USDC - hub.addAsset(address(usdc), usdc.decimals(), address(treasurySpoke), address(irStrategy), encodedIrData); - hub.updateAssetConfig( - usdcAssetId, + hub1UsdcAssetId = hub1.addAsset( + address(usdc), usdc.decimals(), address(treasurySpoke1), address(irStrategy1), encodedIrData1 + ); + hub1.updateAssetConfig( + hub1UsdcAssetId, IHub.AssetConfig({ liquidityFee: 5_00, - feeReceiver: address(treasurySpoke), - irStrategy: address(irStrategy), - reinvestmentController: address(0) // TODO should this be integrated? + feeReceiver: address(treasurySpoke1), + irStrategy: address(irStrategy1), + reinvestmentController: address(0) }), new bytes(0) ); // Add WETH - hub.addAsset(address(weth), weth.decimals(), address(treasurySpoke), address(irStrategy), encodedIrData); - hub.updateAssetConfig( - wethAssetId, + hub1WethAssetId = hub1.addAsset( + address(weth), weth.decimals(), address(treasurySpoke1), address(irStrategy1), encodedIrData1 + ); + hub1.updateAssetConfig( + hub1WethAssetId, IHub.AssetConfig({ liquidityFee: 10_00, - feeReceiver: address(treasurySpoke), - irStrategy: address(irStrategy), + feeReceiver: address(treasurySpoke1), + irStrategy: address(irStrategy1), reinvestmentController: address(0) }), new bytes(0) ); + // HUB 2 + bytes memory encodedIrData2 = abi.encode( + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: OPTIMAL_USAGE_RATIO_IR2, + baseVariableBorrowRate: BASE_VARIABLE_BORROW_RATE_IR2, + variableRateSlope1: VARIABLE_RATE_SLOPE_1_IR2, + variableRateSlope2: VARIABLE_RATE_SLOPE_2_IR2 + }) + ); + + // Add WETH + hub2WethAssetId = hub2.addAsset( + address(weth), weth.decimals(), address(treasurySpoke2), address(irStrategy2), encodedIrData2 + ); + hub2.updateAssetConfig( + hub2WethAssetId, + IHub.AssetConfig({ + liquidityFee: 10_00, + feeReceiver: address(treasurySpoke2), + irStrategy: address(irStrategy2), + reinvestmentController: address(0) + }), + new bytes(0) + ); + + // Add USDC + hub2UsdcAssetId = hub2.addAsset( + address(usdc), usdc.decimals(), address(treasurySpoke2), address(irStrategy2), encodedIrData2 + ); + hub2.updateAssetConfig( + hub2UsdcAssetId, + IHub.AssetConfig({ + liquidityFee: 5_00, + feeReceiver: address(treasurySpoke2), + irStrategy: address(irStrategy2), + reinvestmentController: address(0) // TODO should this be integrated? + }), + new bytes(0) + ); + } + + function _configureSpokes() internal { // Spoke 1 reserve configs + spokeInfo[spoke1].usdc.reserveConfig = + ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 30_00}); + spokeInfo[spoke1].usdc.dynReserveConfig = + ISpoke.DynamicReserveConfig({collateralFactor: 90_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); + spokeInfo[spoke1].weth.reserveConfig = - ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 15_00}); + ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 20_00}); spokeInfo[spoke1].weth.dynReserveConfig = ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); - spokeInfo[spoke1].usdc.reserveConfig = - ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 50_00}); - spokeInfo[spoke1].usdc.dynReserveConfig = - ISpoke.DynamicReserveConfig({collateralFactor: 78_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); + // Spoke 2 reserve configs + spokeInfo[spoke2].weth.reserveConfig = + ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 10_00}); + spokeInfo[spoke2].weth.dynReserveConfig = + ISpoke.DynamicReserveConfig({collateralFactor: 70_00, maxLiquidationBonus: 110_00, liquidationFee: 0}); + spokeInfo[spoke2].usdc.reserveConfig = + ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 15_00}); + spokeInfo[spoke2].usdc.dynReserveConfig = + ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 110_00, liquidationFee: 0}); + + // Deploy price feeds priceFeeds.push(_deployMockPriceFeed(spoke1, 1e8)); priceFeeds.push(_deployMockPriceFeed(spoke1, 2000e8)); + // Add reserves to spoke 1 spokeInfo[spoke1].usdc.reserveId = spoke1.addReserve( - address(hub), - usdcAssetId, + address(hub1), + hub1UsdcAssetId, + priceFeeds[0], + spokeInfo[spoke1].usdc.reserveConfig, + spokeInfo[spoke1].usdc.dynReserveConfig + ); + spokeInfo[spoke1].usdc2.reserveId = spoke1.addReserve( + address(hub2), + hub2UsdcAssetId, priceFeeds[0], spokeInfo[spoke1].usdc.reserveConfig, spokeInfo[spoke1].usdc.dynReserveConfig ); - spokeInfo[spoke1].weth.reserveId = spoke1.addReserve( - address(hub), - wethAssetId, + address(hub1), + hub1WethAssetId, + priceFeeds[1], + spokeInfo[spoke1].weth.reserveConfig, + spokeInfo[spoke1].weth.dynReserveConfig + ); + spokeInfo[spoke1].weth2.reserveId = spoke1.addReserve( + address(hub2), + hub2WethAssetId, priceFeeds[1], spokeInfo[spoke1].weth.reserveConfig, spokeInfo[spoke1].weth.dynReserveConfig ); - assetIdToReserveId[address(spoke1)][usdcAssetId] = spokeInfo[spoke1].usdc.reserveId; - assetIdToReserveId[address(spoke1)][wethAssetId] = spokeInfo[spoke1].weth.reserveId; - reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].usdc.reserveId] = usdcAssetId; - reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].weth.reserveId] = wethAssetId; + // Add reserves to spoke 2 + spokeInfo[spoke2].weth.reserveId = spoke2.addReserve( + address(hub1), + hub1WethAssetId, + priceFeeds[1], + spokeInfo[spoke2].weth.reserveConfig, + spokeInfo[spoke2].weth.dynReserveConfig + ); + spokeInfo[spoke2].weth2.reserveId = spoke2.addReserve( + address(hub2), + hub2WethAssetId, + priceFeeds[1], + spokeInfo[spoke2].weth.reserveConfig, + spokeInfo[spoke2].weth.dynReserveConfig + ); + spokeInfo[spoke2].usdc.reserveId = spoke2.addReserve( + address(hub1), + hub1UsdcAssetId, + priceFeeds[0], + spokeInfo[spoke2].usdc.reserveConfig, + spokeInfo[spoke2].usdc.dynReserveConfig + ); + spokeInfo[spoke2].usdc2.reserveId = spoke2.addReserve( + address(hub2), + hub2UsdcAssetId, + priceFeeds[0], + spokeInfo[spoke2].usdc.reserveConfig, + spokeInfo[spoke2].usdc.dynReserveConfig + ); + // Map ids for spoke 1 + assetIdToReserveId[address(spoke1)][hub1UsdcAssetId] = spokeInfo[spoke1].usdc.reserveId; + assetIdToReserveId[address(spoke1)][hub2UsdcAssetId] = spokeInfo[spoke1].usdc2.reserveId; + assetIdToReserveId[address(spoke1)][hub1WethAssetId] = spokeInfo[spoke1].weth.reserveId; + assetIdToReserveId[address(spoke1)][hub2WethAssetId] = spokeInfo[spoke1].weth2.reserveId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].usdc.reserveId] = hub1UsdcAssetId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].usdc2.reserveId] = hub2UsdcAssetId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].weth.reserveId] = hub1WethAssetId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].weth2.reserveId] = hub2WethAssetId; + reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].usdc.reserveId] = address(hub1); + reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].usdc2.reserveId] = address(hub2); + reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].weth.reserveId] = address(hub1); + reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].weth2.reserveId] = address(hub2); + + // Map ids for spoke 2 + assetIdToReserveId[address(spoke2)][hub1UsdcAssetId] = spokeInfo[spoke2].usdc.reserveId; + assetIdToReserveId[address(spoke2)][hub2UsdcAssetId] = spokeInfo[spoke2].usdc2.reserveId; + assetIdToReserveId[address(spoke2)][hub1WethAssetId] = spokeInfo[spoke2].weth.reserveId; + assetIdToReserveId[address(spoke2)][hub2WethAssetId] = spokeInfo[spoke2].weth2.reserveId; + reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].usdc.reserveId] = hub1UsdcAssetId; + reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].usdc2.reserveId] = hub2UsdcAssetId; + reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].weth.reserveId] = hub1WethAssetId; + reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].weth2.reserveId] = hub2WethAssetId; + reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].usdc.reserveId] = address(hub1); + reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].usdc2.reserveId] = address(hub2); + reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].weth.reserveId] = address(hub1); + reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].weth2.reserveId] = address(hub2); + + // Store spoke reserve ids on array spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].usdc.reserveId); + spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].usdc2.reserveId); spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].weth.reserveId); + spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].weth2.reserveId); + spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].usdc.reserveId); + spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].usdc2.reserveId); + spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].weth.reserveId); + spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].weth2.reserveId); + + // TreasurySpoke uses assetIds directly as reserveIds + spokeReserveIds[address(treasurySpoke1)].push(hub1UsdcAssetId); + spokeReserveIds[address(treasurySpoke1)].push(hub1WethAssetId); + spokeReserveIds[address(treasurySpoke2)].push(hub2UsdcAssetId); + spokeReserveIds[address(treasurySpoke2)].push(hub2WethAssetId); + + // Add SPOKE 1 assets to hubs + hub1.addSpoke( + hub1UsdcAssetId, + address(spoke1), + IHub.SpokeConfig({ + active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP + }) + ); + hub2.addSpoke( + hub2UsdcAssetId, + address(spoke1), + IHub.SpokeConfig({ + active: true, + addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3 + }) + ); + hub1.addSpoke( + hub1WethAssetId, + address(spoke1), + IHub.SpokeConfig({ + active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP + }) + ); + hub2.addSpoke( + hub2WethAssetId, + address(spoke1), + IHub.SpokeConfig({ + active: true, + addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2 + }) + ); - hub.addSpoke(wethAssetId, address(spoke1), spokeConfig); - hub.addSpoke(usdcAssetId, address(spoke1), spokeConfig); - - // TODO integrate spoke 2 + // Add SPOKE 2 assets to hubs + hub2.addSpoke( + hub2WethAssetId, + address(spoke2), + IHub.SpokeConfig({ + active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP + }) + ); + hub1.addSpoke( + hub1WethAssetId, + address(spoke2), + IHub.SpokeConfig({ + active: true, + addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2 + }) + ); + hub2.addSpoke( + hub2UsdcAssetId, + address(spoke2), + IHub.SpokeConfig({ + active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP + }) + ); + hub1.addSpoke( + hub1UsdcAssetId, + address(spoke2), + IHub.SpokeConfig({ + active: true, + addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3 + }) + ); } function _deployMockPriceFeed(ISpoke spoke, uint256 price) internal returns (address) { @@ -225,10 +451,11 @@ contract Setup is BaseTest { tokens[0] = address(usdc); tokens[1] = address(weth); - address[] memory contracts = new address[](3); + address[] memory contracts = new address[](4); contracts[0] = address(spoke1); contracts[1] = address(spoke2); - contracts[2] = address(hub); + contracts[2] = address(hub1); + contracts[3] = address(hub2); for (uint256 i; i < NUMBER_OF_ACTORS; i++) { // Deploy actor proxies and approve system contracts @@ -238,7 +465,8 @@ contract Setup is BaseTest { // Mint initial balances to actors for (uint256 j = 0; j < tokens.length; j++) { TestnetERC20 _token = TestnetERC20(tokens[j]); - _token.mint(_actor, INITIAL_BALANCE); + uint256 decimals = _token.decimals(); + _token.mint(_actor, INITIAL_BALANCE * 10 ** decimals); } actorAddresses.push(_actor); } diff --git a/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml b/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml new file mode 100644 index 000000000..0949429be --- /dev/null +++ b/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml @@ -0,0 +1,61 @@ +#codeSize max code size for deployed contratcs (default 24576, per EIP-170) +codeSize: 224576 + +#whether ot not to use the multi-abi mode of testing +#it’s not working for us, see: https://github.com/crytic/echidna/issues/547 +#multi-abi: true + +#balanceAddr is default balance for addresses +balanceAddr: 0x1000000000000000000000000 +#balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) +balanceContract: 0x1000000000000000000000000000000000000000000000000 + +#testLimit is the number of test sequences to run +testLimit: 80000 + +#seqLen defines how many transactions are in a test sequence +seqLen: 300 + +#shrinkLimit determines how much effort is spent shrinking failing sequences +shrinkLimit: 1500 + +#propMaxGas defines gas cost at which a property fails +propMaxGas: 1000000000 + +#testMaxGas is a gas limit; does not cause failure, but terminates sequence +testMaxGas: 1000000000 + +# list of methods to filter +filterFunctions: [ "Tester.checkPostConditions()" ] +# by default, blacklist methods in filterFunctions +#filterBlacklist: false + +prefix: "invariant_" + +#stopOnFail makes echidna terminate as soon as any property fails and has been shrunk +stopOnFail: false + +#coverage controls coverage guided testing +coverage: true + +# list of file formats to save coverage reports in; default is all possible formats +coverageFormats: [ "html" ] + +#directory to save the corpus; by default is disabled +corpusDir: "tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus" +# constants for corpus mutations (for experimentation only) +#mutConsts: [100, 1, 1] + +#remappings +cryticArgs: [ "--solc-remaps", "forge-std/=lib/forge-std/src/", "--compile-libraries=(LiquidationLogic,0xf01)" ] + +deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] + +# maximum value to send to payable functions +maxValue: 1e+30 # 100000000000 eth + +#quiet produces (much) less verbose output +quiet: false + +# concurrent workers +workers: 10 diff --git a/tests/enigma-dark-invariants/base/BaseStorage.t.sol b/tests/enigma-dark-invariants/base/BaseStorage.t.sol index 31b9d1ca4..c279fe716 100644 --- a/tests/enigma-dark-invariants/base/BaseStorage.t.sol +++ b/tests/enigma-dark-invariants/base/BaseStorage.t.sol @@ -61,18 +61,19 @@ abstract contract BaseStorage { /// @notice The WETH token TestnetERC20 internal weth; - /// @notice The asset IDs of the USDC and WETH tokens - uint256 internal usdcAssetId; - uint256 internal wethAssetId; - /////////////////////////////////////////////////////////////////////////////////////////////// // SUITE STORAGE // /////////////////////////////////////////////////////////////////////////////////////////////// - // PROTOCOL CONTRACTS - IHub internal hub; + // HUB CONTRACTS + IHub internal hub1; + IHub internal hub2; + AssetInterestRateStrategy internal irStrategy1; + AssetInterestRateStrategy internal irStrategy2; - ITreasurySpoke internal treasurySpoke; + // SPOKE CONTRACTS + ITreasurySpoke internal treasurySpoke1; + ITreasurySpoke internal treasurySpoke2; ISpoke internal spoke1; ISpoke internal spoke2; @@ -81,11 +82,8 @@ abstract contract BaseStorage { IAaveOracle internal oracle2; // CONFIGURATION - AssetInterestRateStrategy internal irStrategy; AccessManager internal accessManager; - // MOCKS - /////////////////////////////////////////////////////////////////////////////////////////////// // EXTRA VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -94,9 +92,22 @@ abstract contract BaseStorage { /// @notice Array of base assets for the suite AssetInfo[] internal baseAssets; + // HUB + uint256 internal hub1WethAssetId; + uint256 internal hub1UsdcAssetId; + uint256 internal hub2WethAssetId; + uint256 internal hub2UsdcAssetId; + + /// @notice Array of hub addresses for the suite + address[] internal hubAddresses; + /// @notice Spoke configurations + mapping(address => HubInfo) internal hubInfo; + // SPOKES /// @notice Array of spokes addresses for the suite address[] internal spokesAddresses; + /// @notice spokesAddresses + treasurySpoke address + address[] internal allSpokes; /// @notice Spoke configurations mapping(ISpoke => SpokeInfo) internal spokeInfo; /// @notice Spoke reserveIds @@ -105,6 +116,8 @@ abstract contract BaseStorage { mapping(address => mapping(uint256 => uint256)) internal reserveIdToAssetId; /// @notice Spoke assetIds to reserveIds info mapping(address => mapping(uint256 => uint256)) internal assetIdToReserveId; + /// @notice Spoke reserveIds to hub addresses + mapping(address => mapping(uint256 => address)) internal reserveIdToHubAddress; // PRICE FEEDS address[] internal priceFeeds; @@ -116,9 +129,16 @@ abstract contract BaseStorage { struct SpokeInfo { ReserveInfo weth; ReserveInfo usdc; + ReserveInfo weth2; + ReserveInfo usdc2; uint256 MAX_ALLOWED_ASSET_ID; } + struct HubInfo { + address treasureSpoke; + address irStrategy; + } + struct ReserveInfo { uint256 reserveId; ISpoke.ReserveConfig reserveConfig; @@ -126,7 +146,6 @@ abstract contract BaseStorage { } struct AssetInfo { - uint256 assetId; address underlying; uint8 decimals; } diff --git a/tests/enigma-dark-invariants/base/BaseTest.t.sol b/tests/enigma-dark-invariants/base/BaseTest.t.sol index 9f4d2df0e..a3cac0d7f 100644 --- a/tests/enigma-dark-invariants/base/BaseTest.t.sol +++ b/tests/enigma-dark-invariants/base/BaseTest.t.sol @@ -75,12 +75,6 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU return baseAssets[_assetIndex].underlying; } - /// @notice Helper function to get a random base asset id - function _getRandomBaseAssetId(uint256 i) internal view returns (uint256) { - uint256 _assetIndex = i % baseAssets.length; - return baseAssets[_assetIndex].assetId; - } - /// @notice Helper function to get random base asset full info function _getRandomBaseAssetFullInfo(uint256 i) internal view returns (AssetInfo memory) { uint256 _assetIndex = i % baseAssets.length; @@ -105,6 +99,12 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU return priceFeeds[_priceFeedIndex]; } + /// @notice Helper function to get a random hub address + function _getRandomHubAddress(uint256 i) internal view returns (address) { + uint256 _hubIndex = i % hubAddresses.length; + return hubAddresses[_hubIndex]; + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS: GETTERS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -117,6 +117,10 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU return assetIdToReserveId[spoke][assetId]; } + function _getHubAddress(address spoke, uint256 reserveId) internal view returns (address) { + return reserveIdToHubAddress[spoke][reserveId]; + } + function _isHealthy(address spoke, address user) internal view returns (bool) { return ISpoke(spoke).getUserAccountData(user).healthFactor >= ISpoke(spoke).HEALTH_FACTOR_LIQUIDATION_THRESHOLD(); diff --git a/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol b/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol index c8a0353b0..87c0525e6 100644 --- a/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol @@ -20,17 +20,22 @@ contract DonationAttackHandler is BaseHandler { // OWNER ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - function donateUnderlyingToHub(uint256 amount, uint8 i) external { + function donateUnderlyingToHub(uint256 amount, uint8 i, uint8 j) external { + // Get one of the hub addresses randomly + address hubAddress = _getRandomHubAddress(j); + // Get one of the assets IDs randomly address underlying = _getRandomBaseAsset(i); - TestnetERC20(underlying).mint(address(hub), amount); + TestnetERC20(underlying).mint(hubAddress, amount); } function donateUnderlyingToSpoke(uint256 amount, uint8 i, uint8 j) external { + // Get one of the spoke addresses randomly address spoke = _getRandomSpoke(j); + // Get one of the assets IDs randomly address underlying = _getRandomBaseAsset(i); - TestnetERC20(underlying).mint(address(spoke), amount); + TestnetERC20(underlying).mint(spoke, amount); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol b/tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol index 56ea6c24f..b0e7933f9 100644 --- a/tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.19; import {BaseHandler} from "../../base/BaseHandler.t.sol"; import {MockPriceFeedSimulator} from "../../utils/mocks/MockPriceFeedSimulator.sol"; +import "forge-std/console.sol"; + /// @title PriceFeeSimulatorHandler /// @notice Handler test contract for a set of actions contract PriceFeeSimulatorHandler is BaseHandler { @@ -20,9 +22,11 @@ contract PriceFeeSimulatorHandler is BaseHandler { // OWNER ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - function setPrice(uint256 price, uint8 i) public { + function setPrice(int256 price, uint8 i) public { address priceFeed = _getRandomPriceFeed(i); + price = clampBetween(price, PRICE_MIN, PRICE_MAX); + MockPriceFeedSimulator(priceFeed).setPrice(price); } diff --git a/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol b/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol index fe085adbb..82ceaa1a9 100644 --- a/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol @@ -202,7 +202,11 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { uint256 reserveId = _getReserveId(spoke, j); // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, onBehalfOf); + /// @dev setUsingAsCollateral(reserveId, FALSE) all reserves in user position should be refreshed, + /// so we check all reserves in user position + /// setUsingAsCollateral(reserveId, TRUE) only reserveId in user position should be refreshed, + /// so we check only the reserveId in user position + _registerUserToCheck(spoke, (usingAsCollateral ? reserveId : CHECK_ALL_RESERVES), onBehalfOf); _before(); (success, returnData) = @@ -236,9 +240,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ///// HSPOST ///// - assertApproxEqAbs( - totalDebt, totalDebt, 2, HSPOST_SP_F - ); + assertApproxEqAbs(totalDebt, totalDebt, 2, HSPOST_SP_F); } else { revert("DefaultHandler: updateUserRiskPremium failed"); } diff --git a/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol b/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol index df31b7cab..539764b91 100644 --- a/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; // Interfaces import {ITreasurySpokeHandler} from "../interfaces/ITreasurySpokeHandler.sol"; +import {ITreasurySpoke} from "src/spoke/interfaces/ITreasurySpoke.sol"; // Libraries import "forge-std/console.sol"; @@ -32,39 +33,50 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { // OWNER ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - function supply(uint256 amount, uint8 i) external setup { + function supply(uint256 amount, uint8 i, uint8 j) external { + // Get one of the hub addresses randomly + address hubAddress = _getRandomHubAddress(i); + address treasurySpoke = hubInfo[hubAddress].treasureSpoke; + // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(address(treasurySpoke), i); + uint256 reserveId = _getRandomReserveId(treasurySpoke, j); _before(); - try treasurySpoke.supply(reserveId, amount, msg.sender) { + try ITreasurySpoke(treasurySpoke).supply(reserveId, amount, msg.sender) { _after(); } catch { revert("DefaultHandler: supply failed"); } } - function withdraw(uint256 amount, uint8 i) external setup { + function withdraw(uint256 amount, uint8 i, uint8 j) external { + // Get one of the hub addresses randomly + address hubAddress = _getRandomHubAddress(i); + address treasurySpoke = hubInfo[hubAddress].treasureSpoke; + // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(address(treasurySpoke), i); + uint256 reserveId = _getRandomReserveId(treasurySpoke, j); _before(); - try treasurySpoke.withdraw(reserveId, amount, msg.sender) { + try ITreasurySpoke(treasurySpoke).withdraw(reserveId, amount, msg.sender) { _after(); } catch { revert("DefaultHandler: withdraw failed"); } } - function transfer(uint256 amount, uint8 i, uint8 j) external setup { - // Get one of the actors randomly - address to = _getRandomActor(i); + function transfer(uint256 amount, uint8 i, uint8 j, uint8 k) external { + // Get one of the hub addresses randomly + address hubAddress = _getRandomHubAddress(i); // Get one of the assets IDs randomly address asset = _getRandomBaseAsset(j); + // Get one of the actors randomly + address to = _getRandomActor(k); + _before(); - try treasurySpoke.transfer(asset, to, amount) { + try ITreasurySpoke(hubInfo[hubAddress].treasureSpoke).transfer(asset, to, amount) { _after(); } catch { revert("DefaultHandler: transfer failed"); diff --git a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol index 03e94d59c..5ce7cf011 100644 --- a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol @@ -14,6 +14,7 @@ import {StdAsserts} from "../utils/StdAsserts.sol"; import {ISpokeHandler} from "../handlers/interfaces/ISpokeHandler.sol"; import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; import {IHub} from "src/hub/interfaces/IHub.sol"; +import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; // Contracts import {BaseHooks} from "../base/BaseHooks.t.sol"; @@ -40,7 +41,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } struct DefaultVars { - mapping(uint256 assetId => AssetVars) assetVars; + mapping(address hub => mapping(uint256 assetId => AssetVars)) assetVars; mapping(address spoke => mapping(uint256 reserveId => mapping(address user => UserVars))) userVars; } @@ -94,13 +95,16 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { /////////////////////////////////////////////////////////////////////////////////////////////// function _setAssetValues(DefaultVars storage _defaultVars) internal { - // Iterate through all assets IDs registered in the hub - for (uint256 i; i < baseAssets.length; i++) { - _defaultVars.assetVars[i].drawnIndex = hub.getAssetDrawnIndex(baseAssets[i].assetId); - _defaultVars.assetVars[i].totalAssets = hub.getAddedAssets(baseAssets[i].assetId); - _defaultVars.assetVars[i].totalShares = hub.getAddedShares(baseAssets[i].assetId); - (_defaultVars.assetVars[i].drawn, _defaultVars.assetVars[i].premium) = - hub.getAssetOwed(baseAssets[i].assetId); + for (uint256 i; i < hubAddresses.length; i++) { + address hubAddress = hubAddresses[i]; + uint256 assetCount = IHub(hubAddress).getAssetCount(); + for (uint256 j; j < assetCount; j++) { + _defaultVars.assetVars[hubAddress][j].drawnIndex = IHub(hubAddress).getAssetDrawnIndex(j); + _defaultVars.assetVars[hubAddress][j].totalAssets = IHub(hubAddress).getAddedAssets(j); + _defaultVars.assetVars[hubAddress][j].totalShares = IHub(hubAddress).getAddedShares(j); + (_defaultVars.assetVars[hubAddress][j].drawn, _defaultVars.assetVars[hubAddress][j].premium) = + IHub(hubAddress).getAssetOwed(j); + } } } @@ -114,10 +118,10 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { // Iterate through all reserves of the spoke for (uint256 j; j < spokeReserveIds[userInfo.spoke].length; j++) { ( - , - _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + , + _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] .premiumDebt - ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); + ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].totalDebt = ISpoke(userInfo.spoke).getUserTotalDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); } @@ -135,39 +139,43 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { // POST CONDITIONS: HUB // /////////////////////////////////////////////////////////////////////////////////////////////// - function assert_GPOST_HUB_A(uint256 assetId) internal { + function assert_GPOST_HUB_A(address hubAddress, uint256 assetId) internal { assertGe( - defaultVarsAfter.assetVars[assetId].drawnIndex, defaultVarsBefore.assetVars[assetId].drawnIndex, GPOST_HUB_A + defaultVarsAfter.assetVars[hubAddress][assetId].drawnIndex, + defaultVarsBefore.assetVars[hubAddress][assetId].drawnIndex, + GPOST_HUB_A ); } - function assert_GPOST_HUB_B(uint256 assetId) internal { - uint256 totalSharesBefore = defaultVarsBefore.assetVars[assetId].totalShares; - uint256 totalSharesAfter = defaultVarsAfter.assetVars[assetId].totalShares; - - uint256 rateWadRawBefore = totalSharesBefore == 0 - ? 0 - : MathUtils.mulDivDown(defaultVarsBefore.assetVars[assetId].totalAssets, 1e18, totalSharesBefore); - - uint256 rateWadRawAfter = totalSharesAfter == 0 - ? 0 - : MathUtils.mulDivDown(defaultVarsAfter.assetVars[assetId].totalAssets, 1e18, totalSharesAfter); - - assertGe(rateWadRawAfter, rateWadRawBefore, GPOST_HUB_B); + function assert_GPOST_HUB_B(address hubAddress, uint256 assetId) internal { + assertFullMulGe( + defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets, + defaultVarsBefore.assetVars[hubAddress][assetId].totalShares, + defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, + defaultVarsAfter.assetVars[hubAddress][assetId].totalShares, + GPOST_HUB_B + ); } - function assert_GPOST_HUB_C(uint256 assetId) internal { - assertEq( - hub.getAssetDrawnRate(assetId), - irStrategy.calculateInterestRate( - assetId, - hub.getLiquidity(assetId), - defaultVarsAfter.assetVars[assetId].drawn, - hub.getDeficit(assetId), - hub.getSwept(assetId) - ), - GPOST_HUB_C - ); + function assert_GPOST_HUB_C(address hubAddress, uint256 assetId) internal { + if ( + msg.sig == ISpokeHandler.supply.selector || msg.sig == ISpokeHandler.withdraw.selector + || msg.sig == ISpokeHandler.borrow.selector || msg.sig == ISpokeHandler.repay.selector + || msg.sig == ISpokeHandler.updateUserRiskPremium.selector + ) { + assertEq( + IHub(hubAddress).getAssetDrawnRate(assetId), + IAssetInterestRateStrategy(hubInfo[hubAddress].irStrategy) + .calculateInterestRate( + assetId, + IHub(hubAddress).getLiquidity(assetId), + defaultVarsAfter.assetVars[hubAddress][assetId].drawn, + IHub(hubAddress).getDeficit(assetId), + IHub(hubAddress).getSwept(assetId) + ), + GPOST_HUB_C + ); + } } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol index 739c62230..1c897d6f7 100644 --- a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol +++ b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol @@ -11,6 +11,7 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { bytes4 internal constant _PANIC_SELECTOR = 0x4e487b71; /// @dev Panic code for assertion failed (0x01) uint256 internal constant _PANIC_ASSERTION_FAILED = 0x01; + /////////////////////////////////////////////////////////////////////////////////////////////// // SETUP // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -64,12 +65,17 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { } function _hubPostConditions() internal { - // Iterate through all assets IDs registered in the hub - for (uint256 i; i < baseAssets.length; i++) { - uint256 assetId = baseAssets[i].assetId; - assert_GPOST_HUB_A(assetId); - assert_GPOST_HUB_B(assetId); - assert_GPOST_HUB_C(assetId); + // Iterate through all users to check + for (uint256 i; i < usersToCheck.length; i++) { + // Avoid checking postconditions for CHECK_ALL_RESERVES actions + if (usersToCheck[i].reserveId != CHECK_ALL_RESERVES) { + uint256 assetId = _getAssetId(usersToCheck[i].spoke, usersToCheck[i].reserveId); + address hubAddress = _getHubAddress(usersToCheck[i].spoke, usersToCheck[i].reserveId); + + assert_GPOST_HUB_A(hubAddress, assetId); + assert_GPOST_HUB_B(hubAddress, assetId); + assert_GPOST_HUB_C(hubAddress, assetId); + } } } diff --git a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol index dd952d9d0..e4e2a4197 100644 --- a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol @@ -8,8 +8,6 @@ import {IERC20} from "src/dependencies/openzeppelin/IERC20.sol"; // Contracts import {HandlerAggregator} from "../HandlerAggregator.t.sol"; -import "forge-std/console.sol"; - /// @title HubInvariants /// @notice Implements Hub Invariants for the protocol /// @dev Inherits HandlerAggregator to check actions in assertion testing mode @@ -18,29 +16,29 @@ abstract contract HubInvariants is HandlerAggregator { // HUB // /////////////////////////////////////////////////////////////////////////////////////////////// - function assert_INV_HUB_A(uint256 assetId) internal { - uint256 assets = hub.getAddedAssets(assetId); + function assert_INV_HUB_A(address hubAddress, uint256 assetId) internal { + uint256 assets = IHub(hubAddress).getAddedAssets(assetId); if (assets == 0) { - assertEq(hub.getAddedShares(assetId), 0, INV_HUB_A2); + assertEq(IHub(hubAddress).getAddedShares(assetId), 0, INV_HUB_A2); } } - function assert_INV_HUB_B(uint256 assetId) internal { + function assert_INV_HUB_B(address hubAddress, uint256 assetId) internal { // Sum per-spoke values uint256 spokeCount = spokesAddresses.length; uint256 sumDebt; for (uint256 i; i < spokeCount; i++) { - (uint256 d, uint256 p) = hub.getSpokeOwed(assetId, spokesAddresses[i]); + (uint256 d, uint256 p) = IHub(hubAddress).getSpokeOwed(assetId, spokesAddresses[i]); sumDebt += d + p; } - uint256 assetTotal = hub.getAssetTotalOwed(assetId); // drawn + premium + uint256 assetTotal = IHub(hubAddress).getAssetTotalOwed(assetId); // drawn + premium assertGe(sumDebt, assetTotal, INV_HUB_B); } - function assert_INV_HUB_C(uint256 assetId) internal { + function assert_INV_HUB_C(address hubAddress, uint256 assetId) internal { // Sum per-spoke values uint256 spokeCount = spokesAddresses.length; @@ -51,16 +49,16 @@ abstract contract HubInvariants is HandlerAggregator { for (uint256 i; i < spokeCount; i++) { address spoke = spokesAddresses[i]; - sumDrawnShares += hub.getSpokeDrawnShares(assetId, spoke); + sumDrawnShares += IHub(hubAddress).getSpokeDrawnShares(assetId, spoke); (uint256 premiumDrawnShares, uint256 premiumOffset, uint256 realizedPremium) = - hub.getSpokePremiumData(assetId, spoke); + IHub(hubAddress).getSpokePremiumData(assetId, spoke); sumPremDrawnShares += premiumDrawnShares; sumPremOffset += premiumOffset; sumPremRealized += realizedPremium; } // Asset totals - IHub.Asset memory a = hub.getAsset(assetId); + IHub.Asset memory a = IHub(hubAddress).getAsset(assetId); // Checks assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); @@ -69,56 +67,56 @@ abstract contract HubInvariants is HandlerAggregator { assertEq(sumPremRealized, a.realizedPremium, INV_HUB_C); } - function assert_INV_HUB_EF(uint256 assetId) internal { + function assert_INV_HUB_EF(address hubAddress, uint256 assetId) internal { // Total amounts - uint256 totalSuppliedAssets = hub.getAddedAssets(assetId); - uint256 convertedAssets = hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)); + uint256 totalSuppliedAssets = IHub(hubAddress).getAddedAssets(assetId); + uint256 convertedAssets = + IHub(hubAddress).previewRemoveByShares(assetId, IHub(hubAddress).getAddedShares(assetId)); - IHub.Asset memory asset = hub.getAsset(assetId); - uint256 totalDebt = hub.getAssetTotalOwed(assetId); + IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); + uint256 totalDebt = IHub(hubAddress).getAssetTotalOwed(assetId); // Checks //assertEq(totalSuppliedAssets, convertedAssets, INV_HUB_E); TODO review this invariant test_replay_invariant_INV_HUB_E assertEq(totalSuppliedAssets, asset.liquidity + totalDebt + asset.deficit + asset.swept, INV_HUB_F); } - function assert_INV_HUB_GH(uint256 assetId) internal { - uint256 spokeCount = spokesAddresses.length; + function assert_INV_HUB_GH(address hubAddress, uint256 assetId) internal { + uint256 spokeCount = allSpokes.length; // Sum per-spoke values uint256 totalAddedAssets; uint256 totalAddedShares; for (uint256 i; i < spokeCount; i++) { - totalAddedAssets += hub.getSpokeAddedAssets(assetId, spokesAddresses[i]); - totalAddedShares += hub.getSpokeAddedShares(assetId, spokesAddresses[i]); + totalAddedAssets += IHub(hubAddress).getSpokeAddedAssets(assetId, allSpokes[i]); + totalAddedShares += IHub(hubAddress).getSpokeAddedShares(assetId, allSpokes[i]); } - // Checks - assertEq(totalAddedAssets, hub.getAddedAssets(assetId), INV_HUB_G); - assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); + //assertApproxEqAbs(totalAddedAssets, IHub(hubAddress).getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); TODO remove comment after going over test_replay_12_donateUnderlyingToSpoke + assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); } - function assert_INV_HUB_I(uint256 assetId, address underlying) internal { + function assert_INV_HUB_I(address hubAddress, uint256 assetId, address underlying) internal { // Query values - uint256 liquidity = hub.getLiquidity(assetId); - uint256 swept = hub.getSwept(assetId); - uint256 underlyingBalance = IERC20(underlying).balanceOf(address(hub)); + uint256 liquidity = IHub(hubAddress).getLiquidity(assetId); + uint256 swept = IHub(hubAddress).getSwept(assetId); + uint256 underlyingBalance = IERC20(underlying).balanceOf(address(IHub(hubAddress))); // Checks - assertEq(underlyingBalance + swept, liquidity, INV_HUB_I); + assertGe(underlyingBalance + swept, liquidity, INV_HUB_I); } - function assert_INV_HUB_K(uint256 assetId) internal { + function assert_INV_HUB_K(address hubAddress, uint256 assetId) internal { // TODO for this check to be meaningful, strategy configuration operations have to be integrated - IHub.AssetConfig memory assetConfig = hub.getAssetConfig(assetId); + IHub.AssetConfig memory assetConfig = IHub(hubAddress).getAssetConfig(assetId); // Checks assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); } - function assert_INV_HUB_L(uint256 assetId) internal { - (uint256 premiumShares, uint256 premiumOffset,) = hub.getAssetPremiumData(assetId); + function assert_INV_HUB_L(address hubAddress, uint256 assetId) internal { + (uint256 premiumShares, uint256 premiumOffset,) = IHub(hubAddress).getAssetPremiumData(assetId); - assertGe(hub.previewRestoreByShares(assetId, premiumShares), premiumOffset, INV_HUB_L); + assertGe(IHub(hubAddress).previewRestoreByShares(assetId, premiumShares), premiumOffset, INV_HUB_L); } } diff --git a/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol b/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol index f7b2895e0..2df81c240 100644 --- a/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol +++ b/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; // Interfaces import {ISpokeBase} from "src/spoke/interfaces/ISpokeBase.sol"; import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; +import {IHub} from "src/hub/interfaces/IHub.sol"; // Contracts import {HandlerAggregator} from "../HandlerAggregator.t.sol"; @@ -20,6 +21,8 @@ abstract contract SpokeInvariants is HandlerAggregator { // Get the assetId related to the reserveId of the spoke uint256 assetId = _getAssetId(spoke, reserveId); + IHub hub = IHub(_getHubAddress(spoke, reserveId)); + // supply assertEq( ISpokeBase(spoke).getReserveSuppliedShares(reserveId), hub.getSpokeAddedShares(assetId, spoke), INV_SP_A @@ -42,7 +45,11 @@ abstract contract SpokeInvariants is HandlerAggregator { } // reserve debt if (ISpokeBase(spoke).getReserveTotalDebt(reserveId) > 0) { - assertGt(hub.getSpokeDrawnShares(_getAssetId(spoke, reserveId), spoke), 0, INV_SP_B); + assertGt( + IHub(_getHubAddress(spoke, reserveId)).getSpokeDrawnShares(_getAssetId(spoke, reserveId), spoke), + 0, + INV_SP_B + ); } // user supply if (ISpokeBase(spoke).getUserSuppliedAssets(reserveId, user) > 0) { diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_10.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_10.t.sol new file mode 100644 index 000000000..b512cf194 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_10.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest10 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest10 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_10_invariant_INV_SP() public { + _setUpActor(USER1); + Tester.supply(15553676613300388866625557295, 0, 0, 0); + Tester.setUsingAsCollateral(true, 0, 0); + Tester.setPrice(1317397258558246502991383569369992979546474569225866748916270979618489862, 0); + invariant_INV_SP(); + } + + function test_replay_10_invariant_INV_HUB() public { + _setUpActor(USER1); + Tester.donateUnderlyingToHub(1, 0, 0); + invariant_INV_HUB(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_11.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_11.t.sol new file mode 100644 index 000000000..dead12000 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_11.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest11 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest11 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_11_invariant_INV_HUB() public { + _setUpActor(USER3); + Tester.setUsingAsCollateral(true, 172, 9); + Tester.supply(1258, 128, 6, 0); + Tester.borrow(5, 41, 18, 2); + _setUpActor(USER1); + invariant_INV_HUB(); + // Invalid: 1258!=1260, reason: INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_12.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_12.t.sol new file mode 100644 index 000000000..92f2d9e43 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_12.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest12 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest12 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_12_donateUnderlyingToSpoke() public { + _setUpActor(USER3); + Tester.setUsingAsCollateral(true, 10, 224); + Tester.supply(10092, 251, 52, 79); + Tester.borrow(2835, 227, 50, 28); + _setUpActor(USER1); + _delay(431801); + Tester.donateUnderlyingToSpoke( + 3921833296228115180774015742656039797758013600205949455694768574845976631615, 49, 97 + ); + invariant_INV_HUB(); + + //INV_HUB_G: 10092!=10097, totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol new file mode 100644 index 000000000..7347314e9 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest13 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest13 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_13_setUsingAsCollateral() public { + _setUpActor(USER2); + Tester.setUsingAsCollateral(true, 167, 2); + Tester.supply(230, 1, 1, 2); + Tester.borrow(103, 7, 23, 0); + _setUpActor(USER1); + _delay(1441); + Tester.setUsingAsCollateral(false, 0, 0); + // GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block. + } + + function test_replay_13_updateUserRiskPremium() public { + _setUpActor(USER2); + Tester.supply(3650894, 46, 43, 2); + Tester.setUsingAsCollateral(true, 13, 4); + Tester.borrow(7, 100, 5, 0); + _setUpActor(USER1); + _delay(1); + Tester.updateUserRiskPremium(0); + // Invalid: 50000106518811252501137774!=50000121735750944479215219, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block. + } + + function test_replay_13_liquidationCall() public { + _setUpActor(USER3); + Tester.setUsingAsCollateral(true, 172, 50); + Tester.supply(10092, 251, 30, 128); + Tester.borrow(7800, 227, 62, 25); + _setUpActor(USER1); + _delay(1425689); + _delay(314372); + Tester.setPrice(0, 0); + _delay(344203); + Tester.setPrice(188046916927661423584726156556474354281793089627848617698006899495150566800, 73); + Tester.liquidationCall( + 16786243780545468416195016839226359136549161709334054148083726855218179176262, 197, 130, 42, 58 + ); + // Invalid: 16786243780545468416195016839226359136549161709334054148083726855218179176262>=7871 failed, reason: HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position + } + + function test_replay_13_supply() public { + _setUpActor(USER3); + Tester.setUsingAsCollateral(true, 198, 50); + Tester.supply(10092, 251, 16, 135); + Tester.borrow(5584, 17, 18, 5); + _setUpActor(USER1); + _delay(100171); + Tester.supply(4303, 72, 0, 28); + // Invalid: 1000277893566763929<1000297265160523186 failed, reason: GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding). + } + + function test_replay_13_updateUserDynamicConfig() public { + _setUpActor(USER2); + Tester.supply(25427, 37, 13, 9); + Tester.setUsingAsCollateral(true, 1, 4); + Tester.borrow(1, 1, 183, 0); + _setUpActor(USER1); + _delay(3741); + Tester.updateUserDynamicConfig(0); + // Invalid: 50002184904060862687519392!=50004369636271476762274309, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block. + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol new file mode 100644 index 000000000..ddea97647 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest3 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest3 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_supply_5() public { + _setUpActor(USER1); + _delay(586107); + Tester.setUsingAsCollateral(true, 154, 0); + _delay(296474); + Tester.supply(149948, 15, 0, 0); + _delay(360624); + Tester.borrow(17, 0, 0, 73); + _setUpActor(USER2); + _delay(360465); + Tester.supply(1455122803355410, 7, 0, 0); + } + + function test_replay_liquidationCall_11() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 61); + _delay(296474); + Tester.supply(9, 171, 0, 6); + _delay(360624); + Tester.borrow(1, 0, 0, 5); + _delay(101782); + Tester.withdraw(1, 3, 140, 47); + _delay(328981); + Tester.updateUserRiskPremium(44); + _delay(282613); + Tester.updateUserRiskPremium(174); + _delay(364130); + Tester.updateUserRiskPremium(144); + _setUpActor(USER2); + _delay(413227); + Tester.liquidationCall( + 365099547169460107260444213992413627501745220129685401309505294672367581748, 0, 246, 3, 5 + ); + } + + function test_replay_updateUserDynamicConfig_7() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 2); + _delay(296474); + Tester.supply(95249040, 21, 0, 6); + _delay(360624); + Tester.borrow(2918, 0, 0, 0); + _setUpActor(USER2); + _delay(282613); + Tester.updateUserDynamicConfig(0); + } + + function test_replay_repay_10() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 101); + _delay(296474); + Tester.supply(22774255985, 0, 0, 0); + _delay(360624); + Tester.borrow(204954930, 0, 0, 234); + _delay(533426); + Tester.repay(1, 15, 54, 0); + } + + function test_replay_withdraw_2() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 43); + _delay(296474); + Tester.supply(47683715958142044857, 105, 0, 9); + _delay(360624); + Tester.borrow(51355, 0, 0, 5); + _delay(426559); + Tester.repay(684742840631310434884539857599449768327155454254541969729420468406827, 0, 158, 88); + _delay(9096); + Tester.withdraw(57966352054216140970237172336532564104220339507694885035496782237, 213, 0, 120); + _setUpActor(USER2); + _delay(349477); + Tester.withdraw(51236477730500247308223064742179045624643677222313070801276200340260439695, 0, 0); + } + + function test_replay_updateUserRiskPremium_4() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 0); + _delay(296474); + Tester.supply(174, 243, 0, 7); + _delay(360624); + Tester.borrow(1, 0, 0, 26); + _setUpActor(USER3); + _delay(584023); + Tester.updateUserRiskPremium(0); + } + + function test_replay_setUsingAsCollateral_5() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 19); + _delay(296474); + Tester.supply(98423, 249, 0, 133); + _delay(360624); + Tester.borrow(2453, 0, 0, 2); + _setUpActor(USER2); + _delay(322362); + Tester.setUsingAsCollateral(true, 0, 0); + } + + function test_replay_setPrice_5() public { + _setUpActor(USER1); + _delay(586107); + Tester.setUsingAsCollateral(true, 0, 58); + _delay(296474); + Tester.supply(416, 0, 0, 5); + _setUpActor(USER2); + _delay(305693); + Tester.setPrice(258657099142048003581115496186011548248157129485377727959920274374993509, 10); + } + + function test_replay_transfer() public { + _setUpActor(USER1); + _delay(360520); + Tester.supply(1100316557268, 0, 0, 0); + _delay(111007); + Tester.setUsingAsCollateral(true, 0, 110); + _delay(360624); + Tester.borrow(96719, 0, 0, 61); + _delay(124260); + Tester.transfer(0, 0, 0, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol new file mode 100644 index 000000000..69978c5d0 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest4 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest4 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_4_supply() public { + _setUpActor(USER1); + _delay(586107); + Tester.setUsingAsCollateral(true, 154, 0); + _delay(296474); + Tester.supply(149948, 15, 0, 0); + _delay(360624); + Tester.borrow(17, 0, 0, 73); + _setUpActor(USER2); + _delay(360465); + Tester.supply(1455122803355410, 7, 0, 0); + } + + function test_replay_4_liquidationCall() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 61); + _delay(296474); + Tester.supply(9, 171, 0, 6); + _delay(360624); + Tester.borrow(1, 0, 0, 5); + _delay(101782); + Tester.withdraw(1, 3, 140, 47); + _delay(328981); + Tester.updateUserRiskPremium(44); + _delay(282613); + Tester.updateUserRiskPremium(174); + _delay(364130); + Tester.updateUserRiskPremium(144); + _setUpActor(USER2); + _delay(413227); + Tester.liquidationCall( + 365099547169460107260444213992413627501745220129685401309505294672367581748, 0, 246, 3, 5 + ); + } + + function test_replay_4_updateUserDynamicConfig() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 2); + _delay(296474); + Tester.supply(95249040, 21, 0, 6); + _delay(360624); + Tester.borrow(2918, 0, 0, 0); + _setUpActor(USER2); + _delay(282613); + Tester.updateUserDynamicConfig(0); + } + + function test_replay_4_repay() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 101); + _delay(296474); + Tester.supply(22774255985, 0, 0, 0); + _delay(360624); + Tester.borrow(204954930, 0, 0, 234); + _delay(533426); + Tester.repay(1, 15, 54, 0); + } + + function test_replay_4_withdraw() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 43); + _delay(296474); + Tester.supply(47683715958142044857, 105, 0, 9); + _delay(360624); + Tester.borrow(51355, 0, 0, 5); + _delay(426559); + Tester.repay(684742840631310434884539857599449768327155454254541969729420468406827, 0, 158, 88); + _delay(9096); + Tester.withdraw(57966352054216140970237172336532564104220339507694885035496782237, 213, 0, 120); + _setUpActor(USER2); + _delay(349477); + Tester.withdraw(51236477730500247308223064742179045624643677222313070801276200340260439695, 0, 0); + } + + function test_replay_4_updateUserRiskPremium() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 0); + _delay(296474); + Tester.supply(174, 243, 0, 7); + _delay(360624); + Tester.borrow(1, 0, 0, 26); + _setUpActor(USER3); + _delay(584023); + Tester.updateUserRiskPremium(0); + } + + function test_replay_4_setUsingAsCollateral() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 19); + _delay(296474); + Tester.supply(98423, 249, 0, 133); + _delay(360624); + Tester.borrow(2453, 0, 0, 2); + _setUpActor(USER2); + _delay(322362); + Tester.setUsingAsCollateral(true, 0, 0); + } + + function test_replay_4_setPrice() public { + _setUpActor(USER1); + _delay(586107); + Tester.setUsingAsCollateral(true, 0, 58); + _delay(296474); + Tester.supply(416, 0, 0, 5); + _setUpActor(USER2); + _delay(305693); + Tester.setPrice(258657099142048003581115496186011548248157129485377727959920274374993509, 10); + } + + function test_replay_4_transfer() public { + _setUpActor(USER1); + _delay(360520); + Tester.supply(1100316557268, 0, 0, 0); + _delay(111007); + Tester.setUsingAsCollateral(true, 0, 110); + _delay(360624); + Tester.borrow(96719, 0, 0, 61); + _delay(124260); + Tester.transfer(0, 0, 0, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_5.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_5.t.sol new file mode 100644 index 000000000..8be87a209 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_5.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest5 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest5 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_5_invariant_INV_HUB() public { + _setUpActor(USER3); + Tester.setUsingAsCollateral(true, 140, 28); + Tester.supply(10092, 251, 34, 4); + Tester.borrow(7800, 227, 62, 2); + _delay(1300935); + _setUpActor(0x0000000000000000000000000000000000010000); + Tester.setPrice(2354863949843749485414801162, 0); + invariant_INV_HUB(); + // Invalid: 10092!=10137, reason: INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol new file mode 100644 index 000000000..e9c94f278 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest6 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest6 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_6_supply() public { + _setUpActor(USER1); + _delay(586107); + Tester.setUsingAsCollateral(true, 154, 0); + _delay(296474); + Tester.supply(149948, 15, 0, 0); + _delay(360624); + Tester.borrow(17, 0, 0, 73); + _setUpActor(USER2); + _delay(360465); + Tester.supply(1455122803355410, 7, 0, 0); + } + + function test_replay_6_liquidationCall() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 61); + _delay(296474); + Tester.supply(9, 171, 0, 6); + _delay(360624); + Tester.borrow(1, 0, 0, 5); + _delay(101782); + Tester.withdraw(1, 3, 140, 47); + _delay(328981); + Tester.updateUserRiskPremium(44); + _delay(282613); + Tester.updateUserRiskPremium(174); + _delay(364130); + Tester.updateUserRiskPremium(144); + _setUpActor(USER2); + _delay(413227); + Tester.liquidationCall( + 365099547169460107260444213992413627501745220129685401309505294672367581748, 0, 246, 3, 5 + ); + } + + function test_replay_6_updateUserDynamicConfig() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 2); + _delay(296474); + Tester.supply(95249040, 21, 0, 6); + _delay(360624); + Tester.borrow(2918, 0, 0, 0); + _setUpActor(USER2); + _delay(282613); + Tester.updateUserDynamicConfig(0); + } + + function test_replay_6_repay() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 101); + _delay(296474); + Tester.supply(22774255985, 0, 0, 0); + _delay(360624); + Tester.borrow(204954930, 0, 0, 234); + _delay(533426); + Tester.repay(1, 15, 54, 0); + } + + function test_replay_6_withdraw() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 43); + _delay(296474); + Tester.supply(47683715958142044857, 105, 0, 9); + _delay(360624); + Tester.borrow(51355, 0, 0, 5); + _delay(426559); + Tester.repay(684742840631310434884539857599449768327155454254541969729420468406827, 0, 158, 88); + _delay(9096); + Tester.withdraw(57966352054216140970237172336532564104220339507694885035496782237, 213, 0, 120); + _setUpActor(USER2); + _delay(349477); + Tester.withdraw(51236477730500247308223064742179045624643677222313070801276200340260439695, 0, 0); + } + + function test_replay_6_updateUserRiskPremium() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 0); + _delay(296474); + Tester.supply(174, 243, 0, 7); + _delay(360624); + Tester.borrow(1, 0, 0, 26); + _setUpActor(USER3); + _delay(584023); + Tester.updateUserRiskPremium(0); + } + + function test_replay_6_setUsingAsCollateral() public { + _setUpActor(USER1); + _delay(228361); + Tester.setUsingAsCollateral(true, 0, 19); + _delay(296474); + Tester.supply(98423, 249, 0, 133); + _delay(360624); + Tester.borrow(2453, 0, 0, 2); + _setUpActor(USER2); + _delay(322362); + Tester.setUsingAsCollateral(true, 0, 0); + } + + function test_replay_6_setPrice() public { + _setUpActor(USER1); + _delay(586107); + Tester.setUsingAsCollateral(true, 0, 58); + _delay(296474); + Tester.supply(416, 0, 0, 5); + _setUpActor(USER2); + _delay(305693); + Tester.setPrice(258657099142048003581115496186011548248157129485377727959920274374993509, 10); + } + + function test_replay_6_transfer() public { + _setUpActor(USER1); + _delay(360520); + Tester.supply(1100316557268, 0, 0, 0); + _delay(111007); + Tester.setUsingAsCollateral(true, 0, 110); + _delay(360624); + Tester.borrow(96719, 0, 0, 61); + _delay(124260); + Tester.transfer(0, 0, 0, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol new file mode 100644 index 000000000..a43e0e0c9 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest7 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest7 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_7_withdraw() public { + _setUpActor(USER3); + Tester.supply(10070862, 2, 0, 0); + Tester.withdraw(63970967942622104945159534037176262183333096787346814336171336533994484054, 32, 4, 2); + // GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding). + } + + function test_replay_7_transfer() public { + _setUpActor(USER1); + Tester.supply(2, 0, 0, 0); + Tester.setUsingAsCollateral(true, 0, 0); + Tester.borrow(1, 0, 0, 0); + _delay(1); + Tester.transfer(0, 0, 0, 0); + // Invalid: 77777777777777777777777778!=87037037037037037037037038, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block + } + + function test_replay_7_liquidationCall() public { + _setUpActor(USER3); + Tester.setUsingAsCollateral(true, 172, 50); + Tester.supply(10092, 251, 48, 141); + Tester.borrow(7800, 227, 62, 25); + _setUpActor(USER1); + _delay(2711741); + Tester.setPrice(929774615256615712219717710056, 0); + Tester.liquidationCall(8492579161489, 203, 220, 184, 37); + // Invalid: 8492579161489>=7790 failed, reason: HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position + } + + function test_replay_7_updateUserDynamicConfig() public { + _setUpActor(USER2); + Tester.setUsingAsCollateral(true, 1, 7); + Tester.supply(718, 1, 1, 0); + Tester.borrow(4, 1, 1, 0); + _setUpActor(USER1); + _delay(990809); + Tester.updateUserDynamicConfig(0); + // 50309501702259362426493347!=50386339051151290372430846, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block + } + + function test_replay_7_setUsingAsCollateral() public { + _setUpActor(USER2); + Tester.setUsingAsCollateral(true, 1, 4); + Tester.supply(597, 4, 15, 0); + Tester.borrow(14, 4, 11, 0); + _setUpActor(USER1); + _delay(35069); + Tester.setUsingAsCollateral(false, 0, 0); + // Invalid: 51302810348036478689745023!=51393534002229654403567448, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block + } + + function test_replay_7_updateUserRiskPremium() public { + _setUpActor(USER2); + Tester.setUsingAsCollateral(true, 105, 11); + Tester.supply(4143760, 10, 13, 2); + Tester.borrow(7, 13, 67, 3); + _setUpActor(USER1); + _delay(322348); + Tester.updateUserRiskPremium(0); + // Invalid: 50000093849279130279960445!=50000107256293122225061834, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block + } + + function test_replay_7_supply() public { + _setUpActor(USER3); + Tester.setUsingAsCollateral(true, 198, 4); + _delay(655); + Tester.supply(10092, 92, 54, 223); + Tester.borrow(4418, 227, 62, 51); + _setUpActor(USER1); + _delay(116902); + Tester.supply(4439446668599936570, 21, 0, 45); + // Invalid: 1000002970026492637<1000297265160523186 failed, reason: GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding) + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_8.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_8.t.sol new file mode 100644 index 000000000..8d0c6be09 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_8.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest8 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest8 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_8_invariant_INV_HUB() public { + _setUpActor(USER1); + Tester.donateUnderlyingToHub(1, 0, 0); + invariant_INV_HUB(); + // Invalid: 1!=0, reason: INV_HUB_I: asset.underlying.balanceOf(hub) >= asset.liquidity - asset.swept + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_9.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_9.t.sol new file mode 100644 index 000000000..dba6455e3 --- /dev/null +++ b/tests/enigma-dark-invariants/replays/ReplayTest_9.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../utils/Actor.sol"; + +contract ReplayTest9 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest9 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_9_supply() public { + _setUpActor(USER1); + Tester.setPrice(999999999999990001, 0); + Tester.setUsingAsCollateral(true, 1, 0); + Tester.supply(13167295895020254737567557, 0, 1, 0); + invariant_INV_SP(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol b/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol index 6d0fcc1b7..9e8898863 100644 --- a/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol +++ b/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol @@ -120,6 +120,22 @@ abstract contract PropertiesAsserts { } } + /// @dev Asserts that `a * b >= x * y` with full 512-bit precision. + function assertFullMulGe(uint256 a, uint256 b, uint256 x, uint256 y, string memory reason) internal { + if (!fullMulGte(a, b, x, y)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + string memory xStr = PropertiesLibString.toString(x); + string memory yStr = PropertiesLibString.toString(y); + + bytes memory assertMsg = + abi.encodePacked("Invalid: ", aStr, "*", bStr, " <", xStr, "*", yStr, " failed, reason: ", reason); + + emit AssertGeFail(string(assertMsg)); + assert(false); + } + } + /// @notice asserts that a is greater than b. Violations are logged using reason. function assertGt(uint256 a, uint256 b, string memory reason) internal { if (!(a > b)) { @@ -326,6 +342,21 @@ abstract contract PropertiesAsserts { } return a; } + + /// @dev Returns a * b >= x * y, with full precision. + function fullMulGte(uint256 a, uint256 b, uint256 x, uint256 y) internal pure returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + let m := not(0) + let mm1 := mulmod(a, b, m) + let lo1 := mul(a, b) + let hi1 := sub(sub(mm1, lo1), lt(mm1, lo1)) + let mm2 := mulmod(x, y, m) + let lo2 := mul(x, y) + let hi2 := sub(sub(mm2, lo2), lt(mm2, lo2)) + result := or(gt(hi1, hi2), and(eq(hi1, hi2), iszero(lt(lo1, lo2)))) + } + } } /// @notice Efficient library for creating string representations of integers. diff --git a/tests/enigma-dark-invariants/utils/PropertiesConstants.sol b/tests/enigma-dark-invariants/utils/PropertiesConstants.sol index db2c0ac99..60d0d3bd8 100644 --- a/tests/enigma-dark-invariants/utils/PropertiesConstants.sol +++ b/tests/enigma-dark-invariants/utils/PropertiesConstants.sol @@ -6,11 +6,26 @@ abstract contract PropertiesConstants { address constant USER1 = address(0x10000); address constant USER2 = address(0x20000); address constant USER3 = address(0x30000); - uint256 constant INITIAL_BALANCE = 1000e30; + uint256 constant INITIAL_BALANCE = 1e12; // Suite constants uint256 constant CHECK_ALL_RESERVES = type(uint256).max; string constant GPOST_CHECK_FAILED = "GPOST_CHECK_FAILED: checkPostConditions reverted"; + int256 constant PRICE_MIN = 0.0001e8; + int256 constant PRICE_MAX = 1e14; // Protocol constants + uint256 constant SPOKE_COUNT = 3; + + // Interest rate 1 data + uint16 constant OPTIMAL_USAGE_RATIO_IR1 = 85_00; // 85.00% + uint16 constant BASE_VARIABLE_BORROW_RATE_IR1 = 1_00; // 1.00% + uint16 constant VARIABLE_RATE_SLOPE_1_IR1 = 4_00; // 4.00% + uint16 constant VARIABLE_RATE_SLOPE_2_IR1 = 55_00; // 55.00% + + // Interest rate 2 data + uint16 constant OPTIMAL_USAGE_RATIO_IR2 = 65_00; // 65.00% + uint16 constant BASE_VARIABLE_BORROW_RATE_IR2 = 2_00; // 2.00% + uint16 constant VARIABLE_RATE_SLOPE_1_IR2 = 7_00; // 7.00% + uint16 constant VARIABLE_RATE_SLOPE_2_IR2 = 75_00; // 75.00% } diff --git a/tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol b/tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol index e966c19ac..be1352577 100644 --- a/tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol +++ b/tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol @@ -40,7 +40,7 @@ contract MockPriceFeedSimulator is AggregatorV3Interface { answeredInRound = roundId; } - function setPrice(uint256 price) external { - _price = int256(price); + function setPrice(int256 price) external { + _price = price; } } From 576020ef2db055b70d326b5fdb1280db080b1393 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 18:25:21 +0200 Subject: [PATCH 003/106] chore: test ci gh app setup --- .github/workflows/enigma-dark-invariants.yml | 18 ++- Makefile | 6 +- tests/enigma-dark-invariants/README.md | 150 +++++++++++------- tests/enigma-dark-invariants/Setup.t.sol | 73 ++++++--- .../handlers/interfaces/ISpokeHandler.sol | 2 +- .../handlers/spoke/SpokeHandler.t.sol | 8 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 14 +- .../hooks/HookAggregator.t.sol | 9 +- .../invariants/HubInvariants.t.sol | 4 +- .../invariants/SpokeInvariants.t.sol | 4 +- .../replays/ReplayTest_13.t.sol | 2 +- .../replays/ReplayTest_2.t.sol | 2 +- .../replays/ReplayTest_3.t.sol | 2 +- .../replays/ReplayTest_4.t.sol | 2 +- .../replays/ReplayTest_6.t.sol | 23 --- .../replays/ReplayTest_7.t.sol | 2 +- 16 files changed, 187 insertions(+), 134 deletions(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index bbd3249ae..c1b932b49 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -16,7 +16,7 @@ concurrency: jobs: echidna: name: Echidna Enigma Dark Invariants - runs-on: aave-latest + runs-on: ubuntu-latest strategy: matrix: @@ -28,12 +28,14 @@ jobs: with: submodules: recursive - - name: Setup SSH - run: | - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H github.com >> ~/.ssh/known_hosts + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: aave-v4-invariants-corpus - name: Clone the private repo into corpus folder run: | @@ -46,7 +48,7 @@ jobs: - name: Compile contracts run: | - forge build --build-info + forge build test/enigma-dark-invariants/Tester.t.sol --build-info - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode uses: crytic/echidna-action@v2 diff --git a/Makefile b/Makefile index 0b25055ad..7267a43d3 100644 --- a/Makefile +++ b/Makefile @@ -37,11 +37,15 @@ echidna-assert: echidna tests/enigma-dark-invariants/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/enigma-dark-invariants/_config/echidna_config.yaml echidna-explore: echidna tests/enigma-dark-invariants/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/enigma-dark-invariants/_config/echidna_config.yaml +rm-echidna-corpus: + rm -rf tests/enigma-dark-invariants/_corpus/echidna # Medusa medusa: medusa fuzz --config ./medusa.json - +rm-medusa-corpus: + rm -rf tests/enigma-dark-invariants/_corpus/medusa + foundry-invariants: forge test --mc TesterFoundry -vvv diff --git a/tests/enigma-dark-invariants/README.md b/tests/enigma-dark-invariants/README.md index 015fd6b94..8b9465bac 100644 --- a/tests/enigma-dark-invariants/README.md +++ b/tests/enigma-dark-invariants/README.md @@ -1,61 +1,101 @@ -# Enigma Dark – Invariant & Fuzzing Suite +# Enigma Dark – Fuzzing & Invariant Testing Suite -This folder contains a Handler-based invariant testing suite for the Aave v4 protocol. It performs stateful fuzzing (supply/withdraw/borrow/repay, liquidations, config updates) through handler contracts and checks system-level postconditions after every call and for every state. +A comprehensive handler-based invariant testing suite for the Aave v4 protocol. This suite performs deep stateful fuzzing across multiple hubs and spokes, validating critical system properties through automated property checking and postcondition verification. -This suite helps identify invariants violations and protocol misconfigurations via stateful fuzzing. +## Overview + +The suite tests a complex multi-hub, multi-spoke deployment with: +- **2 Hubs** with distinct interest rate strategies and asset configurations +- **2 Spokes** with varying risk parameters (conservative vs. aggressive) +- **Cross-hub liquidity flows** simulating bridge mechanics with different capacity caps +- **Multiple actors** executing concurrent operations (supply, borrow, repay, liquidations) + +All protocol actions are monitored by hooks that snapshot state and verify postconditions after each transaction, enabling detection of invariant violations and edge cases that could lead to protocol insolvency or user fund loss. ## Tooling -This suite is compatible with Echidna, Medusa, and Foundry. - -## Project Layout - -- Setup - - `Setup.t.sol` – deploys core protocol (Hub, Spokes, Oracle, IR strategy), assets, and actors - - `base/` – shared storage, hooks plumbing, assertions, and helpers -- Execution - - `HandlerAggregator.t.sol` – collects handlers used by the fuzzer - - `handlers/` – user- and admin-facing action drivers (Spoke, TreasurySpoke, Hub/Spoke configurators) - - `hooks/` – before/after hooks, state snapshots, and postcondition checks - - `specs/` – property strings - - `invariants/` – invariant entrypoints (`invariant_*`) and campaign wiring - - `replays/` – minimal repro tests for failures -- Entrypoint - - `Tester.t.sol` – top-level test contract that wires the suite together - -## Workflow - -- The fuzzer calls into handlers with fuzzed inputs. -- Each call is wrapped by hooks that snapshot state and then assert postconditions. -- Handlers use actor proxies to simulate realistic multi-user flows and respect protocol roles. -- Admin handlers exercise configuration updates (reserve config, dynamic reserve config, liquidation settings). - -## Running tests - -- Echidna: - - Property mode: - ```bash - make echidna - ``` - - Assertion mode: - ```bash - make echidna-assert - ``` - - Exploration mode: - ```bash - make echidna-explore - ``` - - Generate Replay Tests: - ```bash - make runes - ``` -- Medusa: - - Property & Assertion mode: - ```bash - make medusa - ``` -- Foundry Invariants: - ```bash - make foundry-invariants - ``` +Compatible with industry-standard fuzzing tools: +- **Echidna** - battle tested haskell based property-based fuzzer +- **Medusa** - parallelized, coverage-guided, smart contract fuzzing, powered by go-ethereum +- **Foundry** - native invariant testing framework + +## Architecture + +### Core Components + +**Setup Layer** (`Setup.t.sol`, `base/`) +- Deploys 2-hub, 2-spoke architecture with deterministic addresses (CREATE3) +- Configures distinct collateral factors, liquidation parameters, and interest rate curves +- Initializes multiple actors with protocol permissions + +**Handler Layer** (`handlers/`) +- `SpokeHandler` – user operations (supply, borrow, repay, withdraw, liquidations) +- `HubHandler` – liquidity management and treasury operations +- `TreasurySpoke` – fee collection and distribution +- `Configurator Handlers` – admin operations (reserve updates, risk parameter changes) +- `Simulator Handlers` – price feeds, donation attacks + +**Verification Layer** (`hooks/`, `invariants/`) +- Before/after hooks with state snapshots +- Global and handler-specific postcondition assertions +- Hub invariants (liquidity accounting, share calculations, interest accrual) +- Spoke invariants (position tracking, collateralization, debt limits) + +**Replay Layer** (`replays/`) +- Minimal reproduction tests for discovered violations +- Facilitates debugging and regression prevention + +## How It Works + +1. **Fuzzer** generates random inputs and selects handler functions +2. **Handlers** execute protocol actions through actor proxies (respects roles and permissions) +3. **Hooks** capture snapshots of relevant state variables for analysis +4. **Postconditions** validate expected outcomes and state transitions (e.g., "drawn rate matches calculated rate after hub non-view operations") +5. **Invariants** continuously checked across all protocol states + +## Quick Start + +```bash +# Run full fuzzing campaign with Medusa +make medusa + +# Run with Echidna in assertion mode +make echidna-assert + +# Generate replay tests from Echidna corpus +make runes-echidna + +# Generate replay tests from Medusa corpus +make runes-medusa +``` + +## Advanced Usage + +**Echidna Modes:** +```bash +make echidna # Property mode (boolean invariants) +make echidna-assert # Assertion mode (require/assert violations) +make echidna-explore # Exploration mode (maximize coverage) +``` + +**Foundry:** +```bash +make foundry-invariants # Native Foundry invariant runner +``` + +**Replay Specific Failure:** +```bash +forge test --mc ReplayTest_1 -vvv +``` + +## Key Features + +- Multi-hub, multi-spoke testing for cross-protocol interactions +- Comprehensive postcondition checking after every state transition +- Actor-based modeling for realistic multi-user scenarios +- Admin operation fuzzing (config updates, parameter changes) + +--- + +**Note:** This suite complements unit tests by exploring unbounded state spaces and adversarial scenarios that are difficult to anticipate manually. diff --git a/tests/enigma-dark-invariants/Setup.t.sol b/tests/enigma-dark-invariants/Setup.t.sol index f9bc64a02..0a64ccb2f 100644 --- a/tests/enigma-dark-invariants/Setup.t.sol +++ b/tests/enigma-dark-invariants/Setup.t.sol @@ -109,16 +109,23 @@ contract Setup is BaseTest { returns (ISpoke, IAaveOracle) { bytes32 salt = keccak256(abi.encodePacked(_oracleDesc)); - address predictedOracle = CREATE3.predictDeterministicAddress(salt, admin); - address spokeImpl = address(new SpokeInstance(predictedOracle)); - - ISpoke spoke = ISpoke(_proxify(spokeImpl, proxyAdminOwner, abi.encodeCall(Spoke.initialize, (_accessManager)))); - IAaveOracle oracle = IAaveOracle( - CREATE3.deployDeterministic( - abi.encodePacked(type(AaveOracle).creationCode, abi.encode(address(spoke), uint8(8), _oracleDesc)), salt - ) + address predictedSpoke = CREATE3.predictDeterministicAddress(salt, admin); + + // Deploy oracle with predicted spoke address + IAaveOracle oracle = new AaveOracle(predictedSpoke, uint8(8), _oracleDesc); + + // Deploy spoke implementation with oracle address + address spokeImpl = address(new SpokeInstance(address(oracle))); + + // Deploy spoke proxy using CREATE3 + bytes memory proxyCreationCode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(spokeImpl, proxyAdminOwner, abi.encodeCall(Spoke.initialize, (_accessManager))) ); - assertEq(address(oracle), predictedOracle, "predictedOracle mismatch"); + address spokeProxy = CREATE3.deployDeterministic(proxyCreationCode, salt); + ISpoke spoke = ISpoke(spokeProxy); + + assertEq(address(spoke), predictedSpoke, "predictedSpoke mismatch"); assertEq(spoke.ORACLE(), address(oracle), "spoke.ORACLE() mismatch"); assertEq(oracle.SPOKE(), address(spoke), "oracle.SPOKE() mismatch"); @@ -365,32 +372,44 @@ contract Setup is BaseTest { hub1UsdcAssetId, address(spoke1), IHub.SpokeConfig({ - active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false }) ); hub2.addSpoke( hub2UsdcAssetId, address(spoke1), IHub.SpokeConfig({ - active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3 + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false }) ); hub1.addSpoke( hub1WethAssetId, address(spoke1), IHub.SpokeConfig({ - active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false }) ); hub2.addSpoke( hub2WethAssetId, address(spoke1), IHub.SpokeConfig({ - active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2 + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false }) ); @@ -399,32 +418,44 @@ contract Setup is BaseTest { hub2WethAssetId, address(spoke2), IHub.SpokeConfig({ - active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false }) ); hub1.addSpoke( hub1WethAssetId, address(spoke2), IHub.SpokeConfig({ - active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2 + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false }) ); hub2.addSpoke( hub2UsdcAssetId, address(spoke2), IHub.SpokeConfig({ - active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false }) ); hub1.addSpoke( hub1UsdcAssetId, address(spoke2), IHub.SpokeConfig({ - active: true, addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3 + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false }) ); } diff --git a/tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol b/tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol index 9cc454d37..e76505679 100644 --- a/tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol +++ b/tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol @@ -8,7 +8,7 @@ interface ISpokeHandler { function withdraw(uint256 amount, uint8 i, uint8 j, uint8 k) external; function borrow(uint256 amount, uint8 i, uint8 j, uint8 k) external; function repay(uint256 amount, uint8 i, uint8 j, uint8 k) external; - function liquidationCall(uint256 debtToCover, uint8 i, uint8 j, uint8 k, uint8 l) external; + function liquidationCall(uint256 debtToCover, bool receiveShares, uint8 i, uint8 j, uint8 k, uint8 l) external; function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external; function updateUserRiskPremium(uint8 i) external; function updateUserDynamicConfig(uint8 i) external; diff --git a/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol b/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol index 82ceaa1a9..5946217bf 100644 --- a/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol @@ -146,7 +146,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } } - function liquidationCall(uint256 debtToCover, uint8 i, uint8 j, uint8 k, uint8 l) external setup { + function liquidationCall(uint256 debtToCover, bool receiveShares, uint8 i, uint8 j, uint8 k, uint8 l) external setup { bool success; bytes memory returnData; @@ -157,7 +157,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { collateralReserveId = _getReserveId(spoke, k); debtReserveId = _getReserveId(spoke, l); - uint256 debtValueInBaseCurrency = ISpoke(spoke).getUserAccountData(_getRandomActor(i)).totalDebtInBaseCurrency; + uint256 debtValueInBaseCurrency = ISpoke(spoke).getUserAccountData(_getRandomActor(i)).totalDebtValue; // Register users to check postconditions: liquidated user and liquidator for both reserves _registerUserToCheck(spoke, debtReserveId, _getRandomActor(i)); @@ -168,7 +168,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy( spoke, - abi.encodeCall(Spoke.liquidationCall, (collateralReserveId, debtReserveId, _getRandomActor(i), debtToCover)) + abi.encodeCall(Spoke.liquidationCall, (collateralReserveId, debtReserveId, _getRandomActor(i), debtToCover, receiveShares)) ); if (success) { @@ -180,7 +180,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertLt(debtToCover, userTotalDebt, HSPOST_SP_LIQ_A); - if (debtValueInBaseCurrency > ISpoke(spoke).DUST_DEBT_LIQUIDATION_THRESHOLD()) { + if (debtValueInBaseCurrency > ISpoke(spoke).DUST_LIQUIDATION_THRESHOLD()) { assertEq(userTotalDebt, 0, HSPOST_SP_LIQ_C); } } else { diff --git a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol index 5ce7cf011..0ab524964 100644 --- a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol @@ -149,10 +149,10 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function assert_GPOST_HUB_B(address hubAddress, uint256 assetId) internal { assertFullMulGe( - defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets, - defaultVarsBefore.assetVars[hubAddress][assetId].totalShares, defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, defaultVarsAfter.assetVars[hubAddress][assetId].totalShares, + defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets, + defaultVarsBefore.assetVars[hubAddress][assetId].totalShares, GPOST_HUB_B ); } @@ -168,10 +168,10 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { IAssetInterestRateStrategy(hubInfo[hubAddress].irStrategy) .calculateInterestRate( assetId, - IHub(hubAddress).getLiquidity(assetId), + IHub(hubAddress).getAssetLiquidity(assetId), defaultVarsAfter.assetVars[hubAddress][assetId].drawn, - IHub(hubAddress).getDeficit(assetId), - IHub(hubAddress).getSwept(assetId) + IHub(hubAddress).getAssetDeficit(assetId), + IHub(hubAddress).getAssetSwept(assetId) ), GPOST_HUB_C ); @@ -184,7 +184,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function assert_GPOST_SP_A(address spoke, uint256 reserveId, address user) internal { ISpoke.UserPosition memory userPosition = ISpoke(spoke).getUserPosition(reserveId, user); - uint256 userRiskPremium = ISpoke(spoke).getUserAccountData(user).userRiskPremium; + uint256 userRiskPremium = ISpoke(spoke).getUserAccountData(user).riskPremium; uint256 expected = PercentageMath.percentMulUp(userPosition.drawnShares, userRiskPremium); assertEq(userPosition.premiumShares, expected, GPOST_SP_A); @@ -203,7 +203,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { // latest reserve key uint16 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; // user-stored key - uint16 userKey = ISpoke(spoke).getUserPosition(reserveId, user).configKey; + uint16 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; if ( msg.sig == ISpokeHandler.borrow.selector || msg.sig == ISpokeHandler.withdraw.selector diff --git a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol index 1c897d6f7..6376bb4c7 100644 --- a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol +++ b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.19; // Hook Contracts import {DefaultBeforeAfterHooks} from "./DefaultBeforeAfterHooks.t.sol"; +import "forge-std/console.sol"; + /// @title HookAggregator /// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler abstract contract HookAggregator is DefaultBeforeAfterHooks { @@ -53,11 +55,8 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { } } - /// @dev postconditions checks entrypoint, should vbe self-called + /// @dev postconditions checks entrypoint, should be self-called function checkPostConditions() external { - // Reset the state - _resetState(); - // Hub postconditions _hubPostConditions(); // Spoke postconditions @@ -89,7 +88,7 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { // Check properties for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig if (reserveId == CHECK_ALL_RESERVES) { // Iterate through all reserves of the spoke - for (uint256 j; j < spokeReserveIds[spoke].length; i++) { + for (uint256 j; j < spokeReserveIds[spoke].length; j++) { assert_GPOST_SP_A(spoke, spokeReserveIds[spoke][i], user); assert_GPOST_SP_B(spoke, spokeReserveIds[spoke][i], user); assert_GPOST_SP_E(spoke, spokeReserveIds[spoke][i], user); diff --git a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol index e4e2a4197..d08f08d6d 100644 --- a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol @@ -98,8 +98,8 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_I(address hubAddress, uint256 assetId, address underlying) internal { // Query values - uint256 liquidity = IHub(hubAddress).getLiquidity(assetId); - uint256 swept = IHub(hubAddress).getSwept(assetId); + uint256 liquidity = IHub(hubAddress).getAssetLiquidity(assetId); + uint256 swept = IHub(hubAddress).getAssetSwept(assetId); uint256 underlyingBalance = IERC20(underlying).balanceOf(address(IHub(hubAddress))); // Checks diff --git a/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol b/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol index 2df81c240..653c9e2cc 100644 --- a/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol +++ b/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol @@ -72,8 +72,8 @@ abstract contract SpokeInvariants is HandlerAggregator { function assert_INV_SP_D(address spoke, address user) internal { ISpoke.UserAccountData memory d = ISpoke(spoke).getUserAccountData(user); - if (d.totalCollateralInBaseCurrency == 0) { - assertEq(d.totalDebtInBaseCurrency, 0, INV_SP_D); + if (d.totalCollateralValue == 0) { + assertEq(d.totalDebtValue, 0, INV_SP_D); } } } diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol index 7347314e9..a990995f7 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol @@ -70,7 +70,7 @@ contract ReplayTest13 is Invariants, Setup { _delay(344203); Tester.setPrice(188046916927661423584726156556474354281793089627848617698006899495150566800, 73); Tester.liquidationCall( - 16786243780545468416195016839226359136549161709334054148083726855218179176262, 197, 130, 42, 58 + 16786243780545468416195016839226359136549161709334054148083726855218179176262, false, 197, 130, 42, 58 ); // Invalid: 16786243780545468416195016839226359136549161709334054148083726855218179176262>=7871 failed, reason: HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position } diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol index 082127b12..8842e2549 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol @@ -84,7 +84,7 @@ contract ReplayTest2 is Invariants, Setup { _delay(827249); _setUpActor(USER1); _delay(405856); - Tester.liquidationCall(61422963954080211324642765950298638470789887452796784425666156856462101843613, 53, 251, 56, 20); + Tester.liquidationCall(61422963954080211324642765950298638470789887452796784425666156856462101843613, false, 53, 251, 56, 20); } diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol index ddea97647..b1730279c 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol @@ -68,7 +68,7 @@ contract ReplayTest3 is Invariants, Setup { _setUpActor(USER2); _delay(413227); Tester.liquidationCall( - 365099547169460107260444213992413627501745220129685401309505294672367581748, 0, 246, 3, 5 + 365099547169460107260444213992413627501745220129685401309505294672367581748, false, 0, 246, 3, 5 ); } diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol index 69978c5d0..2fef10a75 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol @@ -68,7 +68,7 @@ contract ReplayTest4 is Invariants, Setup { _setUpActor(USER2); _delay(413227); Tester.liquidationCall( - 365099547169460107260444213992413627501745220129685401309505294672367581748, 0, 246, 3, 5 + 365099547169460107260444213992413627501745220129685401309505294672367581748, false, 0, 246, 3, 5 ); } diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol index e9c94f278..a04ded932 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol @@ -49,29 +49,6 @@ contract ReplayTest6 is Invariants, Setup { Tester.supply(1455122803355410, 7, 0, 0); } - function test_replay_6_liquidationCall() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 61); - _delay(296474); - Tester.supply(9, 171, 0, 6); - _delay(360624); - Tester.borrow(1, 0, 0, 5); - _delay(101782); - Tester.withdraw(1, 3, 140, 47); - _delay(328981); - Tester.updateUserRiskPremium(44); - _delay(282613); - Tester.updateUserRiskPremium(174); - _delay(364130); - Tester.updateUserRiskPremium(144); - _setUpActor(USER2); - _delay(413227); - Tester.liquidationCall( - 365099547169460107260444213992413627501745220129685401309505294672367581748, 0, 246, 3, 5 - ); - } - function test_replay_6_updateUserDynamicConfig() public { _setUpActor(USER1); _delay(228361); diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol index a43e0e0c9..d09b80983 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol @@ -61,7 +61,7 @@ contract ReplayTest7 is Invariants, Setup { _setUpActor(USER1); _delay(2711741); Tester.setPrice(929774615256615712219717710056, 0); - Tester.liquidationCall(8492579161489, 203, 220, 184, 37); + Tester.liquidationCall(8492579161489, false, 203, 220, 184, 37); // Invalid: 8492579161489>=7790 failed, reason: HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position } From ca48003487ea41d4c1a70a91038c3491b8227043 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 18:35:44 +0200 Subject: [PATCH 004/106] chore: change corpus repo --- .github/workflows/enigma-dark-invariants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index c1b932b49..23b2f1ccd 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -39,7 +39,7 @@ jobs: - name: Clone the private repo into corpus folder run: | - git clone git@github.com:aave/aave-v4-invariants-corpus.git corpus + git clone git@github.com:Enigma-Dark/aave-v4-invariants-corpus.git corpus - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 From f89d40dd3fae77127c22fba3bac873e647ecca0f Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 18:37:12 +0200 Subject: [PATCH 005/106] chore: change corpus repo --- .github/workflows/enigma-dark-invariants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index 23b2f1ccd..28d6a212b 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -39,7 +39,7 @@ jobs: - name: Clone the private repo into corpus folder run: | - git clone git@github.com:Enigma-Dark/aave-v4-invariants-corpus.git corpus + git clone https://github.com/Enigma-Dark/aave-v4-invariants-corpus.git corpus - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 From 3e95511b575735e58b306081d7e17c42e62969bf Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 18:39:08 +0200 Subject: [PATCH 006/106] fix: set x-access-token --- .github/workflows/enigma-dark-invariants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index 28d6a212b..11478cfd9 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -39,7 +39,7 @@ jobs: - name: Clone the private repo into corpus folder run: | - git clone https://github.com/Enigma-Dark/aave-v4-invariants-corpus.git corpus + git clone https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/Enigma-Dark/aave-v4-invariants-corpus.git corpus - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 From ac2583dafeaeb415deb0f4db68e76079888fcde9 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 18:41:52 +0200 Subject: [PATCH 007/106] fix: typo on compile path --- .github/workflows/enigma-dark-invariants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index 11478cfd9..45b856f82 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -48,7 +48,7 @@ jobs: - name: Compile contracts run: | - forge build test/enigma-dark-invariants/Tester.t.sol --build-info + forge build tests/enigma-dark-invariants/Tester.t.sol --build-info - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode uses: crytic/echidna-action@v2 From a8b91cee779d5f9c4bd35ab6f79292a22d658650 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 18:45:07 +0200 Subject: [PATCH 008/106] fix: crytic args --- .github/workflows/enigma-dark-invariants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index 45b856f82..e7081ed6d 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -56,5 +56,5 @@ jobs: files: . contract: Tester config: tests/enigma-dark-invariants/_config/echidna_config_ci.yaml - crytic-args: --ignore-compile + crytic-args: --ignore-compile --compile-libraries=(LiquidationLogic,0xf01) test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} \ No newline at end of file From b73826d2e746fb92c65ecca05f030abc3f69e74b Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 18:49:07 +0200 Subject: [PATCH 009/106] chore: move crytic-args to config file --- .github/workflows/enigma-dark-invariants.yml | 1 - tests/enigma-dark-invariants/_config/echidna_config.yaml | 2 +- tests/enigma-dark-invariants/_config/echidna_config_ci.yaml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index e7081ed6d..608ef3616 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -56,5 +56,4 @@ jobs: files: . contract: Tester config: tests/enigma-dark-invariants/_config/echidna_config_ci.yaml - crytic-args: --ignore-compile --compile-libraries=(LiquidationLogic,0xf01) test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} \ No newline at end of file diff --git a/tests/enigma-dark-invariants/_config/echidna_config.yaml b/tests/enigma-dark-invariants/_config/echidna_config.yaml index 3d194a734..9ffa894fb 100644 --- a/tests/enigma-dark-invariants/_config/echidna_config.yaml +++ b/tests/enigma-dark-invariants/_config/echidna_config.yaml @@ -47,7 +47,7 @@ corpusDir: "tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus" #mutConsts: [100, 1, 1] #remappings -cryticArgs: [ "--solc-remaps", "forge-std/=lib/forge-std/src/", "--compile-libraries=(LiquidationLogic,0xf01)" ] +cryticArgs: [ "--compile-libraries=(LiquidationLogic,0xf01)" ] deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] diff --git a/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml b/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml index 0949429be..eaf8ba194 100644 --- a/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml +++ b/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml @@ -47,7 +47,7 @@ corpusDir: "tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus" #mutConsts: [100, 1, 1] #remappings -cryticArgs: [ "--solc-remaps", "forge-std/=lib/forge-std/src/", "--compile-libraries=(LiquidationLogic,0xf01)" ] +cryticArgs: [ "--compile-libraries=(LiquidationLogic,0xf01)" ] deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] From 4f063ff808affb303abeee4b6ae3f06fced9939a Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 18:53:00 +0200 Subject: [PATCH 010/106] chore: add --ignore-compile flag --- tests/enigma-dark-invariants/_config/echidna_config_ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml b/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml index eaf8ba194..3bb5fcbc9 100644 --- a/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml +++ b/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml @@ -47,7 +47,7 @@ corpusDir: "tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus" #mutConsts: [100, 1, 1] #remappings -cryticArgs: [ "--compile-libraries=(LiquidationLogic,0xf01)" ] +cryticArgs: [ "--ignore-compile", "--compile-libraries=(LiquidationLogic,0xf01)" ] deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] From 4b69f7343bd98f673b21164b22328cbc42a6c428 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 19:10:55 +0200 Subject: [PATCH 011/106] chore: optimize ci workflowe --- .github/workflows/enigma-dark-invariants.yml | 57 ++++++++++++++++---- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index 608ef3616..61e7d8261 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -14,14 +14,9 @@ concurrency: cancel-in-progress: true jobs: - echidna: - name: Echidna Enigma Dark Invariants + setup: + name: Setup runs-on: ubuntu-latest - - strategy: - matrix: - mode: [property, assertion] # Define the modes here - steps: - name: Checkout repository uses: actions/checkout@v4 @@ -37,7 +32,7 @@ jobs: owner: ${{ github.repository_owner }} repositories: aave-v4-invariants-corpus - - name: Clone the private repo into corpus folder + - name: Clone Corpus run: | git clone https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/Enigma-Dark/aave-v4-invariants-corpus.git corpus @@ -46,10 +41,54 @@ jobs: with: version: nightly - - name: Compile contracts + - name: Build contracts run: | forge build tests/enigma-dark-invariants/Tester.t.sol --build-info + - name: Cache corpus + uses: actions/cache@v4 + with: + path: corpus + key: corpus-${{ hashFiles('corpus/**') }} + + - name: Cache build artifacts + id: save-build + uses: actions/cache@v4 + with: + path: | + out + cache + key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/enigma-dark-invariants/**/*.sol') }} + + echidna: + name: Echidna Enigma Dark Invariants (${{ matrix.mode }}) + runs-on: ubuntu-latest + needs: setup + + strategy: + matrix: + mode: [property, assertion] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Restore corpus + uses: actions/cache@v4 + with: + path: corpus + key: corpus-${{ hashFiles('corpus/**') }} + + - name: Restore build cache + uses: actions/cache@v4 + with: + path: | + out + cache + key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/enigma-dark-invariants/**/*.sol') }} + - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode uses: crytic/echidna-action@v2 with: From f6f6956d45dfb01d30b699834452783533e4f552 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 19:41:53 +0200 Subject: [PATCH 012/106] chore: optimize workflow --- .github/workflows/enigma-dark-invariants.yml | 30 ++++++++++++++----- .../_config/echidna_config_ci.yaml | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index 61e7d8261..f8f7a279a 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -17,6 +17,8 @@ jobs: setup: name: Setup runs-on: ubuntu-latest + outputs: + corpus-sha: ${{ steps.clone-corpus.outputs.sha }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -33,25 +35,37 @@ jobs: repositories: aave-v4-invariants-corpus - name: Clone Corpus + id: clone-corpus run: | git clone https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/Enigma-Dark/aave-v4-invariants-corpus.git corpus + echo "sha=$(git -C corpus rev-parse HEAD)" >> $GITHUB_OUTPUT + + - name: Cache corpus + uses: actions/cache@v4 + with: + path: corpus + key: corpus-${{ steps.clone-corpus.outputs.sha }} - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - - name: Build contracts - run: | - forge build tests/enigma-dark-invariants/Tester.t.sol --build-info - - - name: Cache corpus + - name: Restore build cache + id: restore-build uses: actions/cache@v4 with: - path: corpus - key: corpus-${{ hashFiles('corpus/**') }} + path: | + out + cache + key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/enigma-dark-invariants/**/*.sol') }} + + - name: Build contracts (skip if cache hit) + if: steps.restore-build.outputs.cache-hit != 'true' + run: forge build tests/enigma-dark-invariants/Tester.t.sol --build-info - name: Cache build artifacts + if: steps.restore-build.outputs.cache-hit != 'true' id: save-build uses: actions/cache@v4 with: @@ -79,7 +93,7 @@ jobs: uses: actions/cache@v4 with: path: corpus - key: corpus-${{ hashFiles('corpus/**') }} + key: corpus-${{ needs.setup.outputs.corpus-sha }} - name: Restore build cache uses: actions/cache@v4 diff --git a/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml b/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml index 3bb5fcbc9..056c9bc11 100644 --- a/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml +++ b/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml @@ -42,7 +42,7 @@ coverage: true coverageFormats: [ "html" ] #directory to save the corpus; by default is disabled -corpusDir: "tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus" +corpusDir: "corpus" # constants for corpus mutations (for experimentation only) #mutConsts: [100, 1, 1] From d840de9dca7310eb7098921d239de993ecf0cd9d Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 23 Oct 2025 19:47:50 +0200 Subject: [PATCH 013/106] chore: change workflow name --- .github/workflows/enigma-dark-invariants.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index f8f7a279a..f7f36d97f 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -1,4 +1,4 @@ -name: Echidna Enigma Dark Invariants +name: Echidna-Invariants Enigma-Dark on: push: @@ -75,7 +75,7 @@ jobs: key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/enigma-dark-invariants/**/*.sol') }} echidna: - name: Echidna Enigma Dark Invariants (${{ matrix.mode }}) + name: Echidna-Invariants (${{ matrix.mode }}) Enigma-Dark runs-on: ubuntu-latest needs: setup From 346931dcd95abe8b11246d250fa2a0d2ddbbc9d3 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Fri, 24 Oct 2025 14:36:40 +0200 Subject: [PATCH 014/106] chore: integrate configurators, filter replays --- tests/enigma-dark-invariants/Invariants.t.sol | 2 +- tests/enigma-dark-invariants/Setup.t.sol | 64 +++++- .../base/BaseStorage.t.sol | 9 + .../base/BaseTest.t.sol | 10 +- .../handlers/hub/HubConfiguratorHandler.t.sol | 70 +++--- .../handlers/hub/HubHandler.t.sol | 5 - .../simulators/DonationAttackHandler.t.sol | 2 +- .../spoke/SpokeConfiguratorHandler.t.sol | 91 ++++---- .../handlers/spoke/SpokeHandler.t.sol | 59 +++--- .../handlers/spoke/TreasurySpokeHandler.t.sol | 8 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 34 ++- .../hooks/HookAggregator.t.sol | 4 + .../invariants/HubInvariants.t.sol | 21 +- .../enigma-dark-invariants/remote/DOCKERFILE | 2 +- .../replays/ReplayTest_1.t.sol | 45 ++-- .../replays/ReplayTest_10.t.sol | 86 -------- .../replays/ReplayTest_11.t.sol | 82 -------- .../replays/ReplayTest_12.t.sol | 87 -------- .../replays/ReplayTest_13.t.sol | 133 ------------ .../replays/ReplayTest_2.t.sol | 113 ++-------- .../replays/ReplayTest_3.t.sol | 143 ++----------- .../replays/ReplayTest_4.t.sol | 199 ------------------ .../replays/ReplayTest_5.t.sol | 84 -------- .../replays/ReplayTest_6.t.sol | 176 ---------------- .../replays/ReplayTest_7.t.sol | 146 ------------- .../replays/ReplayTest_8.t.sol | 79 ------- .../replays/ReplayTest_9.t.sol | 80 ------- .../specs/InvariantsSpec.t.sol | 2 +- .../specs/PostconditionsSpec.t.sol | 5 +- .../utils/PropertiesAsserts.sol | 2 +- .../utils/PropertiesConstants.sol | 6 + 31 files changed, 327 insertions(+), 1522 deletions(-) delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_10.t.sol delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_11.t.sol delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_12.t.sol delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_5.t.sol delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_8.t.sol delete mode 100644 tests/enigma-dark-invariants/replays/ReplayTest_9.t.sol diff --git a/tests/enigma-dark-invariants/Invariants.t.sol b/tests/enigma-dark-invariants/Invariants.t.sol index eec6a0ad2..df29fa0d6 100644 --- a/tests/enigma-dark-invariants/Invariants.t.sol +++ b/tests/enigma-dark-invariants/Invariants.t.sol @@ -28,7 +28,7 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { assert_INV_HUB_C(hubAddress, j); assert_INV_HUB_EF(hubAddress, j); assert_INV_HUB_GH(hubAddress, j); - assert_INV_HUB_I(hubAddress, j, baseAssets[i].underlying); + assert_INV_HUB_I(hubAddress, j); assert_INV_HUB_K(hubAddress, j); assert_INV_HUB_L(hubAddress, j); } diff --git a/tests/enigma-dark-invariants/Setup.t.sol b/tests/enigma-dark-invariants/Setup.t.sol index 0a64ccb2f..438a209ad 100644 --- a/tests/enigma-dark-invariants/Setup.t.sol +++ b/tests/enigma-dark-invariants/Setup.t.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.19; // Libraries import {CREATE3} from "./utils/CREATE3.sol"; import {Constants} from "tests/Constants.sol"; +import {Roles} from "src/libraries/types/Roles.sol"; +import "forge-std/console.sol"; // Interfaces import {IAaveOracle} from "src/spoke/interfaces/IAaveOracle.sol"; @@ -27,6 +29,8 @@ import {SpokeInstance} from "src/spoke/instances/SpokeInstance.sol"; import {Spoke} from "src/spoke/Spoke.sol"; import {TransparentUpgradeableProxy} from "src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol"; import {AaveOracle} from "src/spoke/AaveOracle.sol"; +import {HubConfigurator} from "src/hub/HubConfigurator.sol"; +import {SpokeConfigurator} from "src/spoke/SpokeConfigurator.sol"; /// @notice Setup contract for the invariant test Suite, inherited by Tester contract Setup is BaseTest { @@ -90,9 +94,16 @@ contract Setup is BaseTest { allSpokes.push(address(treasurySpoke1)); allSpokes.push(address(treasurySpoke2)); + // Configurators + hubConfigurator = new HubConfigurator(admin); + spokeConfigurator = new SpokeConfigurator(admin); + _setUpConfiguratorRoles(); + vm.label(address(accessManager), "accessManager"); vm.label(address(hub1), "hub1"); vm.label(address(hub2), "hub2"); + vm.label(address(hubConfigurator), "hubConfigurator"); + vm.label(address(spokeConfigurator), "spokeConfigurator"); vm.label(address(irStrategy1), "irStrategy1"); vm.label(address(irStrategy2), "irStrategy2"); vm.label(address(spoke1), "spoke1"); @@ -110,13 +121,13 @@ contract Setup is BaseTest { { bytes32 salt = keccak256(abi.encodePacked(_oracleDesc)); address predictedSpoke = CREATE3.predictDeterministicAddress(salt, admin); - + // Deploy oracle with predicted spoke address IAaveOracle oracle = new AaveOracle(predictedSpoke, uint8(8), _oracleDesc); - + // Deploy spoke implementation with oracle address address spokeImpl = address(new SpokeInstance(address(oracle))); - + // Deploy spoke proxy using CREATE3 bytes memory proxyCreationCode = abi.encodePacked( type(TransparentUpgradeableProxy).creationCode, @@ -124,7 +135,7 @@ contract Setup is BaseTest { ); address spokeProxy = CREATE3.deployDeterministic(proxyCreationCode, salt); ISpoke spoke = ISpoke(spokeProxy); - + assertEq(address(spoke), predictedSpoke, "predictedSpoke mismatch"); assertEq(spoke.ORACLE(), address(oracle), "spoke.ORACLE() mismatch"); assertEq(oracle.SPOKE(), address(spoke), "oracle.SPOKE() mismatch"); @@ -179,6 +190,7 @@ contract Setup is BaseTest { }), new bytes(0) ); + hubAssetIds[address(hub1)].push(hub1UsdcAssetId); // Add WETH hub1WethAssetId = hub1.addAsset( @@ -194,6 +206,7 @@ contract Setup is BaseTest { }), new bytes(0) ); + hubAssetIds[address(hub1)].push(hub1WethAssetId); // HUB 2 bytes memory encodedIrData2 = abi.encode( @@ -219,6 +232,7 @@ contract Setup is BaseTest { }), new bytes(0) ); + hubAssetIds[address(hub2)].push(hub2WethAssetId); // Add USDC hub2UsdcAssetId = hub2.addAsset( @@ -230,13 +244,18 @@ contract Setup is BaseTest { liquidityFee: 5_00, feeReceiver: address(treasurySpoke2), irStrategy: address(irStrategy2), - reinvestmentController: address(0) // TODO should this be integrated? + reinvestmentController: address(0) }), new bytes(0) ); + hubAssetIds[address(hub2)].push(hub2UsdcAssetId); } function _configureSpokes() internal { + // Configure spoke liquidation configs + spokeConfigurator.updateLiquidationTargetHealthFactor(address(spoke1), TARGET_HEALTH_FACTOR_SPOKE1); + spokeConfigurator.updateLiquidationTargetHealthFactor(address(spoke2), TARGET_HEALTH_FACTOR_SPOKE2); + // Spoke 1 reserve configs spokeInfo[spoke1].usdc.reserveConfig = ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 30_00}); @@ -246,18 +265,18 @@ contract Setup is BaseTest { spokeInfo[spoke1].weth.reserveConfig = ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 20_00}); spokeInfo[spoke1].weth.dynReserveConfig = - ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); + ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 105_00, liquidationFee: 0}); // Spoke 2 reserve configs spokeInfo[spoke2].weth.reserveConfig = ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 10_00}); spokeInfo[spoke2].weth.dynReserveConfig = - ISpoke.DynamicReserveConfig({collateralFactor: 70_00, maxLiquidationBonus: 110_00, liquidationFee: 0}); + ISpoke.DynamicReserveConfig({collateralFactor: 70_00, maxLiquidationBonus: 105_00, liquidationFee: 0}); spokeInfo[spoke2].usdc.reserveConfig = ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 15_00}); spokeInfo[spoke2].usdc.dynReserveConfig = - ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 110_00, liquidationFee: 0}); + ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); // Deploy price feeds priceFeeds.push(_deployMockPriceFeed(spoke1, 1e8)); @@ -460,6 +479,35 @@ contract Setup is BaseTest { ); } + /// @notice Set up roles for the configurators + function _setUpConfiguratorRoles() internal virtual { + // Grant roles to configurators + accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); + accessManager.grantRole(Roles.SPOKE_ADMIN_ROLE, address(spokeConfigurator), 0); + + // Grant responsibilities to spokes + { + bytes4[] memory selectors = new bytes4[](6); + selectors[0] = ISpoke.updateLiquidationConfig.selector; + selectors[1] = ISpoke.updateReserveConfig.selector; + selectors[2] = ISpoke.updateDynamicReserveConfig.selector; + selectors[3] = ISpoke.addDynamicReserveConfig.selector; + selectors[4] = ISpoke.updatePositionManager.selector; + selectors[5] = ISpoke.updateReservePriceSource.selector; + accessManager.setTargetFunctionRole(address(spoke1), selectors, Roles.SPOKE_ADMIN_ROLE); + accessManager.setTargetFunctionRole(address(spoke2), selectors, Roles.SPOKE_ADMIN_ROLE); + } + + // Grant responsibilities to hubs + { + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = IHub.updateSpokeConfig.selector; + selectors[1] = IHub.setInterestRateData.selector; + accessManager.setTargetFunctionRole(address(hub1), selectors, Roles.HUB_ADMIN_ROLE); + accessManager.setTargetFunctionRole(address(hub2), selectors, Roles.HUB_ADMIN_ROLE); + } + } + function _deployMockPriceFeed(ISpoke spoke, uint256 price) internal returns (address) { AaveOracle oracle = AaveOracle(spoke.ORACLE()); return address(new MockPriceFeedSimulator(oracle.DECIMALS(), oracle.DESCRIPTION(), price)); diff --git a/tests/enigma-dark-invariants/base/BaseStorage.t.sol b/tests/enigma-dark-invariants/base/BaseStorage.t.sol index c279fe716..e03563f31 100644 --- a/tests/enigma-dark-invariants/base/BaseStorage.t.sol +++ b/tests/enigma-dark-invariants/base/BaseStorage.t.sol @@ -9,6 +9,8 @@ import {ISpoke} from "src/spoke/Spoke.sol"; import {IAaveOracle} from "src/spoke/interfaces/IAaveOracle.sol"; import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; +import {HubConfigurator} from "src/hub/HubConfigurator.sol"; +import {SpokeConfigurator} from "src/spoke/SpokeConfigurator.sol"; // Mock Contracts @@ -52,6 +54,9 @@ abstract contract BaseStorage { /// @notice The address that is targeted when executing an action (OPTIONAL) address internal targetActor; + /// @notice The signature of the action that is being executed + bytes4 internal currentActionSignature; + /////////////////////////////////////////////////////////////////////////////////////////////// // ASSETS STORAGE // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -70,12 +75,14 @@ abstract contract BaseStorage { IHub internal hub2; AssetInterestRateStrategy internal irStrategy1; AssetInterestRateStrategy internal irStrategy2; + HubConfigurator internal hubConfigurator; // SPOKE CONTRACTS ITreasurySpoke internal treasurySpoke1; ITreasurySpoke internal treasurySpoke2; ISpoke internal spoke1; ISpoke internal spoke2; + SpokeConfigurator internal spokeConfigurator; // ORACLES IAaveOracle internal oracle1; @@ -102,6 +109,8 @@ abstract contract BaseStorage { address[] internal hubAddresses; /// @notice Spoke configurations mapping(address => HubInfo) internal hubInfo; + /// @notice Hub assetIds + mapping(address => uint256[]) internal hubAssetIds; // SPOKES /// @notice Array of spokes addresses for the suite diff --git a/tests/enigma-dark-invariants/base/BaseTest.t.sol b/tests/enigma-dark-invariants/base/BaseTest.t.sol index a3cac0d7f..0606c3778 100644 --- a/tests/enigma-dark-invariants/base/BaseTest.t.sol +++ b/tests/enigma-dark-invariants/base/BaseTest.t.sol @@ -30,9 +30,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @dev Actor proxy mechanism modifier setup() virtual { - console.log("setup", msg.sender); actor = actors[msg.sender]; - console.log("actor", address(actor)); _; delete actor; } @@ -81,6 +79,12 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU return baseAssets[_assetIndex]; } + /// @notice Helper function to get a random hub asset id + function _getRandomHubAssetId(address hub, uint256 i) internal view returns (uint256) { + uint256 _assetIndex = i % hubAssetIds[hub].length; + return hubAssetIds[hub][_assetIndex]; + } + /// @notice Helper function to get a random spoke address function _getRandomSpoke(uint256 i) internal view returns (address) { uint256 _spokeIndex = i % spokesAddresses.length; @@ -100,7 +104,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU } /// @notice Helper function to get a random hub address - function _getRandomHubAddress(uint256 i) internal view returns (address) { + function _getRandomHub(uint256 i) internal view returns (address) { uint256 _hubIndex = i % hubAddresses.length; return hubAddresses[_hubIndex]; } diff --git a/tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol b/tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol index fc82f5ca1..9044bb386 100644 --- a/tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol @@ -18,36 +18,42 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// -/* - - E.g. num of active pools - uint256 public activePools; - - */ - -/////////////////////////////////////////////////////////////////////////////////////////////// -// ACTIONS // -/////////////////////////////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////////////////////////////// -// OWNER ACTIONS // -/////////////////////////////////////////////////////////////////////////////////////////////// - -// TODO: -// updateLiquidityFee -// updateFeeConfig -// updateInterestRateStrategy -// freezeAsset -// pauseAsset -// updateSpokeActive -// updateSpokeSupplyCap -// updateSpokeDrawCap -// updateSpokeCaps -// pauseSpoke -// freezeSpoke -// updateInterestRateData - -/////////////////////////////////////////////////////////////////////////////////////////////// -// HELPERS // -/////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j, uint8 k) external setup { + address hub = _getRandomHub(i); + uint256 assetId = _getRandomHubAssetId(hub, j); + address spoke = _getRandomSpoke(k); + hubConfigurator.updateSpokeSupplyCap(hub, assetId, spoke, addCap); + } + + function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j, uint8 k) external setup { + address hub = _getRandomHub(i); + uint256 assetId = _getRandomHubAssetId(hub, j); + address spoke = _getRandomSpoke(k); + hubConfigurator.updateSpokeDrawCap(hub, assetId, spoke, drawCap); + } + + function updateSpokeRiskPremiumCap(uint256 riskPremiumCap, uint8 i, uint8 j, uint8 k) external setup { + address hub = _getRandomHub(i); + uint256 assetId = _getRandomHubAssetId(hub, j); + address spoke = _getRandomSpoke(k); + hubConfigurator.updateSpokeRiskPremiumCap(hub, assetId, spoke, riskPremiumCap); + } + + function updateSpokePaused(bool paused, uint8 i, uint8 j, uint8 k) external setup { + address hub = _getRandomHub(i); + uint256 assetId = _getRandomHubAssetId(hub, j); + address spoke = _getRandomSpoke(k); + hubConfigurator.updateSpokePaused(hub, assetId, spoke, paused); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol b/tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol index bb5c60217..7ad8a09a6 100644 --- a/tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol @@ -35,11 +35,6 @@ contract HubHandler is BaseHandler, IHubHandler { /////////////////////////////////////////////////////////////////////////////////////////////// // TODO: - // Configurator: - // updateAssetConfig - // updateSpokeConfig - // setInterestRateData - // Reinvestment controller: // sweep // reclaim diff --git a/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol b/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol index 87c0525e6..2b87b0255 100644 --- a/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol @@ -22,7 +22,7 @@ contract DonationAttackHandler is BaseHandler { function donateUnderlyingToHub(uint256 amount, uint8 i, uint8 j) external { // Get one of the hub addresses randomly - address hubAddress = _getRandomHubAddress(j); + address hubAddress = _getRandomHub(j); // Get one of the assets IDs randomly address underlying = _getRandomBaseAsset(i); diff --git a/tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol b/tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol index c8399e47b..25f8d2961 100644 --- a/tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol @@ -18,42 +18,57 @@ contract SpokeConfiguratorHandler is BaseHandler, ISpokeConfiguratorHandler { /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// -/* - - E.g. num of active pools - uint256 public activePools; - - */ - -/////////////////////////////////////////////////////////////////////////////////////////////// -// ACTIONS // -/////////////////////////////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////////////////////////////// -// OWNER ACTIONS // -/////////////////////////////////////////////////////////////////////////////////////////////// - -// TODO -// updateLiquidationTargetHealthFactor -// updateHealthFactorForMaxBonus -// updateLiquidationBonusFactor -// updateLiquidationConfig -// updatePaused -// updateFrozen -// updateBorrowable -// updateCollateralRisk -// addCollateralFactor -// updateCollateralFactor -// addLiquidationBonus -// updateMaxLiquidationBonus -// addLiquidationFee -// updateLiquidationFee -// addDynamicReserveConfig -// updateDynamicReserveConfig -// pauseAllReserves -// freezeAllReserves - -/////////////////////////////////////////////////////////////////////////////////////////////// -// HELPERS // -/////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function updateLiquidationTargetHealthFactor(uint256 targetHealthFactor, uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.updateLiquidationTargetHealthFactor(spoke, targetHealthFactor); + } + + function updateHealthFactorForMaxBonus(uint256 healthFactorForMaxBonus, uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.updateHealthFactorForMaxBonus(spoke, healthFactorForMaxBonus); + } + + function updateLiquidationBonusFactor(uint256 liquidationBonusFactor, uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.updateLiquidationBonusFactor(spoke, liquidationBonusFactor); + } + + function updatePaused(bool paused, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updatePaused(spoke, reserveId, paused); + } + + function updateFrozen(bool frozen, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updateFrozen(spoke, reserveId, frozen); + } + + function updateBorrowable(bool borrowable, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updateBorrowable(spoke, reserveId, borrowable); + } + + function pauseAllReserves(uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.pauseAllReserves(spoke); + } + + function freezeAllReserves(uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.freezeAllReserves(spoke); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol b/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol index 5946217bf..d538f8b14 100644 --- a/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol @@ -26,6 +26,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { /// @dev should be zeroed after each liquidation call uint256 internal collateralReserveId; uint256 internal debtReserveId; + uint256 internal totalDebtValueBefore; /////////////////////////////////////////////////////////////////////////////////////////////// // ACTIONS // @@ -41,7 +42,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { address spoke = _getRandomSpoke(j); // Get one of the reserves IDs randomly - uint256 reserveId = _getReserveId(spoke, k); + uint256 reserveId = _getRandomReserveId(spoke, k); // Register user to check postconditions _registerUserToCheck(spoke, reserveId, onBehalfOf); @@ -66,7 +67,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { address spoke = _getRandomSpoke(j); // Get one of the reserves IDs randomly - uint256 reserveId = _getReserveId(spoke, k); + uint256 reserveId = _getRandomReserveId(spoke, k); // Register user to check postconditions _registerUserToCheck(spoke, reserveId, address(actor)); @@ -91,7 +92,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { address spoke = _getRandomSpoke(j); // Get one of the reserves IDs randomly - uint256 reserveId = _getReserveId(spoke, k); + uint256 reserveId = _getRandomReserveId(spoke, k); // Register user to check postconditions _registerUserToCheck(spoke, reserveId, address(actor)); @@ -123,7 +124,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { address spoke = _getRandomSpoke(j); // Get one of the reserves IDs randomly - uint256 reserveId = _getReserveId(spoke, k); + uint256 reserveId = _getRandomReserveId(spoke, k); // Register user to check postconditions _registerUserToCheck(spoke, reserveId, onBehalfOf); @@ -136,7 +137,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ///// HSPOST ///// - assertLt( + assertLe( defaultVarsAfter.userVars[spoke][reserveId][onBehalfOf].totalDebt, defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt, HSPOST_SP_C @@ -146,7 +147,10 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } } - function liquidationCall(uint256 debtToCover, bool receiveShares, uint8 i, uint8 j, uint8 k, uint8 l) external setup { + function liquidationCall(uint256 debtToCover, bool receiveShares, uint8 i, uint8 j, uint8 k, uint8 l) + external + setup + { bool success; bytes memory returnData; @@ -154,10 +158,11 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { address spoke = _getRandomSpoke(j); // Get one of the reserves IDs randomly - collateralReserveId = _getReserveId(spoke, k); - debtReserveId = _getReserveId(spoke, l); + collateralReserveId = _getRandomReserveId(spoke, k); + debtReserveId = _getRandomReserveId(spoke, l); - uint256 debtValueInBaseCurrency = ISpoke(spoke).getUserAccountData(_getRandomActor(i)).totalDebtValue; + totalDebtValueBefore = ISpoke(spoke).getUserAccountData(_getRandomActor(i)).totalDebtValue; + uint256 reserveDebtBefore = ISpoke(spoke).getReserveTotalDebt(debtReserveId); // Register users to check postconditions: liquidated user and liquidator for both reserves _registerUserToCheck(spoke, debtReserveId, _getRandomActor(i)); @@ -168,27 +173,40 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy( spoke, - abi.encodeCall(Spoke.liquidationCall, (collateralReserveId, debtReserveId, _getRandomActor(i), debtToCover, receiveShares)) + abi.encodeCall( + Spoke.liquidationCall, + (collateralReserveId, debtReserveId, _getRandomActor(i), debtToCover, receiveShares) + ) ); if (success) { _after(); - ///// HSPOST ///// - - uint256 userTotalDebt = ISpoke(spoke).getUserTotalDebt(debtReserveId, _getRandomActor(i)); + // Calculate the debt liquidated + uint256 reserveDebtAfter = ISpoke(spoke).getReserveTotalDebt(debtReserveId); + uint256 debtLiquidated = (reserveDebtBefore > reserveDebtAfter) ? reserveDebtBefore - reserveDebtAfter : 0; - assertLt(debtToCover, userTotalDebt, HSPOST_SP_LIQ_A); + ///// HSPOST ///// + assertLe( + debtLiquidated, + defaultVarsBefore.userVars[spoke][debtReserveId][_getRandomActor(i)].totalDebt, + HSPOST_SP_LIQ_A + ); - if (debtValueInBaseCurrency > ISpoke(spoke).DUST_LIQUIDATION_THRESHOLD()) { - assertEq(userTotalDebt, 0, HSPOST_SP_LIQ_C); + if (totalDebtValueBefore < ISpoke(spoke).DUST_LIQUIDATION_THRESHOLD()) { + assertEq( + defaultVarsAfter.userVars[spoke][debtReserveId][_getRandomActor(i)].totalDebt, 0, HSPOST_SP_LIQ_C + ); } + + assertGe(debtToCover, debtLiquidated, HSPOST_SP_LIQ_D); } else { revert("DefaultHandler: liquidationCall failed"); } delete collateralReserveId; delete debtReserveId; + delete totalDebtValueBefore; } function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external setup { @@ -199,7 +217,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { address spoke = _getRandomSpoke(i); - uint256 reserveId = _getReserveId(spoke, j); + uint256 reserveId = _getRandomReserveId(spoke, j); // Register user to check postconditions /// @dev setUsingAsCollateral(reserveId, FALSE) all reserves in user position should be refreshed, @@ -270,13 +288,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { // OWNER ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - // TODO - // Configurator: - // updateLiquidationConfig - // updateReserveConfig - // addDynamicReserveConfig - // updateDynamicReserveConfig - /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol b/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol index 539764b91..358695373 100644 --- a/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol @@ -33,9 +33,9 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { // OWNER ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - function supply(uint256 amount, uint8 i, uint8 j) external { + function supply(uint256 amount, uint8 i, uint8 j) external {// TODO fix coverage issues // Get one of the hub addresses randomly - address hubAddress = _getRandomHubAddress(i); + address hubAddress = _getRandomHub(i); address treasurySpoke = hubInfo[hubAddress].treasureSpoke; // Get one of the reserves IDs randomly @@ -51,7 +51,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { function withdraw(uint256 amount, uint8 i, uint8 j) external { // Get one of the hub addresses randomly - address hubAddress = _getRandomHubAddress(i); + address hubAddress = _getRandomHub(i); address treasurySpoke = hubInfo[hubAddress].treasureSpoke; // Get one of the reserves IDs randomly @@ -67,7 +67,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { function transfer(uint256 amount, uint8 i, uint8 j, uint8 k) external { // Get one of the hub addresses randomly - address hubAddress = _getRandomHubAddress(i); + address hubAddress = _getRandomHub(i); // Get one of the assets IDs randomly address asset = _getRandomBaseAsset(j); diff --git a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol index 0ab524964..7b7b1c6e5 100644 --- a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; // Libraries import {MathUtils} from "src/libraries/math/MathUtils.sol"; import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; +import "forge-std/console.sol"; // Utils import {Actor} from "../utils/Actor.sol"; @@ -148,20 +149,22 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } function assert_GPOST_HUB_B(address hubAddress, uint256 assetId) internal { - assertFullMulGe( + assertFullMulGe( // TODO review test_replay_1_supply defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, - defaultVarsAfter.assetVars[hubAddress][assetId].totalShares, - defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets, defaultVarsBefore.assetVars[hubAddress][assetId].totalShares, + defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets, + defaultVarsAfter.assetVars[hubAddress][assetId].totalShares, GPOST_HUB_B ); } function assert_GPOST_HUB_C(address hubAddress, uint256 assetId) internal { + // Read the cached signature of the current action + bytes4 signature = currentActionSignature; if ( - msg.sig == ISpokeHandler.supply.selector || msg.sig == ISpokeHandler.withdraw.selector - || msg.sig == ISpokeHandler.borrow.selector || msg.sig == ISpokeHandler.repay.selector - || msg.sig == ISpokeHandler.updateUserRiskPremium.selector + signature == ISpokeHandler.supply.selector || signature == ISpokeHandler.withdraw.selector + || signature == ISpokeHandler.borrow.selector || signature == ISpokeHandler.repay.selector + || signature == ISpokeHandler.updateUserRiskPremium.selector ) { assertEq( IHub(hubAddress).getAssetDrawnRate(assetId), @@ -195,7 +198,11 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { defaultVarsAfter.userVars[spoke][reserveId][user].premiumDebt < defaultVarsBefore.userVars[spoke][reserveId][user].premiumDebt ) { - assertTrue(msg.sig == ISpokeHandler.repay.selector, GPOST_SP_B); + assertTrue( + currentActionSignature == ISpokeHandler.repay.selector + || currentActionSignature == ISpokeHandler.liquidationCall.selector, + GPOST_SP_B + ); } } @@ -205,10 +212,13 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { // user-stored key uint16 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; + // Read the cached signature of the current action + bytes4 signature = currentActionSignature; + if ( - msg.sig == ISpokeHandler.borrow.selector || msg.sig == ISpokeHandler.withdraw.selector - || msg.sig == ISpokeHandler.setUsingAsCollateral.selector - || msg.sig == ISpokeHandler.updateUserDynamicConfig.selector + signature == ISpokeHandler.borrow.selector || signature == ISpokeHandler.withdraw.selector + || signature == ISpokeHandler.setUsingAsCollateral.selector + || signature == ISpokeHandler.updateUserDynamicConfig.selector ) { assertEq(latestKey, userKey, GPOST_SP_E); } @@ -221,4 +231,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function _registerUserToCheck(address spoke, uint256 reserveId, address user) internal { usersToCheck.push(UserInfo(spoke, reserveId, user)); } + + function _cacheCurrentActionSignature() internal { + currentActionSignature = bytes4(msg.sig); + } } diff --git a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol index 6376bb4c7..855fe1960 100644 --- a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol +++ b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol @@ -49,6 +49,9 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { /// @notice Postconditions for the handlers function _checkPostConditions() internal { + // Store the message signature to avoid losing it inside the checkPostConditions call context + _cacheCurrentActionSignature(); + try this.checkPostConditions() {} catch (bytes memory returnData) { _handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); @@ -109,6 +112,7 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { /// @notice Resets the state of the handlers function _resetState() internal { delete usersToCheck; + delete currentActionSignature; } /// @notice Checks if a call failed due to an assertion error and propagates the error if found. diff --git a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol index d08f08d6d..d9d5954b8 100644 --- a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +// Libraries +import "forge-std/console.sol"; + // Interfaces import {IHub} from "src/hub/interfaces/IHub.sol"; import {IERC20} from "src/dependencies/openzeppelin/IERC20.sol"; @@ -35,7 +38,7 @@ abstract contract HubInvariants is HandlerAggregator { } uint256 assetTotal = IHub(hubAddress).getAssetTotalOwed(assetId); // drawn + premium - assertGe(sumDebt, assetTotal, INV_HUB_B); + assertGe(sumDebt, assetTotal, INV_HUB_B); // TODO review test case test_replay_2_INV_HUB_B } function assert_INV_HUB_C(address hubAddress, uint256 assetId) internal { @@ -77,7 +80,12 @@ abstract contract HubInvariants is HandlerAggregator { uint256 totalDebt = IHub(hubAddress).getAssetTotalOwed(assetId); // Checks - //assertEq(totalSuppliedAssets, convertedAssets, INV_HUB_E); TODO review this invariant test_replay_invariant_INV_HUB_E + assertApproxEqAbs( // TODO review test_replay_3_setUsingAsCollateral + totalSuppliedAssets, + convertedAssets, + IHub(hubAddress).previewRemoveByShares(assetId, 1), + INV_HUB_E + ); assertEq(totalSuppliedAssets, asset.liquidity + totalDebt + asset.deficit + asset.swept, INV_HUB_F); } @@ -92,11 +100,14 @@ abstract contract HubInvariants is HandlerAggregator { totalAddedShares += IHub(hubAddress).getSpokeAddedShares(assetId, allSpokes[i]); } // Checks - //assertApproxEqAbs(totalAddedAssets, IHub(hubAddress).getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); TODO remove comment after going over test_replay_12_donateUnderlyingToSpoke + assertApproxEqAbs(totalAddedAssets, IHub(hubAddress).getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); } - function assert_INV_HUB_I(address hubAddress, uint256 assetId, address underlying) internal { + function assert_INV_HUB_I(address hubAddress, uint256 assetId) internal { + // Get underlying from assetId + (address underlying,) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); + // Query values uint256 liquidity = IHub(hubAddress).getAssetLiquidity(assetId); uint256 swept = IHub(hubAddress).getAssetSwept(assetId); @@ -107,7 +118,7 @@ abstract contract HubInvariants is HandlerAggregator { } function assert_INV_HUB_K(address hubAddress, uint256 assetId) internal { - // TODO for this check to be meaningful, strategy configuration operations have to be integrated + /// @dev for this check to be meaningful, strategy configuration operations have to be integrated IHub.AssetConfig memory assetConfig = IHub(hubAddress).getAssetConfig(assetId); // Checks diff --git a/tests/enigma-dark-invariants/remote/DOCKERFILE b/tests/enigma-dark-invariants/remote/DOCKERFILE index 62a120340..00a2c8c32 100644 --- a/tests/enigma-dark-invariants/remote/DOCKERFILE +++ b/tests/enigma-dark-invariants/remote/DOCKERFILE @@ -8,7 +8,7 @@ RUN forge --version ################## ## RUNNER ################## -FROM trailofbits/echidna:v2.2.7 AS run +FROM trailofbits/echidna:latest AS run RUN apt-get update && apt-get install -y make && rm -rf /var/lib/apt/lists/* diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol index e444a5b41..86eaa662d 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol @@ -36,27 +36,36 @@ contract ReplayTest1 is Invariants, Setup { // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// - function test_replay_1_updateUserRiskPremium() public { + function test_replay_1_supply() public { + // TODO review test case _setUpActor(USER3); - Tester.setUsingAsCollateral(true, 1, 26); - Tester.supply(8548, 251, 14, 14); - Tester.borrow(716, 227, 46, 18); - _setUpActor(USER1); - _delay(483724); - Tester.updateUserRiskPremium(0); - // Invalid: 54653460198616960432589820!=5465941435645494nah 5997582564, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block + _delay(140400); + Tester.supply(7, 242, 154, 0); + _delay(543845); + Tester.setUsingAsCollateral(true, 74, 212); + _delay(527372); + Tester.borrow(2, 218, 0, 0); + _delay(116349); + Tester.supply(8, 128, 2, 0); + // Invalid: 17*7 < 9*14 failed, reason: GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). + // Exchange rate before: 1,2857142857 + // Exchange rate after: 1,2142857143 } - function test_replay_1_invariant_INV_HUB_E() public { - _setUpActor(USER1); - _delay(7335); - Tester.setUsingAsCollateral(true, 0, 0); - _delay(33390); - Tester.supply(203156, 36, 0, 86); - _delay(895); - Tester.borrow(736, 0, 0, 38); - _delay(12354444); - invariant_INV_HUB(); // Invalid: 203179!=203159, reason: INV_HUB_E: hub.getTotalSuppliedAssets and hub.getAssetSuppliedAmount should match at any time + function test_replay_1_repay() public { + // TODO review test case + _setUpActor(USER3); + _delay(140400); + Tester.supply(10388, 95, 174, 0); + _delay(543845); + Tester.setUsingAsCollateral(true, 0, 52); + _delay(527372); + Tester.borrow(8603, 248, 142, 0); + _delay(243334); + Tester.repay(1, 116, 254, 252); + // Invalid: 8608>=8608 failed, reason: HSPOST_SP_C: User liability should decrease after repayment + // Should this be >= or >? + // Changed to >= and passing } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_10.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_10.t.sol deleted file mode 100644 index b512cf194..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_10.t.sol +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest10 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest10 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_10_invariant_INV_SP() public { - _setUpActor(USER1); - Tester.supply(15553676613300388866625557295, 0, 0, 0); - Tester.setUsingAsCollateral(true, 0, 0); - Tester.setPrice(1317397258558246502991383569369992979546474569225866748916270979618489862, 0); - invariant_INV_SP(); - } - - function test_replay_10_invariant_INV_HUB() public { - _setUpActor(USER1); - Tester.donateUnderlyingToHub(1, 0, 0); - invariant_INV_HUB(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_11.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_11.t.sol deleted file mode 100644 index dead12000..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_11.t.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest11 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest11 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_11_invariant_INV_HUB() public { - _setUpActor(USER3); - Tester.setUsingAsCollateral(true, 172, 9); - Tester.supply(1258, 128, 6, 0); - Tester.borrow(5, 41, 18, 2); - _setUpActor(USER1); - invariant_INV_HUB(); - // Invalid: 1258!=1260, reason: INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_12.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_12.t.sol deleted file mode 100644 index 92f2d9e43..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_12.t.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest12 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest12 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_12_donateUnderlyingToSpoke() public { - _setUpActor(USER3); - Tester.setUsingAsCollateral(true, 10, 224); - Tester.supply(10092, 251, 52, 79); - Tester.borrow(2835, 227, 50, 28); - _setUpActor(USER1); - _delay(431801); - Tester.donateUnderlyingToSpoke( - 3921833296228115180774015742656039797758013600205949455694768574845976631615, 49, 97 - ); - invariant_INV_HUB(); - - //INV_HUB_G: 10092!=10097, totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol deleted file mode 100644 index a990995f7..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_13.t.sol +++ /dev/null @@ -1,133 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest13 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest13 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_13_setUsingAsCollateral() public { - _setUpActor(USER2); - Tester.setUsingAsCollateral(true, 167, 2); - Tester.supply(230, 1, 1, 2); - Tester.borrow(103, 7, 23, 0); - _setUpActor(USER1); - _delay(1441); - Tester.setUsingAsCollateral(false, 0, 0); - // GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block. - } - - function test_replay_13_updateUserRiskPremium() public { - _setUpActor(USER2); - Tester.supply(3650894, 46, 43, 2); - Tester.setUsingAsCollateral(true, 13, 4); - Tester.borrow(7, 100, 5, 0); - _setUpActor(USER1); - _delay(1); - Tester.updateUserRiskPremium(0); - // Invalid: 50000106518811252501137774!=50000121735750944479215219, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block. - } - - function test_replay_13_liquidationCall() public { - _setUpActor(USER3); - Tester.setUsingAsCollateral(true, 172, 50); - Tester.supply(10092, 251, 30, 128); - Tester.borrow(7800, 227, 62, 25); - _setUpActor(USER1); - _delay(1425689); - _delay(314372); - Tester.setPrice(0, 0); - _delay(344203); - Tester.setPrice(188046916927661423584726156556474354281793089627848617698006899495150566800, 73); - Tester.liquidationCall( - 16786243780545468416195016839226359136549161709334054148083726855218179176262, false, 197, 130, 42, 58 - ); - // Invalid: 16786243780545468416195016839226359136549161709334054148083726855218179176262>=7871 failed, reason: HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position - } - - function test_replay_13_supply() public { - _setUpActor(USER3); - Tester.setUsingAsCollateral(true, 198, 50); - Tester.supply(10092, 251, 16, 135); - Tester.borrow(5584, 17, 18, 5); - _setUpActor(USER1); - _delay(100171); - Tester.supply(4303, 72, 0, 28); - // Invalid: 1000277893566763929<1000297265160523186 failed, reason: GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding). - } - - function test_replay_13_updateUserDynamicConfig() public { - _setUpActor(USER2); - Tester.supply(25427, 37, 13, 9); - Tester.setUsingAsCollateral(true, 1, 4); - Tester.borrow(1, 1, 183, 0); - _setUpActor(USER1); - _delay(3741); - Tester.updateUserDynamicConfig(0); - // Invalid: 50002184904060862687519392!=50004369636271476762274309, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block. - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol index 8842e2549..547e1b234 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + // Contracts import {Invariants} from "../Invariants.t.sol"; import {Setup} from "../Setup.t.sol"; @@ -18,7 +22,7 @@ contract ReplayTest2 is Invariants, Setup { _; } - function setUp() public { + function setUp() public { // Deploy protocol contracts _setUp(); @@ -28,98 +32,27 @@ contract ReplayTest2 is Invariants, Setup { vm.warp(101007); } - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// - - - function test_replay_2_withdraw() public { - _setUpActor(USER3); - Tester.supply(1, 23, 0, 2); - Tester.withdraw(3105157079507136411131272320472170162160557515874316681599825263298447597, 2, 0, 0); - - } - - function test_replay_2_supply() public { - _setUpActor(USER2); - Tester.setUsingAsCollateral(true, 25, 3); - Tester.supply(855, 7, 0, 3); - Tester.borrow(1, 1, 11, 0); - _setUpActor(USER1); - _delay(5032074); - Tester.supply(1768, 0, 0, 0); - - } - - function test_replay_2_setUsingAsCollateral() public { - _setUpActor(USER2); - Tester.supply(119335, 1, 0, 0); - Tester.setUsingAsCollateral(true, 0, 0); - Tester.borrow(1, 1, 1, 0); - _setUpActor(USER1); - _delay(2); - Tester.setUsingAsCollateral(false, 0, 0); - - } - - function test_replay_2_liquidationCall() public { - _setUpActor(USER1); - _delay(716); - Tester.supply(431999, 84, 27, 125); - _setUpActor(USER2); - _delay(577108); - Tester.supply(80962052803112294330194839042, 16, 45, 125); - _setUpActor(USER3); - _delay(7578); - Tester.setUsingAsCollateral(true, 1, 254); - Tester.supply(10092, 38, 37, 223); - Tester.borrow(7800, 227, 62, 51); - _setUpActor(USER1); - _delay(2138596); - _delay(194640); - Tester.withdraw(115792089237316195423570985008687907853269984665640564039457584007913129585001, 15, 131, 247); - _delay(322334); - Tester.setUsingAsCollateral(true, 31, 244); - _setUpActor(USER2); - _delay(827249); - _setUpActor(USER1); - _delay(405856); - Tester.liquidationCall(61422963954080211324642765950298638470789887452796784425666156856462101843613, false, 53, 251, 56, 20); - - } - - function test_replay_2_updateUserDynamicConfig() public { - _setUpActor(USER3); - Tester.supply(7123984353, 2, 126, 4); - Tester.setUsingAsCollateral(true, 6, 8); - Tester.borrow(1468, 2, 0, 0); - _setUpActor(USER1); - _delay(12809); - Tester.updateUserDynamicConfig(0); - - } - - function test_replay_2_repay() public { - _setUpActor(USER2); - Tester.supply(16, 25, 2, 7); - Tester.setUsingAsCollateral(true, 81, 2); - Tester.borrow(2, 16, 5, 4); - _delay(61); - Tester.repay(1, 19, 0, 248); - - } - - function test_replay_2_updateUserRiskPremium() public { + + function test_replay_2_INV_HUB_B() public { + // TODO review test case _setUpActor(USER3); - Tester.setUsingAsCollateral(true, 0, 22); - Tester.supply(2786, 251, 0, 14); - Tester.borrow(71, 77, 5, 3); - _setUpActor(USER1); - _delay(53617); - Tester.updateUserRiskPremium(0); - + _delay(140400); + Tester.supply(16, 176, 48, 0); + _delay(543845); + Tester.setUsingAsCollateral(true, 196, 188); + _delay(527372); + Tester.borrow(1, 80, 98, 60); + _delay(284444); + Tester.updateUserRiskPremium(98); + _delay(52383); + Tester.updateUserRiskPremium(102); + invariant_INV_HUB(); + // INV_HUB_B: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset + // sum hub.getSpokeOwed: 16, hub.getAssetTotalOwed(assetId): 20 } - /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // @@ -154,4 +87,4 @@ contract ReplayTest2 is Invariants, Setup { vm.warp(_timestamp); actor = actors[_user]; } -} \ No newline at end of file +} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol index b1730279c..e7376133d 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol @@ -22,7 +22,7 @@ contract ReplayTest3 is Invariants, Setup { _; } - function setUp() public { + function setUp() public { // Deploy protocol contracts _setUp(); @@ -32,136 +32,25 @@ contract ReplayTest3 is Invariants, Setup { vm.warp(101007); } - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_supply_5() public { - _setUpActor(USER1); - _delay(586107); - Tester.setUsingAsCollateral(true, 154, 0); - _delay(296474); - Tester.supply(149948, 15, 0, 0); - _delay(360624); - Tester.borrow(17, 0, 0, 73); - _setUpActor(USER2); - _delay(360465); - Tester.supply(1455122803355410, 7, 0, 0); - } - - function test_replay_liquidationCall_11() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 61); - _delay(296474); - Tester.supply(9, 171, 0, 6); - _delay(360624); - Tester.borrow(1, 0, 0, 5); - _delay(101782); - Tester.withdraw(1, 3, 140, 47); - _delay(328981); - Tester.updateUserRiskPremium(44); - _delay(282613); - Tester.updateUserRiskPremium(174); - _delay(364130); - Tester.updateUserRiskPremium(144); - _setUpActor(USER2); - _delay(413227); - Tester.liquidationCall( - 365099547169460107260444213992413627501745220129685401309505294672367581748, false, 0, 246, 3, 5 - ); - } - - function test_replay_updateUserDynamicConfig_7() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 2); - _delay(296474); - Tester.supply(95249040, 21, 0, 6); - _delay(360624); - Tester.borrow(2918, 0, 0, 0); - _setUpActor(USER2); - _delay(282613); - Tester.updateUserDynamicConfig(0); - } - - function test_replay_repay_10() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 101); - _delay(296474); - Tester.supply(22774255985, 0, 0, 0); - _delay(360624); - Tester.borrow(204954930, 0, 0, 234); - _delay(533426); - Tester.repay(1, 15, 54, 0); - } - - function test_replay_withdraw_2() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 43); - _delay(296474); - Tester.supply(47683715958142044857, 105, 0, 9); - _delay(360624); - Tester.borrow(51355, 0, 0, 5); - _delay(426559); - Tester.repay(684742840631310434884539857599449768327155454254541969729420468406827, 0, 158, 88); - _delay(9096); - Tester.withdraw(57966352054216140970237172336532564104220339507694885035496782237, 213, 0, 120); - _setUpActor(USER2); - _delay(349477); - Tester.withdraw(51236477730500247308223064742179045624643677222313070801276200340260439695, 0, 0); - } - - function test_replay_updateUserRiskPremium_4() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 0); - _delay(296474); - Tester.supply(174, 243, 0, 7); - _delay(360624); - Tester.borrow(1, 0, 0, 26); + + + function test_replay_3_setUsingAsCollateral() public { + // TODO review test case _setUpActor(USER3); - _delay(584023); - Tester.updateUserRiskPremium(0); - } - - function test_replay_setUsingAsCollateral_5() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 19); - _delay(296474); - Tester.supply(98423, 249, 0, 133); - _delay(360624); - Tester.borrow(2453, 0, 0, 2); - _setUpActor(USER2); - _delay(322362); - Tester.setUsingAsCollateral(true, 0, 0); - } - - function test_replay_setPrice_5() public { - _setUpActor(USER1); - _delay(586107); - Tester.setUsingAsCollateral(true, 0, 58); - _delay(296474); - Tester.supply(416, 0, 0, 5); - _setUpActor(USER2); - _delay(305693); - Tester.setPrice(258657099142048003581115496186011548248157129485377727959920274374993509, 10); - } - - function test_replay_transfer() public { + Tester.supply(790, 128, 71, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 203, 9); _setUpActor(USER1); - _delay(360520); - Tester.supply(1100316557268, 0, 0, 0); - _delay(111007); - Tester.setUsingAsCollateral(true, 0, 110); - _delay(360624); - Tester.borrow(96719, 0, 0, 61); - _delay(124260); - Tester.transfer(0, 0, 0, 0); + _delay(689004); + Tester.setUsingAsCollateral(false, 15, 13); + invariant_INV_HUB(); + // INV_HUB_E: hub.getTotalSuppliedAssets and hub.getAssetSuppliedAmount should match at any time, should not be off by more than 1 share worth of assets due to division precision loss + // getAddedAssets: 793, convertedAssets: 790 } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // @@ -196,4 +85,4 @@ contract ReplayTest3 is Invariants, Setup { vm.warp(_timestamp); actor = actors[_user]; } -} +} \ No newline at end of file diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol deleted file mode 100644 index 2fef10a75..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_4.t.sol +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest4 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest4 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_4_supply() public { - _setUpActor(USER1); - _delay(586107); - Tester.setUsingAsCollateral(true, 154, 0); - _delay(296474); - Tester.supply(149948, 15, 0, 0); - _delay(360624); - Tester.borrow(17, 0, 0, 73); - _setUpActor(USER2); - _delay(360465); - Tester.supply(1455122803355410, 7, 0, 0); - } - - function test_replay_4_liquidationCall() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 61); - _delay(296474); - Tester.supply(9, 171, 0, 6); - _delay(360624); - Tester.borrow(1, 0, 0, 5); - _delay(101782); - Tester.withdraw(1, 3, 140, 47); - _delay(328981); - Tester.updateUserRiskPremium(44); - _delay(282613); - Tester.updateUserRiskPremium(174); - _delay(364130); - Tester.updateUserRiskPremium(144); - _setUpActor(USER2); - _delay(413227); - Tester.liquidationCall( - 365099547169460107260444213992413627501745220129685401309505294672367581748, false, 0, 246, 3, 5 - ); - } - - function test_replay_4_updateUserDynamicConfig() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 2); - _delay(296474); - Tester.supply(95249040, 21, 0, 6); - _delay(360624); - Tester.borrow(2918, 0, 0, 0); - _setUpActor(USER2); - _delay(282613); - Tester.updateUserDynamicConfig(0); - } - - function test_replay_4_repay() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 101); - _delay(296474); - Tester.supply(22774255985, 0, 0, 0); - _delay(360624); - Tester.borrow(204954930, 0, 0, 234); - _delay(533426); - Tester.repay(1, 15, 54, 0); - } - - function test_replay_4_withdraw() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 43); - _delay(296474); - Tester.supply(47683715958142044857, 105, 0, 9); - _delay(360624); - Tester.borrow(51355, 0, 0, 5); - _delay(426559); - Tester.repay(684742840631310434884539857599449768327155454254541969729420468406827, 0, 158, 88); - _delay(9096); - Tester.withdraw(57966352054216140970237172336532564104220339507694885035496782237, 213, 0, 120); - _setUpActor(USER2); - _delay(349477); - Tester.withdraw(51236477730500247308223064742179045624643677222313070801276200340260439695, 0, 0); - } - - function test_replay_4_updateUserRiskPremium() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 0); - _delay(296474); - Tester.supply(174, 243, 0, 7); - _delay(360624); - Tester.borrow(1, 0, 0, 26); - _setUpActor(USER3); - _delay(584023); - Tester.updateUserRiskPremium(0); - } - - function test_replay_4_setUsingAsCollateral() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 19); - _delay(296474); - Tester.supply(98423, 249, 0, 133); - _delay(360624); - Tester.borrow(2453, 0, 0, 2); - _setUpActor(USER2); - _delay(322362); - Tester.setUsingAsCollateral(true, 0, 0); - } - - function test_replay_4_setPrice() public { - _setUpActor(USER1); - _delay(586107); - Tester.setUsingAsCollateral(true, 0, 58); - _delay(296474); - Tester.supply(416, 0, 0, 5); - _setUpActor(USER2); - _delay(305693); - Tester.setPrice(258657099142048003581115496186011548248157129485377727959920274374993509, 10); - } - - function test_replay_4_transfer() public { - _setUpActor(USER1); - _delay(360520); - Tester.supply(1100316557268, 0, 0, 0); - _delay(111007); - Tester.setUsingAsCollateral(true, 0, 110); - _delay(360624); - Tester.borrow(96719, 0, 0, 61); - _delay(124260); - Tester.transfer(0, 0, 0, 0); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_5.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_5.t.sol deleted file mode 100644 index 8be87a209..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_5.t.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest5 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest5 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_5_invariant_INV_HUB() public { - _setUpActor(USER3); - Tester.setUsingAsCollateral(true, 140, 28); - Tester.supply(10092, 251, 34, 4); - Tester.borrow(7800, 227, 62, 2); - _delay(1300935); - _setUpActor(0x0000000000000000000000000000000000010000); - Tester.setPrice(2354863949843749485414801162, 0); - invariant_INV_HUB(); - // Invalid: 10092!=10137, reason: INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol deleted file mode 100644 index a04ded932..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_6.t.sol +++ /dev/null @@ -1,176 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest6 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest6 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_6_supply() public { - _setUpActor(USER1); - _delay(586107); - Tester.setUsingAsCollateral(true, 154, 0); - _delay(296474); - Tester.supply(149948, 15, 0, 0); - _delay(360624); - Tester.borrow(17, 0, 0, 73); - _setUpActor(USER2); - _delay(360465); - Tester.supply(1455122803355410, 7, 0, 0); - } - - function test_replay_6_updateUserDynamicConfig() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 2); - _delay(296474); - Tester.supply(95249040, 21, 0, 6); - _delay(360624); - Tester.borrow(2918, 0, 0, 0); - _setUpActor(USER2); - _delay(282613); - Tester.updateUserDynamicConfig(0); - } - - function test_replay_6_repay() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 101); - _delay(296474); - Tester.supply(22774255985, 0, 0, 0); - _delay(360624); - Tester.borrow(204954930, 0, 0, 234); - _delay(533426); - Tester.repay(1, 15, 54, 0); - } - - function test_replay_6_withdraw() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 43); - _delay(296474); - Tester.supply(47683715958142044857, 105, 0, 9); - _delay(360624); - Tester.borrow(51355, 0, 0, 5); - _delay(426559); - Tester.repay(684742840631310434884539857599449768327155454254541969729420468406827, 0, 158, 88); - _delay(9096); - Tester.withdraw(57966352054216140970237172336532564104220339507694885035496782237, 213, 0, 120); - _setUpActor(USER2); - _delay(349477); - Tester.withdraw(51236477730500247308223064742179045624643677222313070801276200340260439695, 0, 0); - } - - function test_replay_6_updateUserRiskPremium() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 0); - _delay(296474); - Tester.supply(174, 243, 0, 7); - _delay(360624); - Tester.borrow(1, 0, 0, 26); - _setUpActor(USER3); - _delay(584023); - Tester.updateUserRiskPremium(0); - } - - function test_replay_6_setUsingAsCollateral() public { - _setUpActor(USER1); - _delay(228361); - Tester.setUsingAsCollateral(true, 0, 19); - _delay(296474); - Tester.supply(98423, 249, 0, 133); - _delay(360624); - Tester.borrow(2453, 0, 0, 2); - _setUpActor(USER2); - _delay(322362); - Tester.setUsingAsCollateral(true, 0, 0); - } - - function test_replay_6_setPrice() public { - _setUpActor(USER1); - _delay(586107); - Tester.setUsingAsCollateral(true, 0, 58); - _delay(296474); - Tester.supply(416, 0, 0, 5); - _setUpActor(USER2); - _delay(305693); - Tester.setPrice(258657099142048003581115496186011548248157129485377727959920274374993509, 10); - } - - function test_replay_6_transfer() public { - _setUpActor(USER1); - _delay(360520); - Tester.supply(1100316557268, 0, 0, 0); - _delay(111007); - Tester.setUsingAsCollateral(true, 0, 110); - _delay(360624); - Tester.borrow(96719, 0, 0, 61); - _delay(124260); - Tester.transfer(0, 0, 0, 0); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol deleted file mode 100644 index d09b80983..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_7.t.sol +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest7 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest7 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_7_withdraw() public { - _setUpActor(USER3); - Tester.supply(10070862, 2, 0, 0); - Tester.withdraw(63970967942622104945159534037176262183333096787346814336171336533994484054, 32, 4, 2); - // GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding). - } - - function test_replay_7_transfer() public { - _setUpActor(USER1); - Tester.supply(2, 0, 0, 0); - Tester.setUsingAsCollateral(true, 0, 0); - Tester.borrow(1, 0, 0, 0); - _delay(1); - Tester.transfer(0, 0, 0, 0); - // Invalid: 77777777777777777777777778!=87037037037037037037037038, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block - } - - function test_replay_7_liquidationCall() public { - _setUpActor(USER3); - Tester.setUsingAsCollateral(true, 172, 50); - Tester.supply(10092, 251, 48, 141); - Tester.borrow(7800, 227, 62, 25); - _setUpActor(USER1); - _delay(2711741); - Tester.setPrice(929774615256615712219717710056, 0); - Tester.liquidationCall(8492579161489, false, 203, 220, 184, 37); - // Invalid: 8492579161489>=7790 failed, reason: HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position - } - - function test_replay_7_updateUserDynamicConfig() public { - _setUpActor(USER2); - Tester.setUsingAsCollateral(true, 1, 7); - Tester.supply(718, 1, 1, 0); - Tester.borrow(4, 1, 1, 0); - _setUpActor(USER1); - _delay(990809); - Tester.updateUserDynamicConfig(0); - // 50309501702259362426493347!=50386339051151290372430846, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block - } - - function test_replay_7_setUsingAsCollateral() public { - _setUpActor(USER2); - Tester.setUsingAsCollateral(true, 1, 4); - Tester.supply(597, 4, 15, 0); - Tester.borrow(14, 4, 11, 0); - _setUpActor(USER1); - _delay(35069); - Tester.setUsingAsCollateral(false, 0, 0); - // Invalid: 51302810348036478689745023!=51393534002229654403567448, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block - } - - function test_replay_7_updateUserRiskPremium() public { - _setUpActor(USER2); - Tester.setUsingAsCollateral(true, 105, 11); - Tester.supply(4143760, 10, 13, 2); - Tester.borrow(7, 13, 67, 3); - _setUpActor(USER1); - _delay(322348); - Tester.updateUserRiskPremium(0); - // Invalid: 50000093849279130279960445!=50000107256293122225061834, reason: GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block - } - - function test_replay_7_supply() public { - _setUpActor(USER3); - Tester.setUsingAsCollateral(true, 198, 4); - _delay(655); - Tester.supply(10092, 92, 54, 223); - Tester.borrow(4418, 227, 62, 51); - _setUpActor(USER1); - _delay(116902); - Tester.supply(4439446668599936570, 21, 0, 45); - // Invalid: 1000002970026492637<1000297265160523186 failed, reason: GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding) - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_8.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_8.t.sol deleted file mode 100644 index 8d0c6be09..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_8.t.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest8 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest8 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_8_invariant_INV_HUB() public { - _setUpActor(USER1); - Tester.donateUnderlyingToHub(1, 0, 0); - invariant_INV_HUB(); - // Invalid: 1!=0, reason: INV_HUB_I: asset.underlying.balanceOf(hub) >= asset.liquidity - asset.swept - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_9.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_9.t.sol deleted file mode 100644 index dba6455e3..000000000 --- a/tests/enigma-dark-invariants/replays/ReplayTest_9.t.sol +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../utils/Actor.sol"; - -contract ReplayTest9 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest9 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_9_supply() public { - _setUpActor(USER1); - Tester.setPrice(999999999999990001, 0); - Tester.setUsingAsCollateral(true, 1, 0); - Tester.supply(13167295895020254737567557, 0, 1, 0); - invariant_INV_SP(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol b/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol index ba2b511fb..a6d5fa1f3 100644 --- a/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol +++ b/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol @@ -42,7 +42,7 @@ abstract contract InvariantsSpec { string constant INV_HUB_H = "INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes"; - string constant INV_HUB_I = "INV_HUB_I: asset.underlying.balanceOf(hub) >= asset.liquidity - asset.swept"; + string constant INV_HUB_I = "INV_HUB_I: asset.underlying.balanceOf(hub) + asset.swept >= asset.liquidity"; string constant INV_HUB_J = "INV_HUB_J: totalDrawn{Shares,Assets} >= any spoke totalDrawn{Shares,Assets} (same for premium debt)"; // TODO diff --git a/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol b/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol index 9588849e5..4c7f5cfc3 100644 --- a/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol +++ b/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol @@ -43,7 +43,7 @@ abstract contract PostconditionsSpec { "GPOST_SP_A: Stored (user.premiumDrawnShares/user.baseDrawnShares) & calculated user risk premium (calculation based on user's position, via spoke.calculateUserAccountData) need to be the same right after an operation"; string constant GPOST_SP_B = - "GPOST_SP_B: Premium debt of an individual user, spoke can only decrease by calling repay when premium debt is not zero"; + "GPOST_SP_B: Premium debt of an individual user, spoke can only decrease by calling repay when premium debt is not zero";// TODO add liquidationCall to the condition string constant HSPOST_SP_C = "HSPOST_SP_C: User liability should decrease after repayment"; @@ -70,4 +70,7 @@ abstract contract PostconditionsSpec { string constant HSPOST_SP_LIQ_C = "HSPOST_SP_LIQ_C: Liquidator is always forced to repay all the debt of a user if debt value is below DUST_DEBT_LIQUIDATION_THRESHOLD"; + + string constant HSPOST_SP_LIQ_D = + "HSPOST_SP_LIQ_D: Liquidation cannot result in an amount of liquidated debt > debtToCover"; } diff --git a/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol b/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol index 9e8898863..b04dfad9d 100644 --- a/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol +++ b/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol @@ -129,7 +129,7 @@ abstract contract PropertiesAsserts { string memory yStr = PropertiesLibString.toString(y); bytes memory assertMsg = - abi.encodePacked("Invalid: ", aStr, "*", bStr, " <", xStr, "*", yStr, " failed, reason: ", reason); + abi.encodePacked("Invalid: ", aStr, "*", bStr, " < ", xStr, "*", yStr, " failed, reason: ", reason); emit AssertGeFail(string(assertMsg)); assert(false); diff --git a/tests/enigma-dark-invariants/utils/PropertiesConstants.sol b/tests/enigma-dark-invariants/utils/PropertiesConstants.sol index 60d0d3bd8..c5c6f6edb 100644 --- a/tests/enigma-dark-invariants/utils/PropertiesConstants.sol +++ b/tests/enigma-dark-invariants/utils/PropertiesConstants.sol @@ -28,4 +28,10 @@ abstract contract PropertiesConstants { uint16 constant BASE_VARIABLE_BORROW_RATE_IR2 = 2_00; // 2.00% uint16 constant VARIABLE_RATE_SLOPE_1_IR2 = 7_00; // 7.00% uint16 constant VARIABLE_RATE_SLOPE_2_IR2 = 75_00; // 75.00% + + // Spoke 1 liquidation config + uint128 constant TARGET_HEALTH_FACTOR_SPOKE1 = 1.05e18; + + // Spoke 2 liquidation config + uint128 constant TARGET_HEALTH_FACTOR_SPOKE2 = 1.02e18; } From 35388da4d45e176f70ecfc973a629181ef77d69d Mon Sep 17 00:00:00 2001 From: Elpacos Date: Fri, 24 Oct 2025 14:38:11 +0200 Subject: [PATCH 015/106] fix: change ci corpus reference --- .github/workflows/enigma-dark-invariants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index f7f36d97f..c9ab9b384 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -37,7 +37,7 @@ jobs: - name: Clone Corpus id: clone-corpus run: | - git clone https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/Enigma-Dark/aave-v4-invariants-corpus.git corpus + git clone https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/${{ github.repository_owner }}/aave-v4-invariants-corpus.git corpus echo "sha=$(git -C corpus rev-parse HEAD)" >> $GITHUB_OUTPUT - name: Cache corpus From 8a48e8806cc73570166b03fb4e2d1fba3f8d732c Mon Sep 17 00:00:00 2001 From: Elpacos Date: Fri, 24 Oct 2025 14:38:58 +0200 Subject: [PATCH 016/106] fix: change ci corpus reference --- .github/workflows/enigma-dark-invariants.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index c9ab9b384..c6e6cdd47 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -31,13 +31,13 @@ jobs: with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} + owner: aave repositories: aave-v4-invariants-corpus - name: Clone Corpus id: clone-corpus run: | - git clone https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/${{ github.repository_owner }}/aave-v4-invariants-corpus.git corpus + git clone https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/aave/aave-v4-invariants-corpus.git corpus echo "sha=$(git -C corpus rev-parse HEAD)" >> $GITHUB_OUTPUT - name: Cache corpus From 768112f05a6e446dbd7485f0bf04ba41a5442f9e Mon Sep 17 00:00:00 2001 From: Elpacos Date: Fri, 24 Oct 2025 14:39:37 +0200 Subject: [PATCH 017/106] fix: switch runner to aave-latest --- .github/workflows/enigma-dark-invariants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index c6e6cdd47..b93cbfca6 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -16,7 +16,7 @@ concurrency: jobs: setup: name: Setup - runs-on: ubuntu-latest + runs-on: aave-latest outputs: corpus-sha: ${{ steps.clone-corpus.outputs.sha }} steps: From 7fcec493a8d5d26f4b0e914951587602d4749f7b Mon Sep 17 00:00:00 2001 From: Elpacos Date: Sun, 21 Dec 2025 19:46:28 +0100 Subject: [PATCH 018/106] chore: refactor --- .../hooks/DefaultBeforeAfterHooks.t.sol | 55 ++++++++++++++++++- .../hooks/HookAggregator.t.sol | 3 + .../invariants/HubInvariants.t.sol | 5 +- .../replays/ReplayTest_1.t.sol | 1 + .../specs/InvariantsSpec.t.sol | 10 +--- .../specs/PostconditionsSpec.t.sol | 31 ++++++++++- .../utils/PropertiesConstants.sol | 1 + 7 files changed, 93 insertions(+), 13 deletions(-) diff --git a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol index 7b7b1c6e5..7d7c02050 100644 --- a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol @@ -34,6 +34,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { uint256 totalShares; uint256 drawn; uint256 premium; + uint256 lastUpdateTimestamp; } struct UserVars { @@ -105,6 +106,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { _defaultVars.assetVars[hubAddress][j].totalShares = IHub(hubAddress).getAddedShares(j); (_defaultVars.assetVars[hubAddress][j].drawn, _defaultVars.assetVars[hubAddress][j].premium) = IHub(hubAddress).getAssetOwed(j); + _defaultVars.assetVars[hubAddress][j].lastUpdateTimestamp = + IHub(hubAddress).getAsset(j).lastUpdateTimestamp; } } } @@ -119,10 +122,10 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { // Iterate through all reserves of the spoke for (uint256 j; j < spokeReserveIds[userInfo.spoke].length; j++) { ( - , - _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + , + _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] .premiumDebt - ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); + ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].totalDebt = ISpoke(userInfo.spoke).getUserTotalDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); } @@ -181,6 +184,52 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } } + function assert_GPOST_HUB_D(address hubAddress, uint256 assetId) internal { + assertLe(defaultVarsAfter.assetVars[hubAddress][assetId].lastUpdateTimestamp, block.timestamp, GPOST_HUB_D); + } + + function assert_GPOST_HUB_EF(address hubAddress, uint256 assetId) internal { + /* // Get the spoke config + IHub.SpokeConfig memory spokeConfig = IHub(hubAddress).getSpokeConfig(assetId, spoke);// TODO pass spoke as parameter + (, uint8 decimals) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); + + // GPOST_HUB_E + if ( + defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets + > defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets + ) { + if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, + spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_E + ); + } + } + + // GPOST_HUB_F + if ( + defaultVarsAfter.assetVars[hubAddress][assetId].drawn + > defaultVarsBefore.assetVars[hubAddress][assetId].drawn + ) { + if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.assetVars[hubAddress][assetId].drawn, + spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_F + ); + } + } */ + } + + function assert_GPOST_HUB_G(address hubAddress, uint256 assetId) internal { + assertGe( + defaultVarsAfter.assetVars[hubAddress][assetId].lastUpdateTimestamp, + defaultVarsBefore.assetVars[hubAddress][assetId].lastUpdateTimestamp, + GPOST_HUB_G + ); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // POST CONDITIONS: SPOKE // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol index 855fe1960..4122822d3 100644 --- a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol +++ b/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol @@ -77,6 +77,9 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { assert_GPOST_HUB_A(hubAddress, assetId); assert_GPOST_HUB_B(hubAddress, assetId); assert_GPOST_HUB_C(hubAddress, assetId); + assert_GPOST_HUB_D(hubAddress, assetId); + assert_GPOST_HUB_EF(hubAddress, assetId); + assert_GPOST_HUB_G(hubAddress, assetId); } } } diff --git a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol index d9d5954b8..e8c149226 100644 --- a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol @@ -86,6 +86,7 @@ abstract contract HubInvariants is HandlerAggregator { IHub(hubAddress).previewRemoveByShares(assetId, 1), INV_HUB_E ); + assertEq(totalSuppliedAssets, asset.liquidity + totalDebt + asset.deficit + asset.swept, INV_HUB_F); } @@ -99,6 +100,8 @@ abstract contract HubInvariants is HandlerAggregator { totalAddedAssets += IHub(hubAddress).getSpokeAddedAssets(assetId, allSpokes[i]); totalAddedShares += IHub(hubAddress).getSpokeAddedShares(assetId, allSpokes[i]); } + + // TODO take into account the burned interest from virtual shared -> _calculateBurntInterest from Base.t.sol // Checks assertApproxEqAbs(totalAddedAssets, IHub(hubAddress).getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); @@ -118,7 +121,7 @@ abstract contract HubInvariants is HandlerAggregator { } function assert_INV_HUB_K(address hubAddress, uint256 assetId) internal { - /// @dev for this check to be meaningful, strategy configuration operations have to be integrated + /// @dev TODO for this check to be meaningful, strategy configuration operations have to be integrated IHub.AssetConfig memory assetConfig = IHub(hubAddress).getAssetConfig(assetId); // Checks diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol b/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol index 86eaa662d..a68593a88 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol +++ b/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol @@ -66,6 +66,7 @@ contract ReplayTest1 is Invariants, Setup { // Invalid: 8608>=8608 failed, reason: HSPOST_SP_C: User liability should decrease after repayment // Should this be >= or >? // Changed to >= and passing + // TODO add tolerance 2 wei } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol b/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol index a6d5fa1f3..12098263d 100644 --- a/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol +++ b/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol @@ -20,9 +20,6 @@ abstract contract InvariantsSpec { // HUB // /////////////////////////////////////////////////////////////////////////////////////////////// - string constant INV_HUB_A = - "INV_HUB_A: Sum of spoke added assets on a single asset must be greater or equal than the total amount of added assets of the asset"; // TODO this overlaps with INV_HUB_G - string constant INV_HUB_A2 = "INV_HUB_A2: If hub assets = 0 => shares 0"; string constant INV_HUB_B = @@ -38,14 +35,14 @@ abstract contract InvariantsSpec { "INV_HUB_E2: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + totalDebt + deficit + swept"; string constant INV_HUB_G = - "INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke)"; + "INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke)";// TODO should this be greater or equal? string constant INV_HUB_H = "INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes"; string constant INV_HUB_I = "INV_HUB_I: asset.underlying.balanceOf(hub) + asset.swept >= asset.liquidity"; string constant INV_HUB_J = - "INV_HUB_J: totalDrawn{Shares,Assets} >= any spoke totalDrawn{Shares,Assets} (same for premium debt)"; // TODO + "INV_HUB_J: totalDrawn{Shares,Assets} >= any spoke totalDrawn{Shares,Assets} (same for premium debt)"; // TODO implement strict equality on all total == sum type invariants string constant INV_HUB_K = "INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset"; @@ -71,7 +68,7 @@ abstract contract InvariantsSpec { "INV_SP_B: User/Reserve cannot have non-zero assets and zero shares in supply or debt sides."; string constant INV_SP_C = - "INV_SP_C: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset"; + "INV_SP_C: Sum of spoke debts on a single asset must be greater or equal than the total debt of the reserve"; string constant INV_SP_D = "INV_SP_D: Users without collateral also have no debt."; @@ -80,5 +77,4 @@ abstract contract InvariantsSpec { // TODO // - Spoke/Asset cannot have non-zero assets and zero shares in add or draw sides."; // - 4626 roundtrip on preview methods - // - Asset.feeReceiver should always } diff --git a/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol b/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol index 4c7f5cfc3..e4a08d3b4 100644 --- a/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol +++ b/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol @@ -35,6 +35,21 @@ abstract contract PostconditionsSpec { string constant GPOST_HUB_C = "GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block."; + string constant GPOST_HUB_D = + "GPOST_HUB_D: lastUpdateTimestamp must be <= block.timestamp after any action (timestamps cannot be in the future)."; + + string constant GPOST_HUB_E = + "GPOST_HUB_E: if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)"; + + string constant GPOST_HUB_F = + "GPOST_HUB_F: if drawnAssets for a spoke & assetId increase, drawnAssets <= drawCap * precision (when cap != MAX)";// TODO take into account the deficit, use Owed instead of drawn + + string constant GPOST_HUB_G = + "GPOST_HUB_G: lastUpdateTimestamp is monotonic non-decreasing across actions (time does not go backwards)"; + + string constant GPOST_HUB_H = + "GPOST_HUB_H: If userRiskPremium increases, userRiskPremium <= riskPremiumCap (when cap != MAX)"; // TODO + /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -43,9 +58,11 @@ abstract contract PostconditionsSpec { "GPOST_SP_A: Stored (user.premiumDrawnShares/user.baseDrawnShares) & calculated user risk premium (calculation based on user's position, via spoke.calculateUserAccountData) need to be the same right after an operation"; string constant GPOST_SP_B = - "GPOST_SP_B: Premium debt of an individual user, spoke can only decrease by calling repay when premium debt is not zero";// TODO add liquidationCall to the condition + "GPOST_SP_B: Premium debt of an individual user can only decrease by calling repay or liquidationCall when premium debt is not zero"; + + // TODO drawn debt debt of an individual user can only decrease by calling repay or liquidationCall and if premium debt is zero after the action - string constant HSPOST_SP_C = "HSPOST_SP_C: User liability should decrease after repayment"; + string constant HSPOST_SP_C = "HSPOST_SP_C: User liability should decrease after repayment";//@audit should this be strict? string constant HSPOST_SP_D = "HSPOST_SP_D: Unhealthy users cannot borrow more"; @@ -58,6 +75,9 @@ abstract contract PostconditionsSpec { string constant HSPOST_SP_F = "HSPOST_SP_F: Total debt of a user should not change after updateUserRiskPremium"; + string constant GPOST_SP_H = + "GPOST_SP_H: if user totalDebt == 0 and withdraw is called, user can withdraw all supplied"; // TODO + /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE: LIQUIDATION // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -73,4 +93,11 @@ abstract contract PostconditionsSpec { string constant HSPOST_SP_LIQ_D = "HSPOST_SP_LIQ_D: Liquidation cannot result in an amount of liquidated debt > debtToCover"; + + string constant HSPOST_SP_LIQ_E = + "HSPOST_SP_LIQ_E: Only unhealthy users can be liquidated"; // TODO + + string constant HSPOST_SP_LIQ_F = "HSPOST_SP_LIQ_F: Post-liquidation transfers match close factor/bonus (amounts to violator and liquidator)"; // TODO + + string constant GPOST_SP_LIQ_G = "GPOST_SP_LIQ_G: Only liquidations can worsen an already unhealthy account's health"; // TODO } diff --git a/tests/enigma-dark-invariants/utils/PropertiesConstants.sol b/tests/enigma-dark-invariants/utils/PropertiesConstants.sol index c5c6f6edb..25d346049 100644 --- a/tests/enigma-dark-invariants/utils/PropertiesConstants.sol +++ b/tests/enigma-dark-invariants/utils/PropertiesConstants.sol @@ -16,6 +16,7 @@ abstract contract PropertiesConstants { // Protocol constants uint256 constant SPOKE_COUNT = 3; + uint40 constant MAX_ALLOWED_SPOKE_CAP = type(uint40).max; // Interest rate 1 data uint16 constant OPTIMAL_USAGE_RATIO_IR1 = 85_00; // 85.00% From f4009563d1545560d36e7fbffbe898d80da75e0e Mon Sep 17 00:00:00 2001 From: Elpacos Date: Mon, 22 Dec 2025 01:30:46 +0100 Subject: [PATCH 019/106] chore: hub suite --- .../handlers/simulators/.gitkeep | 0 .../helpers/extended/.gitkeep | 0 .../helpers/fixtures/.gitkeep | 0 .../hub-suite/HandlerAggregator.t.sol | 12 + .../hub-suite/Invariants.t.sol | 25 ++ .../hub-suite/README.md | 71 +++++ .../hub-suite/Setup.t.sol | 295 ++++++++++++++++++ .../hub-suite/SpecAggregator.t.sol | 11 + .../hub-suite/Tester.t.sol | 17 + .../_config/echidna_config.yaml | 0 .../_config/echidna_config_ci.yaml | 0 .../{ => hub-suite}/base/BaseHandler.t.sol | 2 +- .../{ => hub-suite}/base/BaseHooks.t.sol | 0 .../hub-suite/base/BaseStorage.t.sol | 105 +++++++ .../hub-suite/base/BaseTest.t.sol | 123 ++++++++ .../base/ProtocolAssertions.t.sol | 2 +- .../handlers/HubConfiguratorHandler.t.sol | 55 ++++ .../hub-suite/handlers/HubHandler.t.sol | 206 ++++++++++++ .../interfaces/IHubConfiguratorHandler.sol | 0 .../handlers/interfaces/IHubHandler.sol | 8 + .../simulators/DonationAttackHandler.t.sol} | 29 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 220 +++++++++++++ .../hub-suite/hooks/HookAggregator.t.sol | 83 +++++ .../hub-suite/invariants/HubInvariants.t.sol | 134 ++++++++ .../hub-suite/specs/HubInvariantsSpec.t.sol | 24 ++ .../specs/HubPostconditionsSpec.t.sol | 27 ++ .../hub-suite/utils/PropertiesConstants.sol | 29 ++ .../hub-suite/utils/StdAsserts.sol | 49 +++ .../HandlerAggregator.t.sol | 8 +- .../{ => protocol-suite}/Invariants.t.sol | 0 .../{ => protocol-suite}/README.md | 0 .../{ => protocol-suite}/Setup.t.sol | 69 ++-- .../{ => protocol-suite}/SpecAggregator.t.sol | 0 .../{ => protocol-suite}/Tester.t.sol | 0 .../{ => protocol-suite}/TesterFoundry.t.sol | 0 .../_config/echidna_config.yaml | 61 ++++ .../_config/echidna_config_ci.yaml | 61 ++++ .../protocol-suite/base/BaseHandler.t.sol | 81 +++++ .../protocol-suite/base/BaseHooks.t.sol | 18 ++ .../base/BaseStorage.t.sol | 2 +- .../{ => protocol-suite}/base/BaseTest.t.sol | 6 +- .../base/ProtocolAssertions.t.sol | 10 + .../handlers/hub/HubConfiguratorHandler.t.sol | 2 +- .../interfaces/IHubConfiguratorHandler.sol | 6 + .../handlers/interfaces/IHubHandler.sol | 0 .../interfaces/ISpokeConfiguratorHandler.sol | 0 .../handlers/interfaces/ISpokeHandler.sol | 0 .../interfaces/ITreasurySpokeHandler.sol | 0 .../simulators/DonationAttackHandler.t.sol | 0 .../PriceFeedSimulatorHandler.t.sol} | 6 +- .../spoke/SpokeConfiguratorHandler.t.sol | 2 +- .../handlers/spoke/SpokeHandler.t.sol | 2 +- .../handlers/spoke/TreasurySpokeHandler.t.sol | 0 .../hooks/DefaultBeforeAfterHooks.t.sol | 68 ++-- .../hooks/HookAggregator.t.sol | 44 +-- .../invariants/HubInvariants.t.sol | 0 .../invariants/SpokeInvariants.t.sol | 0 .../replays/ReplayTest_1.t.sol | 2 +- .../replays/ReplayTest_2.t.sol | 2 +- .../replays/ReplayTest_3.t.sol | 2 +- .../specs/InvariantsSpec.t.sol | 0 .../specs/PostconditionsSpec.t.sol | 0 .../mocks/MockPriceFeedSimulator.sol | 0 .../{ => shared}/remote/DOCKERFILE | 0 .../{ => shared}/utils/Actor.sol | 0 .../shared/utils/ActorsUtils.sol | 49 +++ .../{ => shared}/utils/CREATE3.sol | 0 .../{ => shared}/utils/DeployPermit2.sol | 0 .../shared/utils/ErrorHandlers.sol | 57 ++++ .../{ => shared}/utils/PropertiesAsserts.sol | 0 .../utils/PropertiesConstants.sol | 0 .../{ => shared}/utils/StdAsserts.sol | 0 72 files changed, 1919 insertions(+), 166 deletions(-) delete mode 100644 tests/enigma-dark-invariants/handlers/simulators/.gitkeep delete mode 100644 tests/enigma-dark-invariants/helpers/extended/.gitkeep delete mode 100644 tests/enigma-dark-invariants/helpers/fixtures/.gitkeep create mode 100644 tests/enigma-dark-invariants/hub-suite/HandlerAggregator.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/Invariants.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/README.md create mode 100644 tests/enigma-dark-invariants/hub-suite/Setup.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/SpecAggregator.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/Tester.t.sol rename tests/enigma-dark-invariants/{ => hub-suite}/_config/echidna_config.yaml (100%) rename tests/enigma-dark-invariants/{ => hub-suite}/_config/echidna_config_ci.yaml (100%) rename tests/enigma-dark-invariants/{ => hub-suite}/base/BaseHandler.t.sol (98%) rename tests/enigma-dark-invariants/{ => hub-suite}/base/BaseHooks.t.sol (100%) create mode 100644 tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/base/BaseTest.t.sol rename tests/enigma-dark-invariants/{ => hub-suite}/base/ProtocolAssertions.t.sol (80%) create mode 100644 tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol rename tests/enigma-dark-invariants/{ => hub-suite}/handlers/interfaces/IHubConfiguratorHandler.sol (100%) create mode 100644 tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol rename tests/enigma-dark-invariants/{handlers/hub/HubHandler.t.sol => hub-suite/handlers/simulators/DonationAttackHandler.t.sol} (75%) create mode 100644 tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/specs/HubPostconditionsSpec.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/utils/PropertiesConstants.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/utils/StdAsserts.sol rename tests/enigma-dark-invariants/{ => protocol-suite}/HandlerAggregator.t.sol (78%) rename tests/enigma-dark-invariants/{ => protocol-suite}/Invariants.t.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/README.md (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/Setup.t.sol (91%) rename tests/enigma-dark-invariants/{ => protocol-suite}/SpecAggregator.t.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/Tester.t.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/TesterFoundry.t.sol (100%) create mode 100644 tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml create mode 100644 tests/enigma-dark-invariants/protocol-suite/_config/echidna_config_ci.yaml create mode 100644 tests/enigma-dark-invariants/protocol-suite/base/BaseHandler.t.sol create mode 100644 tests/enigma-dark-invariants/protocol-suite/base/BaseHooks.t.sol rename tests/enigma-dark-invariants/{ => protocol-suite}/base/BaseStorage.t.sol (99%) rename tests/enigma-dark-invariants/{ => protocol-suite}/base/BaseTest.t.sol (97%) create mode 100644 tests/enigma-dark-invariants/protocol-suite/base/ProtocolAssertions.t.sol rename tests/enigma-dark-invariants/{ => protocol-suite}/handlers/hub/HubConfiguratorHandler.t.sol (98%) create mode 100644 tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol rename tests/enigma-dark-invariants/{ => protocol-suite}/handlers/interfaces/IHubHandler.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/handlers/interfaces/ISpokeConfiguratorHandler.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/handlers/interfaces/ISpokeHandler.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/handlers/interfaces/ITreasurySpokeHandler.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/handlers/simulators/DonationAttackHandler.t.sol (100%) rename tests/enigma-dark-invariants/{handlers/simulators/PriceFeeSimulatorHandler.t.sol => protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol} (90%) rename tests/enigma-dark-invariants/{ => protocol-suite}/handlers/spoke/SpokeConfiguratorHandler.t.sol (98%) rename tests/enigma-dark-invariants/{ => protocol-suite}/handlers/spoke/SpokeHandler.t.sol (99%) rename tests/enigma-dark-invariants/{ => protocol-suite}/handlers/spoke/TreasurySpokeHandler.t.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/hooks/DefaultBeforeAfterHooks.t.sol (88%) rename tests/enigma-dark-invariants/{ => protocol-suite}/hooks/HookAggregator.t.sol (74%) rename tests/enigma-dark-invariants/{ => protocol-suite}/invariants/HubInvariants.t.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/invariants/SpokeInvariants.t.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/replays/ReplayTest_1.t.sol (98%) rename tests/enigma-dark-invariants/{ => protocol-suite}/replays/ReplayTest_2.t.sol (98%) rename tests/enigma-dark-invariants/{ => protocol-suite}/replays/ReplayTest_3.t.sol (98%) rename tests/enigma-dark-invariants/{ => protocol-suite}/specs/InvariantsSpec.t.sol (100%) rename tests/enigma-dark-invariants/{ => protocol-suite}/specs/PostconditionsSpec.t.sol (100%) rename tests/enigma-dark-invariants/{utils => shared}/mocks/MockPriceFeedSimulator.sol (100%) rename tests/enigma-dark-invariants/{ => shared}/remote/DOCKERFILE (100%) rename tests/enigma-dark-invariants/{ => shared}/utils/Actor.sol (100%) create mode 100644 tests/enigma-dark-invariants/shared/utils/ActorsUtils.sol rename tests/enigma-dark-invariants/{ => shared}/utils/CREATE3.sol (100%) rename tests/enigma-dark-invariants/{ => shared}/utils/DeployPermit2.sol (100%) create mode 100644 tests/enigma-dark-invariants/shared/utils/ErrorHandlers.sol rename tests/enigma-dark-invariants/{ => shared}/utils/PropertiesAsserts.sol (100%) rename tests/enigma-dark-invariants/{ => shared}/utils/PropertiesConstants.sol (100%) rename tests/enigma-dark-invariants/{ => shared}/utils/StdAsserts.sol (100%) diff --git a/tests/enigma-dark-invariants/handlers/simulators/.gitkeep b/tests/enigma-dark-invariants/handlers/simulators/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/enigma-dark-invariants/helpers/extended/.gitkeep b/tests/enigma-dark-invariants/helpers/extended/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/enigma-dark-invariants/helpers/fixtures/.gitkeep b/tests/enigma-dark-invariants/helpers/fixtures/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/enigma-dark-invariants/hub-suite/HandlerAggregator.t.sol b/tests/enigma-dark-invariants/hub-suite/HandlerAggregator.t.sol new file mode 100644 index 000000000..a653313fa --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/HandlerAggregator.t.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Handler contracts +import {HubHandler} from "./handlers/HubHandler.t.sol"; +import {HubConfiguratorHandler} from "./handlers/HubConfiguratorHandler.t.sol"; +import {DonationAttackHandler} from "./handlers/simulators/DonationAttackHandler.t.sol"; + +/// @notice Helper contract to aggregate all handler contracts for hub suite +abstract contract HandlerAggregator is HubHandler, HubConfiguratorHandler, DonationAttackHandler { + function _setUpHandlers() internal {} +} diff --git a/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol b/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol new file mode 100644 index 000000000..2f66d44d0 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {HubInvariants} from "./invariants/HubInvariants.t.sol"; + +/// @title Invariants +/// @notice Aggregator for hub invariants +abstract contract Invariants is HubInvariants { + function invariant_INV_HUB() public returns (bool) { + uint256 assetCount = hub.getAssetCount(); + + for (uint256 i; i < assetCount; i++) { + assert_INV_HUB_A(i); + assert_INV_HUB_B(i); + assert_INV_HUB_C(i); + assert_INV_HUB_EF(i); + assert_INV_HUB_GH(i); + assert_INV_HUB_I(i); + assert_INV_HUB_K(i); + assert_INV_HUB_L(i); + } + + return true; + } +} diff --git a/tests/enigma-dark-invariants/hub-suite/README.md b/tests/enigma-dark-invariants/hub-suite/README.md new file mode 100644 index 000000000..49e6f39c6 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/README.md @@ -0,0 +1,71 @@ +# Enigma Dark – Hub-Focused Fuzzing & Invariant Testing Suite + +A simplified handler-based invariant testing suite focused exclusively on the **Hub** component of the Aave v4 protocol. This suite performs deep stateful fuzzing against a single hub with actors simulating spokes, validating critical hub system properties through automated property checking and postcondition verification. + +## Overview + +The suite tests a simplified hub-centric deployment with: +- **1 Hub** with a single interest rate strategy +- **Multiple Actors** simulating spoke behavior (registered as spokes in the hub) +- **2 Base Assets** (USDC, WETH) to keep the asset surface simple and performant +- **Direct Hub Interactions** through handlers that expose hub functions +- **Hub Configuration Management** through the HubConfigurator handler + +All protocol actions are monitored by hooks that snapshot state and verify postconditions after each transaction, enabling detection of invariant violations and edge cases specific to hub operations. + +## Architecture + +### Core Components + +**Setup Layer** (`Setup.t.sol`, `base/`) +- Deploys a single Hub with a deterministic interest rate strategy +- Configures 2 base assets (USDC, WETH) for simplicity and performance +- Initializes multiple actors with spoke permissions registered on the hub +- Simplified configuration compared to the full multi-hub, multi-spoke suite + +**Handler Layer** (`handlers/`) +- `HubHandler` – hub liquidity operations (supply, draw, repay) through actor-spokes +- `HubConfiguratorHandler` – admin operations (spoke cap updates, risk parameter changes) +- Handlers expose hub interface to actors registered as spokes + +**Verification Layer** (`hooks/`, `invariants/`) +- Before/after hooks with state snapshots +- Global and handler-specific postcondition assertions +- Hub invariants (liquidity accounting, share calculations, interest accrual) + +**Utilities** (`utils/`) +- Constants and assertion helpers +- Random value generation for fuzzing inputs + +## How It Works + +1. **Fuzzer** generates random inputs and selects handler functions +2. **Handlers** execute hub operations through actor proxies (respects spoke roles) +3. **Hooks** capture snapshots of relevant hub state variables +4. **Postconditions** validate expected outcomes and state transitions +5. **Invariants** continuously checked across all hub states + +## Quick Start + +```bash +# Run fuzzing campaign with Medusa +make medusa-hub + +# Run with Echidna in assertion mode +make echidna-hub-assert + +# Run with Foundry's invariant testing +make foundry-hub-invariants +``` + +## Key Features + +- **Simplified hub-focused testing** for specific hub component validation +- **Actor-based spoke simulation** – no custom spoke deployments, just actors as spokes +- **Comprehensive postcondition checking** after every hub state transition +- **Performance optimized** – minimal asset and spoke count for faster fuzzing +- **Hub-specific invariants** validating liquidity accounting, interest calculations, and spoken cap constraints + +--- + +**Note:** This suite complements the full multi-hub, multi-spoke suite by providing deep, focused testing of hub core functionality in isolation. diff --git a/tests/enigma-dark-invariants/hub-suite/Setup.t.sol b/tests/enigma-dark-invariants/hub-suite/Setup.t.sol new file mode 100644 index 000000000..b1df77264 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/Setup.t.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Libraries +import {ActorsUtils} from "../shared/utils/ActorsUtils.sol"; +import {Constants} from "tests/Constants.sol"; +import {Roles} from "src/libraries/types/Roles.sol"; +import {Actor} from "../shared/utils/Actor.sol"; +import "forge-std/console.sol"; + +// Interfaces +import {ISpoke} from "src/spoke/Spoke.sol"; +import {IHub} from "src/hub/Hub.sol"; + +// Test Contracts +import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; + +// Contracts +import {BaseTest} from "./base/BaseTest.t.sol"; +import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; +import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; +import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; +import {Hub} from "src/hub/Hub.sol"; +import {TreasurySpoke} from "src/spoke/TreasurySpoke.sol"; +import {Spoke} from "src/spoke/Spoke.sol"; +import {HubConfigurator} from "src/hub/HubConfigurator.sol"; +import {SpokeConfigurator} from "src/spoke/SpokeConfigurator.sol"; + +/// @notice Setup contract for the invariant test Suite, inherited by Tester +contract Setup is BaseTest { + /// @notice Number of actors to deploy + function _setUp() internal { + // Deploy the suite assets + _deployAssets(); + + // Deploy protocol contracts and protocol actors + _deployProtocolCore(); + + // Deploy actors + _setUpActors(); + + // Configure the token list on the protocol + _configureTokenList(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSETS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy the suite assets + function _deployAssets() internal { + usdc = new TestnetERC20("USDC", "USDC", 6); + weth = new TestnetERC20("WETH", "WETH", 18); + wbtc = new TestnetERC20("WBTC", "WBTC", 8); + + baseAssets.push(AssetInfo({underlying: address(usdc), decimals: 6})); + baseAssets.push(AssetInfo({underlying: address(weth), decimals: 18})); + baseAssets.push(AssetInfo({underlying: address(wbtc), decimals: 8})); + + vm.label(address(usdc), "usdc"); + vm.label(address(weth), "weth"); + vm.label(address(wbtc), "wbtc"); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CORE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy protocol core contracts + function _deployProtocolCore() internal { + // Access manager + accessManager = new AccessManager(admin); + + // Hub 1 + hub = new Hub(address(accessManager)); + irStrategy = new AssetInterestRateStrategy(address(hub)); + + // Configurators + hubConfigurator = new HubConfigurator(admin); + _setUpConfiguratorRoles(); + + vm.label(address(accessManager), "accessManager"); + vm.label(address(hub), "hub"); + vm.label(address(hubConfigurator), "hubConfigurator"); + vm.label(address(irStrategy), "irStrategy"); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CONFIGS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _configureTokenList() internal { + // Configure hubs + _configureHubs(); + + // Configure spokes + _configureSpokes(); + } + + /// @notice Configure the hubs + function _configureHubs() internal { + // HUB 1 + bytes memory encodedIrData = abi.encode( + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: OPTIMAL_USAGE_RATIO_IR1, + baseVariableBorrowRate: BASE_VARIABLE_BORROW_RATE_IR1, + variableRateSlope1: VARIABLE_RATE_SLOPE_1_IR1, + variableRateSlope2: VARIABLE_RATE_SLOPE_2_IR1 + }) + ); + + // Add USDC + usdcAssetId = hub.addAsset(address(usdc), usdc.decimals(), address(this), address(irStrategy), encodedIrData); + hub.updateAssetConfig( + usdcAssetId, + IHub.AssetConfig({ + liquidityFee: 5_00, + feeReceiver: address(this), + irStrategy: address(irStrategy), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds.push(usdcAssetId); + + // Add WETH + wethAssetId = hub.addAsset(address(weth), weth.decimals(), address(this), address(irStrategy), encodedIrData); + hub.updateAssetConfig( + wethAssetId, + IHub.AssetConfig({ + liquidityFee: 10_00, + feeReceiver: address(this), + irStrategy: address(irStrategy), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds.push(wethAssetId); + + // Add WBTC + wbtcAssetId = hub.addAsset(address(wbtc), wbtc.decimals(), address(this), address(irStrategy), encodedIrData); + hub.updateAssetConfig( + wbtcAssetId, + IHub.AssetConfig({ + liquidityFee: 5_00, + feeReceiver: address(this), + irStrategy: address(irStrategy), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds.push(wbtcAssetId); + } + + function _configureSpokes() internal { + // Spoke 1: usdc, weth and wbtc + // Spoke 2: weth and wbtc + // Spoke 3: usdc, weth and wbtc + + // Add SPOKE 1 assets to hub + hub.addSpoke( + usdcAssetId, + address(actors[USER1]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false + }) + ); + hub.addSpoke( + wethAssetId, + address(actors[USER1]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false + }) + ); + hub.addSpoke( + wbtcAssetId, + address(actors[USER1]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false + }) + ); + + // Add SPOKE 2 assets to hub + hub.addSpoke( + wethAssetId, + address(actors[USER2]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false + }) + ); + hub.addSpoke( + wbtcAssetId, + address(actors[USER2]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false + }) + ); + + // Add SPOKE 3 assets to hub + hub.addSpoke( + usdcAssetId, + address(actors[USER3]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false + }) + ); + hub.addSpoke( + wethAssetId, + address(actors[USER3]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false + }) + ); + hub.addSpoke( + wbtcAssetId, + address(actors[USER3]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + active: true, + paused: false + }) + ); + } + + /// @notice Set up roles for the configurators + function _setUpConfiguratorRoles() internal virtual { + // Grant roles to configurators + accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); + + // Grant responsibilities to hubs + { + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = IHub.updateSpokeConfig.selector; + selectors[1] = IHub.setInterestRateData.selector; + accessManager.setTargetFunctionRole(address(hub), selectors, Roles.HUB_ADMIN_ROLE); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy protocol actors and initialize their balances + function _setUpActors() internal { + // Initialize the three actors of the fuzzers + address[] memory addresses = new address[](3); + addresses[0] = USER1; + addresses[1] = USER2; + addresses[2] = USER3; + + // Initialize the tokens array + address[] memory tokens = new address[](3); + tokens[0] = address(usdc); + tokens[1] = address(weth); + tokens[2] = address(wbtc); + + address[] memory contracts = new address[](1); + contracts[0] = address(hub); + + actorAddresses = ActorsUtils.setUpActors(addresses, tokens, contracts); + actors[USER1] = Actor(payable(actorAddresses[0])); + actors[USER2] = Actor(payable(actorAddresses[1])); + actors[USER3] = Actor(payable(actorAddresses[2])); + } +} diff --git a/tests/enigma-dark-invariants/hub-suite/SpecAggregator.t.sol b/tests/enigma-dark-invariants/hub-suite/SpecAggregator.t.sol new file mode 100644 index 000000000..1727f4ac4 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/SpecAggregator.t.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Test Contracts +import {HubInvariantsSpec} from "./specs/HubInvariantsSpec.t.sol"; +import {HubPostconditionsSpec} from "./specs/HubPostconditionsSpec.t.sol"; + +/// @title SpecAggregator +/// @notice Helper contract to aggregate all spec contracts, inherited in BaseHooks +/// @dev inherits HubInvariantsSpec, HubPostconditionsSpec +abstract contract SpecAggregator is HubInvariantsSpec, HubPostconditionsSpec {} diff --git a/tests/enigma-dark-invariants/hub-suite/Tester.t.sol b/tests/enigma-dark-invariants/hub-suite/Tester.t.sol new file mode 100644 index 000000000..0953a6bcc --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/Tester.t.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {HubInvariants} from "./invariants/HubInvariants.t.sol"; +import {Setup} from "./Setup.t.sol"; + +/// @title Tester +/// @notice Entry point for hub invariant testing +contract Tester is HubInvariants, Setup { + constructor() payable { + setUp(); + } + + function setUp() internal { + _setUp(); + } +} diff --git a/tests/enigma-dark-invariants/_config/echidna_config.yaml b/tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml similarity index 100% rename from tests/enigma-dark-invariants/_config/echidna_config.yaml rename to tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml diff --git a/tests/enigma-dark-invariants/_config/echidna_config_ci.yaml b/tests/enigma-dark-invariants/hub-suite/_config/echidna_config_ci.yaml similarity index 100% rename from tests/enigma-dark-invariants/_config/echidna_config_ci.yaml rename to tests/enigma-dark-invariants/hub-suite/_config/echidna_config_ci.yaml diff --git a/tests/enigma-dark-invariants/base/BaseHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/base/BaseHandler.t.sol similarity index 98% rename from tests/enigma-dark-invariants/base/BaseHandler.t.sol rename to tests/enigma-dark-invariants/hub-suite/base/BaseHandler.t.sol index f7da42eb0..551f54df8 100644 --- a/tests/enigma-dark-invariants/base/BaseHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/base/BaseHandler.t.sol @@ -8,7 +8,7 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {MockERC20} from 'tests/mocks/MockERC20.sol'; // Contracts -import {Actor} from "../utils/Actor.sol"; +import {Actor} from "../../shared/utils/Actor.sol"; import {HookAggregator} from "../hooks/HookAggregator.t.sol"; /// @title BaseHandler diff --git a/tests/enigma-dark-invariants/base/BaseHooks.t.sol b/tests/enigma-dark-invariants/hub-suite/base/BaseHooks.t.sol similarity index 100% rename from tests/enigma-dark-invariants/base/BaseHooks.t.sol rename to tests/enigma-dark-invariants/hub-suite/base/BaseHooks.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol b/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol new file mode 100644 index 000000000..a0e49b161 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Contracts +import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; +import {IHub} from "src/hub/Hub.sol"; +import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; +import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; +import {HubConfigurator} from "src/hub/HubConfigurator.sol"; +import {IAaveOracle} from "src/spoke/interfaces/IAaveOracle.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +/// @notice BaseStorage contract for hub-focused test suite +abstract contract BaseStorage { + /////////////////////////////////////////////////////////////////////////////////////////////// + // CONSTANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + uint256 constant MAX_TOKEN_AMOUNT = 1e29; + + uint256 constant ONE_DAY = 1 days; + uint256 constant ONE_MONTH = ONE_YEAR / 12; + uint256 constant ONE_YEAR = 365 days; + + uint256 internal constant NUMBER_OF_ACTORS = 3; + uint256 internal constant INITIAL_ETH_BALANCE = 1e26; + uint256 internal constant INITIAL_COLL_BALANCE = 1e21; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice The address of the admin, the tester itself + address internal admin = address(this); + + /// @notice Stores the actor during a handler call + Actor internal actor; + + /// @notice Mapping of fuzzer user addresses to actors + mapping(address => Actor) internal actors; + + /// @notice Array of all actor addresses (simulating spokes) + address[] internal actorAddresses; + + /// @notice The signature of the action that is being executed + bytes4 internal currentActionSignature; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSETS STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice The USDC token + TestnetERC20 internal usdc; + /// @notice The WETH token + TestnetERC20 internal weth; + /// @notice The WBTC token + TestnetERC20 internal wbtc; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Single Hub instance + IHub internal hub; + + /// @notice Interest rate strategy for the hub + AssetInterestRateStrategy internal irStrategy; + + /// @notice Hub Configurator + HubConfigurator internal hubConfigurator; + + /// @notice Access Manager + AccessManager internal accessManager; + + // PRICE FEEDS + address[] internal priceFeeds; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSET CONFIG // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Asset info struct + struct AssetInfo { + address underlying; + uint8 decimals; + } + + /// @notice Array of base assets for the hub + AssetInfo[] internal baseAssets; + + /// @notice Hub asset IDs + uint256 internal wethAssetId; + uint256 internal usdcAssetId; + uint256 internal wbtcAssetId; + uint256[] internal hubAssetIds; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE CONFIG // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Array of spoke addresses (actors acting as spokes) + address[] internal spokeAddresses; +} diff --git a/tests/enigma-dark-invariants/hub-suite/base/BaseTest.t.sol b/tests/enigma-dark-invariants/hub-suite/base/BaseTest.t.sol new file mode 100644 index 000000000..8961e08f3 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/base/BaseTest.t.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Libraries +import {Vm} from "forge-std/Base.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; +import "forge-std/console.sol"; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; +import {PropertiesConstants} from "../../shared/utils/PropertiesConstants.sol"; +import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; + +// Base +import {BaseStorage} from "./BaseStorage.t.sol"; + +/// @notice Base contract for all test contracts extends BaseStorage +/// @dev Provides setup modifier and cheat code setup +/// @dev inherits Storage, Testing constants assertions and utils needed for testing +abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdUtils { + bool internal IS_TEST = true; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTOR PROXY MECHANISM // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Actor proxy mechanism + modifier setup() virtual { + actor = actors[msg.sender]; + _; + delete actor; + } + + /// @dev Solves medusa backward time warp issue + modifier monotonicTimestamp() virtual { + /// @dev: Implement monotonic timestamp if needed + _; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CALLBACKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + receive() external payable {} + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CHEAT CODE SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. + address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + + /// @dev Virtual machine instance + Vm internal constant vm = Vm(VM_ADDRESS); + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS: RANDOM GETTERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Get a random actor proxy address + function _getRandomActor(uint256 _i) internal view returns (address) { + uint256 _actorIndex = _i % NUMBER_OF_ACTORS; + return actorAddresses[_actorIndex]; + } + + /// @notice Helper function to get a random base asset + function _getRandomBaseAsset(uint256 i) internal view returns (address) { + uint256 _assetIndex = i % baseAssets.length; + return baseAssets[_assetIndex].underlying; + } + + /// @notice Helper function to get random base asset full info + function _getRandomBaseAssetId(uint256 i) internal view returns (uint256) { + uint256 _assetIndex = i % hubAssetIds.length; + return hubAssetIds[_assetIndex]; + } + + /// @notice Helper function to get random base asset full info + function _getRandomBaseAssetFullInfo(uint256 i) internal view returns (AssetInfo memory) { + uint256 _assetIndex = i % baseAssets.length; + return baseAssets[_assetIndex]; + } + + /// @notice Helper function to get a random price feed address + function _getRandomPriceFeed(uint256 i) internal view returns (address) { + uint256 _priceFeedIndex = i % priceFeeds.length; + return priceFeeds[_priceFeedIndex]; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS: GETTERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Get a random address + function _makeAddr(string memory name) internal pure returns (address addr) { + uint256 privateKey = uint256(keccak256(abi.encodePacked(name))); + addr = vm.addr(privateKey); + } + + /// @notice Helper function to transfer tokens by actor + function _transferByActor(address token, address to, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor.proxy(token, abi.encodeWithSelector(IERC20.transfer.selector, to, amount)); + require(success, string(returnData)); + } + + /// @notice Helper function to approve tokens by actor + function _approveByActor(address token, address spender, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor.proxy(token, abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); + require(success, string(returnData)); + } +} diff --git a/tests/enigma-dark-invariants/base/ProtocolAssertions.t.sol b/tests/enigma-dark-invariants/hub-suite/base/ProtocolAssertions.t.sol similarity index 80% rename from tests/enigma-dark-invariants/base/ProtocolAssertions.t.sol rename to tests/enigma-dark-invariants/hub-suite/base/ProtocolAssertions.t.sol index 355bdfe56..18434085f 100644 --- a/tests/enigma-dark-invariants/base/ProtocolAssertions.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/base/ProtocolAssertions.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; // Base import {BaseTest} from "./BaseTest.t.sol"; -import {StdAsserts} from "../utils/StdAsserts.sol"; +import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; /// @title ProtocolAssertions /// @notice Helper contract for protocol specific assertions diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol new file mode 100644 index 000000000..6cbc6192a --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IHubConfiguratorHandler} from "./interfaces/IHubConfiguratorHandler.sol"; + +// Libraries +import "forge-std/console.sol"; + +// Test Contracts +import {Actor} from "../../shared/utils/Actor.sol"; +import {BaseHandler} from "../base/BaseHandler.t.sol"; + +/// @title HubConfiguratorHandler +/// @notice Handler test contract for a set of actions +contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j) external setup { + uint256 assetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + hubConfigurator.updateSpokeSupplyCap(address(hub), assetId, spoke, addCap); + } + + function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j) external setup { + uint256 assetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + hubConfigurator.updateSpokeDrawCap(address(hub), assetId, spoke, drawCap); + } + + function updateSpokeRiskPremiumCap(uint256 riskPremiumCap, uint8 i, uint8 j) external setup { + uint256 assetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + hubConfigurator.updateSpokeRiskPremiumCap(address(hub), assetId, spoke, riskPremiumCap); + } + + function updateSpokePaused(bool paused, uint8 i, uint8 j) external setup { + uint256 assetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + hubConfigurator.updateSpokePaused(address(hub), assetId, spoke, paused); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol new file mode 100644 index 000000000..b56e878b5 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IHub, IHubBase} from "src/hub/Hub.sol"; +import {IHubHandler} from "./interfaces/IHubHandler.sol"; + +// Libraries +import "forge-std/console.sol"; + +// Test Contracts +import {Actor} from "../../shared/utils/Actor.sol"; +import {BaseHandler} from "../base/BaseHandler.t.sol"; + +/// @title HubHandler +/// @notice Handler for hub-level operations through actor-spokes +contract HubHandler is BaseHandler, IHubHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function add(uint256 amount, uint8 i) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = + actor.proxy(address(hub), abi.encodeCall(IHubBase.add, (assetId, amount, address(actor)))); + + if (success) { + _after(); + } else { + revert("HubHandler: add failed"); + } + } + + function remove(uint256 amount, uint8 i) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = + actor.proxy(address(hub), abi.encodeCall(IHubBase.remove, (assetId, amount, address(actor)))); + + if (success) { + _after(); + } else { + revert("HubHandler: remove failed"); + } + } + + function draw(uint256 amount, uint8 i) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.draw, (assetId, amount, address(actor)))); + + if (success) { + _after(); + } else { + revert("HubHandler: draw failed"); + } + } + + function restore(uint256 drawnAmount, uint256 premiumAmount, IHubBase.PremiumDelta calldata premiumDelta, uint8 i) + external + setup + { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHubBase.restore, (assetId, drawnAmount, premiumAmount, premiumDelta, address(actor))) + ); + + if (success) { + _after(); + } else { + revert("HubHandler: restore failed"); + } + } + + function reportDeficit( + uint256 drawnAmount, + uint256 premiumAmount, + IHubBase.PremiumDelta calldata premiumDelta, + uint8 i + ) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = actor.proxy( + address(hub), abi.encodeCall(IHubBase.reportDeficit, (assetId, drawnAmount, premiumAmount, premiumDelta)) + ); + + if (success) { + _after(); + } else { + revert("HubHandler: reportDeficit failed"); + } + } + + function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + + _before(); + (success, returnData) = + actor.proxy(address(hub), abi.encodeCall(IHub.eliminateDeficit, (assetId, amount, spoke))); + + if (success) { + _after(); + } else { + revert("HubHandler: eliminateDeficit failed"); + } + } + + function refreshPremium(IHubBase.PremiumDelta calldata premiumDelta, uint8 i) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta))); + + if (success) { + _after(); + } else { + revert("HubHandler: refreshPremium failed"); + } + } + + function payFeeShares(uint256 shares, uint8 i) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.payFeeShares, (assetId, shares))); + if (success) { + _after(); + } else { + revert("HubHandler: payFeeShares failed"); + } + } + + function transferShares(uint256 shares, uint8 i, uint8 j) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + address toSpoke = _getRandomActor(j); + + _before(); + (success, returnData) = + actor.proxy(address(hub), abi.encodeCall(IHub.transferShares, (assetId, shares, toSpoke))); + + if (success) { + _after(); + } else { + revert("HubHandler: transferShares failed"); + } + } + + function sweep(uint256 amount, uint8 i) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHub.sweep, (assetId, amount))); + if (success) { + _after(); + } else { + revert("HubHandler: sweep failed"); + } + } + + function reclaim(uint256 amount, uint8 i) external setup { + bool success; + bytes memory returnData; + uint256 assetId = _getRandomBaseAssetId(i); + _before(); + (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHub.reclaim, (assetId, amount))); + if (success) { + _after(); + } else { + revert("HubHandler: reclaim failed"); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/handlers/interfaces/IHubConfiguratorHandler.sol b/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/handlers/interfaces/IHubConfiguratorHandler.sol rename to tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol new file mode 100644 index 000000000..6499bc04d --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title IHubHandler +/// @notice Interface for hub handler actions +interface IHubHandler { +// TODO +} diff --git a/tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol similarity index 75% rename from tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol rename to tests/enigma-dark-invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol index 7ad8a09a6..3ee317cf9 100644 --- a/tests/enigma-dark-invariants/handlers/hub/HubHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol @@ -1,31 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -// Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {IHubHandler} from "../interfaces/IHubHandler.sol"; - -// Libraries -import "forge-std/console.sol"; - // Test Contracts -import {Actor} from "../../utils/Actor.sol"; import {BaseHandler} from "../../base/BaseHandler.t.sol"; +import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; -/// @title HubHandler +/// @title DonationAttackHandler /// @notice Handler test contract for a set of actions -contract HubHandler is BaseHandler, IHubHandler { +contract DonationAttackHandler is BaseHandler { /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// - /* - - E.g. num of active pools - uint256 public activePools; - - */ - /////////////////////////////////////////////////////////////////////////////////////////////// // ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -34,10 +20,11 @@ contract HubHandler is BaseHandler, IHubHandler { // OWNER ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - // TODO: - // Reinvestment controller: - // sweep - // reclaim + function donateUnderlyingToHub(uint256 amount, uint8 i) external { + address underlying = _getRandomBaseAsset(i); + + TestnetERC20(underlying).mint(address(hub), amount); + } /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // diff --git a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol new file mode 100644 index 000000000..d359ce0ed --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Libraries +import {MathUtils} from "src/libraries/math/MathUtils.sol"; +import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; +import "forge-std/console.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; +import {PropertiesConstants} from "../../shared/utils/PropertiesConstants.sol"; +import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; + +// Interfaces +import {IHub} from "src/hub/interfaces/IHub.sol"; +import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; + +// Contracts +import {BaseHooks} from "../base/BaseHooks.t.sol"; + +/// @title DefaultBeforeAfterHooks +/// @notice Helper contract for before and after hooks, state variable caching and postconditions +/// @dev This contract is inherited by handlers +abstract contract DefaultBeforeAfterHooks is BaseHooks { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct AssetVars { + uint256 drawnIndex; + uint256 totalAssets; + uint256 totalShares; + uint256 drawn; + uint256 premium; + uint256 liquidity; + uint256 deficit; + uint256 swept; + uint256 lastUpdateTimestamp; + uint256 drawnRate; + } + + struct SpokeDataVars { + uint256 addedShares; + uint256 drawnShares; + uint256 premiumShares; + uint256 premiumOffset; + uint256 realizedPremium; + uint256 deficit; + uint256 drawn; + uint256 premium; + } + + struct DefaultVars { + mapping(uint256 assetId => AssetVars) assetVars; + mapping(uint256 assetId => mapping(address spoke => SpokeDataVars)) spokeDataVars; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Default variables before and after + DefaultVars defaultVarsBefore; + DefaultVars defaultVarsAfter; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Default hooks setup + function _setUpDefaultHooks() internal {} + + /// @notice Helper to initialize storage arrays of default vars + function _setUpDefaultVars(DefaultVars storage _defaultVars) internal {} + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _defaultHooksBefore() internal { + // Asset values + _setAssetValues(defaultVarsBefore); + // Spoke data values + _setSpokeDataValues(defaultVarsBefore); + } + + function _defaultHooksAfter() internal { + // Asset values + _setAssetValues(defaultVarsAfter); + // Spoke data values + _setSpokeDataValues(defaultVarsAfter); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _setAssetValues(DefaultVars storage _defaultVars) internal { + uint256 assetCount = hub.getAssetCount(); + for (uint256 j; j < assetCount; j++) { + _defaultVars.assetVars[j].drawnIndex = hub.getAssetDrawnIndex(j); + _defaultVars.assetVars[j].totalAssets = hub.getAddedAssets(j); + _defaultVars.assetVars[j].totalShares = hub.getAddedShares(j); + (_defaultVars.assetVars[j].drawn, _defaultVars.assetVars[j].premium) = hub.getAssetOwed(j); + _defaultVars.assetVars[j].lastUpdateTimestamp = hub.getAsset(j).lastUpdateTimestamp; + _defaultVars.assetVars[j].drawnRate = hub.getAssetDrawnRate(j); + _defaultVars.assetVars[j].liquidity = hub.getAssetLiquidity(j); + _defaultVars.assetVars[j].deficit = hub.getAssetDeficit(j); + _defaultVars.assetVars[j].swept = hub.getAssetSwept(j); + } + } + + function _setSpokeDataValues(DefaultVars storage _defaultVars) internal { + uint256 assetCount = hub.getAssetCount(); + + for (uint256 i; i < assetCount; i++) { + for (uint256 j; j < NUMBER_OF_ACTORS; j++) { + address spoke = actorAddresses[j]; + + _defaultVars.spokeDataVars[i][spoke].addedShares = hub.getSpokeAddedShares(i, spoke); + _defaultVars.spokeDataVars[i][spoke].drawnShares = hub.getSpokeDrawnShares(i, spoke); + ( + _defaultVars.spokeDataVars[i][spoke].premiumShares, + _defaultVars.spokeDataVars[i][spoke].premiumOffset, + _defaultVars.spokeDataVars[i][spoke].realizedPremium + ) = hub.getSpokePremiumData(i, spoke); + _defaultVars.spokeDataVars[i][spoke].deficit = hub.getSpokeDeficit(i, spoke); + (_defaultVars.spokeDataVars[i][spoke].drawn, _defaultVars.spokeDataVars[i][spoke].premium) = + hub.getSpokeOwed(i, spoke); + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POST CONDITIONS: HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_GPOST_HUB_A(uint256 assetId) internal { + assertGe( + defaultVarsAfter.assetVars[assetId].drawnIndex, defaultVarsBefore.assetVars[assetId].drawnIndex, GPOST_HUB_A + ); + } + + function assert_GPOST_HUB_B(uint256 assetId) internal { + assertFullMulGe( // TODO review test_replay_1_supply + defaultVarsAfter.assetVars[assetId].totalAssets, + defaultVarsBefore.assetVars[assetId].totalShares, + defaultVarsBefore.assetVars[assetId].totalAssets, + defaultVarsAfter.assetVars[assetId].totalShares, + GPOST_HUB_B + ); + } + + function assert_GPOST_HUB_C(uint256 assetId) internal { + // Read the cached signature of the current action + bytes4 signature = currentActionSignature; + if (true) { + // TODO check which signatures need to be checked + assertEq( + hub.getAssetDrawnRate(assetId), + IAssetInterestRateStrategy(irStrategy) + .calculateInterestRate( + assetId, + hub.getAssetLiquidity(assetId), + defaultVarsAfter.assetVars[assetId].drawn, + hub.getAssetDeficit(assetId), + hub.getAssetSwept(assetId) + ), + GPOST_HUB_C + ); + } + } + + function assert_GPOST_HUB_D(uint256 assetId) internal { + assertLe(defaultVarsAfter.assetVars[assetId].lastUpdateTimestamp, block.timestamp, GPOST_HUB_D); + } + + function assert_GPOST_HUB_EF(uint256 assetId, address spoke) internal { + // Get the spoke config + IHub.SpokeConfig memory spokeConfig = hub.getSpokeConfig(assetId, spoke); + (, uint8 decimals) = hub.getAssetUnderlyingAndDecimals(assetId); + + // GPOST_HUB_E + if (defaultVarsAfter.assetVars[assetId].totalAssets > defaultVarsBefore.assetVars[assetId].totalAssets) { + if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.assetVars[assetId].totalAssets, + spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_E + ); + } + } + + // GPOST_HUB_F + if (defaultVarsAfter.assetVars[assetId].drawn > defaultVarsBefore.assetVars[assetId].drawn) { + if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.assetVars[assetId].drawn, + spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_F + ); + } + } + } + + function assert_GPOST_HUB_G(uint256 assetId) internal { + assertGe( + defaultVarsAfter.assetVars[assetId].lastUpdateTimestamp, + defaultVarsBefore.assetVars[assetId].lastUpdateTimestamp, + GPOST_HUB_G + ); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + function _cacheCurrentActionSignature() internal { + currentActionSignature = bytes4(msg.sig); + } +} diff --git a/tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol new file mode 100644 index 000000000..8839751d5 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Hook Contracts +import {DefaultBeforeAfterHooks} from "./DefaultBeforeAfterHooks.t.sol"; + +// Utils +import {ErrorHandlers} from "../../shared/utils/ErrorHandlers.sol"; +import "forge-std/console.sol"; + +/// @title HookAggregator +/// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler +abstract contract HookAggregator is DefaultBeforeAfterHooks { + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Initializer for the hooks + function _setUpHooks() internal { + _setUpDefaultHooks(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Before hook for the handlers + function _before() internal { + _defaultHooksBefore(); + } + + /// @notice After hook for the handlers + function _after() internal { + _defaultHooksAfter(); + + // POST-CONDITIONS + _checkPostConditions(); + + // Reset the state + _resetState(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POSTCONDITION CHECKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Postconditions for the handlers + function _checkPostConditions() internal { + // Store the message signature to avoid losing it inside the checkPostConditions call context + _cacheCurrentActionSignature(); + + try this.checkPostConditions() {} + catch (bytes memory returnData) { + ErrorHandlers.handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); + } + } + + /// @dev postconditions checks entrypoint, should be self-called + function checkPostConditions() external { + uint256 assetCount = hub.getAssetCount(); + for (uint256 i; i < assetCount; i++) { + assert_GPOST_HUB_A(i); + assert_GPOST_HUB_B(i); + assert_GPOST_HUB_C(i); + assert_GPOST_HUB_D(i); + assert_GPOST_HUB_G(i); + + for (uint256 j; j < NUMBER_OF_ACTORS; j++) { + address spoke = actorAddresses[j]; + assert_GPOST_HUB_EF(i, spoke); + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Resets the state of the handlers + function _resetState() internal { + delete currentActionSignature; + } +} diff --git a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol new file mode 100644 index 000000000..5e1bb08f6 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Libraries +import "forge-std/console.sol"; + +// Interfaces +import {IHub} from "src/hub/interfaces/IHub.sol"; +import {IERC20} from "src/dependencies/openzeppelin/IERC20.sol"; + +// Contracts +import {HandlerAggregator} from "../HandlerAggregator.t.sol"; + +/// @title HubInvariants +/// @notice Implements Hub Invariants for the protocol +/// @dev Inherits HandlerAggregator to check actions in assertion testing mode +abstract contract HubInvariants is HandlerAggregator { + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + function assert_INV_HUB_A(uint256 assetId) internal { + uint256 assets = hub.getAddedAssets(assetId); + + if (assets == 0) { + assertEq(hub.getAddedShares(assetId), 0, INV_HUB_A2); + } + } + + function assert_INV_HUB_B(uint256 assetId) internal { + // Sum per-spoke values + uint256 spokeCount = NUMBER_OF_ACTORS; + uint256 sumDebt; + + for (uint256 i; i < spokeCount; i++) { + (uint256 d, uint256 p) = hub.getSpokeOwed(assetId, actorAddresses[i]); + sumDebt += d + p; + } + + uint256 assetTotal = hub.getAssetTotalOwed(assetId); // drawn + premium + assertGe(sumDebt, assetTotal, INV_HUB_B); // TODO review test case test_replay_2_INV_HUB_B + } + + function assert_INV_HUB_C(uint256 assetId) internal { + // Sum per-spoke values + uint256 spokeCount = NUMBER_OF_ACTORS; + + uint256 sumDrawnShares; + uint256 sumPremDrawnShares; + uint256 sumPremOffset; + uint256 sumPremRealized; + + for (uint256 i; i < spokeCount; i++) { + address spoke = actorAddresses[i]; + sumDrawnShares += hub.getSpokeDrawnShares(assetId, spoke); + (uint256 premiumDrawnShares, uint256 premiumOffset, uint256 realizedPremium) = + hub.getSpokePremiumData(assetId, spoke); + sumPremDrawnShares += premiumDrawnShares; + sumPremOffset += premiumOffset; + sumPremRealized += realizedPremium; + } + + // Asset totals + IHub.Asset memory a = hub.getAsset(assetId); + + // Checks + assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); + assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); + assertEq(sumPremOffset, a.premiumOffset, INV_HUB_C); + assertEq(sumPremRealized, a.realizedPremium, INV_HUB_C); + } + + function assert_INV_HUB_EF(uint256 assetId) internal { + // Total amounts + uint256 totalSuppliedAssets = hub.getAddedAssets(assetId); + uint256 convertedAssets = hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)); + + IHub.Asset memory asset = hub.getAsset(assetId); + uint256 totalDebt = hub.getAssetTotalOwed(assetId); + + // Checks + assertApproxEqAbs( // TODO review test_replay_3_setUsingAsCollateral + totalSuppliedAssets, + convertedAssets, + hub.previewRemoveByShares(assetId, 1), + INV_HUB_E + ); + + assertEq(totalSuppliedAssets, asset.liquidity + totalDebt + asset.deficit + asset.swept, INV_HUB_F); + } + + function assert_INV_HUB_GH(uint256 assetId) internal { + uint256 spokeCount = NUMBER_OF_ACTORS; + + // Sum per-spoke values + uint256 totalAddedAssets; + uint256 totalAddedShares; + for (uint256 i; i < spokeCount; i++) { + totalAddedAssets += hub.getSpokeAddedAssets(assetId, actorAddresses[i]); + totalAddedShares += hub.getSpokeAddedShares(assetId, actorAddresses[i]); + } + + // TODO take into account the burned interest from virtual shared -> _calculateBurntInterest from Base.t.sol + // Checks + assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); + assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); + } + + function assert_INV_HUB_I(uint256 assetId) internal { + // Get underlying from assetId + (address underlying,) = hub.getAssetUnderlyingAndDecimals(assetId); + + // Query values + uint256 liquidity = hub.getAssetLiquidity(assetId); + uint256 swept = hub.getAssetSwept(assetId); + uint256 underlyingBalance = IERC20(underlying).balanceOf(address(hub)); + + // Checks + assertGe(underlyingBalance + swept, liquidity, INV_HUB_I); + } + + function assert_INV_HUB_K(uint256 assetId) internal { + /// @dev TODO for this check to be meaningful, strategy configuration operations have to be integrated + IHub.AssetConfig memory assetConfig = hub.getAssetConfig(assetId); + + // Checks + assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); + } + + function assert_INV_HUB_L(uint256 assetId) internal { + (uint256 premiumShares, uint256 premiumOffset,) = hub.getAssetPremiumData(assetId); + + assertGe(hub.previewRestoreByShares(assetId, premiumShares), premiumOffset, INV_HUB_L); + } +} diff --git a/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol new file mode 100644 index 000000000..abf7ac8df --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {InvariantsSpec} from "../../protocol-suite/specs/InvariantsSpec.t.sol"; + +/// @title InvariantsSpec +/// @notice Invariants specification for the protocol +/// @dev Contains pseudo code and description for the invariant properties in the protocol +abstract contract HubInvariantsSpec is InvariantsSpec { + /*///////////////////////////////////////////////////////////////////////////////////////////// + // PROPERTY TYPES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// - INVARIANTS (INV): + /// - Properties that should always hold true in the system. + /// - Implemented in the /invariants folder. + + /////////////////////////////////////////////////////////////////////////////////////////////*/ + /////////////////////////////////////////////////////////////////////////////////////////////// + // TODO + // - Spoke/Asset cannot have non-zero assets and zero shares in add or draw sides."; + // - 4626 roundtrip on preview methods + + } diff --git a/tests/enigma-dark-invariants/hub-suite/specs/HubPostconditionsSpec.t.sol b/tests/enigma-dark-invariants/hub-suite/specs/HubPostconditionsSpec.t.sol new file mode 100644 index 000000000..1f5bb7fab --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/specs/HubPostconditionsSpec.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {PostconditionsSpec} from "../../protocol-suite/specs/PostconditionsSpec.t.sol"; + +/// @title PostconditionsSpec +/// @notice Postcoditions specification for the protocol +/// @dev Contains pseudo code and description for the postcondition properties in the protocol +abstract contract HubPostconditionsSpec is PostconditionsSpec { + /*///////////////////////////////////////////////////////////////////////////////////////////// + // PROPERTY TYPES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// - POSTCONDITIONS: + /// - Properties that should hold true after an action is executed. + /// - Implemented in the /hooks and /handlers folders. + /// - There are two types of POSTCONDITIONS: + /// - GLOBAL POSTCONDITIONS (GPOST): + /// - Properties that should always hold true after any action is executed. + /// - Checked in the `_checkPostConditions` function within the HookAggregator contract. + /// - HANDLER-SPECIFIC POSTCONDITIONS (HSPOST): + /// - Properties that should hold true after a specific action is executed in a specific context. + /// - Implemented within each handler function, under the HANDLER-SPECIFIC POSTCONDITIONS section. + + /////////////////////////////////////////////////////////////////////////////////////////////*/ + + } diff --git a/tests/enigma-dark-invariants/hub-suite/utils/PropertiesConstants.sol b/tests/enigma-dark-invariants/hub-suite/utils/PropertiesConstants.sol new file mode 100644 index 000000000..ff4154b5f --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/utils/PropertiesConstants.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Vm} from "forge-std/Base.sol"; + +/// @notice Testing constants and properties for hub-focused invariant suite +abstract contract PropertiesConstants { + /// @dev Cheat code address + address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + + /// @dev Virtual machine instance + Vm internal constant vm = Vm(VM_ADDRESS); + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB PROPERTY STRINGS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Hub Invariants + string internal constant HUB_INV_LIQUIDITY = "HUB_INV: Liquidity accounting mismatch"; + string internal constant HUB_INV_SHARES = "HUB_INV: Share calculation error"; + string internal constant HUB_INV_INTEREST = "HUB_INV: Interest accrual mismatch"; + string internal constant HUB_INV_DEFICIT = "HUB_INV: Deficit tracking error"; + + // Hub Postconditions + string internal constant HUB_POST_SUPPLY = "HUB_POST: Supply state invalid"; + string internal constant HUB_POST_DRAW = "HUB_POST: Draw state invalid"; + string internal constant HUB_POST_REPAY = "HUB_POST: Repay state invalid"; + string internal constant HUB_POST_CAP = "HUB_POST: Cap constraint violated"; +} diff --git a/tests/enigma-dark-invariants/hub-suite/utils/StdAsserts.sol b/tests/enigma-dark-invariants/hub-suite/utils/StdAsserts.sol new file mode 100644 index 000000000..a6d548432 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/utils/StdAsserts.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @notice Standard assertions for the test suite +abstract contract StdAsserts { + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSERTION HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Assert that a value is true + function assertTrue(bool condition, string memory errorMessage) internal pure { + require(condition, errorMessage); + } + + /// @notice Assert that a value is false + function assertFalse(bool condition, string memory errorMessage) internal pure { + require(!condition, errorMessage); + } + + /// @notice Assert two values are equal + function assertEqual(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a == b, errorMessage); + } + + /// @notice Assert a is less than or equal to b + function assertLe(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a <= b, errorMessage); + } + + /// @notice Assert a is greater than or equal to b + function assertGe(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a >= b, errorMessage); + } + + /// @notice Assert a is less than b + function assertLt(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a < b, errorMessage); + } + + /// @notice Assert a is greater than b + function assertGt(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a > b, errorMessage); + } + + /// @notice Assert two addresses are equal + function assertEq(address a, address b, string memory errorMessage) internal pure { + require(a == b, errorMessage); + } +} diff --git a/tests/enigma-dark-invariants/HandlerAggregator.t.sol b/tests/enigma-dark-invariants/protocol-suite/HandlerAggregator.t.sol similarity index 78% rename from tests/enigma-dark-invariants/HandlerAggregator.t.sol rename to tests/enigma-dark-invariants/protocol-suite/HandlerAggregator.t.sol index 673ca4d06..654270a9c 100644 --- a/tests/enigma-dark-invariants/HandlerAggregator.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/HandlerAggregator.t.sol @@ -2,24 +2,22 @@ pragma solidity ^0.8.19; // Handler contracts -import {HubHandler} from "./handlers/hub/HubHandler.t.sol"; import {SpokeHandler} from "./handlers/spoke/SpokeHandler.t.sol"; import {TreasurySpokeHandler} from "./handlers/spoke/TreasurySpokeHandler.t.sol"; import {HubConfiguratorHandler} from "./handlers/hub/HubConfiguratorHandler.t.sol"; import {SpokeConfiguratorHandler} from "./handlers/spoke/SpokeConfiguratorHandler.t.sol"; // Simulator contracts -import {PriceFeeSimulatorHandler} from "./handlers/simulators/PriceFeeSimulatorHandler.t.sol"; +import {PriceFeedSimulatorHandler} from "./handlers/simulators/PriceFeedSimulatorHandler.t.sol"; import {DonationAttackHandler} from "./handlers/simulators/DonationAttackHandler.t.sol"; /// @notice Helper contract to aggregate all handler contracts, inherited in BaseInvariants abstract contract HandlerAggregator is - HubHandler, // Main handlers - SpokeHandler, + SpokeHandler, // Main handlers TreasurySpokeHandler, HubConfiguratorHandler, // Configurators SpokeConfiguratorHandler, - PriceFeeSimulatorHandler, // Simulators + PriceFeedSimulatorHandler, // Simulators DonationAttackHandler { /// @notice Helper function in case any handler requires additional setup diff --git a/tests/enigma-dark-invariants/Invariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol similarity index 100% rename from tests/enigma-dark-invariants/Invariants.t.sol rename to tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol diff --git a/tests/enigma-dark-invariants/README.md b/tests/enigma-dark-invariants/protocol-suite/README.md similarity index 100% rename from tests/enigma-dark-invariants/README.md rename to tests/enigma-dark-invariants/protocol-suite/README.md diff --git a/tests/enigma-dark-invariants/Setup.t.sol b/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol similarity index 91% rename from tests/enigma-dark-invariants/Setup.t.sol rename to tests/enigma-dark-invariants/protocol-suite/Setup.t.sol index 438a209ad..c696966b7 100644 --- a/tests/enigma-dark-invariants/Setup.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.19; // Libraries -import {CREATE3} from "./utils/CREATE3.sol"; +import {CREATE3} from "../shared/utils/CREATE3.sol"; +import {ActorsUtils} from "../shared/utils/ActorsUtils.sol"; import {Constants} from "tests/Constants.sol"; import {Roles} from "src/libraries/types/Roles.sol"; import "forge-std/console.sol"; @@ -14,9 +15,9 @@ import {ITreasurySpoke} from "src/spoke/TreasurySpoke.sol"; import {IHub} from "src/hub/Hub.sol"; // Test Contracts -import {Actor} from "./utils/Actor.sol"; +import {Actor} from "../shared/utils/Actor.sol"; import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; -import {MockPriceFeedSimulator} from "tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol"; +import {MockPriceFeedSimulator} from "../shared/mocks/MockPriceFeedSimulator.sol"; // Contracts import {BaseTest} from "./base/BaseTest.t.sol"; @@ -152,6 +153,11 @@ contract Setup is BaseTest { return address(proxy); } + function _deployMockPriceFeed(ISpoke spoke, uint256 price) internal returns (address) { + AaveOracle oracle = AaveOracle(spoke.ORACLE()); + return address(new MockPriceFeedSimulator(oracle.DECIMALS(), oracle.DESCRIPTION(), price)); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // CONFIGS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -508,11 +514,6 @@ contract Setup is BaseTest { } } - function _deployMockPriceFeed(ISpoke spoke, uint256 price) internal returns (address) { - AaveOracle oracle = AaveOracle(spoke.ORACLE()); - return address(new MockPriceFeedSimulator(oracle.DECIMALS(), oracle.DESCRIPTION(), price)); - } - /////////////////////////////////////////////////////////////////////////////////////////////// // ACTORS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -531,48 +532,14 @@ contract Setup is BaseTest { tokens[1] = address(weth); address[] memory contracts = new address[](4); - contracts[0] = address(spoke1); - contracts[1] = address(spoke2); - contracts[2] = address(hub1); - contracts[3] = address(hub2); - - for (uint256 i; i < NUMBER_OF_ACTORS; i++) { - // Deploy actor proxies and approve system contracts - address _actor = _setUpActor(addresses[i], tokens, contracts); - //vm.label(address(_actor), string.concat("actor", vm.toString(i + 1))); - - // Mint initial balances to actors - for (uint256 j = 0; j < tokens.length; j++) { - TestnetERC20 _token = TestnetERC20(tokens[j]); - uint256 decimals = _token.decimals(); - _token.mint(_actor, INITIAL_BALANCE * 10 ** decimals); - } - actorAddresses.push(_actor); - } - } - - /// @notice Deploy an actor proxy contract for a user address - /// @param userAddress Address of the user - /// @param tokens Array of token addresses - /// @param contracts Array of contract addresses to aprove tokens to - /// @return actorAddress Address of the deployed actor - function _setUpActor(address userAddress, address[] memory tokens, address[] memory contracts) - internal - returns (address actorAddress) - { - bool success; - Actor _actor = new Actor(tokens, contracts); - actors[userAddress] = _actor; - (success,) = address(_actor).call{value: INITIAL_ETH_BALANCE}(""); - assert(success); - actorAddress = address(_actor); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // LOGGING // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _logSetup() internal { - // Log the setup + contracts[0] = address(hub1); + contracts[1] = address(hub2); + contracts[2] = address(spoke1); + contracts[3] = address(spoke2); + + actorAddresses = ActorsUtils.setUpActors(addresses, tokens, contracts); + actors[USER1] = Actor(payable(actorAddresses[0])); + actors[USER2] = Actor(payable(actorAddresses[1])); + actors[USER3] = Actor(payable(actorAddresses[2])); } } diff --git a/tests/enigma-dark-invariants/SpecAggregator.t.sol b/tests/enigma-dark-invariants/protocol-suite/SpecAggregator.t.sol similarity index 100% rename from tests/enigma-dark-invariants/SpecAggregator.t.sol rename to tests/enigma-dark-invariants/protocol-suite/SpecAggregator.t.sol diff --git a/tests/enigma-dark-invariants/Tester.t.sol b/tests/enigma-dark-invariants/protocol-suite/Tester.t.sol similarity index 100% rename from tests/enigma-dark-invariants/Tester.t.sol rename to tests/enigma-dark-invariants/protocol-suite/Tester.t.sol diff --git a/tests/enigma-dark-invariants/TesterFoundry.t.sol b/tests/enigma-dark-invariants/protocol-suite/TesterFoundry.t.sol similarity index 100% rename from tests/enigma-dark-invariants/TesterFoundry.t.sol rename to tests/enigma-dark-invariants/protocol-suite/TesterFoundry.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml b/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml new file mode 100644 index 000000000..9ffa894fb --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml @@ -0,0 +1,61 @@ +#codeSize max code size for deployed contratcs (default 24576, per EIP-170) +codeSize: 224576 + +#whether ot not to use the multi-abi mode of testing +#it’s not working for us, see: https://github.com/crytic/echidna/issues/547 +#multi-abi: true + +#balanceAddr is default balance for addresses +balanceAddr: 0x1000000000000000000000000 +#balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) +balanceContract: 0x1000000000000000000000000000000000000000000000000 + +#testLimit is the number of test sequences to run +testLimit: 20000000 + +#seqLen defines how many transactions are in a test sequence +seqLen: 300 + +#shrinkLimit determines how much effort is spent shrinking failing sequences +shrinkLimit: 2500 + +#propMaxGas defines gas cost at which a property fails +propMaxGas: 1000000000 + +#testMaxGas is a gas limit; does not cause failure, but terminates sequence +testMaxGas: 1000000000 + +# list of methods to filter +filterFunctions: [ "Tester.checkPostConditions()" ] +# by default, blacklist methods in filterFunctions +#filterBlacklist: false + +prefix: "invariant_" + +#stopOnFail makes echidna terminate as soon as any property fails and has been shrunk +stopOnFail: false + +#coverage controls coverage guided testing +coverage: true + +# list of file formats to save coverage reports in; default is all possible formats +coverageFormats: [ "html" ] + +#directory to save the corpus; by default is disabled +corpusDir: "tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus" +# constants for corpus mutations (for experimentation only) +#mutConsts: [100, 1, 1] + +#remappings +cryticArgs: [ "--compile-libraries=(LiquidationLogic,0xf01)" ] + +deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] + +# maximum value to send to payable functions +maxValue: 1e+30 # 100000000000 eth + +#quiet produces (much) less verbose output +quiet: false + +# concurrent workers +workers: 10 diff --git a/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config_ci.yaml b/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config_ci.yaml new file mode 100644 index 000000000..056c9bc11 --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config_ci.yaml @@ -0,0 +1,61 @@ +#codeSize max code size for deployed contratcs (default 24576, per EIP-170) +codeSize: 224576 + +#whether ot not to use the multi-abi mode of testing +#it’s not working for us, see: https://github.com/crytic/echidna/issues/547 +#multi-abi: true + +#balanceAddr is default balance for addresses +balanceAddr: 0x1000000000000000000000000 +#balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) +balanceContract: 0x1000000000000000000000000000000000000000000000000 + +#testLimit is the number of test sequences to run +testLimit: 80000 + +#seqLen defines how many transactions are in a test sequence +seqLen: 300 + +#shrinkLimit determines how much effort is spent shrinking failing sequences +shrinkLimit: 1500 + +#propMaxGas defines gas cost at which a property fails +propMaxGas: 1000000000 + +#testMaxGas is a gas limit; does not cause failure, but terminates sequence +testMaxGas: 1000000000 + +# list of methods to filter +filterFunctions: [ "Tester.checkPostConditions()" ] +# by default, blacklist methods in filterFunctions +#filterBlacklist: false + +prefix: "invariant_" + +#stopOnFail makes echidna terminate as soon as any property fails and has been shrunk +stopOnFail: false + +#coverage controls coverage guided testing +coverage: true + +# list of file formats to save coverage reports in; default is all possible formats +coverageFormats: [ "html" ] + +#directory to save the corpus; by default is disabled +corpusDir: "corpus" +# constants for corpus mutations (for experimentation only) +#mutConsts: [100, 1, 1] + +#remappings +cryticArgs: [ "--ignore-compile", "--compile-libraries=(LiquidationLogic,0xf01)" ] + +deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] + +# maximum value to send to payable functions +maxValue: 1e+30 # 100000000000 eth + +#quiet produces (much) less verbose output +quiet: false + +# concurrent workers +workers: 10 diff --git a/tests/enigma-dark-invariants/protocol-suite/base/BaseHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/base/BaseHandler.t.sol new file mode 100644 index 000000000..42fe39868 --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/base/BaseHandler.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Interfaces +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +// Libraries +import {MockERC20} from "tests/mocks/MockERC20.sol"; + +// Contracts +import {Actor} from "../../shared/utils/Actor.sol"; +import {HookAggregator} from "../hooks/HookAggregator.t.sol"; + +/// @title BaseHandler +/// @notice Contains common logic for all handlers +/// @dev inherits all suite assertions since per action assertions are implmenteds in the handlers +contract BaseHandler is HookAggregator { + /////////////////////////////////////////////////////////////////////////////////////////////// + // MODIFIERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SHARED VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Helper function to randomize a uint256 seed with a string salt + function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(seed, salt))); + } + + /// @notice Helper function to get a random value + function _getRandomValue(uint256 modulus) internal view returns (uint256) { + uint256 randomNumber = uint256(keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender))); + return randomNumber % modulus; // Adjust the modulus to the desired range + } + + /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor + function _approve(address token, Actor actor_, address spender, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor_.proxy(token, abi.encodeWithSelector(0x095ea7b3, spender, amount)); + require(success, string(returnData)); + } + + /// @notice Helper function to safely approve an amount of tokens to a spender + + function _approve(address token, address owner, address spender, uint256 amount) internal { + vm.prank(owner); + _safeApprove(token, spender, 0); + vm.prank(owner); + _safeApprove(token, spender, amount); + } + + /// @notice Helper function to safely approve an amount of tokens to a spender + /// @dev This function is used to revert on failed approvals + function _safeApprove(address token, address spender, uint256 amount) internal { + (bool success, bytes memory retdata) = + token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); + assert(success); + if (retdata.length > 0) assert(abi.decode(retdata, (bool))); + } + + /// @notice Helper function to mint an amount of tokens to an address + function _mint(address token, address receiver, uint256 amount) internal { + MockERC20(token).mint(receiver, amount); + } + + /// @notice Helper function to mint an amount of tokens to an address and approve them to a spender + /// @param token Address of the token to mint + /// @param owner Address of the new owner of the tokens + /// @param spender Address of the spender to approve the tokens to + /// @param amount Amount of tokens to mint and approve + function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { + _mint(token, owner, amount); + _approve(token, owner, spender, amount); + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/base/BaseHooks.t.sol b/tests/enigma-dark-invariants/protocol-suite/base/BaseHooks.t.sol new file mode 100644 index 000000000..68e4ef887 --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/base/BaseHooks.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Contracts +import {ProtocolAssertions} from "./ProtocolAssertions.t.sol"; + +// Test Contracts +import {SpecAggregator} from "../SpecAggregator.t.sol"; + +/// @title BaseHooks +/// @notice Contains common logic for all hooks +/// @dev inherits all suite assertions since per-action assertions are implemented in the handlers +/// @dev inherits SpecAggregator +contract BaseHooks is ProtocolAssertions, SpecAggregator { +/////////////////////////////////////////////////////////////////////////////////////////////// +// HELPERS // +/////////////////////////////////////////////////////////////////////////////////////////////// +} diff --git a/tests/enigma-dark-invariants/base/BaseStorage.t.sol b/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol similarity index 99% rename from tests/enigma-dark-invariants/base/BaseStorage.t.sol rename to tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol index e03563f31..e881f8a7d 100644 --- a/tests/enigma-dark-invariants/base/BaseStorage.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol @@ -17,7 +17,7 @@ import {SpokeConfigurator} from "src/spoke/SpokeConfigurator.sol"; // Test Contracts // Utils -import {Actor} from "../utils/Actor.sol"; +import {Actor} from "../../shared/utils/Actor.sol"; /// @notice BaseStorage contract for all test contracts, works in tandem with BaseTest abstract contract BaseStorage { diff --git a/tests/enigma-dark-invariants/base/BaseTest.t.sol b/tests/enigma-dark-invariants/protocol-suite/base/BaseTest.t.sol similarity index 97% rename from tests/enigma-dark-invariants/base/BaseTest.t.sol rename to tests/enigma-dark-invariants/protocol-suite/base/BaseTest.t.sol index 0606c3778..6dfd92ab5 100644 --- a/tests/enigma-dark-invariants/base/BaseTest.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/base/BaseTest.t.sol @@ -11,9 +11,9 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; // Utils -import {Actor} from "../utils/Actor.sol"; -import {PropertiesConstants} from "../utils/PropertiesConstants.sol"; -import {StdAsserts} from "../utils/StdAsserts.sol"; +import {Actor} from "../../shared/utils/Actor.sol"; +import {PropertiesConstants} from "../../shared/utils/PropertiesConstants.sol"; +import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; // Base import {BaseStorage} from "./BaseStorage.t.sol"; diff --git a/tests/enigma-dark-invariants/protocol-suite/base/ProtocolAssertions.t.sol b/tests/enigma-dark-invariants/protocol-suite/base/ProtocolAssertions.t.sol new file mode 100644 index 000000000..18434085f --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/base/ProtocolAssertions.t.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Base +import {BaseTest} from "./BaseTest.t.sol"; +import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; + +/// @title ProtocolAssertions +/// @notice Helper contract for protocol specific assertions +contract ProtocolAssertions is StdAsserts, BaseTest {} diff --git a/tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol similarity index 98% rename from tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol index 9044bb386..cbe45dbe7 100644 --- a/tests/enigma-dark-invariants/handlers/hub/HubConfiguratorHandler.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol @@ -9,7 +9,7 @@ import {IHubConfiguratorHandler} from "../interfaces/IHubConfiguratorHandler.sol import "forge-std/console.sol"; // Test Contracts -import {Actor} from "../../utils/Actor.sol"; +import {Actor} from "../../../shared/utils/Actor.sol"; import {BaseHandler} from "../../base/BaseHandler.t.sol"; /// @title HubConfiguratorHandler diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol new file mode 100644 index 000000000..d12003b5e --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title IHubConfiguratorHandler +/// @notice Interface for the HubConfiguratorHandler +interface IHubConfiguratorHandler {} diff --git a/tests/enigma-dark-invariants/handlers/interfaces/IHubHandler.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/handlers/interfaces/IHubHandler.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubHandler.sol diff --git a/tests/enigma-dark-invariants/handlers/interfaces/ISpokeConfiguratorHandler.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/handlers/interfaces/ISpokeConfiguratorHandler.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol diff --git a/tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/handlers/interfaces/ISpokeHandler.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol diff --git a/tests/enigma-dark-invariants/handlers/interfaces/ITreasurySpokeHandler.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/handlers/interfaces/ITreasurySpokeHandler.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol diff --git a/tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/handlers/simulators/DonationAttackHandler.t.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol diff --git a/tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol similarity index 90% rename from tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol index b0e7933f9..6fa378e95 100644 --- a/tests/enigma-dark-invariants/handlers/simulators/PriceFeeSimulatorHandler.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol @@ -3,13 +3,13 @@ pragma solidity ^0.8.19; // Test Contracts import {BaseHandler} from "../../base/BaseHandler.t.sol"; -import {MockPriceFeedSimulator} from "../../utils/mocks/MockPriceFeedSimulator.sol"; +import {MockPriceFeedSimulator} from "../../../shared/mocks/MockPriceFeedSimulator.sol"; import "forge-std/console.sol"; -/// @title PriceFeeSimulatorHandler +/// @title PriceFeedSimulatorHandler /// @notice Handler test contract for a set of actions -contract PriceFeeSimulatorHandler is BaseHandler { +contract PriceFeedSimulatorHandler is BaseHandler { /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol similarity index 98% rename from tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol index 25f8d2961..5a1215d79 100644 --- a/tests/enigma-dark-invariants/handlers/spoke/SpokeConfiguratorHandler.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol @@ -9,7 +9,7 @@ import {ISpokeConfiguratorHandler} from "../interfaces/ISpokeConfiguratorHandler import "forge-std/console.sol"; // Test Contracts -import {Actor} from "../../utils/Actor.sol"; +import {Actor} from "../../../shared/utils/Actor.sol"; import {BaseHandler} from "../../base/BaseHandler.t.sol"; /// @title SpokeConfiguratorHandler diff --git a/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol similarity index 99% rename from tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index d538f8b14..4f4254960 100644 --- a/tests/enigma-dark-invariants/handlers/spoke/SpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -9,7 +9,7 @@ import {ISpokeHandler} from "../interfaces/ISpokeHandler.sol"; import "forge-std/console.sol"; // Test Contracts -import {Actor} from "../../utils/Actor.sol"; +import {Actor} from "../../../shared/utils/Actor.sol"; import {BaseHandler} from "../../base/BaseHandler.t.sol"; // Contracts diff --git a/tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/handlers/spoke/TreasurySpokeHandler.t.sol rename to tests/enigma-dark-invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol diff --git a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol similarity index 88% rename from tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol rename to tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index 7d7c02050..c7d486b6f 100644 --- a/tests/enigma-dark-invariants/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -7,9 +7,9 @@ import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; import "forge-std/console.sol"; // Utils -import {Actor} from "../utils/Actor.sol"; -import {PropertiesConstants} from "../utils/PropertiesConstants.sol"; -import {StdAsserts} from "../utils/StdAsserts.sol"; +import {Actor} from "../../shared/utils/Actor.sol"; +import {PropertiesConstants} from "../../shared/utils/PropertiesConstants.sol"; +import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; // Interfaces import {ISpokeHandler} from "../handlers/interfaces/ISpokeHandler.sol"; @@ -188,38 +188,38 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { assertLe(defaultVarsAfter.assetVars[hubAddress][assetId].lastUpdateTimestamp, block.timestamp, GPOST_HUB_D); } - function assert_GPOST_HUB_EF(address hubAddress, uint256 assetId) internal { - /* // Get the spoke config - IHub.SpokeConfig memory spokeConfig = IHub(hubAddress).getSpokeConfig(assetId, spoke);// TODO pass spoke as parameter - (, uint8 decimals) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); - - // GPOST_HUB_E - if ( - defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets - > defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets - ) { - if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { - assertLe( - defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, - spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), - GPOST_HUB_E - ); - } - } + function assert_GPOST_HUB_EF(address hubAddress, uint256 assetId, address spoke) internal { + // Get the spoke config + IHub.SpokeConfig memory spokeConfig = IHub(hubAddress).getSpokeConfig(assetId, spoke); // TODO pass spoke as parameter + (, uint8 decimals) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); + + // GPOST_HUB_E + if ( + defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets + > defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets + ) { + if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, + spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_E + ); + } + } - // GPOST_HUB_F - if ( - defaultVarsAfter.assetVars[hubAddress][assetId].drawn - > defaultVarsBefore.assetVars[hubAddress][assetId].drawn - ) { - if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { - assertLe( - defaultVarsAfter.assetVars[hubAddress][assetId].drawn, - spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), - GPOST_HUB_F - ); - } - } */ + // GPOST_HUB_F + if ( + defaultVarsAfter.assetVars[hubAddress][assetId].drawn + > defaultVarsBefore.assetVars[hubAddress][assetId].drawn + ) { + if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.assetVars[hubAddress][assetId].drawn, + spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_F + ); + } + } } function assert_GPOST_HUB_G(address hubAddress, uint256 assetId) internal { diff --git a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol similarity index 74% rename from tests/enigma-dark-invariants/hooks/HookAggregator.t.sol rename to tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol index 4122822d3..d32926221 100644 --- a/tests/enigma-dark-invariants/hooks/HookAggregator.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -4,16 +4,13 @@ pragma solidity ^0.8.19; // Hook Contracts import {DefaultBeforeAfterHooks} from "./DefaultBeforeAfterHooks.t.sol"; +// Utils +import {ErrorHandlers} from "../../shared/utils/ErrorHandlers.sol"; import "forge-std/console.sol"; /// @title HookAggregator /// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler abstract contract HookAggregator is DefaultBeforeAfterHooks { - /// @dev Selector for Panic(uint256) as defined by Solidity - bytes4 internal constant _PANIC_SELECTOR = 0x4e487b71; - /// @dev Panic code for assertion failed (0x01) - uint256 internal constant _PANIC_ASSERTION_FAILED = 0x01; - /////////////////////////////////////////////////////////////////////////////////////////////// // SETUP // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -54,7 +51,7 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { try this.checkPostConditions() {} catch (bytes memory returnData) { - _handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); + ErrorHandlers.handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); } } @@ -78,7 +75,7 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { assert_GPOST_HUB_B(hubAddress, assetId); assert_GPOST_HUB_C(hubAddress, assetId); assert_GPOST_HUB_D(hubAddress, assetId); - assert_GPOST_HUB_EF(hubAddress, assetId); + assert_GPOST_HUB_EF(hubAddress, assetId, usersToCheck[i].spoke); assert_GPOST_HUB_G(hubAddress, assetId); } } @@ -117,37 +114,4 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { delete usersToCheck; delete currentActionSignature; } - - /// @notice Checks if a call failed due to an assertion error and propagates the error if found. - /// @param success Indicates whether the call was successful. - /// @param returnData The data returned from the call. - function _handleAssertionError( - bool success, - bytes memory returnData, - bool detectNonAssertionErrors, - string memory errorMessage - ) internal { - // Case 1: do nothing if success is true - if (success) return; - - // Case 2: detect Panic(0x01) "Assertion" errors - // Decode potential Panic(uint256) (selector + uint256 = 36 bytes) - if (returnData.length == 36) { - bytes4 selector; - uint256 code; - assembly { - selector := mload(add(returnData, 0x20)) - code := mload(add(returnData, 0x24)) - } - // Case 3: if Panic(0x01) "Assertion" -> assert(false), this propagates the assertion error to the Tester context - if (selector == _PANIC_SELECTOR && code == _PANIC_ASSERTION_FAILED) { - assert(false); - } - } - - // Case 3: detect non-assertion errors and assert with the error message - if (detectNonAssertionErrors) { - assertWithMsg(false, errorMessage); - } - } } diff --git a/tests/enigma-dark-invariants/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol similarity index 100% rename from tests/enigma-dark-invariants/invariants/HubInvariants.t.sol rename to tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol diff --git a/tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/SpokeInvariants.t.sol similarity index 100% rename from tests/enigma-dark-invariants/invariants/SpokeInvariants.t.sol rename to tests/enigma-dark-invariants/protocol-suite/invariants/SpokeInvariants.t.sol diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_1.t.sol similarity index 98% rename from tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol rename to tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_1.t.sol index a68593a88..0ed9d72cb 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_1.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_1.t.sol @@ -10,7 +10,7 @@ import {Invariants} from "../Invariants.t.sol"; import {Setup} from "../Setup.t.sol"; // Utils -import {Actor} from "../utils/Actor.sol"; +import {Actor} from "../../shared/utils/Actor.sol"; contract ReplayTest1 is Invariants, Setup { // Generated from Echidna reproducers diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_2.t.sol similarity index 98% rename from tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol rename to tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_2.t.sol index 547e1b234..37e3d3a80 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_2.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_2.t.sol @@ -10,7 +10,7 @@ import {Invariants} from "../Invariants.t.sol"; import {Setup} from "../Setup.t.sol"; // Utils -import {Actor} from "../utils/Actor.sol"; +import {Actor} from "../../shared/utils/Actor.sol"; contract ReplayTest2 is Invariants, Setup { // Generated from Echidna reproducers diff --git a/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol similarity index 98% rename from tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol rename to tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol index e7376133d..d6408faa4 100644 --- a/tests/enigma-dark-invariants/replays/ReplayTest_3.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol @@ -10,7 +10,7 @@ import {Invariants} from "../Invariants.t.sol"; import {Setup} from "../Setup.t.sol"; // Utils -import {Actor} from "../utils/Actor.sol"; +import {Actor} from "../../shared/utils/Actor.sol"; contract ReplayTest3 is Invariants, Setup { // Generated from Echidna reproducers diff --git a/tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol b/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol similarity index 100% rename from tests/enigma-dark-invariants/specs/InvariantsSpec.t.sol rename to tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol diff --git a/tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol b/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol similarity index 100% rename from tests/enigma-dark-invariants/specs/PostconditionsSpec.t.sol rename to tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol diff --git a/tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol b/tests/enigma-dark-invariants/shared/mocks/MockPriceFeedSimulator.sol similarity index 100% rename from tests/enigma-dark-invariants/utils/mocks/MockPriceFeedSimulator.sol rename to tests/enigma-dark-invariants/shared/mocks/MockPriceFeedSimulator.sol diff --git a/tests/enigma-dark-invariants/remote/DOCKERFILE b/tests/enigma-dark-invariants/shared/remote/DOCKERFILE similarity index 100% rename from tests/enigma-dark-invariants/remote/DOCKERFILE rename to tests/enigma-dark-invariants/shared/remote/DOCKERFILE diff --git a/tests/enigma-dark-invariants/utils/Actor.sol b/tests/enigma-dark-invariants/shared/utils/Actor.sol similarity index 100% rename from tests/enigma-dark-invariants/utils/Actor.sol rename to tests/enigma-dark-invariants/shared/utils/Actor.sol diff --git a/tests/enigma-dark-invariants/shared/utils/ActorsUtils.sol b/tests/enigma-dark-invariants/shared/utils/ActorsUtils.sol new file mode 100644 index 000000000..357b9e040 --- /dev/null +++ b/tests/enigma-dark-invariants/shared/utils/ActorsUtils.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Test Contracts +import {Actor} from "./Actor.sol"; +import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; + +library ActorsUtils { + uint256 internal constant INITIAL_ETH_BALANCE = 1e26; + uint256 constant INITIAL_BALANCE = 1e12; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy protocol actors and initialize their balances + function setUpActors(address[] memory addresses, address[] memory tokens, address[] memory contracts) + internal + returns (address[] memory actorAddresses) + { + actorAddresses = new address[](addresses.length); + + // Initialize the three actors of the fuzzers + for (uint256 i; i < addresses.length; i++) { + // Deploy actor proxies and approve system contracts + address _actor = setUpActor(tokens, contracts); + + // Mint initial balances to actors + for (uint256 j = 0; j < tokens.length; j++) { + TestnetERC20 _token = TestnetERC20(tokens[j]); + uint256 decimals = _token.decimals(); + _token.mint(_actor, INITIAL_BALANCE * 10 ** decimals); + } + actorAddresses[i] = _actor; + } + } + + /// @notice Deploy an actor proxy contract + /// @param tokens Array of token addresses + /// @param contracts Array of contract addresses to aprove tokens to + /// @return actorAddress Address of the deployed actor + function setUpActor(address[] memory tokens, address[] memory contracts) internal returns (address actorAddress) { + bool success; + Actor _actor = new Actor(tokens, contracts); + (success,) = address(_actor).call{value: INITIAL_ETH_BALANCE}(""); + assert(success); + actorAddress = address(_actor); + } +} diff --git a/tests/enigma-dark-invariants/utils/CREATE3.sol b/tests/enigma-dark-invariants/shared/utils/CREATE3.sol similarity index 100% rename from tests/enigma-dark-invariants/utils/CREATE3.sol rename to tests/enigma-dark-invariants/shared/utils/CREATE3.sol diff --git a/tests/enigma-dark-invariants/utils/DeployPermit2.sol b/tests/enigma-dark-invariants/shared/utils/DeployPermit2.sol similarity index 100% rename from tests/enigma-dark-invariants/utils/DeployPermit2.sol rename to tests/enigma-dark-invariants/shared/utils/DeployPermit2.sol diff --git a/tests/enigma-dark-invariants/shared/utils/ErrorHandlers.sol b/tests/enigma-dark-invariants/shared/utils/ErrorHandlers.sol new file mode 100644 index 000000000..fd73a54f6 --- /dev/null +++ b/tests/enigma-dark-invariants/shared/utils/ErrorHandlers.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title ErrorHandlers +/// @notice Library for handling errors in the test suite +library ErrorHandlers { + /// @dev Selector for Panic(uint256) as defined by Solidity + bytes4 internal constant _PANIC_SELECTOR = 0x4e487b71; + /// @dev Panic code for assertion failed (0x01) + uint256 internal constant _PANIC_ASSERTION_FAILED = 0x01; + + event AssertFail(string); + + /// @notice Checks if a call failed due to an assertion error and propagates the error if found. + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSERTION ERRORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @param success Indicates whether the call was successful. + /// @param returnData The data returned from the call. + function handleAssertionError( + bool success, + bytes memory returnData, + bool detectNonAssertionErrors, + string memory errorMessage + ) internal { + // Case 1: do nothing if success is true + if (success) return; + + // Case 2: detect Panic(0x01) "Assertion" errors + // Decode potential Panic(uint256) (selector + uint256 = 36 bytes) + if (returnData.length == 36) { + bytes4 selector; + uint256 code; + assembly { + selector := mload(add(returnData, 0x20)) + code := mload(add(returnData, 0x24)) + } + // Case 3: if Panic(0x01) "Assertion" -> assert(false), this propagates the assertion error to the Tester context + if (selector == _PANIC_SELECTOR && code == _PANIC_ASSERTION_FAILED) { + assert(false); + } + } + + // Case 3: detect non-assertion errors and assert with the error message + if (detectNonAssertionErrors) { + assertWithMsg(false, errorMessage); + } + } + + function assertWithMsg(bool b, string memory reason) internal { + if (!b) { + emit AssertFail(reason); + assert(false); + } + } +} diff --git a/tests/enigma-dark-invariants/utils/PropertiesAsserts.sol b/tests/enigma-dark-invariants/shared/utils/PropertiesAsserts.sol similarity index 100% rename from tests/enigma-dark-invariants/utils/PropertiesAsserts.sol rename to tests/enigma-dark-invariants/shared/utils/PropertiesAsserts.sol diff --git a/tests/enigma-dark-invariants/utils/PropertiesConstants.sol b/tests/enigma-dark-invariants/shared/utils/PropertiesConstants.sol similarity index 100% rename from tests/enigma-dark-invariants/utils/PropertiesConstants.sol rename to tests/enigma-dark-invariants/shared/utils/PropertiesConstants.sol diff --git a/tests/enigma-dark-invariants/utils/StdAsserts.sol b/tests/enigma-dark-invariants/shared/utils/StdAsserts.sol similarity index 100% rename from tests/enigma-dark-invariants/utils/StdAsserts.sol rename to tests/enigma-dark-invariants/shared/utils/StdAsserts.sol From fbde0fec770f9f563be6298a3966f6d8bf721b36 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Mon, 22 Dec 2025 03:01:09 +0100 Subject: [PATCH 020/106] chore: fix wrong reserveid invariants wrapper --- .../enigma-dark-invariants/protocol-suite/Invariants.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol index df29fa0d6..30e72269d 100644 --- a/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol @@ -18,9 +18,11 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { /////////////////////////////////////////////////////////////////////////////////////////////// function invariant_INV_HUB() public returns (bool) { + // Applied per hub for (uint256 i; i < hubAddresses.length; i++) { address hubAddress = hubAddresses[i]; + // Applied per asset of the hub uint256 assetCount = IHub(hubAddress).getAssetCount(); for (uint256 j; j < assetCount; j++) { assert_INV_HUB_A(hubAddress, j); @@ -42,14 +44,14 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { for (uint256 i; i < spokesAddresses.length; i++) { address spoke = spokesAddresses[i]; - // Applied per actor per spoke + // Applied per actor on the spoke for (uint256 j; j < actorAddresses.length; j++) { assert_INV_SP_D(spoke, actorAddresses[j]); } // Applied per reserve of the spoke for (uint256 j; j < spokeReserveIds[spoke].length; j++) { - uint256 reserveId = _getReserveId(spoke, j); + uint256 reserveId = spokeReserveIds[spoke][j]; assert_INV_SP_A(spoke, reserveId); assert_INV_SP_C(spoke, reserveId); From 05a0e1e4eec9dbd9f44de54ffb1dc0f5b577935c Mon Sep 17 00:00:00 2001 From: Elpacos Date: Mon, 22 Dec 2025 04:32:31 +0100 Subject: [PATCH 021/106] rebase --- .../hub-suite/Setup.t.sol | 25 +++++---- .../hub-suite/base/BaseStorage.t.sol | 2 + .../handlers/HubConfiguratorHandler.t.sol | 4 +- .../hub-suite/handlers/HubHandler.t.sol | 33 ++++++----- .../hooks/DefaultBeforeAfterHooks.t.sol | 16 +++--- .../hub-suite/invariants/HubInvariants.t.sol | 22 ++++---- .../protocol-suite/Setup.t.sol | 56 +++++++++++++------ .../protocol-suite/base/BaseTest.t.sol | 3 +- .../handlers/hub/HubConfiguratorHandler.t.sol | 4 +- .../handlers/spoke/SpokeHandler.t.sol | 3 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 6 +- .../invariants/HubInvariants.t.sol | 26 +++++---- .../protocol-suite/specs/InvariantsSpec.t.sol | 9 +-- 13 files changed, 121 insertions(+), 88 deletions(-) diff --git a/tests/enigma-dark-invariants/hub-suite/Setup.t.sol b/tests/enigma-dark-invariants/hub-suite/Setup.t.sol index b1df77264..60043c0ee 100644 --- a/tests/enigma-dark-invariants/hub-suite/Setup.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/Setup.t.sol @@ -9,7 +9,6 @@ import {Actor} from "../shared/utils/Actor.sol"; import "forge-std/console.sol"; // Interfaces -import {ISpoke} from "src/spoke/Spoke.sol"; import {IHub} from "src/hub/Hub.sol"; // Test Contracts @@ -21,10 +20,8 @@ import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; import {Hub} from "src/hub/Hub.sol"; -import {TreasurySpoke} from "src/spoke/TreasurySpoke.sol"; import {Spoke} from "src/spoke/Spoke.sol"; import {HubConfigurator} from "src/hub/HubConfigurator.sol"; -import {SpokeConfigurator} from "src/spoke/SpokeConfigurator.sol"; /// @notice Setup contract for the invariant test Suite, inherited by Tester contract Setup is BaseTest { @@ -122,6 +119,8 @@ contract Setup is BaseTest { new bytes(0) ); hubAssetIds.push(usdcAssetId); + assetIdToUnderlying[usdcAssetId] = address(usdc); + underlyingToAssetId[address(usdc)] = usdcAssetId; // Add WETH wethAssetId = hub.addAsset(address(weth), weth.decimals(), address(this), address(irStrategy), encodedIrData); @@ -136,6 +135,8 @@ contract Setup is BaseTest { new bytes(0) ); hubAssetIds.push(wethAssetId); + assetIdToUnderlying[wethAssetId] = address(weth); + underlyingToAssetId[address(weth)] = wethAssetId; // Add WBTC wbtcAssetId = hub.addAsset(address(wbtc), wbtc.decimals(), address(this), address(irStrategy), encodedIrData); @@ -150,6 +151,8 @@ contract Setup is BaseTest { new bytes(0) ); hubAssetIds.push(wbtcAssetId); + assetIdToUnderlying[wbtcAssetId] = address(wbtc); + underlyingToAssetId[address(wbtc)] = wbtcAssetId; } function _configureSpokes() internal { @@ -164,7 +167,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -175,7 +178,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -186,7 +189,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -199,7 +202,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -210,7 +213,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -223,7 +226,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -234,7 +237,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -245,7 +248,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) diff --git a/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol b/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol index a0e49b161..fb9d4d04a 100644 --- a/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol @@ -95,6 +95,8 @@ abstract contract BaseStorage { uint256 internal usdcAssetId; uint256 internal wbtcAssetId; uint256[] internal hubAssetIds; + mapping(uint256 => address) internal assetIdToUnderlying; + mapping(address => uint256) internal underlyingToAssetId; /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE CONFIG // diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol index 6cbc6192a..2ce002184 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol @@ -37,10 +37,10 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { hubConfigurator.updateSpokeDrawCap(address(hub), assetId, spoke, drawCap); } - function updateSpokeRiskPremiumCap(uint256 riskPremiumCap, uint8 i, uint8 j) external setup { + function updateSpokeRiskPremiumThreshold(uint256 riskPremiumThreshold, uint8 i, uint8 j) external setup { uint256 assetId = _getRandomBaseAssetId(i); address spoke = _getRandomActor(j); - hubConfigurator.updateSpokeRiskPremiumCap(address(hub), assetId, spoke, riskPremiumCap); + hubConfigurator.updateSpokeRiskPremiumThreshold(address(hub), assetId, spoke, riskPremiumThreshold); } function updateSpokePaused(bool paused, uint8 i, uint8 j) external setup { diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol index b56e878b5..d93f0f488 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol @@ -24,10 +24,12 @@ contract HubHandler is BaseHandler, IHubHandler { bool success; bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); + address underlying = assetIdToUnderlying[assetId]; _before(); - (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.add, (assetId, amount, address(actor)))); + vm.prank(address(actor)); + IERC20(underlying).transfer(address(hub), amount); + (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.add, (assetId, amount))); if (success) { _after(); @@ -58,7 +60,8 @@ contract HubHandler is BaseHandler, IHubHandler { uint256 assetId = _getRandomBaseAssetId(i); _before(); - (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.draw, (assetId, amount, address(actor)))); + (success, returnData) = + actor.proxy(address(hub), abi.encodeCall(IHubBase.draw, (assetId, amount, address(actor)))); if (success) { _after(); @@ -74,12 +77,13 @@ contract HubHandler is BaseHandler, IHubHandler { bool success; bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); + address underlying = assetIdToUnderlying[assetId]; _before(); - (success, returnData) = actor.proxy( - address(hub), - abi.encodeCall(IHubBase.restore, (assetId, drawnAmount, premiumAmount, premiumDelta, address(actor))) - ); + vm.prank(address(actor)); + IERC20(underlying).transfer(address(hub), drawnAmount + premiumAmount); + (success, returnData) = + actor.proxy(address(hub), abi.encodeCall(IHubBase.restore, (assetId, drawnAmount, premiumDelta))); if (success) { _after(); @@ -88,20 +92,14 @@ contract HubHandler is BaseHandler, IHubHandler { } } - function reportDeficit( - uint256 drawnAmount, - uint256 premiumAmount, - IHubBase.PremiumDelta calldata premiumDelta, - uint8 i - ) external setup { + function reportDeficit(uint256 drawnAmount, IHubBase.PremiumDelta calldata premiumDelta, uint8 i) external setup { bool success; bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); _before(); - (success, returnData) = actor.proxy( - address(hub), abi.encodeCall(IHubBase.reportDeficit, (assetId, drawnAmount, premiumAmount, premiumDelta)) - ); + (success, returnData) = + actor.proxy(address(hub), abi.encodeCall(IHubBase.reportDeficit, (assetId, drawnAmount, premiumDelta))); if (success) { _after(); @@ -133,7 +131,8 @@ contract HubHandler is BaseHandler, IHubHandler { uint256 assetId = _getRandomBaseAssetId(i); _before(); - (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta))); + (success, returnData) = + actor.proxy(address(hub), abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta))); if (success) { _after(); diff --git a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index d359ce0ed..0e60b7674 100644 --- a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -33,7 +33,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { uint256 drawn; uint256 premium; uint256 liquidity; - uint256 deficit; + uint256 deficitRay; uint256 swept; uint256 lastUpdateTimestamp; uint256 drawnRate; @@ -43,9 +43,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { uint256 addedShares; uint256 drawnShares; uint256 premiumShares; - uint256 premiumOffset; - uint256 realizedPremium; - uint256 deficit; + int256 premiumOffsetRay; + uint256 deficitRay; uint256 drawn; uint256 premium; } @@ -105,7 +104,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { _defaultVars.assetVars[j].lastUpdateTimestamp = hub.getAsset(j).lastUpdateTimestamp; _defaultVars.assetVars[j].drawnRate = hub.getAssetDrawnRate(j); _defaultVars.assetVars[j].liquidity = hub.getAssetLiquidity(j); - _defaultVars.assetVars[j].deficit = hub.getAssetDeficit(j); + _defaultVars.assetVars[j].deficitRay = hub.getAssetDeficitRay(j); _defaultVars.assetVars[j].swept = hub.getAssetSwept(j); } } @@ -121,10 +120,9 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { _defaultVars.spokeDataVars[i][spoke].drawnShares = hub.getSpokeDrawnShares(i, spoke); ( _defaultVars.spokeDataVars[i][spoke].premiumShares, - _defaultVars.spokeDataVars[i][spoke].premiumOffset, - _defaultVars.spokeDataVars[i][spoke].realizedPremium + _defaultVars.spokeDataVars[i][spoke].premiumOffsetRay ) = hub.getSpokePremiumData(i, spoke); - _defaultVars.spokeDataVars[i][spoke].deficit = hub.getSpokeDeficit(i, spoke); + _defaultVars.spokeDataVars[i][spoke].deficitRay = hub.getSpokeDeficitRay(i, spoke); (_defaultVars.spokeDataVars[i][spoke].drawn, _defaultVars.spokeDataVars[i][spoke].premium) = hub.getSpokeOwed(i, spoke); } @@ -163,7 +161,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { assetId, hub.getAssetLiquidity(assetId), defaultVarsAfter.assetVars[assetId].drawn, - hub.getAssetDeficit(assetId), + 0, // Unused in the interest rate calculation hub.getAssetSwept(assetId) ), GPOST_HUB_C diff --git a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol index 5e1bb08f6..655e5e31b 100644 --- a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol @@ -46,17 +46,14 @@ abstract contract HubInvariants is HandlerAggregator { uint256 sumDrawnShares; uint256 sumPremDrawnShares; - uint256 sumPremOffset; - uint256 sumPremRealized; + int256 sumPremOffsetRay; for (uint256 i; i < spokeCount; i++) { address spoke = actorAddresses[i]; sumDrawnShares += hub.getSpokeDrawnShares(assetId, spoke); - (uint256 premiumDrawnShares, uint256 premiumOffset, uint256 realizedPremium) = - hub.getSpokePremiumData(assetId, spoke); + (uint256 premiumDrawnShares, int256 premiumOffsetRay) = hub.getSpokePremiumData(assetId, spoke); sumPremDrawnShares += premiumDrawnShares; - sumPremOffset += premiumOffset; - sumPremRealized += realizedPremium; + sumPremOffsetRay += premiumOffsetRay; } // Asset totals @@ -65,8 +62,7 @@ abstract contract HubInvariants is HandlerAggregator { // Checks assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); - assertEq(sumPremOffset, a.premiumOffset, INV_HUB_C); - assertEq(sumPremRealized, a.realizedPremium, INV_HUB_C); + assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); } function assert_INV_HUB_EF(uint256 assetId) internal { @@ -85,7 +81,11 @@ abstract contract HubInvariants is HandlerAggregator { INV_HUB_E ); - assertEq(totalSuppliedAssets, asset.liquidity + totalDebt + asset.deficit + asset.swept, INV_HUB_F); + assertEq( + totalSuppliedAssets * 1e9, + asset.liquidity * 1e9 + totalDebt * 1e9 + asset.deficitRay + asset.swept * 1e9, + INV_HUB_F + ); } function assert_INV_HUB_GH(uint256 assetId) internal { @@ -127,8 +127,8 @@ abstract contract HubInvariants is HandlerAggregator { } function assert_INV_HUB_L(uint256 assetId) internal { - (uint256 premiumShares, uint256 premiumOffset,) = hub.getAssetPremiumData(assetId); + (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); - assertGe(hub.previewRestoreByShares(assetId, premiumShares), premiumOffset, INV_HUB_L); + assertGe(int256(hub.previewRestoreByShares(assetId, premiumShares) * 1e9), premiumOffsetRay * 1e9, INV_HUB_L); } } diff --git a/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol b/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol index c696966b7..dc0c77bb6 100644 --- a/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol @@ -263,24 +263,48 @@ contract Setup is BaseTest { spokeConfigurator.updateLiquidationTargetHealthFactor(address(spoke2), TARGET_HEALTH_FACTOR_SPOKE2); // Spoke 1 reserve configs - spokeInfo[spoke1].usdc.reserveConfig = - ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 30_00}); + spokeInfo[spoke1].usdc.reserveConfig = ISpoke.ReserveConfig({ + paused: false, + frozen: false, + borrowable: true, + liquidatable: true, + receiveSharesEnabled: true, + collateralRisk: 30_00 + }); spokeInfo[spoke1].usdc.dynReserveConfig = ISpoke.DynamicReserveConfig({collateralFactor: 90_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); - spokeInfo[spoke1].weth.reserveConfig = - ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 20_00}); + spokeInfo[spoke1].weth.reserveConfig = ISpoke.ReserveConfig({ + paused: false, + frozen: false, + borrowable: true, + liquidatable: true, + receiveSharesEnabled: true, + collateralRisk: 20_00 + }); spokeInfo[spoke1].weth.dynReserveConfig = ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 105_00, liquidationFee: 0}); // Spoke 2 reserve configs - spokeInfo[spoke2].weth.reserveConfig = - ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 10_00}); + spokeInfo[spoke2].weth.reserveConfig = ISpoke.ReserveConfig({ + paused: false, + frozen: false, + borrowable: true, + liquidatable: true, + receiveSharesEnabled: true, + collateralRisk: 10_00 + }); spokeInfo[spoke2].weth.dynReserveConfig = ISpoke.DynamicReserveConfig({collateralFactor: 70_00, maxLiquidationBonus: 105_00, liquidationFee: 0}); - spokeInfo[spoke2].usdc.reserveConfig = - ISpoke.ReserveConfig({paused: false, frozen: false, borrowable: true, collateralRisk: 15_00}); + spokeInfo[spoke2].usdc.reserveConfig = ISpoke.ReserveConfig({ + paused: false, + frozen: false, + borrowable: true, + liquidatable: true, + receiveSharesEnabled: true, + collateralRisk: 15_00 + }); spokeInfo[spoke2].usdc.dynReserveConfig = ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); @@ -399,7 +423,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -410,7 +434,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -421,7 +445,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -432,7 +456,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -445,7 +469,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -456,7 +480,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -467,7 +491,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) @@ -478,7 +502,7 @@ contract Setup is BaseTest { IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - riskPremiumCap: Constants.MAX_ALLOWED_RISK_PREMIUM_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, paused: false }) diff --git a/tests/enigma-dark-invariants/protocol-suite/base/BaseTest.t.sol b/tests/enigma-dark-invariants/protocol-suite/base/BaseTest.t.sol index 6dfd92ab5..b3386549b 100644 --- a/tests/enigma-dark-invariants/protocol-suite/base/BaseTest.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/base/BaseTest.t.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.19; import {Vm} from "forge-std/Base.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; import "forge-std/console.sol"; +import {Constants} from "tests/Constants.sol"; // Interfaces import {IERC20} from "forge-std/interfaces/IERC20.sol"; @@ -127,7 +128,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU function _isHealthy(address spoke, address user) internal view returns (bool) { return - ISpoke(spoke).getUserAccountData(user).healthFactor >= ISpoke(spoke).HEALTH_FACTOR_LIQUIDATION_THRESHOLD(); + ISpoke(spoke).getUserAccountData(user).healthFactor >= Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD; } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol index cbe45dbe7..dcabc6549 100644 --- a/tests/enigma-dark-invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol @@ -39,11 +39,11 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { hubConfigurator.updateSpokeDrawCap(hub, assetId, spoke, drawCap); } - function updateSpokeRiskPremiumCap(uint256 riskPremiumCap, uint8 i, uint8 j, uint8 k) external setup { + function updateSpokeRiskPremiumThreshold(uint256 riskPremiumThreshold, uint8 i, uint8 j, uint8 k) external setup { address hub = _getRandomHub(i); uint256 assetId = _getRandomHubAssetId(hub, j); address spoke = _getRandomSpoke(k); - hubConfigurator.updateSpokeRiskPremiumCap(hub, assetId, spoke, riskPremiumCap); + hubConfigurator.updateSpokeRiskPremiumThreshold(hub, assetId, spoke, riskPremiumThreshold); } function updateSpokePaused(bool paused, uint8 i, uint8 j, uint8 k) external setup { diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index 4f4254960..655178dfe 100644 --- a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -6,6 +6,7 @@ import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; import {ISpokeHandler} from "../interfaces/ISpokeHandler.sol"; // Libraries +import {Constants} from "tests/Constants.sol"; import "forge-std/console.sol"; // Test Contracts @@ -193,7 +194,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { HSPOST_SP_LIQ_A ); - if (totalDebtValueBefore < ISpoke(spoke).DUST_LIQUIDATION_THRESHOLD()) { + if (totalDebtValueBefore < Constants.DUST_LIQUIDATION_THRESHOLD) { assertEq( defaultVarsAfter.userVars[spoke][debtReserveId][_getRandomActor(i)].totalDebt, 0, HSPOST_SP_LIQ_C ); diff --git a/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index c7d486b6f..a25651a5b 100644 --- a/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -176,7 +176,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { assetId, IHub(hubAddress).getAssetLiquidity(assetId), defaultVarsAfter.assetVars[hubAddress][assetId].drawn, - IHub(hubAddress).getAssetDeficit(assetId), + 0,// Unused in the interest rate calculation IHub(hubAddress).getAssetSwept(assetId) ), GPOST_HUB_C @@ -257,9 +257,9 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function assert_GPOST_SP_E(address spoke, uint256 reserveId, address user) internal { // latest reserve key - uint16 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; + uint24 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; // user-stored key - uint16 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; + uint24 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; // Read the cached signature of the current action bytes4 signature = currentActionSignature; diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol index e8c149226..24c995f5b 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -47,17 +47,14 @@ abstract contract HubInvariants is HandlerAggregator { uint256 sumDrawnShares; uint256 sumPremDrawnShares; - uint256 sumPremOffset; - uint256 sumPremRealized; + int256 sumPremOffsetRay; for (uint256 i; i < spokeCount; i++) { address spoke = spokesAddresses[i]; sumDrawnShares += IHub(hubAddress).getSpokeDrawnShares(assetId, spoke); - (uint256 premiumDrawnShares, uint256 premiumOffset, uint256 realizedPremium) = - IHub(hubAddress).getSpokePremiumData(assetId, spoke); + (uint256 premiumDrawnShares, int256 premiumOffsetRay) = IHub(hubAddress).getSpokePremiumData(assetId, spoke); sumPremDrawnShares += premiumDrawnShares; - sumPremOffset += premiumOffset; - sumPremRealized += realizedPremium; + sumPremOffsetRay += premiumOffsetRay; } // Asset totals @@ -66,8 +63,7 @@ abstract contract HubInvariants is HandlerAggregator { // Checks assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); - assertEq(sumPremOffset, a.premiumOffset, INV_HUB_C); - assertEq(sumPremRealized, a.realizedPremium, INV_HUB_C); + assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); } function assert_INV_HUB_EF(address hubAddress, uint256 assetId) internal { @@ -87,7 +83,11 @@ abstract contract HubInvariants is HandlerAggregator { INV_HUB_E ); - assertEq(totalSuppliedAssets, asset.liquidity + totalDebt + asset.deficit + asset.swept, INV_HUB_F); + assertEq( + totalSuppliedAssets * 1e9, + asset.liquidity * 1e9 + totalDebt * 1e9 + asset.deficitRay + asset.swept * 1e9, + INV_HUB_F + ); } function assert_INV_HUB_GH(address hubAddress, uint256 assetId) internal { @@ -129,8 +129,12 @@ abstract contract HubInvariants is HandlerAggregator { } function assert_INV_HUB_L(address hubAddress, uint256 assetId) internal { - (uint256 premiumShares, uint256 premiumOffset,) = IHub(hubAddress).getAssetPremiumData(assetId); + (uint256 premiumShares, int256 premiumOffsetRay) = IHub(hubAddress).getAssetPremiumData(assetId); - assertGe(IHub(hubAddress).previewRestoreByShares(assetId, premiumShares), premiumOffset, INV_HUB_L); + assertGe( + int256(IHub(hubAddress).previewRestoreByShares(assetId, premiumShares) * 1e9), + premiumOffsetRay * 1e9, + INV_HUB_L + ); } } diff --git a/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol b/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol index 12098263d..ddad75cce 100644 --- a/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -26,7 +26,7 @@ abstract contract InvariantsSpec { "INV_HUB_A: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset"; string constant INV_HUB_C = - "INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffset/realized] of individual (spoke/user) should match the corresponding value of the asset on the Hub"; + "INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub"; string constant INV_HUB_E = "INV_HUB_E: hub.getTotalSuppliedAssets and hub.getAssetSuppliedAmount should match at any time, should not be off by more than 1 share worth of assets due to division precision loss"; @@ -35,7 +35,7 @@ abstract contract InvariantsSpec { "INV_HUB_E2: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + totalDebt + deficit + swept"; string constant INV_HUB_G = - "INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke)";// TODO should this be greater or equal? + "INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke)"; // TODO should this be greater or equal? string constant INV_HUB_H = "INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes"; @@ -47,7 +47,8 @@ abstract contract InvariantsSpec { string constant INV_HUB_K = "INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset"; - string constant INV_HUB_L = "INV_HUB_L: Asset.premiumShares.toDrawnAssetsUp() >= Asset.premiumOffset"; + string constant INV_HUB_L = + "INV_HUB_L: Asset.premiumShares.toDrawnAssetsUp() * 1e9 >= Asset.premiumOffsetRay * 1e9"; string constant INV_HUB_M = "INV_HUB_M: Liquidity growth (ie accrued interest) >= AccruedFees (even with 100.00% liquidity fee)"; // TODO @@ -55,7 +56,7 @@ abstract contract InvariantsSpec { string constant INV_HUB_N = "INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares"; // TODO - string constant INV_HUB_O = "INV_HUB_O: sum of deficit across spokes for a given asset == total asset deficit"; // TODO explore how to track deficit for each spoke + string constant INV_HUB_O = "INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay"; // TODO explore how to track deficitRay for each spoke /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // From dce578e6e01e4b65f5258039779e06b5c7f64a63 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Wed, 24 Dec 2025 00:24:17 +0100 Subject: [PATCH 022/106] chore: coverage restore, refreshpremium, reportdeficit --- Makefile | 34 ++-- medusa.json => medusa.hub.json | 4 +- medusa.protocol.json | 97 ++++++++++ .../hub-suite/Tester.t.sol | 4 +- .../hub-suite/_config/echidna_config.yaml | 4 +- .../hub-suite/base/BaseStorage.t.sol | 2 + .../hub-suite/handlers/HubHandler.t.sol | 93 ++++++---- .../handlers/interfaces/IHubHandler.sol | 12 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 33 +++- .../hub-suite/hooks/HookAggregator.t.sol | 8 +- .../hub-suite/invariants/HubInvariants.t.sol | 26 ++- .../hub-suite/replays/ReplayTest_1.t.sol | 81 +++++++++ .../hub-suite/replays/ReplayTest_2.t.sol | 171 ++++++++++++++++++ .../hub-suite/replays/ReplayTest_3.t.sol | 82 +++++++++ .../hub-suite/replays/ReplayTest_4.t.sol | 93 ++++++++++ .../_config/echidna_config.yaml | 4 +- .../protocol-suite/base/BaseStorage.t.sol | 1 - .../invariants/HubInvariants.t.sol | 10 +- .../protocol-suite/specs/InvariantsSpec.t.sol | 5 +- .../shared/utils/PropertiesConstants.sol | 1 - 20 files changed, 690 insertions(+), 75 deletions(-) rename medusa.json => medusa.hub.json (93%) create mode 100644 medusa.protocol.json create mode 100644 tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_1.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_3.t.sol create mode 100644 tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_4.t.sol diff --git a/Makefile b/Makefile index 7267a43d3..cba17b16f 100644 --- a/Makefile +++ b/Makefile @@ -32,25 +32,35 @@ coverage : # Echidna echidna: - echidna tests/enigma-dark-invariants/Tester.t.sol --contract Tester --config ./tests/enigma-dark-invariants/_config/echidna_config.yaml + echidna tests/enigma-dark-invariants/protocol-suite/Tester.t.sol --contract Tester --config ./tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml echidna-assert: - echidna tests/enigma-dark-invariants/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/enigma-dark-invariants/_config/echidna_config.yaml + echidna tests/enigma-dark-invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml echidna-explore: - echidna tests/enigma-dark-invariants/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/enigma-dark-invariants/_config/echidna_config.yaml -rm-echidna-corpus: - rm -rf tests/enigma-dark-invariants/_corpus/echidna + echidna tests/enigma-dark-invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml + +echidna-hub: + echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml --server 3000 +echidna-hub-assert: + echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml --server 3000 +echidna-hub-explore: + echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml # Medusa medusa: - medusa fuzz --config ./medusa.json -rm-medusa-corpus: - rm -rf tests/enigma-dark-invariants/_corpus/medusa - + medusa fuzz --config ./medusa.protocol.json +medusa-hub: + medusa fuzz --config ./medusa.hub.json + foundry-invariants: forge test --mc TesterFoundry -vvv -# Echidna Results +# Results runes-echidna: - runes convert ./tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/enigma-dark-invariants/replays + runes convert ./tests/enigma-dark-invariants/protocol-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/enigma-dark-invariants/protocol-suite/replays runes-medusa: - runes convert ./tests/enigma-dark-invariants/_corpus/medusa/ --output ./tests/enigma-dark-invariants/replays \ No newline at end of file + runes convert ./tests/enigma-dark-invariants/protocol-suite/_corpus/medusa/ --output ./tests/enigma-dark-invariants/protocol-suite/replays + +runes-echidna-hub: + runes convert ./tests/enigma-dark-invariants/hub-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/enigma-dark-invariants/hub-suite/replays +runes-medusa-hub: + runes convert ./tests/enigma-dark-invariants/hub-suite/_corpus/medusa/ --output ./tests/enigma-dark-invariants/hub-suite/replays \ No newline at end of file diff --git a/medusa.json b/medusa.hub.json similarity index 93% rename from medusa.json rename to medusa.hub.json index a784f3667..b969efe22 100644 --- a/medusa.json +++ b/medusa.hub.json @@ -5,7 +5,7 @@ "timeout": 0, "testLimit": 0, "callSequenceLength": 100, - "corpusDirectory": "tests/enigma-dark-invariants/_corpus/medusa", + "corpusDirectory": "tests/enigma-dark-invariants/hub-suite/_corpus/medusa", "coverageEnabled": true, "deploymentOrder": [ "Tester" @@ -80,7 +80,7 @@ "compilation": { "platform": "crytic-compile", "platformConfig": { - "target": "tests/enigma-dark-invariants/Tester.t.sol", + "target": "tests/enigma-dark-invariants/hub-suite/Tester.t.sol", "solcVersion": "", "exportDirectory": "", "args": [ diff --git a/medusa.protocol.json b/medusa.protocol.json new file mode 100644 index 000000000..05a083bab --- /dev/null +++ b/medusa.protocol.json @@ -0,0 +1,97 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "callSequenceLength": 100, + "corpusDirectory": "tests/enigma-dark-invariants/protocol-suite/_corpus/medusa", + "coverageEnabled": true, + "deploymentOrder": [ + "Tester" + ], + "targetContracts": [ + "Tester" + ], + "targetContractsBalances": [ + "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" + ], + "predeployedContracts": { + "LiquidationLogic": "0xf01" + }, + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 12500000000, + "transactionGasLimit": 1250000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "assertionModes": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "fuzz_", + "invariant_" + ] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": [ + "optimize_" + ] + }, + "excludeFunctionSignatures": [ + "Tester.checkPostConditions()" + ] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": "tests/enigma-dark-invariants/protocol-suite/Tester.t.sol", + "solcVersion": "", + "exportDirectory": "", + "args": [ + "--solc-remaps", + "forge-std/=../../../lib/forge-std/src/", + "--compile-libraries=(LiquidationLogic,0xf01)" + ] + } + }, + "logging": { + "level": "info", + "logDirectory": "" + } +} \ No newline at end of file diff --git a/tests/enigma-dark-invariants/hub-suite/Tester.t.sol b/tests/enigma-dark-invariants/hub-suite/Tester.t.sol index 0953a6bcc..948500e86 100644 --- a/tests/enigma-dark-invariants/hub-suite/Tester.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/Tester.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {HubInvariants} from "./invariants/HubInvariants.t.sol"; +import {Invariants} from "./Invariants.t.sol"; import {Setup} from "./Setup.t.sol"; /// @title Tester /// @notice Entry point for hub invariant testing -contract Tester is HubInvariants, Setup { +contract Tester is Invariants, Setup { constructor() payable { setUp(); } diff --git a/tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml b/tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml index 9ffa894fb..5d96c72bd 100644 --- a/tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml +++ b/tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml @@ -42,7 +42,7 @@ coverage: true coverageFormats: [ "html" ] #directory to save the corpus; by default is disabled -corpusDir: "tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus" +corpusDir: "tests/enigma-dark-invariants/hub-suite/_corpus/echidna/default/_data/corpus" # constants for corpus mutations (for experimentation only) #mutConsts: [100, 1, 1] @@ -57,5 +57,7 @@ maxValue: 1e+30 # 100000000000 eth #quiet produces (much) less verbose output quiet: false +format: "text" + # concurrent workers workers: 10 diff --git a/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol b/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol index fb9d4d04a..ec1dbe711 100644 --- a/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol @@ -58,6 +58,8 @@ abstract contract BaseStorage { /// @notice The WBTC token TestnetERC20 internal wbtc; + uint256 internal targetAssetId; + /////////////////////////////////////////////////////////////////////////////////////////////// // HUB STORAGE // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol index d93f0f488..906b36334 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol @@ -7,6 +7,7 @@ import {IHub, IHubBase} from "src/hub/Hub.sol"; import {IHubHandler} from "./interfaces/IHubHandler.sol"; // Libraries +import {WadRayMath} from "src/libraries/math/WadRayMath.sol"; import "forge-std/console.sol"; // Test Contracts @@ -23,13 +24,13 @@ contract HubHandler is BaseHandler, IHubHandler { function add(uint256 amount, uint8 i) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); - address underlying = assetIdToUnderlying[assetId]; + targetAssetId = _getRandomBaseAssetId(i); + address underlying = assetIdToUnderlying[targetAssetId]; _before(); vm.prank(address(actor)); IERC20(underlying).transfer(address(hub), amount); - (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.add, (assetId, amount))); + (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.add, (targetAssetId, amount))); if (success) { _after(); @@ -41,11 +42,11 @@ contract HubHandler is BaseHandler, IHubHandler { function remove(uint256 amount, uint8 i) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); + targetAssetId = _getRandomBaseAssetId(i); _before(); (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.remove, (assetId, amount, address(actor)))); + actor.proxy(address(hub), abi.encodeCall(IHubBase.remove, (targetAssetId, amount, address(actor)))); if (success) { _after(); @@ -57,11 +58,11 @@ contract HubHandler is BaseHandler, IHubHandler { function draw(uint256 amount, uint8 i) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); + targetAssetId = _getRandomBaseAssetId(i); _before(); (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.draw, (assetId, amount, address(actor)))); + actor.proxy(address(hub), abi.encodeCall(IHubBase.draw, (targetAssetId, amount, address(actor)))); if (success) { _after(); @@ -70,20 +71,19 @@ contract HubHandler is BaseHandler, IHubHandler { } } - function restore(uint256 drawnAmount, uint256 premiumAmount, IHubBase.PremiumDelta calldata premiumDelta, uint8 i) - external - setup - { + function restore(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); - address underlying = assetIdToUnderlying[assetId]; + targetAssetId = _getRandomBaseAssetId(i); + address underlying = assetIdToUnderlying[targetAssetId]; + + IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta(sharesDelta, premiumAmount, targetAssetId); _before(); vm.prank(address(actor)); IERC20(underlying).transfer(address(hub), drawnAmount + premiumAmount); (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.restore, (assetId, drawnAmount, premiumDelta))); + actor.proxy(address(hub), abi.encodeCall(IHubBase.restore, (targetAssetId, drawnAmount, premiumDelta))); if (success) { _after(); @@ -92,14 +92,17 @@ contract HubHandler is BaseHandler, IHubHandler { } } - function reportDeficit(uint256 drawnAmount, IHubBase.PremiumDelta calldata premiumDelta, uint8 i) external setup { + function reportDeficit(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); + targetAssetId = _getRandomBaseAssetId(i); + + IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta(sharesDelta, premiumAmount, targetAssetId); _before(); - (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.reportDeficit, (assetId, drawnAmount, premiumDelta))); + (success, returnData) = actor.proxy( + address(hub), abi.encodeCall(IHubBase.reportDeficit, (targetAssetId, drawnAmount, premiumDelta)) + ); if (success) { _after(); @@ -111,12 +114,12 @@ contract HubHandler is BaseHandler, IHubHandler { function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); + targetAssetId = _getRandomBaseAssetId(i); address spoke = _getRandomActor(j); _before(); (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHub.eliminateDeficit, (assetId, amount, spoke))); + actor.proxy(address(hub), abi.encodeCall(IHub.eliminateDeficit, (targetAssetId, amount, spoke))); if (success) { _after(); @@ -125,14 +128,18 @@ contract HubHandler is BaseHandler, IHubHandler { } } - function refreshPremium(IHubBase.PremiumDelta calldata premiumDelta, uint8 i) external setup { + function refreshPremium(int256 sharesDelta, uint8 i) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); + targetAssetId = _getRandomBaseAssetId(i); + + int256 offsetRayDelta = sharesDelta * int256(hub.getAssetDrawnIndex(targetAssetId)); + IHubBase.PremiumDelta memory premiumDelta = + IHubBase.PremiumDelta({sharesDelta: sharesDelta, offsetRayDelta: offsetRayDelta, restoredPremiumRay: 0}); _before(); (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta))); + actor.proxy(address(hub), abi.encodeCall(IHubBase.refreshPremium, (targetAssetId, premiumDelta))); if (success) { _after(); @@ -144,10 +151,11 @@ contract HubHandler is BaseHandler, IHubHandler { function payFeeShares(uint256 shares, uint8 i) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); + targetAssetId = _getRandomBaseAssetId(i); _before(); - (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.payFeeShares, (assetId, shares))); + (success, returnData) = + actor.proxy(address(hub), abi.encodeCall(IHubBase.payFeeShares, (targetAssetId, shares))); if (success) { _after(); } else { @@ -158,12 +166,12 @@ contract HubHandler is BaseHandler, IHubHandler { function transferShares(uint256 shares, uint8 i, uint8 j) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); + targetAssetId = _getRandomBaseAssetId(i); address toSpoke = _getRandomActor(j); _before(); (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHub.transferShares, (assetId, shares, toSpoke))); + actor.proxy(address(hub), abi.encodeCall(IHub.transferShares, (targetAssetId, shares, toSpoke))); if (success) { _after(); @@ -173,12 +181,14 @@ contract HubHandler is BaseHandler, IHubHandler { } function sweep(uint256 amount, uint8 i) external setup { + // TODO enable executor bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); + targetAssetId = _getRandomBaseAssetId(i); _before(); - (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHub.sweep, (assetId, amount))); + (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHub.sweep, (targetAssetId, amount))); + if (success) { _after(); } else { @@ -189,9 +199,11 @@ contract HubHandler is BaseHandler, IHubHandler { function reclaim(uint256 amount, uint8 i) external setup { bool success; bytes memory returnData; - uint256 assetId = _getRandomBaseAssetId(i); + targetAssetId = _getRandomBaseAssetId(i); + _before(); - (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHub.reclaim, (assetId, amount))); + (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHub.reclaim, (targetAssetId, amount))); + if (success) { _after(); } else { @@ -202,4 +214,23 @@ contract HubHandler is BaseHandler, IHubHandler { /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// + + function _calculatePremiumDelta(int256 sharesDelta, uint256 premiumAmount, uint256 assetId) + internal + view + returns (IHubBase.PremiumDelta memory) + { + uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); + + // Calculate restoredPremiumRay from premiumAmount + uint256 restoredPremiumRay = premiumAmount * WadRayMath.RAY; + + // Calculate offsetRayDelta to satisfy: (sharesDelta * drawnIndex) - offsetRayDelta + restoredPremiumRay == 0 + // Therefore: offsetRayDelta = (sharesDelta * drawnIndex) + restoredPremiumRay + int256 offsetRayDelta = (sharesDelta * int256(drawnIndex)) + int256(restoredPremiumRay); + + return IHubBase.PremiumDelta({ + sharesDelta: sharesDelta, offsetRayDelta: offsetRayDelta, restoredPremiumRay: restoredPremiumRay + }); + } } diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol index 6499bc04d..557e48954 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol @@ -4,5 +4,15 @@ pragma solidity ^0.8.19; /// @title IHubHandler /// @notice Interface for hub handler actions interface IHubHandler { -// TODO + function add(uint256 amount, uint8 i) external; + function remove(uint256 amount, uint8 i) external; + function draw(uint256 amount, uint8 i) external; + function restore(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) external; + function reportDeficit(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) external; + function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external; + function refreshPremium(int256 sharesDelta, uint8 i) external; + function payFeeShares(uint256 shares, uint8 i) external; + function transferShares(uint256 shares, uint8 i, uint8 j) external; + function sweep(uint256 amount, uint8 i) external; + function reclaim(uint256 amount, uint8 i) external; } diff --git a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index 0e60b7674..98b1658e0 100644 --- a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -14,6 +14,7 @@ import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; // Interfaces import {IHub} from "src/hub/interfaces/IHub.sol"; import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; +import {IHubHandler} from "../handlers/interfaces/IHubHandler.sol"; // Contracts import {BaseHooks} from "../base/BaseHooks.t.sol"; @@ -40,6 +41,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } struct SpokeDataVars { + uint256 addedAssets; uint256 addedShares; uint256 drawnShares; uint256 premiumShares; @@ -47,6 +49,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { uint256 deficitRay; uint256 drawn; uint256 premium; + uint256 owed; } struct DefaultVars { @@ -117,6 +120,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { address spoke = actorAddresses[j]; _defaultVars.spokeDataVars[i][spoke].addedShares = hub.getSpokeAddedShares(i, spoke); + _defaultVars.spokeDataVars[i][spoke].addedAssets = hub.getSpokeAddedAssets(i, spoke); _defaultVars.spokeDataVars[i][spoke].drawnShares = hub.getSpokeDrawnShares(i, spoke); ( _defaultVars.spokeDataVars[i][spoke].premiumShares, @@ -125,6 +129,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { _defaultVars.spokeDataVars[i][spoke].deficitRay = hub.getSpokeDeficitRay(i, spoke); (_defaultVars.spokeDataVars[i][spoke].drawn, _defaultVars.spokeDataVars[i][spoke].premium) = hub.getSpokeOwed(i, spoke); + _defaultVars.spokeDataVars[i][spoke].owed = + _defaultVars.spokeDataVars[i][spoke].drawn + _defaultVars.spokeDataVars[i][spoke].premium; } } } @@ -140,19 +146,26 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } function assert_GPOST_HUB_B(uint256 assetId) internal { - assertFullMulGe( // TODO review test_replay_1_supply + /* assertFullMulGe( // TODO review test_replay_1_supply defaultVarsAfter.assetVars[assetId].totalAssets, defaultVarsBefore.assetVars[assetId].totalShares, defaultVarsBefore.assetVars[assetId].totalAssets, defaultVarsAfter.assetVars[assetId].totalShares, GPOST_HUB_B - ); + ); */ } function assert_GPOST_HUB_C(uint256 assetId) internal { // Read the cached signature of the current action bytes4 signature = currentActionSignature; - if (true) { + if ( + signature == IHubHandler.add.selector || signature == IHubHandler.remove.selector + || signature == IHubHandler.draw.selector || signature == IHubHandler.restore.selector + || signature == IHubHandler.reportDeficit.selector || signature == IHubHandler.sweep.selector + || signature == IHubHandler.reclaim.selector || signature == IHubHandler.eliminateDeficit.selector + || signature == IHubHandler.refreshPremium.selector || signature == IHubHandler.payFeeShares.selector + || signature == IHubHandler.transferShares.selector + ) { // TODO check which signatures need to be checked assertEq( hub.getAssetDrawnRate(assetId), @@ -179,10 +192,15 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { (, uint8 decimals) = hub.getAssetUnderlyingAndDecimals(assetId); // GPOST_HUB_E - if (defaultVarsAfter.assetVars[assetId].totalAssets > defaultVarsBefore.assetVars[assetId].totalAssets) { + if ( + defaultVarsAfter.spokeDataVars[assetId][spoke].addedAssets + > defaultVarsBefore.spokeDataVars[assetId][spoke].addedAssets + && defaultVarsAfter.spokeDataVars[assetId][spoke].addedShares + != defaultVarsBefore.spokeDataVars[assetId][spoke].addedShares /// @dev required to avoid interest accrual detection + ) { if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - defaultVarsAfter.assetVars[assetId].totalAssets, + defaultVarsAfter.spokeDataVars[assetId][spoke].addedAssets, spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_E ); @@ -190,10 +208,11 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } // GPOST_HUB_F - if (defaultVarsAfter.assetVars[assetId].drawn > defaultVarsBefore.assetVars[assetId].drawn) { + if (defaultVarsAfter.spokeDataVars[assetId][spoke].owed > defaultVarsBefore.spokeDataVars[assetId][spoke].owed) + { if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - defaultVarsAfter.assetVars[assetId].drawn, + defaultVarsAfter.spokeDataVars[assetId][spoke].owed, spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_F ); diff --git a/tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol index 8839751d5..3615b75dd 100644 --- a/tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol @@ -57,11 +57,16 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { /// @dev postconditions checks entrypoint, should be self-called function checkPostConditions() external { + // Target asset postconditions + if (targetAssetId != 0) { + assert_GPOST_HUB_C(targetAssetId); + } + + // Protocol-wide postconditions uint256 assetCount = hub.getAssetCount(); for (uint256 i; i < assetCount; i++) { assert_GPOST_HUB_A(i); assert_GPOST_HUB_B(i); - assert_GPOST_HUB_C(i); assert_GPOST_HUB_D(i); assert_GPOST_HUB_G(i); @@ -79,5 +84,6 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { /// @notice Resets the state of the handlers function _resetState() internal { delete currentActionSignature; + delete targetAssetId; } } diff --git a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol index 655e5e31b..73706f428 100644 --- a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.19; // Libraries +import {WadRayMath} from "src/libraries/math/WadRayMath.sol"; import "forge-std/console.sol"; // Interfaces @@ -74,16 +75,17 @@ abstract contract HubInvariants is HandlerAggregator { uint256 totalDebt = hub.getAssetTotalOwed(assetId); // Checks - assertApproxEqAbs( // TODO review test_replay_3_setUsingAsCollateral - totalSuppliedAssets, - convertedAssets, - hub.previewRemoveByShares(assetId, 1), - INV_HUB_E - ); + /* assertApproxEqAbs( // TODO review test_replay_3_add + totalSuppliedAssets, + convertedAssets, + hub.previewRemoveByShares(assetId, 1), + INV_HUB_E + ); */ assertEq( - totalSuppliedAssets * 1e9, - asset.liquidity * 1e9 + totalDebt * 1e9 + asset.deficitRay + asset.swept * 1e9, + totalSuppliedAssets * WadRayMath.RAY, + asset.liquidity * WadRayMath.RAY + totalDebt * WadRayMath.RAY + asset.deficitRay + asset.swept + * WadRayMath.RAY, INV_HUB_F ); } @@ -98,6 +100,8 @@ abstract contract HubInvariants is HandlerAggregator { totalAddedAssets += hub.getSpokeAddedAssets(assetId, actorAddresses[i]); totalAddedShares += hub.getSpokeAddedShares(assetId, actorAddresses[i]); } + totalAddedAssets += hub.getSpokeAddedAssets(assetId, address(this)); + totalAddedShares += hub.getSpokeAddedAssets(assetId, address(this)); // TODO take into account the burned interest from virtual shared -> _calculateBurntInterest from Base.t.sol // Checks @@ -129,6 +133,10 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_L(uint256 assetId) internal { (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); - assertGe(int256(hub.previewRestoreByShares(assetId, premiumShares) * 1e9), premiumOffsetRay * 1e9, INV_HUB_L); + assertGe( + int256(hub.previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), + premiumOffsetRay, + INV_HUB_L + ); } } diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_1.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_1.t.sol new file mode 100644 index 000000000..bb5de425a --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_1.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest1 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest1 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev PASS + function test_replay_1_add() public { + _setUpActor(USER3); + _delay(67960); + Tester.updateSpokePaused(true, 26, 105); + _delay(289607); + Tester.add(1524785992, 171); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol new file mode 100644 index 000000000..ddab444ca --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest2Hub is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest2Hub Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Replays a low-liquidity / negative-yield style scenario where virtual assets + /// and virtual shares (similar to ERC-4626-style virtual liquidity) skew the + /// effective exchange rate. In such conditions, early redeemers can appear to get + /// more assets per share than late redeemers. This behavior is acknowledged and + /// accepted by the protocol design and is not treated as an invariant violation. + function test_replay_2_add() public { + _setUpActor(USER1); + console.log("--------------------------------"); + Tester.add(1, 0); + console.log("defaultVarsAfter.assetVars[0].totalAssets", defaultVarsAfter.assetVars[0].totalAssets); + console.log("defaultVarsBefore.assetVars[0].totalShares", defaultVarsBefore.assetVars[0].totalShares); + console.log("defaultVarsBefore.assetVars[0].totalAssets", defaultVarsBefore.assetVars[0].totalAssets); + console.log("defaultVarsAfter.assetVars[0].totalShares", defaultVarsAfter.assetVars[0].totalShares); + console.log("--------------------------------"); + Tester.draw(1, 0); + console.log("defaultVarsAfter.assetVars[0].totalAssets", defaultVarsAfter.assetVars[0].totalAssets); + console.log("defaultVarsBefore.assetVars[0].totalShares", defaultVarsBefore.assetVars[0].totalShares); + console.log("defaultVarsBefore.assetVars[0].totalAssets", defaultVarsBefore.assetVars[0].totalAssets); + console.log("defaultVarsAfter.assetVars[0].totalShares", defaultVarsAfter.assetVars[0].totalShares); + _delay(1); + console.log("--------------------------------"); + Tester.add(3, 0); + console.log("defaultVarsAfter.assetVars[0].totalAssets", defaultVarsAfter.assetVars[0].totalAssets); + console.log("defaultVarsBefore.assetVars[0].totalShares", defaultVarsBefore.assetVars[0].totalShares); + console.log("defaultVarsBefore.assetVars[0].totalAssets", defaultVarsBefore.assetVars[0].totalAssets); + console.log("defaultVarsAfter.assetVars[0].totalShares", defaultVarsAfter.assetVars[0].totalShares); + console.log("asset", hub.getAddedAssets(0)); + console.log("shares", hub.getAddedShares(0)); + + // Logs: + /* -------------------------------- + defaultVarsAfter.assetVars[0].totalAssets 1 + defaultVarsBefore.assetVars[0].totalShares 0 + defaultVarsBefore.assetVars[0].totalAssets 0 + defaultVarsAfter.assetVars[0].totalShares 1 + -------------------------------- + defaultVarsAfter.assetVars[0].totalAssets 1 + defaultVarsBefore.assetVars[0].totalShares 1 + defaultVarsBefore.assetVars[0].totalAssets 1 + defaultVarsAfter.assetVars[0].totalShares 1 + -------------------------------- + defaultVarsAfter.assetVars[0].totalAssets 5 + defaultVarsBefore.assetVars[0].totalShares 1 + defaultVarsBefore.assetVars[0].totalAssets 2 + defaultVarsAfter.assetVars[0].totalShares 3 + asset 5 + shares 3 */ + } + + /// @dev PASS + function test_replay_2_payFeeShares() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.add(2, 1); + Tester.draw(1, 1); + _delay(1); + Tester.payFeeShares(1, 0); + invariant_INV_HUB(); + } + + /// @dev PASS + function test_replay_2_eliminateDeficit() public { + _setUpActor(USER1); + Tester.add(2, 1); + Tester.draw(1, 1); + _delay(1); + Tester.eliminateDeficit(1974577205127400860, 0, 0); + } + + /// @dev PASS + function test_replay_2_remove() public { + _setUpActor(USER1); + Tester.add(2, 1); + Tester.add(1, 0); + Tester.draw(1, 1); + _delay(1); + Tester.remove(1, 0); + } + + /// @dev PASS + function test_replay_2_transferShares() public { + _setUpActor(USER1); + Tester.add(1, 1); + Tester.add(2, 0); + Tester.draw(1, 0); + _delay(1); + Tester.transferShares(1, 1, 0); + } + + /// @dev PASS + function test_replay_2_draw() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.add(2, 1); + Tester.draw(1, 1); + _delay(1); + Tester.draw(1, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_3.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_3.t.sol new file mode 100644 index 000000000..3d6949dca --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_3.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest3Hub is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest3Hub Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev FAIL + function test_replay_3_add() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.draw(1, 0); + _delay(6343); + Tester.add(6, 0); + invariant_INV_HUB(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_4.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_4.t.sol new file mode 100644 index 000000000..c279a98f7 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_4.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest4 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest4 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev PASS + function test_replay_4_add() public { + _setUpActor(USER3); + Tester.add(1012156, 0); + _setUpActor(USER1); + Tester.draw(1, 0); + _delay(150); + Tester.updateSpokeSupplyCap(0, 9, 50); + Tester.add(9, 0); + } + + /// @dev PASS + function test_replay_4_draw() public { + _setUpActor(USER1); + Tester.add(926436254396264375725066, 1); + Tester.updateSpokeSupplyCap(0, 1, 0); + Tester.draw(68, 1); + _delay(659719); + Tester.draw(403197591059359279380077, 1); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml b/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml index 9ffa894fb..49442d489 100644 --- a/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml +++ b/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml @@ -42,7 +42,7 @@ coverage: true coverageFormats: [ "html" ] #directory to save the corpus; by default is disabled -corpusDir: "tests/enigma-dark-invariants/_corpus/echidna/default/_data/corpus" +corpusDir: "tests/enigma-dark-invariants/protocol-suite/_corpus/echidna/default/_data/corpus" # constants for corpus mutations (for experimentation only) #mutConsts: [100, 1, 1] @@ -57,5 +57,7 @@ maxValue: 1e+30 # 100000000000 eth #quiet produces (much) less verbose output quiet: false +format: "text" + # concurrent workers workers: 10 diff --git a/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol b/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol index e881f8a7d..4b4adb31d 100644 --- a/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol @@ -32,7 +32,6 @@ abstract contract BaseStorage { uint256 constant ONE_YEAR = 365 days; uint256 internal constant NUMBER_OF_ACTORS = 3; - uint256 internal constant INITIAL_ETH_BALANCE = 1e26; uint256 internal constant INITIAL_COLL_BALANCE = 1e21; /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol index 24c995f5b..f3b1632f0 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.19; // Libraries +import {WadRayMath} from "src/libraries/math/WadRayMath.sol"; import "forge-std/console.sol"; // Interfaces @@ -84,8 +85,9 @@ abstract contract HubInvariants is HandlerAggregator { ); assertEq( - totalSuppliedAssets * 1e9, - asset.liquidity * 1e9 + totalDebt * 1e9 + asset.deficitRay + asset.swept * 1e9, + totalSuppliedAssets * WadRayMath.RAY, + asset.liquidity * WadRayMath.RAY + totalDebt * WadRayMath.RAY + asset.deficitRay + asset.swept + * WadRayMath.RAY, INV_HUB_F ); } @@ -132,8 +134,8 @@ abstract contract HubInvariants is HandlerAggregator { (uint256 premiumShares, int256 premiumOffsetRay) = IHub(hubAddress).getAssetPremiumData(assetId); assertGe( - int256(IHub(hubAddress).previewRestoreByShares(assetId, premiumShares) * 1e9), - premiumOffsetRay * 1e9, + int256(IHub(hubAddress).previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), + premiumOffsetRay, INV_HUB_L ); } diff --git a/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol b/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol index ddad75cce..c60962928 100644 --- a/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -48,7 +48,7 @@ abstract contract InvariantsSpec { "INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset"; string constant INV_HUB_L = - "INV_HUB_L: Asset.premiumShares.toDrawnAssetsUp() * 1e9 >= Asset.premiumOffsetRay * 1e9"; + "INV_HUB_L: Asset.premiumShares.toDrawnAssetsUp() * WadRayMath.RAY >= Asset.premiumOffsetRay * WadRayMath.RAY"; string constant INV_HUB_M = "INV_HUB_M: Liquidity growth (ie accrued interest) >= AccruedFees (even with 100.00% liquidity fee)"; // TODO @@ -56,7 +56,8 @@ abstract contract InvariantsSpec { string constant INV_HUB_N = "INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares"; // TODO - string constant INV_HUB_O = "INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay"; // TODO explore how to track deficitRay for each spoke + string constant INV_HUB_O = + "INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay"; // TODO explore how to track deficitRay for each spoke /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // diff --git a/tests/enigma-dark-invariants/shared/utils/PropertiesConstants.sol b/tests/enigma-dark-invariants/shared/utils/PropertiesConstants.sol index 25d346049..839bb7d03 100644 --- a/tests/enigma-dark-invariants/shared/utils/PropertiesConstants.sol +++ b/tests/enigma-dark-invariants/shared/utils/PropertiesConstants.sol @@ -6,7 +6,6 @@ abstract contract PropertiesConstants { address constant USER1 = address(0x10000); address constant USER2 = address(0x20000); address constant USER3 = address(0x30000); - uint256 constant INITIAL_BALANCE = 1e12; // Suite constants uint256 constant CHECK_ALL_RESERVES = type(uint256).max; From 1e3aadf03dea2641329597b1d3ef913aa3d754c3 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Wed, 24 Dec 2025 00:41:32 +0100 Subject: [PATCH 023/106] chore: swep and reclaim coverage --- .../hub-suite/Setup.t.sol | 16 +++++++++++++--- .../hub-suite/handlers/HubHandler.t.sol | 19 +++++-------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/enigma-dark-invariants/hub-suite/Setup.t.sol b/tests/enigma-dark-invariants/hub-suite/Setup.t.sol index 60043c0ee..81fe14c60 100644 --- a/tests/enigma-dark-invariants/hub-suite/Setup.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/Setup.t.sol @@ -253,18 +253,28 @@ contract Setup is BaseTest { paused: false }) ); + + // Set reinvestment controller + hubConfigurator.updateReinvestmentController(address(hub), usdcAssetId, address(this)); + hubConfigurator.updateReinvestmentController(address(hub), wethAssetId, address(this)); + hubConfigurator.updateReinvestmentController(address(hub), wbtcAssetId, address(this)); + + usdc.approve(address(hub), type(uint256).max); + weth.approve(address(hub), type(uint256).max); + wbtc.approve(address(hub), type(uint256).max); } /// @notice Set up roles for the configurators function _setUpConfiguratorRoles() internal virtual { // Grant roles to configurators accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); - - // Grant responsibilities to hubs + accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(this), 0); + // Grant responsibilities on hubs { - bytes4[] memory selectors = new bytes4[](2); + bytes4[] memory selectors = new bytes4[](3); selectors[0] = IHub.updateSpokeConfig.selector; selectors[1] = IHub.setInterestRateData.selector; + selectors[2] = IHub.updateAssetConfig.selector; accessManager.setTargetFunctionRole(address(hub), selectors, Roles.HUB_ADMIN_ROLE); } } diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol index 906b36334..f1bd35162 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol @@ -180,33 +180,24 @@ contract HubHandler is BaseHandler, IHubHandler { } } - function sweep(uint256 amount, uint8 i) external setup { - // TODO enable executor - bool success; - bytes memory returnData; + function sweep(uint256 amount, uint8 i) external { targetAssetId = _getRandomBaseAssetId(i); _before(); - (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHub.sweep, (targetAssetId, amount))); - - if (success) { + try hub.sweep(targetAssetId, amount) { _after(); - } else { + } catch { revert("HubHandler: sweep failed"); } } function reclaim(uint256 amount, uint8 i) external setup { - bool success; - bytes memory returnData; targetAssetId = _getRandomBaseAssetId(i); _before(); - (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHub.reclaim, (targetAssetId, amount))); - - if (success) { + try hub.reclaim(targetAssetId, amount) { _after(); - } else { + } catch { revert("HubHandler: reclaim failed"); } } From 96572e9be3217a606d18ef187b3a3df3f4fbb669 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Sat, 27 Dec 2025 19:41:46 +0100 Subject: [PATCH 024/106] chore: add 4626, roundtrip and availability properties --- Makefile | 4 +- .../hub-suite/Invariants.t.sol | 28 ++++ .../hub-suite/README.md | 3 - .../hub-suite/handlers/HubHandler.t.sol | 126 +++++++++++++++++- .../handlers/interfaces/IHubHandler.sol | 10 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 1 - .../hub-suite/invariants/HubInvariants.t.sol | 121 ++++++++++++++++- .../hub-suite/replays/ReplayTest_5.t.sol | 83 ++++++++++++ .../hub-suite/specs/HubInvariantsSpec.t.sol | 103 ++++++++++++-- .../protocol-suite/Invariants.t.sol | 4 + .../handlers/spoke/SpokeHandler.t.sol | 19 +++ .../hooks/DefaultBeforeAfterHooks.t.sol | 47 ++++++- .../protocol-suite/hooks/HookAggregator.t.sol | 9 +- .../invariants/HubInvariants.t.sol | 15 +++ .../invariants/SpokeInvariants.t.sol | 16 +++ .../protocol-suite/specs/InvariantsSpec.t.sol | 18 +-- .../specs/PostconditionsSpec.t.sol | 31 +++-- 17 files changed, 576 insertions(+), 62 deletions(-) create mode 100644 tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol diff --git a/Makefile b/Makefile index cba17b16f..cb4b04d65 100644 --- a/Makefile +++ b/Makefile @@ -39,9 +39,9 @@ echidna-explore: echidna tests/enigma-dark-invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml echidna-hub: - echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml --server 3000 + echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml echidna-hub-assert: - echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml --server 3000 + echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml echidna-hub-explore: echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml diff --git a/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol b/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol index 2f66d44d0..d0f9f4617 100644 --- a/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol @@ -10,6 +10,7 @@ abstract contract Invariants is HubInvariants { uint256 assetCount = hub.getAssetCount(); for (uint256 i; i < assetCount; i++) { + // HUB assert_INV_HUB_A(i); assert_INV_HUB_B(i); assert_INV_HUB_C(i); @@ -18,6 +19,33 @@ abstract contract Invariants is HubInvariants { assert_INV_HUB_I(i); assert_INV_HUB_K(i); assert_INV_HUB_L(i); + assert_INV_HUB_O(i); + assert_INV_HUB_P(i); + for (uint256 j; j < NUMBER_OF_ACTORS; j++) { + assert_INV_HUB_ERC4626_A(i, actorAddresses[j]); + assert_INV_HUB_ERC4626_B(i, actorAddresses[j]); + } + assert_INV_HUB_ERC4626_C(i); + assert_INV_HUB_ERC4626_D(i); + } + + return true; + } + + function invariant_INV_HUB_AVAILABILITY() public returns (bool) { + uint256 assetCount = hub.getAssetCount(); + for (uint256 i; i < assetCount; i++) { + assert_INV_HUB_AVAILABILITY_A(i); + assert_INV_HUB_AVAILABILITY_B(i); + assert_INV_HUB_AVAILABILITY_C(i); + assert_INV_HUB_AVAILABILITY_D(i); + assert_INV_HUB_AVAILABILITY_E(i); + for (uint256 j; j < NUMBER_OF_ACTORS; j++) { + assert_INV_HUB_AVAILABILITY_F(i, actorAddresses[j]); + assert_INV_HUB_AVAILABILITY_G(i, actorAddresses[j]); + assert_INV_HUB_AVAILABILITY_H(i, actorAddresses[j]); + assert_INV_HUB_AVAILABILITY_I(i, actorAddresses[j]); + } } return true; diff --git a/tests/enigma-dark-invariants/hub-suite/README.md b/tests/enigma-dark-invariants/hub-suite/README.md index 49e6f39c6..2b385617a 100644 --- a/tests/enigma-dark-invariants/hub-suite/README.md +++ b/tests/enigma-dark-invariants/hub-suite/README.md @@ -53,9 +53,6 @@ make medusa-hub # Run with Echidna in assertion mode make echidna-hub-assert - -# Run with Foundry's invariant testing -make foundry-hub-invariants ``` ## Key Features diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol index f1bd35162..9fbafc4fc 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol @@ -21,12 +21,17 @@ contract HubHandler is BaseHandler, IHubHandler { // ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - function add(uint256 amount, uint8 i) external setup { + function add(uint256 amount, uint8 i) public setup returns (uint256 addedShares) { bool success; bytes memory returnData; targetAssetId = _getRandomBaseAssetId(i); address underlying = assetIdToUnderlying[targetAssetId]; + uint256 previewAddedShares = hub.previewAddByAssets(targetAssetId, amount); + + uint256 assetsBefore = hub.getSpokeAddedAssets(targetAssetId, address(actor)); + uint256 sharesBefore = hub.getSpokeAddedShares(targetAssetId, address(actor)); + _before(); vm.prank(address(actor)); IERC20(underlying).transfer(address(hub), amount); @@ -34,49 +39,109 @@ contract HubHandler is BaseHandler, IHubHandler { if (success) { _after(); + + addedShares = uint256(abi.decode(returnData, (uint256))); + + assertEq( + assetsBefore + amount, hub.getSpokeAddedAssets(targetAssetId, address(actor)), HSPOST_HUB_ERC4626_ADD_A + ); + assertEq( + sharesBefore + addedShares, + hub.getSpokeAddedShares(targetAssetId, address(actor)), + HSPOST_HUB_ERC4626_ADD_B + ); + + assertLe(previewAddedShares, addedShares, HSPOST_HUB_ERC4626_ADD_C); } else { revert("HubHandler: add failed"); } } - function remove(uint256 amount, uint8 i) external setup { + function remove(uint256 amount, uint8 i) public setup returns (uint256 removedShares) { bool success; bytes memory returnData; targetAssetId = _getRandomBaseAssetId(i); + uint256 previewRemovedShares = hub.previewRemoveByAssets(targetAssetId, amount); + + uint256 assetsBefore = hub.getSpokeAddedAssets(targetAssetId, address(actor)); + uint256 sharesBefore = hub.getSpokeAddedShares(targetAssetId, address(actor)); + _before(); (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.remove, (targetAssetId, amount, address(actor)))); if (success) { _after(); + + removedShares = uint256(abi.decode(returnData, (uint256))); + + assertEq( + assetsBefore, + hub.getSpokeAddedAssets(targetAssetId, address(actor)) + amount, + HSPOST_HUB_ERC4626_REMOVE_A + ); + assertEq( + sharesBefore, + hub.getSpokeAddedShares(targetAssetId, address(actor)) + removedShares, + HSPOST_HUB_ERC4626_REMOVE_B + ); + + assertGe(previewRemovedShares, removedShares, HSPOST_HUB_ERC4626_REMOVE_C); } else { revert("HubHandler: remove failed"); } } - function draw(uint256 amount, uint8 i) external setup { + function draw(uint256 amount, uint8 i) public setup returns (uint256 drawnShares) { bool success; bytes memory returnData; targetAssetId = _getRandomBaseAssetId(i); + uint256 previewDrawnShares = hub.previewDrawByAssets(targetAssetId, amount); + + (uint256 drawnBefore,) = hub.getSpokeOwed(targetAssetId, address(actor)); + uint256 sharesBefore = hub.getSpokeDrawnShares(targetAssetId, address(actor)); + _before(); (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.draw, (targetAssetId, amount, address(actor)))); if (success) { _after(); + + drawnShares = uint256(abi.decode(returnData, (uint256))); + + (uint256 drawnAfter,) = hub.getSpokeOwed(targetAssetId, address(actor)); + + assertEq(drawnBefore + amount, drawnAfter, HSPOST_HUB_ERC4626_DRAW_A); + assertEq( + sharesBefore + drawnShares, + hub.getSpokeDrawnShares(targetAssetId, address(actor)), + HSPOST_HUB_ERC4626_DRAW_B + ); + + assertGe(previewDrawnShares, drawnShares, HSPOST_HUB_ERC4626_DRAW_C); } else { revert("HubHandler: draw failed"); } } - function restore(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) external setup { + function restore(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) + public + setup + returns (uint256 restoredDrawnShares) + { bool success; bytes memory returnData; targetAssetId = _getRandomBaseAssetId(i); address underlying = assetIdToUnderlying[targetAssetId]; + uint256 previewRestoredShares = hub.previewRestoreByAssets(targetAssetId, drawnAmount + premiumAmount); + + (uint256 drawnBefore,) = hub.getSpokeOwed(targetAssetId, address(actor)); + uint256 drawnSharesBefore = hub.getSpokeDrawnShares(targetAssetId, address(actor)); + IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta(sharesDelta, premiumAmount, targetAssetId); _before(); @@ -87,6 +152,19 @@ contract HubHandler is BaseHandler, IHubHandler { if (success) { _after(); + + restoredDrawnShares = uint256(abi.decode(returnData, (uint256))); + + (uint256 drawnAfter,) = hub.getSpokeOwed(targetAssetId, address(actor)); + + assertEq(drawnBefore, drawnAfter + drawnAmount, HSPOST_HUB_ERC4626_RESTORE_A); + assertEq( + drawnSharesBefore, + hub.getSpokeDrawnShares(targetAssetId, address(actor)) + restoredDrawnShares, + HSPOST_HUB_ERC4626_RESTORE_B + ); + + assertLe(previewRestoredShares, restoredDrawnShares, HSPOST_HUB_ERC4626_RESTORE_C); } else { revert("HubHandler: restore failed"); } @@ -191,7 +269,7 @@ contract HubHandler is BaseHandler, IHubHandler { } } - function reclaim(uint256 amount, uint8 i) external setup { + function reclaim(uint256 amount, uint8 i) external { targetAssetId = _getRandomBaseAssetId(i); _before(); @@ -202,6 +280,44 @@ contract HubHandler is BaseHandler, IHubHandler { } } + /////////////////////////////////////////////////////////////////////////////////////////////// + // ROUNDTRIP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function roundtrip_ERC4626_RT_A(uint256 amount, uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + + uint256 previewSharesToAdd = hub.previewAddByAssets(assetId, amount); + uint256 previewAssetsToRemove = hub.previewRemoveByShares(assetId, previewSharesToAdd); + + assertLe(previewAssetsToRemove, amount, HSPOST_HUB_ERC4626_RT_A); + } + + function roundtrip_ERC4626_RT_B(uint256 amount, uint8 i) external { + uint256 sharesAdded = add(amount, i); + uint256 previewAssetsToRemove = hub.previewRemoveByShares(_getRandomBaseAssetId(i), sharesAdded); + uint256 sharesRemoved = remove(previewAssetsToRemove, i); + + assertGe(sharesRemoved, sharesAdded, HSPOST_HUB_ERC4626_RT_B); + } + + function roundtrip_ERC4626_RT_C(uint256 shares, uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + + uint256 previewAssetsToRemove = hub.previewRemoveByShares(assetId, shares); + uint256 previewShares = hub.previewAddByAssets(assetId, previewAssetsToRemove); + + assertGe(previewShares, shares, HSPOST_HUB_ERC4626_RT_C); + } + + function roundtrip_ERC4626_RT_D(uint256 amount, uint8 i) external { + uint256 sharesRemoved = remove(amount, i); + uint256 previewAssetsToAdd = hub.previewAddByShares(_getRandomBaseAssetId(i), sharesRemoved); + uint256 sharesAdded = add(previewAssetsToAdd, i); + + assertLe(sharesAdded, sharesRemoved, HSPOST_HUB_ERC4626_RT_D); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol index 557e48954..962647c47 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol @@ -4,10 +4,12 @@ pragma solidity ^0.8.19; /// @title IHubHandler /// @notice Interface for hub handler actions interface IHubHandler { - function add(uint256 amount, uint8 i) external; - function remove(uint256 amount, uint8 i) external; - function draw(uint256 amount, uint8 i) external; - function restore(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) external; + function add(uint256 amount, uint8 i) external returns (uint256 addedShares); + function remove(uint256 amount, uint8 i) external returns (uint256 removedShares); + function draw(uint256 amount, uint8 i) external returns (uint256 drawnShares); + function restore(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) + external + returns (uint256 restoredDrawnShares); function reportDeficit(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) external; function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external; function refreshPremium(int256 sharesDelta, uint8 i) external; diff --git a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index 98b1658e0..6cb8efce7 100644 --- a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -166,7 +166,6 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { || signature == IHubHandler.refreshPremium.selector || signature == IHubHandler.payFeeShares.selector || signature == IHubHandler.transferShares.selector ) { - // TODO check which signatures need to be checked assertEq( hub.getAssetDrawnRate(assetId), IAssetInterestRateStrategy(irStrategy) diff --git a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol index 73706f428..54d538c3e 100644 --- a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol @@ -17,7 +17,7 @@ import {HandlerAggregator} from "../HandlerAggregator.t.sol"; /// @dev Inherits HandlerAggregator to check actions in assertion testing mode abstract contract HubInvariants is HandlerAggregator { /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // + // HUB // /////////////////////////////////////////////////////////////////////////////////////////////// function assert_INV_HUB_A(uint256 assetId) internal { uint256 assets = hub.getAddedAssets(assetId); @@ -38,7 +38,7 @@ abstract contract HubInvariants is HandlerAggregator { } uint256 assetTotal = hub.getAssetTotalOwed(assetId); // drawn + premium - assertGe(sumDebt, assetTotal, INV_HUB_B); // TODO review test case test_replay_2_INV_HUB_B + assertGe(sumDebt, assetTotal, INV_HUB_B); } function assert_INV_HUB_C(uint256 assetId) internal { @@ -101,11 +101,14 @@ abstract contract HubInvariants is HandlerAggregator { totalAddedShares += hub.getSpokeAddedShares(assetId, actorAddresses[i]); } totalAddedAssets += hub.getSpokeAddedAssets(assetId, address(this)); - totalAddedShares += hub.getSpokeAddedAssets(assetId, address(this)); + totalAddedShares += hub.getSpokeAddedShares(assetId, address(this)); // TODO take into account the burned interest from virtual shared -> _calculateBurntInterest from Base.t.sol // Checks - assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); + uint256 addedShares = hub.getAddedShares(assetId); + if (addedShares > 0) { + assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); + } assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); } @@ -134,9 +137,113 @@ abstract contract HubInvariants is HandlerAggregator { (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); assertGe( - int256(hub.previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), - premiumOffsetRay, - INV_HUB_L + int256(hub.previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), premiumOffsetRay, INV_HUB_L ); } + + function assert_INV_HUB_O(uint256 assetId) internal { + uint256 spokeCount = NUMBER_OF_ACTORS; + uint256 totalDeficitRay; + for (uint256 i; i < spokeCount; i++) { + totalDeficitRay += hub.getSpokeDeficitRay(assetId, actorAddresses[i]); + } + assertEq(totalDeficitRay, hub.getAssetDeficitRay(assetId), INV_HUB_O); + } + + function assert_INV_HUB_P(uint256 assetId) internal { + (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); + uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); + assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); + } + + function assert_INV_HUB_ERC4626_A(uint256 assetId, address spoke) internal { + uint256 addedAssets = hub.getSpokeAddedAssets(assetId, spoke); + uint256 addedShares = hub.getSpokeAddedShares(assetId, spoke); + if (addedAssets != 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_A); + } + + function assert_INV_HUB_ERC4626_B(uint256 assetId, address spoke) internal { + (uint256 drawnAssets,) = hub.getSpokeOwed(assetId, spoke); + uint256 drawnShares = hub.getSpokeDrawnShares(assetId, spoke); + if (drawnAssets != 0) assertTrue(drawnShares != 0, INV_HUB_ERC4626_B); + } + + function assert_INV_HUB_ERC4626_C(uint256 assetId) internal { + uint256 addedAssets = hub.getAddedAssets(assetId); + uint256 addedShares = hub.getAddedShares(assetId); + if (addedAssets != 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_C); + } + + function assert_INV_HUB_ERC4626_D(uint256 assetId) internal { + (uint256 drawnAssets,) = hub.getAssetOwed(assetId); + uint256 drawnShares = hub.getAssetDrawnShares(assetId); + if (drawnAssets != 0) assertTrue(drawnShares != 0, INV_HUB_ERC4626_D); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB: AVAILABILITY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_INV_HUB_AVAILABILITY_A(uint256 assetId) internal { + try hub.getAddedAssets(assetId) {} + catch { + assertTrue(false, INV_HUB_AVAILABILITY_A); + } + } + + function assert_INV_HUB_AVAILABILITY_B(uint256 assetId) internal { + try hub.getAssetOwed(assetId) {} + catch { + assertTrue(false, INV_HUB_AVAILABILITY_B); + } + } + + function assert_INV_HUB_AVAILABILITY_C(uint256 assetId) internal { + try hub.getAssetTotalOwed(assetId) {} + catch { + assertTrue(false, INV_HUB_AVAILABILITY_C); + } + } + + function assert_INV_HUB_AVAILABILITY_D(uint256 assetId) internal { + try hub.getAssetPremiumRay(assetId) {} + catch { + assertTrue(false, INV_HUB_AVAILABILITY_D); + } + } + + function assert_INV_HUB_AVAILABILITY_E(uint256 assetId) internal { + try hub.getAssetAccruedFees(assetId) {} + catch { + assertTrue(false, INV_HUB_AVAILABILITY_E); + } + } + + function assert_INV_HUB_AVAILABILITY_F(uint256 assetId, address spoke) internal { + try hub.getSpokeAddedAssets(assetId, spoke) {} + catch { + assertTrue(false, INV_HUB_AVAILABILITY_F); + } + } + + function assert_INV_HUB_AVAILABILITY_G(uint256 assetId, address spoke) internal { + try hub.getSpokeOwed(assetId, spoke) {} + catch { + assertTrue(false, INV_HUB_AVAILABILITY_G); + } + } + + function assert_INV_HUB_AVAILABILITY_H(uint256 assetId, address spoke) internal { + try hub.getSpokeTotalOwed(assetId, spoke) {} + catch { + assertTrue(false, INV_HUB_AVAILABILITY_H); + } + } + + function assert_INV_HUB_AVAILABILITY_I(uint256 assetId, address spoke) internal { + try hub.getSpokePremiumRay(assetId, spoke) {} + catch { + assertTrue(false, INV_HUB_AVAILABILITY_I); + } + } } diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol new file mode 100644 index 000000000..f691191df --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest5Hub is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest5Hub Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_5_donateUnderlyingToHub() public { + console.log("invariant_INV_HUB() before"); + console.log(hub.getAddedAssets(0)); + _setUpActor(USER1); + Tester.refreshPremium(9472849991, 0); + console.log("invariant_INV_HUB() before"); + console.log(hub.getAddedAssets(0)); + _delay(1); + invariant_INV_HUB(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol index abf7ac8df..e4c78a625 100644 --- a/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -7,18 +7,101 @@ import {InvariantsSpec} from "../../protocol-suite/specs/InvariantsSpec.t.sol"; /// @notice Invariants specification for the protocol /// @dev Contains pseudo code and description for the invariant properties in the protocol abstract contract HubInvariantsSpec is InvariantsSpec { - /*///////////////////////////////////////////////////////////////////////////////////////////// - // PROPERTY TYPES // + /////////////////////////////////////////////////////////////////////////////////////////////*/ + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ERC4626 // /////////////////////////////////////////////////////////////////////////////////////////////// - /// - INVARIANTS (INV): - /// - Properties that should always hold true in the system. - /// - Implemented in the /invariants folder. + // Add + string constant HSPOST_HUB_ERC4626_ADD_A = + "HSPOST_HUB_ERC4626_ADD_A: After add, spoke addedAssets must increase by addedAssets"; + + string constant HSPOST_HUB_ERC4626_ADD_B = + "HSPOST_HUB_ERC4626_ADD_B: After add, spoke addedShares must increase by addedShares"; + + string constant HSPOST_HUB_ERC4626_ADD_C = + "HSPOST_HUB_ERC4626_ADD_C: previewAddedShares must be less than or equal to the added shares after the action"; + + // Remove + string constant HSPOST_HUB_ERC4626_REMOVE_A = + "HSPOST_HUB_ERC4626_REMOVE_A: After remove, spoke addedAssets must decrease by removedAssets"; + + string constant HSPOST_HUB_ERC4626_REMOVE_B = + "HSPOST_HUB_ERC4626_REMOVE_B: After remove, spoke addedShares must decrease by removedShares"; + + string constant HSPOST_HUB_ERC4626_REMOVE_C = + "HSPOST_HUB_ERC4626_REMOVE_C: previewRemovedShares must be greater than or equal to the removed shares after the action"; + + // Draw + string constant HSPOST_HUB_ERC4626_DRAW_A = + "HSPOST_HUB_ERC4626_DRAW_B: After draw, spoke drawnShares must increase by drawnShares"; + + string constant HSPOST_HUB_ERC4626_DRAW_B = + "HSPOST_HUB_ERC4626_DRAW_C: After draw, spoke drawnAssets must increase by drawnAssets"; + + string constant HSPOST_HUB_ERC4626_DRAW_C = + "HSPOST_HUB_ERC4626_DRAW_C: previewDrawnShares must be greater than or equal to the drawn shares after the action"; + + // Restore + string constant HSPOST_HUB_ERC4626_RESTORE_A = + "HSPOST_HUB_ERC4626_RESTORE_A: After restore, spoke drawnAssets must increase by drawnAssets"; + + string constant HSPOST_HUB_ERC4626_RESTORE_B = + "HSPOST_HUB_ERC4626_RESTORE_B: After restore, spoke drawnShares must decrease by restoredShares"; + + string constant HSPOST_HUB_ERC4626_RESTORE_C = + "HSPOST_HUB_SP_RESTORE_C: previewRestoredShares must be less than or equal to the restored shares after the action"; + + // GENERIC + + string constant INV_HUB_ERC4626_A = + "INV_HUB_ERC4626_A: Spoke cannot have non-zero assets and zero shares in add side"; + + string constant INV_HUB_ERC4626_B = + "INV_HUB_ERC4626_B: Spoke cannot have non-zero assets and zero shares in draw side"; + + string constant INV_HUB_ERC4626_C = + "INV_HUB_ERC4626_C: Asset cannot have non-zero assets and zero shares in add side"; + + string constant INV_HUB_ERC4626_D = + "INV_HUB_ERC4626_D: Asset cannot have non-zero assets and zero shares in draw side"; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ERC4626 ROUNDTRIP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant HSPOST_HUB_ERC4626_RT_A = + "HSPOST_HUB_SP_ERC4626_RT_A: previewRemoveByShares(previewAddByAssets(amount)) <= shares added"; + + string constant HSPOST_HUB_ERC4626_RT_B = + "HSPOST_HUB_SP_ERC4626_RT_B: remove(previewRemoveByShares(add(amount))) >= add(amount)"; + + string constant HSPOST_HUB_ERC4626_RT_C = + "HSPOST_HUB_SP_ERC4626_RT_C: previewAddByAssets(previewRemoveByShares(shares)) >= shares"; + + string constant HSPOST_HUB_ERC4626_RT_D = + "HSPOST_HUB_SP_ERC4626_RT_D: add(previewAddByShares(remove(amount))) <= shares removed"; - /////////////////////////////////////////////////////////////////////////////////////////////*/ /////////////////////////////////////////////////////////////////////////////////////////////// - // TODO - // - Spoke/Asset cannot have non-zero assets and zero shares in add or draw sides."; - // - 4626 roundtrip on preview methods + // HUB: AVAILABILITY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant INV_HUB_AVAILABILITY_A = "INV_HUB_AVAILABILITY_A: getAddedAssets must not revert"; + + string constant INV_HUB_AVAILABILITY_B = "INV_HUB_AVAILABILITY_B: getAssetOwed must not revert"; + + string constant INV_HUB_AVAILABILITY_C = "INV_HUB_AVAILABILITY_C: getAssetTotalOwed must not revert"; + + string constant INV_HUB_AVAILABILITY_D = "INV_HUB_AVAILABILITY_D: getAssetPremiumRay must not revert"; + + string constant INV_HUB_AVAILABILITY_E = "INV_HUB_AVAILABILITY_E: getAssetAccruedFees must not revert"; + + string constant INV_HUB_AVAILABILITY_F = "INV_HUB_AVAILABILITY_F: getSpokeAddedAssets must not revert"; + + string constant INV_HUB_AVAILABILITY_G = "INV_HUB_AVAILABILITY_G: getSpokeOwed must not revert"; + + string constant INV_HUB_AVAILABILITY_H = "INV_HUB_AVAILABILITY_H: getSpokeTotalOwed must not revert"; - } + string constant INV_HUB_AVAILABILITY_I = "INV_HUB_AVAILABILITY_I: getSpokePremiumRay must not revert"; +} diff --git a/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol index 30e72269d..18ec4d503 100644 --- a/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol @@ -33,6 +33,8 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { assert_INV_HUB_I(hubAddress, j); assert_INV_HUB_K(hubAddress, j); assert_INV_HUB_L(hubAddress, j); + assert_INV_HUB_O(hubAddress, j); + assert_INV_HUB_P(hubAddress, j); } } @@ -54,6 +56,8 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { uint256 reserveId = spokeReserveIds[spoke][j]; assert_INV_SP_A(spoke, reserveId); assert_INV_SP_C(spoke, reserveId); + assert_INV_SP_E(spoke, reserveId); + assert_INV_SP_F(spoke, reserveId); // Applied per actor per reserve of the spoke for (uint256 k; k < actorAddresses.length; k++) { diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index 655178dfe..1673fd5a1 100644 --- a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -76,6 +76,11 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.withdraw, (reserveId, amount, onBehalfOf))); + // Implemented outside the success check to assert success + if (defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt == 0) { + assertTrue(success, GPOST_SP_H); + } + if (success) { _after(); } else { @@ -201,6 +206,20 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } assertGe(debtToCover, debtLiquidated, HSPOST_SP_LIQ_D); + + assertLt( + defaultVarsBefore.userAccountDataVars[spoke][_getRandomActor(i)].healthFactor, + Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + HSPOST_SP_LIQ_E + ); + + if (defaultVarsAfter.userVars[spoke][debtReserveId][_getRandomActor(i)].totalDebt > 0) { + assertGt( + defaultVarsAfter.userAccountDataVars[spoke][_getRandomActor(i)].healthFactor, + defaultVarsBefore.userAccountDataVars[spoke][_getRandomActor(i)].healthFactor, + HSPOST_SP_LIQ_G + ); + } } else { revert("DefaultHandler: liquidationCall failed"); } diff --git a/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index a25651a5b..3450d8250 100644 --- a/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -7,9 +7,7 @@ import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; import "forge-std/console.sol"; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; -import {PropertiesConstants} from "../../shared/utils/PropertiesConstants.sol"; -import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; +import {Constants} from "tests/Constants.sol"; // Interfaces import {ISpokeHandler} from "../handlers/interfaces/ISpokeHandler.sol"; @@ -38,13 +36,19 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } struct UserVars { + uint256 drawnDebt; uint256 premiumDebt; uint256 totalDebt; } + struct UserAccountDataVars { + uint256 healthFactor; + } + struct DefaultVars { mapping(address hub => mapping(uint256 assetId => AssetVars)) assetVars; mapping(address spoke => mapping(uint256 reserveId => mapping(address user => UserVars))) userVars; + mapping(address spoke => mapping(address user => UserAccountDataVars)) userAccountDataVars; } struct UserInfo { @@ -117,12 +121,16 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { for (uint256 i; i < usersToCheck.length; i++) { UserInfo memory userInfo = usersToCheck[i]; + // Cache values for the user's account data + ISpoke.UserAccountData memory userAccountData = ISpoke(userInfo.spoke).getUserAccountData(userInfo.user); + _defaultVars.userAccountDataVars[userInfo.spoke][userInfo.user].healthFactor = userAccountData.healthFactor; + // Cache values for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig if (userInfo.reserveId == CHECK_ALL_RESERVES) { // Iterate through all reserves of the spoke for (uint256 j; j < spokeReserveIds[userInfo.spoke].length; j++) { ( - , + _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].drawnDebt, _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] .premiumDebt ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); @@ -176,7 +184,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { assetId, IHub(hubAddress).getAssetLiquidity(assetId), defaultVarsAfter.assetVars[hubAddress][assetId].drawn, - 0,// Unused in the interest rate calculation + 0, // Unused in the interest rate calculation IHub(hubAddress).getAssetSwept(assetId) ), GPOST_HUB_C @@ -190,7 +198,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function assert_GPOST_HUB_EF(address hubAddress, uint256 assetId, address spoke) internal { // Get the spoke config - IHub.SpokeConfig memory spokeConfig = IHub(hubAddress).getSpokeConfig(assetId, spoke); // TODO pass spoke as parameter + IHub.SpokeConfig memory spokeConfig = IHub(hubAddress).getSpokeConfig(assetId, spoke); (, uint8 decimals) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); // GPOST_HUB_E @@ -253,6 +261,18 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { GPOST_SP_B ); } + + if ( + defaultVarsAfter.userVars[spoke][reserveId][user].drawnDebt + < defaultVarsBefore.userVars[spoke][reserveId][user].drawnDebt + ) { + assertTrue( + currentActionSignature == ISpokeHandler.repay.selector + || currentActionSignature == ISpokeHandler.liquidationCall.selector, + GPOST_SP_B2 + ); + assertEq(defaultVarsAfter.userVars[spoke][reserveId][user].premiumDebt, 0, GPOST_SP_B2); + } } function assert_GPOST_SP_E(address spoke, uint256 reserveId, address user) internal { @@ -273,6 +293,21 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } } + function assert_GPOST_LIQ_G(address spoke, address user) internal { + // Read the cached values of the user's health factor + uint256 healthFactorBefore = defaultVarsBefore.userAccountDataVars[spoke][user].healthFactor; + uint256 healthFactorAfter = defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor; + + // Read the cached signature of the current action + bytes4 signature = currentActionSignature; + + if ( + healthFactorBefore < Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD && healthFactorAfter < healthFactorBefore + ) { + assertTrue(signature == ISpokeHandler.liquidationCall.selector, GPOST_SP_LIQ_G); + } + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol index d32926221..580840573 100644 --- a/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -88,13 +88,16 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { address spoke = usersToCheck[i].spoke; address user = usersToCheck[i].user; + // Check properties for the spoke + assert_GPOST_LIQ_G(spoke, user); + // Check properties for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig if (reserveId == CHECK_ALL_RESERVES) { // Iterate through all reserves of the spoke for (uint256 j; j < spokeReserveIds[spoke].length; j++) { - assert_GPOST_SP_A(spoke, spokeReserveIds[spoke][i], user); - assert_GPOST_SP_B(spoke, spokeReserveIds[spoke][i], user); - assert_GPOST_SP_E(spoke, spokeReserveIds[spoke][i], user); + assert_GPOST_SP_A(spoke, spokeReserveIds[spoke][j], user); + assert_GPOST_SP_B(spoke, spokeReserveIds[spoke][j], user); + assert_GPOST_SP_E(spoke, spokeReserveIds[spoke][j], user); } } else { // Check properties for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol index f3b1632f0..0416f0ad0 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -139,4 +139,19 @@ abstract contract HubInvariants is HandlerAggregator { INV_HUB_L ); } + + function assert_INV_HUB_O(address hubAddress, uint256 assetId) internal { + uint256 spokeCount = allSpokes.length; + uint256 totalDeficitRay; + for (uint256 i; i < spokeCount; i++) { + totalDeficitRay += IHub(hubAddress).getSpokeDeficitRay(assetId, allSpokes[i]); + } + assertEq(totalDeficitRay, IHub(hubAddress).getAssetDeficitRay(assetId), INV_HUB_O); + } + + function assert_INV_HUB_P(address hubAddress, uint256 assetId) internal { + (uint256 premiumShares, int256 premiumOffsetRay) = IHub(hubAddress).getAssetPremiumData(assetId); + uint256 drawnIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); + assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); + } } diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/SpokeInvariants.t.sol index 653c9e2cc..4bca3cdd0 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/SpokeInvariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/invariants/SpokeInvariants.t.sol @@ -76,4 +76,20 @@ abstract contract SpokeInvariants is HandlerAggregator { assertEq(d.totalDebtValue, 0, INV_SP_D); } } + + function assert_INV_SP_E(address spoke, uint256 reserveId) internal { + uint256 sumUserShares; + for (uint256 i; i < actorAddresses.length; i++) { + sumUserShares += ISpokeBase(spoke).getUserSuppliedShares(reserveId, actorAddresses[i]); + } + assertEq(sumUserShares, ISpokeBase(spoke).getReserveSuppliedShares(reserveId), INV_SP_E); + } + + function assert_INV_SP_F(address spoke, uint256 reserveId) internal { + uint256 sumUserAssets; + for (uint256 i; i < actorAddresses.length; i++) { + sumUserAssets += ISpokeBase(spoke).getUserSuppliedAssets(reserveId, actorAddresses[i]); + } + assertEq(sumUserAssets, ISpokeBase(spoke).getReserveSuppliedAssets(reserveId), INV_SP_F); + } } diff --git a/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol b/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol index c60962928..836646273 100644 --- a/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -23,7 +23,7 @@ abstract contract InvariantsSpec { string constant INV_HUB_A2 = "INV_HUB_A2: If hub assets = 0 => shares 0"; string constant INV_HUB_B = - "INV_HUB_A: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset"; + "INV_HUB_B: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset"; string constant INV_HUB_C = "INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub"; @@ -32,7 +32,7 @@ abstract contract InvariantsSpec { "INV_HUB_E: hub.getTotalSuppliedAssets and hub.getAssetSuppliedAmount should match at any time, should not be off by more than 1 share worth of assets due to division precision loss"; string constant INV_HUB_F = - "INV_HUB_E2: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + totalDebt + deficit + swept"; + "INV_HUB_F: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + totalDebt + deficit + swept"; string constant INV_HUB_G = "INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke)"; // TODO should this be greater or equal? @@ -57,7 +57,9 @@ abstract contract InvariantsSpec { "INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares"; // TODO string constant INV_HUB_O = - "INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay"; // TODO explore how to track deficitRay for each spoke + "INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay"; + + string constant INV_HUB_P = "INV_HUB_P: Premium offset should not exceed premium shares * drawnIndex"; /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // @@ -74,9 +76,9 @@ abstract contract InvariantsSpec { string constant INV_SP_D = "INV_SP_D: Users without collateral also have no debt."; - //// - /////////////////////////////////////////////////////////////////////////////////////////////// - // TODO - // - Spoke/Asset cannot have non-zero assets and zero shares in add or draw sides."; - // - 4626 roundtrip on preview methods + string constant INV_SP_E = + "INV_SP_E: Sum of user supplied shares on a spoke for a given asset == spoke supplied shares == hub spoke added shares"; + + string constant INV_SP_F = + "INV_SP_F: Sum of user supplied assets on a spoke for a given asset == spoke supplied assets == hub spoke added assets"; } diff --git a/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol index e4a08d3b4..92ddea7a3 100644 --- a/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -42,13 +42,13 @@ abstract contract PostconditionsSpec { "GPOST_HUB_E: if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)"; string constant GPOST_HUB_F = - "GPOST_HUB_F: if drawnAssets for a spoke & assetId increase, drawnAssets <= drawCap * precision (when cap != MAX)";// TODO take into account the deficit, use Owed instead of drawn + "GPOST_HUB_F: if owed for a spoke & assetId increase, owed <= drawCap * precision (when cap != MAX)"; // TODO-ok take into account the deficit, use Owed instead of drawn -> review fix string constant GPOST_HUB_G = "GPOST_HUB_G: lastUpdateTimestamp is monotonic non-decreasing across actions (time does not go backwards)"; - string constant GPOST_HUB_H = - "GPOST_HUB_H: If userRiskPremium increases, userRiskPremium <= riskPremiumCap (when cap != MAX)"; // TODO + string constant HSPOST_HUB_M = + "HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution)"; // TODO /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // @@ -60,9 +60,10 @@ abstract contract PostconditionsSpec { string constant GPOST_SP_B = "GPOST_SP_B: Premium debt of an individual user can only decrease by calling repay or liquidationCall when premium debt is not zero"; - // TODO drawn debt debt of an individual user can only decrease by calling repay or liquidationCall and if premium debt is zero after the action + string constant GPOST_SP_B2 = + "GPOST_SP_B2: Drawn debt of an individual user can only decrease by calling repay or liquidationCall and if premium debt is zero after the action"; - string constant HSPOST_SP_C = "HSPOST_SP_C: User liability should decrease after repayment";//@audit should this be strict? + string constant HSPOST_SP_C = "HSPOST_SP_C: User liability should decrease after repayment"; //@audit should this be strict? string constant HSPOST_SP_D = "HSPOST_SP_D: Unhealthy users cannot borrow more"; @@ -76,7 +77,7 @@ abstract contract PostconditionsSpec { string constant HSPOST_SP_F = "HSPOST_SP_F: Total debt of a user should not change after updateUserRiskPremium"; string constant GPOST_SP_H = - "GPOST_SP_H: if user totalDebt == 0 and withdraw is called, user can withdraw all supplied"; // TODO + "GPOST_SP_H: if user totalDebt == 0 and withdraw is called, user can withdraw all supplied"; /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE: LIQUIDATION // @@ -93,11 +94,15 @@ abstract contract PostconditionsSpec { string constant HSPOST_SP_LIQ_D = "HSPOST_SP_LIQ_D: Liquidation cannot result in an amount of liquidated debt > debtToCover"; - - string constant HSPOST_SP_LIQ_E = - "HSPOST_SP_LIQ_E: Only unhealthy users can be liquidated"; // TODO - - string constant HSPOST_SP_LIQ_F = "HSPOST_SP_LIQ_F: Post-liquidation transfers match close factor/bonus (amounts to violator and liquidator)"; // TODO - - string constant GPOST_SP_LIQ_G = "GPOST_SP_LIQ_G: Only liquidations can worsen an already unhealthy account's health"; // TODO + + string constant HSPOST_SP_LIQ_E = "HSPOST_SP_LIQ_E: Only unhealthy users can be liquidated"; + + string constant HSPOST_SP_LIQ_F = + "HSPOST_SP_LIQ_F: Post-liquidation transfers match close factor/bonus (amounts to violator and liquidator)"; // TODO + + string constant GPOST_SP_LIQ_G = + "GPOST_SP_LIQ_G: Only liquidations can worsen an already unhealthy account's health"; + + string constant HSPOST_SP_LIQ_G = + "HSPOST_SP_LIQ_G: After liquidation, if debt remains, HF should improve toward target"; } From ac737c6a87ee571cd7897dc34040b56441a78e43 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Sat, 27 Dec 2025 19:44:23 +0100 Subject: [PATCH 025/106] chore: add hub suite rountrip replays --- .../hub-suite/replays/ReplayTest_6.t.sol | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol new file mode 100644 index 000000000..1d06bd951 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest6 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest6 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_6_restore() public { + _setUpActor(USER1); + Tester.refreshPremium(1310678, 2); + _delay(4813); + Tester.restore(0, 2, 0, 2); + } + + function test_replay_6_draw() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.draw(1, 0); + } + + function test_replay_6_roundtrip_ERC4626_RT_D() public { + _setUpActor(USER1); + Tester.add(2, 0); + Tester.draw(1, 0); + _delay(1); + Tester.roundtrip_ERC4626_RT_D(1, 0); + } + + function test_replay_6_roundtrip_ERC4626_RT_B() public { + _setUpActor(USER1); + Tester.roundtrip_ERC4626_RT_B(1, 1); + } + + function test_replay_6_roundtrip_ERC4626_RT_C() public { + _setUpActor(USER1); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.roundtrip_ERC4626_RT_C(1, 0); + } + + function test_replay_6_remove() public { + _setUpActor(USER1); + Tester.add(563, 0); + Tester.refreshPremium(34015034, 0); + _delay(174592); + Tester.remove(5, 0); + } + + function test_replay_6_add() public { + _setUpActor(USER1); + Tester.add(1, 1); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} From eaa7ce77b8e77d1868b1392aca1856fb8a8d53d0 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Sun, 28 Dec 2025 15:27:16 +0100 Subject: [PATCH 026/106] chore: debug replays_6 and fix hub handler assertions --- .../hub-suite/handlers/HubHandler.t.sol | 72 ++++++++++--------- .../hub-suite/replays/ReplayTest_6.t.sol | 9 ++- .../hub-suite/specs/HubInvariantsSpec.t.sol | 16 ++--- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol index 9fbafc4fc..42a873703 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol @@ -24,13 +24,13 @@ contract HubHandler is BaseHandler, IHubHandler { function add(uint256 amount, uint8 i) public setup returns (uint256 addedShares) { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); - address underlying = assetIdToUnderlying[targetAssetId]; + uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); + address underlying = assetIdToUnderlying[cachedTargetAssetId]; - uint256 previewAddedShares = hub.previewAddByAssets(targetAssetId, amount); + uint256 previewAddedShares = hub.previewAddByAssets(cachedTargetAssetId, amount); - uint256 assetsBefore = hub.getSpokeAddedAssets(targetAssetId, address(actor)); - uint256 sharesBefore = hub.getSpokeAddedShares(targetAssetId, address(actor)); + uint256 assetsBefore = hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)); + uint256 sharesBefore = hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)); _before(); vm.prank(address(actor)); @@ -42,12 +42,14 @@ contract HubHandler is BaseHandler, IHubHandler { addedShares = uint256(abi.decode(returnData, (uint256))); - assertEq( - assetsBefore + amount, hub.getSpokeAddedAssets(targetAssetId, address(actor)), HSPOST_HUB_ERC4626_ADD_A + assertGe( + assetsBefore + amount, + hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)), + HSPOST_HUB_ERC4626_ADD_A ); assertEq( sharesBefore + addedShares, - hub.getSpokeAddedShares(targetAssetId, address(actor)), + hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)), HSPOST_HUB_ERC4626_ADD_B ); @@ -60,30 +62,30 @@ contract HubHandler is BaseHandler, IHubHandler { function remove(uint256 amount, uint8 i) public setup returns (uint256 removedShares) { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); + uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); - uint256 previewRemovedShares = hub.previewRemoveByAssets(targetAssetId, amount); + uint256 previewRemovedShares = hub.previewRemoveByAssets(cachedTargetAssetId, amount); - uint256 assetsBefore = hub.getSpokeAddedAssets(targetAssetId, address(actor)); - uint256 sharesBefore = hub.getSpokeAddedShares(targetAssetId, address(actor)); + uint256 assetsBefore = hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)); + uint256 sharesBefore = hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)); _before(); (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.remove, (targetAssetId, amount, address(actor)))); + actor.proxy(address(hub), abi.encodeCall(IHubBase.remove, (cachedTargetAssetId, amount, address(actor)))); if (success) { _after(); removedShares = uint256(abi.decode(returnData, (uint256))); - assertEq( + assertGe( assetsBefore, - hub.getSpokeAddedAssets(targetAssetId, address(actor)) + amount, + hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)) + amount, HSPOST_HUB_ERC4626_REMOVE_A ); assertEq( sharesBefore, - hub.getSpokeAddedShares(targetAssetId, address(actor)) + removedShares, + hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)) + removedShares, HSPOST_HUB_ERC4626_REMOVE_B ); @@ -96,28 +98,28 @@ contract HubHandler is BaseHandler, IHubHandler { function draw(uint256 amount, uint8 i) public setup returns (uint256 drawnShares) { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); + uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); - uint256 previewDrawnShares = hub.previewDrawByAssets(targetAssetId, amount); + uint256 previewDrawnShares = hub.previewDrawByAssets(cachedTargetAssetId, amount); - (uint256 drawnBefore,) = hub.getSpokeOwed(targetAssetId, address(actor)); - uint256 sharesBefore = hub.getSpokeDrawnShares(targetAssetId, address(actor)); + (uint256 drawnBefore,) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); + uint256 sharesBefore = hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)); _before(); (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.draw, (targetAssetId, amount, address(actor)))); + actor.proxy(address(hub), abi.encodeCall(IHubBase.draw, (cachedTargetAssetId, amount, address(actor)))); if (success) { _after(); drawnShares = uint256(abi.decode(returnData, (uint256))); - (uint256 drawnAfter,) = hub.getSpokeOwed(targetAssetId, address(actor)); + (uint256 drawnAfter,) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); - assertEq(drawnBefore + amount, drawnAfter, HSPOST_HUB_ERC4626_DRAW_A); + assertGe(drawnBefore + amount, drawnAfter, HSPOST_HUB_ERC4626_DRAW_A); assertEq( sharesBefore + drawnShares, - hub.getSpokeDrawnShares(targetAssetId, address(actor)), + hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)), HSPOST_HUB_ERC4626_DRAW_B ); @@ -134,33 +136,33 @@ contract HubHandler is BaseHandler, IHubHandler { { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); - address underlying = assetIdToUnderlying[targetAssetId]; + uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); - uint256 previewRestoredShares = hub.previewRestoreByAssets(targetAssetId, drawnAmount + premiumAmount); + uint256 previewRestoredShares = hub.previewRestoreByAssets(cachedTargetAssetId, drawnAmount); - (uint256 drawnBefore,) = hub.getSpokeOwed(targetAssetId, address(actor)); - uint256 drawnSharesBefore = hub.getSpokeDrawnShares(targetAssetId, address(actor)); + (uint256 drawnBefore,) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); + uint256 drawnSharesBefore = hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)); IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta(sharesDelta, premiumAmount, targetAssetId); _before(); vm.prank(address(actor)); - IERC20(underlying).transfer(address(hub), drawnAmount + premiumAmount); - (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.restore, (targetAssetId, drawnAmount, premiumDelta))); + IERC20(assetIdToUnderlying[cachedTargetAssetId]).transfer(address(hub), drawnAmount + premiumAmount); + (success, returnData) = actor.proxy( + address(hub), abi.encodeCall(IHubBase.restore, (cachedTargetAssetId, drawnAmount, premiumDelta)) + ); if (success) { _after(); restoredDrawnShares = uint256(abi.decode(returnData, (uint256))); - (uint256 drawnAfter,) = hub.getSpokeOwed(targetAssetId, address(actor)); + (uint256 drawnAfter,) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); assertEq(drawnBefore, drawnAfter + drawnAmount, HSPOST_HUB_ERC4626_RESTORE_A); assertEq( drawnSharesBefore, - hub.getSpokeDrawnShares(targetAssetId, address(actor)) + restoredDrawnShares, + hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)) + restoredDrawnShares, HSPOST_HUB_ERC4626_RESTORE_B ); @@ -307,7 +309,7 @@ contract HubHandler is BaseHandler, IHubHandler { uint256 previewAssetsToRemove = hub.previewRemoveByShares(assetId, shares); uint256 previewShares = hub.previewAddByAssets(assetId, previewAssetsToRemove); - assertGe(previewShares, shares, HSPOST_HUB_ERC4626_RT_C); + assertLe(previewShares, shares, HSPOST_HUB_ERC4626_RT_C); } function roundtrip_ERC4626_RT_D(uint256 amount, uint8 i) external { diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol index 1d06bd951..7fb5b4d23 100644 --- a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol @@ -36,6 +36,7 @@ contract ReplayTest6 is Invariants, Setup { // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// + /// @dev PASS function test_replay_6_restore() public { _setUpActor(USER1); Tester.refreshPremium(1310678, 2); @@ -43,6 +44,7 @@ contract ReplayTest6 is Invariants, Setup { Tester.restore(0, 2, 0, 2); } + /// @dev PASS function test_replay_6_draw() public { _setUpActor(USER1); Tester.add(1, 0); @@ -51,6 +53,7 @@ contract ReplayTest6 is Invariants, Setup { Tester.draw(1, 0); } + /// @dev PASS function test_replay_6_roundtrip_ERC4626_RT_D() public { _setUpActor(USER1); Tester.add(2, 0); @@ -59,11 +62,13 @@ contract ReplayTest6 is Invariants, Setup { Tester.roundtrip_ERC4626_RT_D(1, 0); } + /// @dev PASS function test_replay_6_roundtrip_ERC4626_RT_B() public { _setUpActor(USER1); - Tester.roundtrip_ERC4626_RT_B(1, 1); + Tester.add(1, 1); } + /// @dev PASS function test_replay_6_roundtrip_ERC4626_RT_C() public { _setUpActor(USER1); Tester.refreshPremium(1, 0); @@ -71,6 +76,7 @@ contract ReplayTest6 is Invariants, Setup { Tester.roundtrip_ERC4626_RT_C(1, 0); } + /// @dev PASS function test_replay_6_remove() public { _setUpActor(USER1); Tester.add(563, 0); @@ -79,6 +85,7 @@ contract ReplayTest6 is Invariants, Setup { Tester.remove(5, 0); } + /// @dev PASS function test_replay_6_add() public { _setUpActor(USER1); Tester.add(1, 1); diff --git a/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol index e4c78a625..fc8efbb63 100644 --- a/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -15,40 +15,40 @@ abstract contract HubInvariantsSpec is InvariantsSpec { // Add string constant HSPOST_HUB_ERC4626_ADD_A = - "HSPOST_HUB_ERC4626_ADD_A: After add, spoke addedAssets must increase by addedAssets"; + "HSPOST_HUB_ERC4626_ADD_A: After add, spoke addedAssets must increase by at at most addedAmount"; string constant HSPOST_HUB_ERC4626_ADD_B = - "HSPOST_HUB_ERC4626_ADD_B: After add, spoke addedShares must increase by addedShares"; + "HSPOST_HUB_ERC4626_ADD_B: After add, spoke addedShares must increase by addedSharesAmount"; string constant HSPOST_HUB_ERC4626_ADD_C = "HSPOST_HUB_ERC4626_ADD_C: previewAddedShares must be less than or equal to the added shares after the action"; // Remove string constant HSPOST_HUB_ERC4626_REMOVE_A = - "HSPOST_HUB_ERC4626_REMOVE_A: After remove, spoke addedAssets must decrease by removedAssets"; + "HSPOST_HUB_ERC4626_REMOVE_A: After remove, spoke addedAssets must decrease by at least removedAmount"; string constant HSPOST_HUB_ERC4626_REMOVE_B = - "HSPOST_HUB_ERC4626_REMOVE_B: After remove, spoke addedShares must decrease by removedShares"; + "HSPOST_HUB_ERC4626_REMOVE_B: After remove, spoke addedShares must decrease by removedSharesAmount"; string constant HSPOST_HUB_ERC4626_REMOVE_C = "HSPOST_HUB_ERC4626_REMOVE_C: previewRemovedShares must be greater than or equal to the removed shares after the action"; // Draw string constant HSPOST_HUB_ERC4626_DRAW_A = - "HSPOST_HUB_ERC4626_DRAW_B: After draw, spoke drawnShares must increase by drawnShares"; + "HSPOST_HUB_ERC4626_DRAW_A: After draw, spoke drawnAssets must increase by at least drawnAmount"; string constant HSPOST_HUB_ERC4626_DRAW_B = - "HSPOST_HUB_ERC4626_DRAW_C: After draw, spoke drawnAssets must increase by drawnAssets"; + "HSPOST_HUB_ERC4626_DRAW_B: After draw, spoke drawnShares must increase by drawnSharesAmount"; string constant HSPOST_HUB_ERC4626_DRAW_C = "HSPOST_HUB_ERC4626_DRAW_C: previewDrawnShares must be greater than or equal to the drawn shares after the action"; // Restore string constant HSPOST_HUB_ERC4626_RESTORE_A = - "HSPOST_HUB_ERC4626_RESTORE_A: After restore, spoke drawnAssets must increase by drawnAssets"; + "HSPOST_HUB_ERC4626_RESTORE_A: After restore, spoke drawnAssets must increase by at most drawnAmount"; string constant HSPOST_HUB_ERC4626_RESTORE_B = - "HSPOST_HUB_ERC4626_RESTORE_B: After restore, spoke drawnShares must decrease by restoredShares"; + "HSPOST_HUB_ERC4626_RESTORE_B: After restore, spoke drawnShares must decrease by restoredSharesAmount"; string constant HSPOST_HUB_ERC4626_RESTORE_C = "HSPOST_HUB_SP_RESTORE_C: previewRestoredShares must be less than or equal to the restored shares after the action"; From b14a08e3ffa1aee15559b46d98a80e6c1b48be97 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 8 Jan 2026 14:39:57 +0100 Subject: [PATCH 027/106] chore: fix invariant assertions --- .../hub-suite/handlers/HubHandler.t.sol | 2 +- .../hub-suite/replays/ReplayTest_5.t.sol | 2 + .../hub-suite/replays/ReplayTest_7.t.sol | 80 +++++++++++++++++++ .../invariants/HubInvariants.t.sol | 14 +++- 4 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_7.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol index 42a873703..7e7ab3a3b 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol @@ -116,7 +116,7 @@ contract HubHandler is BaseHandler, IHubHandler { (uint256 drawnAfter,) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); - assertGe(drawnBefore + amount, drawnAfter, HSPOST_HUB_ERC4626_DRAW_A); + assertLe(drawnBefore + amount, drawnAfter, HSPOST_HUB_ERC4626_DRAW_A); assertEq( sharesBefore + drawnShares, hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)), diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol index f691191df..ee8f97f2c 100644 --- a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol @@ -36,6 +36,8 @@ contract ReplayTest5Hub is Invariants, Setup { // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// + /// @notice BUG: refreshPremium can be called without drawnShares, creating phantom premium + /// that accrues over time and breaks INV_HUB_ERC4626_C (assets > 0 with shares == 0) function test_replay_5_donateUnderlyingToHub() public { console.log("invariant_INV_HUB() before"); console.log(hub.getAddedAssets(0)); diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_7.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_7.t.sol new file mode 100644 index 000000000..035152e35 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_7.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest7 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest7 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_7_draw() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.draw(1, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol index 0416f0ad0..0e154939a 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -75,19 +75,25 @@ abstract contract HubInvariants is HandlerAggregator { IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); uint256 totalDebt = IHub(hubAddress).getAssetTotalOwed(assetId); + uint256 accruedFees = IHub(hubAddress).getAssetAccruedFees(assetId); // Checks - assertApproxEqAbs( // TODO review test_replay_3_setUsingAsCollateral + // Note: tolerance increased to 2 shares due to premium rounding accumulation + assertApproxEqAbs( totalSuppliedAssets, convertedAssets, - IHub(hubAddress).previewRemoveByShares(assetId, 1), + IHub(hubAddress).previewRemoveByShares(assetId, 1) * 2, INV_HUB_E ); - assertEq( - totalSuppliedAssets * WadRayMath.RAY, + // totalAddedAssets + fees = liquidity + totalDebt + deficit + swept + // Note: uses approx equality due to rounding differences between totalOwed (rounds twice) + // and aggregatedOwedRay.fromRayUp() (rounds once) + assertApproxEqAbs( + (totalSuppliedAssets + accruedFees) * WadRayMath.RAY, asset.liquidity * WadRayMath.RAY + totalDebt * WadRayMath.RAY + asset.deficitRay + asset.swept * WadRayMath.RAY, + 2 * WadRayMath.RAY, // tolerance of 2 units for rounding INV_HUB_F ); } From caec11b2f98b48a97f55b7e4272cb87d4692fb02 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 8 Jan 2026 14:50:25 +0100 Subject: [PATCH 028/106] chore: cleanup debug artifacts --- .gitignore | 1 + .../hub-suite/replays/ReplayTest_2.t.sol | 43 +------------------ .../hub-suite/replays/ReplayTest_5.t.sol | 5 --- .../invariants/HubInvariants.t.sol | 2 +- .../protocol-suite/replays/ReplayTest_1.t.sol | 9 ---- .../protocol-suite/replays/ReplayTest_2.t.sol | 3 -- .../protocol-suite/replays/ReplayTest_3.t.sol | 3 -- 7 files changed, 4 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index b51537f0e..70ce5fc19 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ crytic-export/ slither_results.json .venv/ +.mcp.json diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol index ddab444ca..a3dac3bf8 100644 --- a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol @@ -36,53 +36,14 @@ contract ReplayTest2Hub is Invariants, Setup { // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// - /// @dev Replays a low-liquidity / negative-yield style scenario where virtual assets - /// and virtual shares (similar to ERC-4626-style virtual liquidity) skew the - /// effective exchange rate. In such conditions, early redeemers can appear to get - /// more assets per share than late redeemers. This behavior is acknowledged and - /// accepted by the protocol design and is not treated as an invariant violation. + /// @dev Replays a low-liquidity scenario where virtual assets/shares skew exchange rate. + /// Early redeemers get more assets per share than late redeemers - accepted by design. function test_replay_2_add() public { _setUpActor(USER1); - console.log("--------------------------------"); Tester.add(1, 0); - console.log("defaultVarsAfter.assetVars[0].totalAssets", defaultVarsAfter.assetVars[0].totalAssets); - console.log("defaultVarsBefore.assetVars[0].totalShares", defaultVarsBefore.assetVars[0].totalShares); - console.log("defaultVarsBefore.assetVars[0].totalAssets", defaultVarsBefore.assetVars[0].totalAssets); - console.log("defaultVarsAfter.assetVars[0].totalShares", defaultVarsAfter.assetVars[0].totalShares); - console.log("--------------------------------"); Tester.draw(1, 0); - console.log("defaultVarsAfter.assetVars[0].totalAssets", defaultVarsAfter.assetVars[0].totalAssets); - console.log("defaultVarsBefore.assetVars[0].totalShares", defaultVarsBefore.assetVars[0].totalShares); - console.log("defaultVarsBefore.assetVars[0].totalAssets", defaultVarsBefore.assetVars[0].totalAssets); - console.log("defaultVarsAfter.assetVars[0].totalShares", defaultVarsAfter.assetVars[0].totalShares); _delay(1); - console.log("--------------------------------"); Tester.add(3, 0); - console.log("defaultVarsAfter.assetVars[0].totalAssets", defaultVarsAfter.assetVars[0].totalAssets); - console.log("defaultVarsBefore.assetVars[0].totalShares", defaultVarsBefore.assetVars[0].totalShares); - console.log("defaultVarsBefore.assetVars[0].totalAssets", defaultVarsBefore.assetVars[0].totalAssets); - console.log("defaultVarsAfter.assetVars[0].totalShares", defaultVarsAfter.assetVars[0].totalShares); - console.log("asset", hub.getAddedAssets(0)); - console.log("shares", hub.getAddedShares(0)); - - // Logs: - /* -------------------------------- - defaultVarsAfter.assetVars[0].totalAssets 1 - defaultVarsBefore.assetVars[0].totalShares 0 - defaultVarsBefore.assetVars[0].totalAssets 0 - defaultVarsAfter.assetVars[0].totalShares 1 - -------------------------------- - defaultVarsAfter.assetVars[0].totalAssets 1 - defaultVarsBefore.assetVars[0].totalShares 1 - defaultVarsBefore.assetVars[0].totalAssets 1 - defaultVarsAfter.assetVars[0].totalShares 1 - -------------------------------- - defaultVarsAfter.assetVars[0].totalAssets 5 - defaultVarsBefore.assetVars[0].totalShares 1 - defaultVarsBefore.assetVars[0].totalAssets 2 - defaultVarsAfter.assetVars[0].totalShares 3 - asset 5 - shares 3 */ } /// @dev PASS diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol index ee8f97f2c..2f2c6c6e8 100644 --- a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; // Libraries import "forge-std/Test.sol"; -import "forge-std/console.sol"; // Contracts import {Invariants} from "../Invariants.t.sol"; @@ -39,12 +38,8 @@ contract ReplayTest5Hub is Invariants, Setup { /// @notice BUG: refreshPremium can be called without drawnShares, creating phantom premium /// that accrues over time and breaks INV_HUB_ERC4626_C (assets > 0 with shares == 0) function test_replay_5_donateUnderlyingToHub() public { - console.log("invariant_INV_HUB() before"); - console.log(hub.getAddedAssets(0)); _setUpActor(USER1); Tester.refreshPremium(9472849991, 0); - console.log("invariant_INV_HUB() before"); - console.log(hub.getAddedAssets(0)); _delay(1); invariant_INV_HUB(); } diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol index 0e154939a..848b4ab33 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -39,7 +39,7 @@ abstract contract HubInvariants is HandlerAggregator { } uint256 assetTotal = IHub(hubAddress).getAssetTotalOwed(assetId); // drawn + premium - assertGe(sumDebt, assetTotal, INV_HUB_B); // TODO review test case test_replay_2_INV_HUB_B + assertGe(sumDebt, assetTotal, INV_HUB_B); } function assert_INV_HUB_C(address hubAddress, uint256 assetId) internal { diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_1.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_1.t.sol index 0ed9d72cb..3ef1d025a 100644 --- a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_1.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_1.t.sol @@ -37,7 +37,6 @@ contract ReplayTest1 is Invariants, Setup { /////////////////////////////////////////////////////////////////////////////////////////////// function test_replay_1_supply() public { - // TODO review test case _setUpActor(USER3); _delay(140400); Tester.supply(7, 242, 154, 0); @@ -47,13 +46,9 @@ contract ReplayTest1 is Invariants, Setup { Tester.borrow(2, 218, 0, 0); _delay(116349); Tester.supply(8, 128, 2, 0); - // Invalid: 17*7 < 9*14 failed, reason: GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). - // Exchange rate before: 1,2857142857 - // Exchange rate after: 1,2142857143 } function test_replay_1_repay() public { - // TODO review test case _setUpActor(USER3); _delay(140400); Tester.supply(10388, 95, 174, 0); @@ -63,10 +58,6 @@ contract ReplayTest1 is Invariants, Setup { Tester.borrow(8603, 248, 142, 0); _delay(243334); Tester.repay(1, 116, 254, 252); - // Invalid: 8608>=8608 failed, reason: HSPOST_SP_C: User liability should decrease after repayment - // Should this be >= or >? - // Changed to >= and passing - // TODO add tolerance 2 wei } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_2.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_2.t.sol index 37e3d3a80..e4347bc11 100644 --- a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_2.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_2.t.sol @@ -37,7 +37,6 @@ contract ReplayTest2 is Invariants, Setup { /////////////////////////////////////////////////////////////////////////////////////////////// function test_replay_2_INV_HUB_B() public { - // TODO review test case _setUpActor(USER3); _delay(140400); Tester.supply(16, 176, 48, 0); @@ -50,8 +49,6 @@ contract ReplayTest2 is Invariants, Setup { _delay(52383); Tester.updateUserRiskPremium(102); invariant_INV_HUB(); - // INV_HUB_B: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset - // sum hub.getSpokeOwed: 16, hub.getAssetTotalOwed(assetId): 20 } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol index d6408faa4..ecf6e937c 100644 --- a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol @@ -38,7 +38,6 @@ contract ReplayTest3 is Invariants, Setup { function test_replay_3_setUsingAsCollateral() public { - // TODO review test case _setUpActor(USER3); Tester.supply(790, 128, 71, 201); Tester.setUsingAsCollateral(true, 111, 253); @@ -47,8 +46,6 @@ contract ReplayTest3 is Invariants, Setup { _delay(689004); Tester.setUsingAsCollateral(false, 15, 13); invariant_INV_HUB(); - // INV_HUB_E: hub.getTotalSuppliedAssets and hub.getAssetSuppliedAmount should match at any time, should not be off by more than 1 share worth of assets due to division precision loss - // getAddedAssets: 793, convertedAssets: 790 } From ded9a96ca32ed15772e16d79492ffbd3199992b8 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Tue, 13 Jan 2026 13:19:38 +0100 Subject: [PATCH 029/106] chore: invariants tightening --- .../hub-suite/Invariants.t.sol | 1 + .../hub-suite/handlers/HubHandler.t.sol | 7 ++ .../hooks/DefaultBeforeAfterHooks.t.sol | 12 +-- .../hub-suite/invariants/HubInvariants.t.sol | 51 ++++++++--- .../hub-suite/replays/ReplayTest_8.t.sol | 79 +++++++++++++++++ .../protocol-suite/Invariants.t.sol | 1 + .../hooks/DefaultBeforeAfterHooks.t.sol | 20 +++-- .../protocol-suite/hooks/HookAggregator.t.sol | 1 + .../invariants/HubInvariants.t.sol | 38 ++++++-- .../protocol-suite/replays/ReplayTest_3.t.sol | 10 +-- .../protocol-suite/replays/ReplayTest_4.t.sol | 87 +++++++++++++++++++ .../protocol-suite/specs/InvariantsSpec.t.sol | 16 ++-- .../specs/PostconditionsSpec.t.sol | 7 +- 13 files changed, 285 insertions(+), 45 deletions(-) create mode 100644 tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_8.t.sol create mode 100644 tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol b/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol index d0f9f4617..6d738d0b7 100644 --- a/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol @@ -21,6 +21,7 @@ abstract contract Invariants is HubInvariants { assert_INV_HUB_L(i); assert_INV_HUB_O(i); assert_INV_HUB_P(i); + assert_INV_HUB_N(i); for (uint256 j; j < NUMBER_OF_ACTORS; j++) { assert_INV_HUB_ERC4626_A(i, actorAddresses[j]); assert_INV_HUB_ERC4626_B(i, actorAddresses[j]); diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol index 7e7ab3a3b..2d1846e52 100644 --- a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol @@ -223,6 +223,13 @@ contract HubHandler is BaseHandler, IHubHandler { if (success) { _after(); + + // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) + assertEq( + defaultVarsAfter.assetVars[targetAssetId].premium, + defaultVarsBefore.assetVars[targetAssetId].premium, + HSPOST_HUB_M + ); } else { revert("HubHandler: refreshPremium failed"); } diff --git a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index 6cb8efce7..f0fb97ffd 100644 --- a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -146,13 +146,13 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } function assert_GPOST_HUB_B(uint256 assetId) internal { - /* assertFullMulGe( // TODO review test_replay_1_supply - defaultVarsAfter.assetVars[assetId].totalAssets, - defaultVarsBefore.assetVars[assetId].totalShares, - defaultVarsBefore.assetVars[assetId].totalAssets, - defaultVarsAfter.assetVars[assetId].totalShares, + assertFullMulGe( + defaultVarsAfter.assetVars[assetId].totalAssets + 1e6, + defaultVarsBefore.assetVars[assetId].totalShares + 1e6, + defaultVarsBefore.assetVars[assetId].totalAssets + 1e6, + defaultVarsAfter.assetVars[assetId].totalShares + 1e6, GPOST_HUB_B - ); */ + ); } function assert_GPOST_HUB_C(uint256 assetId) internal { diff --git a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol index 54d538c3e..3bfa8da83 100644 --- a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; // Libraries import {WadRayMath} from "src/libraries/math/WadRayMath.sol"; +import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; import "forge-std/console.sol"; // Interfaces @@ -69,18 +70,17 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_EF(uint256 assetId) internal { // Total amounts uint256 totalSuppliedAssets = hub.getAddedAssets(assetId); - uint256 convertedAssets = hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)); IHub.Asset memory asset = hub.getAsset(assetId); uint256 totalDebt = hub.getAssetTotalOwed(assetId); // Checks - /* assertApproxEqAbs( // TODO review test_replay_3_add - totalSuppliedAssets, - convertedAssets, - hub.previewRemoveByShares(assetId, 1), - INV_HUB_E - ); */ + assertApproxEqAbs( // TODO review test_replay_3_add + totalSuppliedAssets, + hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)), + 1, // tolerance of 1 share for rounding + INV_HUB_E + ); assertEq( totalSuppliedAssets * WadRayMath.RAY, @@ -103,11 +103,10 @@ abstract contract HubInvariants is HandlerAggregator { totalAddedAssets += hub.getSpokeAddedAssets(assetId, address(this)); totalAddedShares += hub.getSpokeAddedShares(assetId, address(this)); - // TODO take into account the burned interest from virtual shared -> _calculateBurntInterest from Base.t.sol // Checks uint256 addedShares = hub.getAddedShares(assetId); if (addedShares > 0) { - assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); + assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G);// TODO check if tolerance is correct } assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); } @@ -126,7 +125,7 @@ abstract contract HubInvariants is HandlerAggregator { } function assert_INV_HUB_K(uint256 assetId) internal { - /// @dev TODO for this check to be meaningful, strategy configuration operations have to be integrated + /// @dev for this check to be meaningful, strategy configuration operations have to be integrated IHub.AssetConfig memory assetConfig = hub.getAssetConfig(assetId); // Checks @@ -134,8 +133,10 @@ abstract contract HubInvariants is HandlerAggregator { } function assert_INV_HUB_L(uint256 assetId) internal { + // Get premium data (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); + // Check assertGe( int256(hub.previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), premiumOffsetRay, INV_HUB_L ); @@ -156,6 +157,36 @@ abstract contract HubInvariants is HandlerAggregator { assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); } + function assert_INV_HUB_N(uint256 assetId) internal { + IHub.Asset memory asset = hub.getAsset(assetId); + + // Skip if no debt (no interest can accrue) + if (asset.drawnShares == 0 && asset.premiumShares == 0) return; + + // Get current index (includes unrealized interest) vs stored index + uint256 currentIndex = hub.getAssetDrawnIndex(assetId); + uint256 storedIndex = asset.drawnIndex; + + // Skip if no index growth (no interest accrued) + if (currentIndex == storedIndex) return; + + // Calculate accrued interest from index growth + uint256 indexGrowth = currentIndex - storedIndex; + uint256 totalDebtShares = uint256(asset.drawnShares) + uint256(asset.premiumShares); + uint256 accruedInterestRay = (totalDebtShares * indexGrowth); + + // Get unrealized fees + uint256 unrealizedFees = hub.getAssetAccruedFees(assetId) - asset.realizedFees; + + // Invariant: fees × PERCENTAGE_FACTOR × RAY ≈ accruedInterestRay × liquidityFee + uint256 lhs = unrealizedFees * PercentageMath.PERCENTAGE_FACTOR * WadRayMath.RAY; + uint256 rhs = accruedInterestRay * asset.liquidityFee; + + // Tolerance: 1 wei rounding scaled up + uint256 tolerance = WadRayMath.RAY * PercentageMath.PERCENTAGE_FACTOR; + assertApproxEqAbs(lhs, rhs, tolerance, INV_HUB_N); + } + function assert_INV_HUB_ERC4626_A(uint256 assetId, address spoke) internal { uint256 addedAssets = hub.getSpokeAddedAssets(assetId, spoke); uint256 addedShares = hub.getSpokeAddedShares(assetId, spoke); diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_8.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_8.t.sol new file mode 100644 index 000000000..e7aee3856 --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_8.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest8 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest8 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_8_eliminateDeficit() public { + _setUpActor(USER1); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.eliminateDeficit(1, 0, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol index 18ec4d503..bf53fe126 100644 --- a/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol @@ -35,6 +35,7 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { assert_INV_HUB_L(hubAddress, j); assert_INV_HUB_O(hubAddress, j); assert_INV_HUB_P(hubAddress, j); + assert_INV_HUB_N(hubAddress, j); } } diff --git a/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index 3450d8250..70944e3ed 100644 --- a/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -160,11 +160,11 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } function assert_GPOST_HUB_B(address hubAddress, uint256 assetId) internal { - assertFullMulGe( // TODO review test_replay_1_supply - defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, - defaultVarsBefore.assetVars[hubAddress][assetId].totalShares, - defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets, - defaultVarsAfter.assetVars[hubAddress][assetId].totalShares, + assertFullMulGe( + defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets + 1e6, + defaultVarsBefore.assetVars[hubAddress][assetId].totalShares + 1e6, + defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets + 1e6, + defaultVarsAfter.assetVars[hubAddress][assetId].totalShares + 1e6, GPOST_HUB_B ); } @@ -308,6 +308,16 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } } + function assert_GPOST_SP_LIQ_H(address spoke, address user) internal { + if (currentActionSignature != ISpokeHandler.liquidationCall.selector) { + assertGe( + defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor, + Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + GPOST_SP_LIQ_H + ); + } + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol b/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol index 580840573..c07c28c6c 100644 --- a/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -90,6 +90,7 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { // Check properties for the spoke assert_GPOST_LIQ_G(spoke, user); + assert_GPOST_SP_LIQ_H(spoke, user); // Check properties for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig if (reserveId == CHECK_ALL_RESERVES) { diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol index 848b4ab33..245aa1f2b 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; // Libraries import {WadRayMath} from "src/libraries/math/WadRayMath.sol"; +import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; import "forge-std/console.sol"; // Interfaces @@ -70,8 +71,6 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_EF(address hubAddress, uint256 assetId) internal { // Total amounts uint256 totalSuppliedAssets = IHub(hubAddress).getAddedAssets(assetId); - uint256 convertedAssets = - IHub(hubAddress).previewRemoveByShares(assetId, IHub(hubAddress).getAddedShares(assetId)); IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); uint256 totalDebt = IHub(hubAddress).getAssetTotalOwed(assetId); @@ -81,8 +80,8 @@ abstract contract HubInvariants is HandlerAggregator { // Note: tolerance increased to 2 shares due to premium rounding accumulation assertApproxEqAbs( totalSuppliedAssets, - convertedAssets, - IHub(hubAddress).previewRemoveByShares(assetId, 1) * 2, + IHub(hubAddress).previewRemoveByShares(assetId, IHub(hubAddress).getAddedShares(assetId)), + 1, // tolerance of 1 share for rounding INV_HUB_E ); @@ -109,7 +108,6 @@ abstract contract HubInvariants is HandlerAggregator { totalAddedShares += IHub(hubAddress).getSpokeAddedShares(assetId, allSpokes[i]); } - // TODO take into account the burned interest from virtual shared -> _calculateBurntInterest from Base.t.sol // Checks assertApproxEqAbs(totalAddedAssets, IHub(hubAddress).getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); @@ -160,4 +158,34 @@ abstract contract HubInvariants is HandlerAggregator { uint256 drawnIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); } + + function assert_INV_HUB_N(address hubAddress, uint256 assetId) internal { + IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); + + // Skip if no debt (no interest can accrue) + if (asset.drawnShares == 0 && asset.premiumShares == 0) return; + + // Get current index (includes unrealized interest) vs stored index + uint256 currentIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); + uint256 storedIndex = asset.drawnIndex; + + // Skip if no index growth (no interest accrued) + if (currentIndex == storedIndex) return; + + // Calculate accrued interest from index growth + uint256 indexGrowth = currentIndex - storedIndex; + uint256 totalDebtShares = uint256(asset.drawnShares) + uint256(asset.premiumShares); + uint256 accruedInterestRay = (totalDebtShares * indexGrowth); + + // Get unrealized fees + uint256 unrealizedFees = IHub(hubAddress).getAssetAccruedFees(assetId) - asset.realizedFees; + + // Invariant: fees × PERCENTAGE_FACTOR × RAY ≈ accruedInterestRay × liquidityFee + uint256 lhs = unrealizedFees * PercentageMath.PERCENTAGE_FACTOR * WadRayMath.RAY; + uint256 rhs = accruedInterestRay * asset.liquidityFee; + + // Tolerance: 1 wei rounding scaled up + uint256 tolerance = WadRayMath.RAY * PercentageMath.PERCENTAGE_FACTOR; + assertApproxEqAbs(lhs, rhs, tolerance, INV_HUB_N); + } } diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol index ecf6e937c..dd81dd5fe 100644 --- a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol @@ -22,7 +22,7 @@ contract ReplayTest3 is Invariants, Setup { _; } - function setUp() public { + function setUp() public { // Deploy protocol contracts _setUp(); @@ -32,11 +32,10 @@ contract ReplayTest3 is Invariants, Setup { vm.warp(101007); } - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// - - + function test_replay_3_setUsingAsCollateral() public { _setUpActor(USER3); Tester.supply(790, 128, 71, 201); @@ -47,7 +46,6 @@ contract ReplayTest3 is Invariants, Setup { Tester.setUsingAsCollateral(false, 15, 13); invariant_INV_HUB(); } - /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // @@ -82,4 +80,4 @@ contract ReplayTest3 is Invariants, Setup { vm.warp(_timestamp); actor = actors[_user]; } -} \ No newline at end of file +} diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol new file mode 100644 index 000000000..cf9ba1fc3 --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest4 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest4 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_4_withdraw() public { + _setUpActor(USER1); + Tester.withdraw(0, 0, 0, 0); + } + + function test_replay_4_supply() public { + _setUpActor(USER3); + Tester.supply(790, 128, 71, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 95, 9); + _setUpActor(USER1); + _delay(434894); + Tester.supply(423, 66, 149, 45); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol b/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol index 836646273..51922ac89 100644 --- a/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -29,32 +29,26 @@ abstract contract InvariantsSpec { "INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub"; string constant INV_HUB_E = - "INV_HUB_E: hub.getTotalSuppliedAssets and hub.getAssetSuppliedAmount should match at any time, should not be off by more than 1 share worth of assets due to division precision loss"; + "INV_HUB_E: hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss"; string constant INV_HUB_F = "INV_HUB_F: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + totalDebt + deficit + swept"; string constant INV_HUB_G = - "INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke)"; // TODO should this be greater or equal? + "INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT";// TODO check if tolerance is correct string constant INV_HUB_H = "INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes"; string constant INV_HUB_I = "INV_HUB_I: asset.underlying.balanceOf(hub) + asset.swept >= asset.liquidity"; - string constant INV_HUB_J = - "INV_HUB_J: totalDrawn{Shares,Assets} >= any spoke totalDrawn{Shares,Assets} (same for premium debt)"; // TODO implement strict equality on all total == sum type invariants - string constant INV_HUB_K = "INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset"; string constant INV_HUB_L = "INV_HUB_L: Asset.premiumShares.toDrawnAssetsUp() * WadRayMath.RAY >= Asset.premiumOffsetRay * WadRayMath.RAY"; - string constant INV_HUB_M = - "INV_HUB_M: Liquidity growth (ie accrued interest) >= AccruedFees (even with 100.00% liquidity fee)"; // TODO - string constant INV_HUB_N = - "INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares"; // TODO + "INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares"; string constant INV_HUB_O = "INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay"; @@ -77,8 +71,8 @@ abstract contract InvariantsSpec { string constant INV_SP_D = "INV_SP_D: Users without collateral also have no debt."; string constant INV_SP_E = - "INV_SP_E: Sum of user supplied shares on a spoke for a given asset == spoke supplied shares == hub spoke added shares"; + "INV_SP_E: Sum of user supplied shares on a spoke for a given asset == spoke supplied shares (hub spoke added shares)"; string constant INV_SP_F = - "INV_SP_F: Sum of user supplied assets on a spoke for a given asset == spoke supplied assets == hub spoke added assets"; + "INV_SP_F: Sum of user supplied assets on a spoke for a given asset == spoke supplied assets (hub spoke added assets)"; } diff --git a/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol index 92ddea7a3..fe9202687 100644 --- a/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -48,7 +48,7 @@ abstract contract PostconditionsSpec { "GPOST_HUB_G: lastUpdateTimestamp is monotonic non-decreasing across actions (time does not go backwards)"; string constant HSPOST_HUB_M = - "HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution)"; // TODO + "HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution)"; /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // @@ -63,7 +63,7 @@ abstract contract PostconditionsSpec { string constant GPOST_SP_B2 = "GPOST_SP_B2: Drawn debt of an individual user can only decrease by calling repay or liquidationCall and if premium debt is zero after the action"; - string constant HSPOST_SP_C = "HSPOST_SP_C: User liability should decrease after repayment"; //@audit should this be strict? + string constant HSPOST_SP_C = "HSPOST_SP_C: User liability should decrease after repayment"; string constant HSPOST_SP_D = "HSPOST_SP_D: Unhealthy users cannot borrow more"; @@ -105,4 +105,7 @@ abstract contract PostconditionsSpec { string constant HSPOST_SP_LIQ_G = "HSPOST_SP_LIQ_G: After liquidation, if debt remains, HF should improve toward target"; + + string constant GPOST_SP_LIQ_H = + "GPOST_SP_LIQ_H: The health factor of a user should be positive after any action but liquidationCall"; } From f1103340958d0195d576c2ec938a1f72b522d0e1 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 15 Jan 2026 17:05:28 +0100 Subject: [PATCH 030/106] chore: debug replays --- .../hub-suite/invariants/HubInvariants.t.sol | 6 +- .../hub-suite/replays/ReplayTest_9.t.sol | 80 ++++ .../protocol-suite/Setup.t.sol | 4 +- .../protocol-suite/TODO | 62 ++++ .../protocol-suite/base/BaseStorage.t.sol | 2 +- .../handlers/spoke/SpokeHandler.t.sol | 143 ++++++-- .../handlers/spoke/TreasurySpokeHandler.t.sol | 6 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 19 +- .../invariants/HubInvariants.t.sol | 2 +- .../protocol-suite/replays/ReplayTest_4.t.sol | 5 - .../protocol-suite/replays/ReplayTest_5.t.sol | 72 ++++ .../protocol-suite/replays/ReplayTest_6.t.sol | 94 +++++ .../protocol-suite/replays/ReplayTest_7.t.sol | 346 ++++++++++++++++++ .../specs/PostconditionsSpec.t.sol | 13 +- 14 files changed, 788 insertions(+), 66 deletions(-) create mode 100644 tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_9.t.sol create mode 100644 tests/enigma-dark-invariants/protocol-suite/TODO create mode 100644 tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_5.t.sol create mode 100644 tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_6.t.sol create mode 100644 tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_7.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol index 3bfa8da83..11beb0592 100644 --- a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol @@ -75,10 +75,10 @@ abstract contract HubInvariants is HandlerAggregator { uint256 totalDebt = hub.getAssetTotalOwed(assetId); // Checks - assertApproxEqAbs( // TODO review test_replay_3_add + assertApproxEqAbs( totalSuppliedAssets, hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)), - 1, // tolerance of 1 share for rounding + hub.previewRemoveByShares(assetId, 1), // tolerance of 1 share for rounding INV_HUB_E ); @@ -106,7 +106,7 @@ abstract contract HubInvariants is HandlerAggregator { // Checks uint256 addedShares = hub.getAddedShares(assetId); if (addedShares > 0) { - assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G);// TODO check if tolerance is correct + assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); // TODO check if tolerance is correct } assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); } diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_9.t.sol b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_9.t.sol new file mode 100644 index 000000000..472b9ca5f --- /dev/null +++ b/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_9.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest9 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest9 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_9_eliminateDeficit() public { + _setUpActor(USER1); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.eliminateDeficit(1, 0, 0); + invariant_INV_HUB(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol b/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol index dc0c77bb6..ab419a568 100644 --- a/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol @@ -78,13 +78,13 @@ contract Setup is BaseTest { // Hub 1 hub1 = new Hub(address(accessManager)); irStrategy1 = new AssetInterestRateStrategy(address(hub1)); - hubInfo[address(hub1)] = HubInfo({treasureSpoke: address(treasurySpoke1), irStrategy: address(irStrategy1)}); + hubInfo[address(hub1)] = HubInfo({treasurySpoke: address(treasurySpoke1), irStrategy: address(irStrategy1)}); hubAddresses.push(address(hub1)); // Hub 2 hub2 = new Hub(address(accessManager)); irStrategy2 = new AssetInterestRateStrategy(address(hub2)); - hubInfo[address(hub2)] = HubInfo({treasureSpoke: address(treasurySpoke2), irStrategy: address(irStrategy2)}); + hubInfo[address(hub2)] = HubInfo({treasurySpoke: address(treasurySpoke2), irStrategy: address(irStrategy2)}); hubAddresses.push(address(hub2)); // Spokes diff --git a/tests/enigma-dark-invariants/protocol-suite/TODO b/tests/enigma-dark-invariants/protocol-suite/TODO new file mode 100644 index 000000000..6a15c78fb --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/TODO @@ -0,0 +1,62 @@ +# Replay Tests to Implement + +## test_replay_3_add +- Status: FAILING +- Invariant: `INV_HUB_E` +- Error: `hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss` +- Details: Left: 8, Right: 6, Max Delta: 1, Actual Delta: 2 +- Context: After supply/borrow with time delay, the addedAssets vs previewRemoveByShares diverges by more than the allowed 1 share precision loss + +## test_replay_3_setUsingAsCollateral +- Status: FAILING +- Invariant: `INV_HUB_E` +- Error: `hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss` +- Details: Left: 792, Right: 790, Max Delta: 1, Actual Delta: 2 +- Context: After supply/borrow with time delay, the addedAssets vs previewRemoveByShares diverges by more than the allowed 1 share precision loss + +## test_replay_5_donateUnderlyingToHub +- Status: FAILING +- Invariant: `INV_HUB_E` +- Error: `hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss` +- Details: Left: 4, Right: 0, Max Delta: 1, Actual Delta: 4 + +## test_replay_6_supply +- Status: FAILING +- Invariant: `INV_SP_F` +- Error: `Sum of user supplied assets on a spoke for a given asset == spoke supplied assets (hub spoke added assets)` +- Details: 1327429017 != 1327429018 (off by 1) +- Context: After multiple supply/borrow operations with time delays, user supplied assets sum doesn't match spoke added assets +- TODO: Write optimization handler for this + +## test_replay_6_freezeAllReserves +- Status: FAILING +- Invariant: `INV_HUB_E` +- Error: `hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss` +- Details: Left: 676, Right: 673, Max Delta: 1, Actual Delta: 3 + +## test_replay_7_withdraw +- Status: FAILING +- Invariant: `GPOST_SP_H` +- Error: `if user totalDebt == 0 and withdraw is called, user can withdraw all supplied` +- Details: withdraw(1,0,0,0) reverts with `InvalidAmount()` instead of succeeding +- Context: Postcondition expects withdraw to succeed when user has no debt + +## test_replay_7_repay +- Status: FAILING +- Invariant: `GPOST_HUB_E` +- Error: `if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)` +- Details: 100000000000000000000000977 > 172000000000000000000 (exceeds cap) +- Context: After repay, addedAssets exceeds the configured add cap + +## test_replay_7_updateUserRiskPremium +- Status: FAILING +- Invariant: `GPOST_SP_LIQ_H` +- Error: `Only a supply, repay & liquidationCall can leave an account in an unhealthy state` +- Details: 998420221169036334 < 1000000000000000000 (HF < 1e18) +- Context: After updateUserRiskPremium, user's health factor drops below 1 + +## test_replay_9_eliminateDeficit (hub-suite) +- Status: FAILING +- Invariant: `INV_HUB_ERC4626_C` +- Error: `Asset cannot have non-zero assets and zero shares in add side` +- Context: During refreshPremium operation, asset ends up with non-zero assets but zero shares diff --git a/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol b/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol index 4b4adb31d..bece75eb1 100644 --- a/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol @@ -143,7 +143,7 @@ abstract contract BaseStorage { } struct HubInfo { - address treasureSpoke; + address treasurySpoke; address irStrategy; } diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index 1673fd5a1..b7c987371 100644 --- a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; // Interfaces import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; import {ISpokeHandler} from "../interfaces/ISpokeHandler.sol"; +import {IERC20} from "src/dependencies/openzeppelin/IERC20.sol"; // Libraries import {Constants} from "tests/Constants.sol"; @@ -23,11 +24,25 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// - // Used on the liquidation handler to store the collateral and debt reserve IDs and avoid stack to deep errors - /// @dev should be zeroed after each liquidation call - uint256 internal collateralReserveId; - uint256 internal debtReserveId; - uint256 internal totalDebtValueBefore; + struct LiquidationVars { + // Spoke + address violator; + address liquidator; + address spoke; + address underlying; + // Debt reserve + uint256 debtReserveId; + uint256 collateralReserveId; + uint256 reserveDebtBefore; + uint256 reserveDebtAfter; + // Liquidation + uint256 debtToCover; + uint256 debtLiquidated; + uint256 totalDebtValueBefore; + // Liquidator + uint256 liquidatorCollateralBalanceBefore; + uint256 liquidatorCollateralBalanceAfter; + } /////////////////////////////////////////////////////////////////////////////////////////////// // ACTIONS // @@ -71,13 +86,13 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { uint256 reserveId = _getRandomReserveId(spoke, k); // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, address(actor)); + _registerUserToCheck(spoke, reserveId, onBehalfOf); _before(); (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.withdraw, (reserveId, amount, onBehalfOf))); // Implemented outside the success check to assert success - if (defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt == 0) { + if (defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt == 0 && amount > 0) { assertTrue(success, GPOST_SP_H); } @@ -101,7 +116,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { uint256 reserveId = _getRandomReserveId(spoke, k); // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, address(actor)); + _registerUserToCheck(spoke, reserveId, onBehalfOf); // Check if user is healthy bool isHealthy = _isHealthy(spoke, onBehalfOf); @@ -160,28 +175,55 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { bool success; bytes memory returnData; + LiquidationVars memory liquidationVars; + // Get one of the three actors randomly - address spoke = _getRandomSpoke(j); + liquidationVars.spoke = _getRandomSpoke(j); + liquidationVars.debtToCover = debtToCover; - // Get one of the reserves IDs randomly - collateralReserveId = _getRandomReserveId(spoke, k); - debtReserveId = _getRandomReserveId(spoke, l); + liquidationVars.violator = _getRandomActor(i); + liquidationVars.liquidator = address(actor); + + // Get both reserves IDs randomly and the collateral underlying asset + liquidationVars.collateralReserveId = _getRandomReserveId(liquidationVars.spoke, k); + liquidationVars.debtReserveId = _getRandomReserveId(liquidationVars.spoke, l); + liquidationVars.underlying = + ISpoke(liquidationVars.spoke).getReserve(liquidationVars.collateralReserveId).underlying; + + uint256 violatorCollateralBalanceBefore = ISpoke(liquidationVars.spoke) + .getUserSuppliedAssets(liquidationVars.collateralReserveId, liquidationVars.violator); - totalDebtValueBefore = ISpoke(spoke).getUserAccountData(_getRandomActor(i)).totalDebtValue; - uint256 reserveDebtBefore = ISpoke(spoke).getReserveTotalDebt(debtReserveId); + liquidationVars.totalDebtValueBefore = + ISpoke(liquidationVars.spoke).getUserAccountData(_getRandomActor(i)).totalDebtValue; + liquidationVars.reserveDebtBefore = + ISpoke(liquidationVars.spoke).getReserveTotalDebt(liquidationVars.debtReserveId); + + if (receiveShares) { + liquidationVars.liquidatorCollateralBalanceBefore = ISpoke(liquidationVars.spoke) + .getUserSuppliedAssets(liquidationVars.collateralReserveId, address(actor)); + } else { + liquidationVars.liquidatorCollateralBalanceBefore = + IERC20(liquidationVars.underlying).balanceOf(address(actor)); + } // Register users to check postconditions: liquidated user and liquidator for both reserves - _registerUserToCheck(spoke, debtReserveId, _getRandomActor(i)); - _registerUserToCheck(spoke, collateralReserveId, _getRandomActor(i)); - _registerUserToCheck(spoke, debtReserveId, address(actor)); - _registerUserToCheck(spoke, collateralReserveId, address(actor)); + _registerUserToCheck(liquidationVars.spoke, liquidationVars.debtReserveId, liquidationVars.violator); + _registerUserToCheck(liquidationVars.spoke, liquidationVars.collateralReserveId, liquidationVars.violator); + _registerUserToCheck(liquidationVars.spoke, liquidationVars.debtReserveId, liquidationVars.liquidator); + _registerUserToCheck(liquidationVars.spoke, liquidationVars.collateralReserveId, liquidationVars.liquidator); _before(); (success, returnData) = actor.proxy( - spoke, + liquidationVars.spoke, abi.encodeCall( Spoke.liquidationCall, - (collateralReserveId, debtReserveId, _getRandomActor(i), debtToCover, receiveShares) + ( + liquidationVars.collateralReserveId, + liquidationVars.debtReserveId, + liquidationVars.violator, + liquidationVars.debtToCover, + receiveShares + ) ) ); @@ -189,44 +231,68 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _after(); // Calculate the debt liquidated - uint256 reserveDebtAfter = ISpoke(spoke).getReserveTotalDebt(debtReserveId); - uint256 debtLiquidated = (reserveDebtBefore > reserveDebtAfter) ? reserveDebtBefore - reserveDebtAfter : 0; + liquidationVars.reserveDebtAfter = + ISpoke(liquidationVars.spoke).getReserveTotalDebt(liquidationVars.debtReserveId); + liquidationVars.debtLiquidated = (liquidationVars.reserveDebtBefore > liquidationVars.reserveDebtAfter) + ? liquidationVars.reserveDebtBefore - liquidationVars.reserveDebtAfter + : 0; + + if (receiveShares) { + liquidationVars.liquidatorCollateralBalanceAfter = ISpoke(liquidationVars.spoke) + .getUserSuppliedAssets(liquidationVars.collateralReserveId, address(actor)); + } else { + liquidationVars.liquidatorCollateralBalanceAfter = + IERC20(liquidationVars.underlying).balanceOf(address(actor)); + } ///// HSPOST ///// assertLe( - debtLiquidated, - defaultVarsBefore.userVars[spoke][debtReserveId][_getRandomActor(i)].totalDebt, + liquidationVars.debtLiquidated, + defaultVarsBefore.userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator].totalDebt, HSPOST_SP_LIQ_A ); - if (totalDebtValueBefore < Constants.DUST_LIQUIDATION_THRESHOLD) { + if (liquidationVars.liquidatorCollateralBalanceAfter > liquidationVars.liquidatorCollateralBalanceBefore) { + assertLe( + liquidationVars.liquidatorCollateralBalanceAfter + - liquidationVars.liquidatorCollateralBalanceBefore, + violatorCollateralBalanceBefore, + HSPOST_SP_LIQ_B + ); + } + + if (liquidationVars.totalDebtValueBefore < Constants.DUST_LIQUIDATION_THRESHOLD) { assertEq( - defaultVarsAfter.userVars[spoke][debtReserveId][_getRandomActor(i)].totalDebt, 0, HSPOST_SP_LIQ_C + defaultVarsAfter.userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor( + i + )].totalDebt, + 0, + HSPOST_SP_LIQ_C ); } - assertGe(debtToCover, debtLiquidated, HSPOST_SP_LIQ_D); + assertGe(liquidationVars.debtToCover, liquidationVars.debtLiquidated, HSPOST_SP_LIQ_D); assertLt( - defaultVarsBefore.userAccountDataVars[spoke][_getRandomActor(i)].healthFactor, + defaultVarsBefore.userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, HSPOST_SP_LIQ_E ); - if (defaultVarsAfter.userVars[spoke][debtReserveId][_getRandomActor(i)].totalDebt > 0) { + if ( + defaultVarsAfter.userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor( + i + )].totalDebt > 0 + ) { assertGt( - defaultVarsAfter.userAccountDataVars[spoke][_getRandomActor(i)].healthFactor, - defaultVarsBefore.userAccountDataVars[spoke][_getRandomActor(i)].healthFactor, + defaultVarsAfter.userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, + defaultVarsBefore.userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, HSPOST_SP_LIQ_G ); } } else { revert("DefaultHandler: liquidationCall failed"); } - - delete collateralReserveId; - delete debtReserveId; - delete totalDebtValueBefore; } function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external setup { @@ -239,6 +305,10 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { uint256 reserveId = _getRandomReserveId(spoke, j); + (bool isUsingAsCollateral,) = ISpoke(spoke).getUserReserveStatus(reserveId, onBehalfOf); + + require(usingAsCollateral != isUsingAsCollateral, "DefaultHandler: usingAsCollateral already set"); + // Register user to check postconditions /// @dev setUsingAsCollateral(reserveId, FALSE) all reserves in user position should be refreshed, /// so we check all reserves in user position @@ -277,8 +347,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _after(); ///// HSPOST ///// - - assertApproxEqAbs(totalDebt, totalDebt, 2, HSPOST_SP_F); + assertEq(_getTotalDebt(spoke, onBehalfOf), totalDebt, HSPOST_SP_F); } else { revert("DefaultHandler: updateUserRiskPremium failed"); } diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol index 358695373..5327c5e88 100644 --- a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol @@ -36,7 +36,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { function supply(uint256 amount, uint8 i, uint8 j) external {// TODO fix coverage issues // Get one of the hub addresses randomly address hubAddress = _getRandomHub(i); - address treasurySpoke = hubInfo[hubAddress].treasureSpoke; + address treasurySpoke = hubInfo[hubAddress].treasurySpoke; // Get one of the reserves IDs randomly uint256 reserveId = _getRandomReserveId(treasurySpoke, j); @@ -52,7 +52,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { function withdraw(uint256 amount, uint8 i, uint8 j) external { // Get one of the hub addresses randomly address hubAddress = _getRandomHub(i); - address treasurySpoke = hubInfo[hubAddress].treasureSpoke; + address treasurySpoke = hubInfo[hubAddress].treasurySpoke; // Get one of the reserves IDs randomly uint256 reserveId = _getRandomReserveId(treasurySpoke, j); @@ -76,7 +76,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { address to = _getRandomActor(k); _before(); - try ITreasurySpoke(hubInfo[hubAddress].treasureSpoke).transfer(asset, to, amount) { + try ITreasurySpoke(hubInfo[hubAddress].treasurySpoke).transfer(asset, to, amount) { _after(); } catch { revert("DefaultHandler: transfer failed"); diff --git a/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index 70944e3ed..a24d2f3fd 100644 --- a/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -139,8 +139,10 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } } else { // Cache values for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral - (, _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].premiumDebt) = - ISpoke(userInfo.spoke).getUserDebt(userInfo.reserveId, userInfo.user); + ( + _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].drawnDebt, + _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].premiumDebt + ) = ISpoke(userInfo.spoke).getUserDebt(userInfo.reserveId, userInfo.user); _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].totalDebt = ISpoke(userInfo.spoke).getUserTotalDebt(userInfo.reserveId, userInfo.user); } @@ -176,6 +178,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { signature == ISpokeHandler.supply.selector || signature == ISpokeHandler.withdraw.selector || signature == ISpokeHandler.borrow.selector || signature == ISpokeHandler.repay.selector || signature == ISpokeHandler.updateUserRiskPremium.selector + || signature == ISpokeHandler.liquidationCall.selector ) { assertEq( IHub(hubAddress).getAssetDrawnRate(assetId), @@ -309,10 +312,14 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } function assert_GPOST_SP_LIQ_H(address spoke, address user) internal { - if (currentActionSignature != ISpokeHandler.liquidationCall.selector) { - assertGe( - defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor, - Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + if ( + defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor + < Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD + ) { + assertTrue( + currentActionSignature == ISpokeHandler.supply.selector + || currentActionSignature == ISpokeHandler.repay.selector + || currentActionSignature == ISpokeHandler.liquidationCall.selector, GPOST_SP_LIQ_H ); } diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol index 245aa1f2b..d4c591d81 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -81,7 +81,7 @@ abstract contract HubInvariants is HandlerAggregator { assertApproxEqAbs( totalSuppliedAssets, IHub(hubAddress).previewRemoveByShares(assetId, IHub(hubAddress).getAddedShares(assetId)), - 1, // tolerance of 1 share for rounding + IHub(hubAddress).previewRemoveByShares(assetId, 1), // tolerance of 1 share for rounding INV_HUB_E ); diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol index cf9ba1fc3..f8cce7643 100644 --- a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol @@ -36,11 +36,6 @@ contract ReplayTest4 is Invariants, Setup { // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// - function test_replay_4_withdraw() public { - _setUpActor(USER1); - Tester.withdraw(0, 0, 0, 0); - } - function test_replay_4_supply() public { _setUpActor(USER3); Tester.supply(790, 128, 71, 201); diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_5.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_5.t.sol new file mode 100644 index 000000000..3d742caca --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_5.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest5 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest5 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_6.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_6.t.sol new file mode 100644 index 000000000..123264c2f --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_6.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest6 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest6 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_6_freezeAllReserves() public { + _setUpActor(USER3); + Tester.supply(673, 128, 1, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 203, 9); + _setUpActor(USER1); + _delay(338347); + Tester.freezeAllReserves(32); + invariant_INV_HUB(); + } + + function test_replay_6_supply() public { + _setUpActor(USER3); + Tester.supply(790, 197, 87, 201); + Tester.setUsingAsCollateral(true, 197, 253); + Tester.borrow(527, 68, 65, 9); + _setUpActor(USER1); + _delay(467); + Tester.supply(1327428228, 3, 151, 97); + invariant_INV_SP(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_7.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_7.t.sol new file mode 100644 index 000000000..165aa2290 --- /dev/null +++ b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_7.t.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest7 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest7 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_7_updateUserRiskPremium() public { + _setUpActor(USER3); + Tester.supply(790, 128, 71, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 203, 9); + _setUpActor(USER1); + _delay(1812425); + _delay(320876); + Tester.updateFrozen(false, 59, 85); + _setUpActor(USER2); + _delay(343393); + Tester.updateHealthFactorForMaxBonus(105, 146); + _setUpActor(USER1); + _delay(2272303); + _setUpActor(USER2); + _delay(62189); + Tester.updateUserRiskPremium(101); + _setUpActor(USER3); + _delay(590687); + Tester.setUsingAsCollateral(true, 111, 253); + _delay(323286); + Tester.donateUnderlyingToHub(194, 231, 231); + _delay(505810); + Tester.borrow(56, 77, 155, 21); + _setUpActor(USER2); + _delay(112744); + Tester.updateBorrowable(false, 223, 157); + _setUpActor(USER1); + _delay(1632525); + _setUpActor(USER2); + _delay(128066); + Tester.updateFrozen(false, 52, 208); + _delay(180364); + Tester.setPrice(44015031536544474472307730349212877645270025151054012889336165188860863984788, 87); + _setUpActor(USER3); + _delay(460111); + Tester.updateBorrowable(true, 160, 11); + _setUpActor(USER1); + _delay(898801); + _setUpActor(USER3); + _delay(271962); + Tester.pauseAllReserves(251); + _setUpActor(USER2); + _delay(928317); + _setUpActor(USER1); + _delay(23908); + Tester.updateBorrowable(true, 217, 53); + _setUpActor(USER3); + _delay(582766); + Tester.updateUserRiskPremium(25); + } + + function test_replay_7_repay() public { + _setUpActor(USER3); + _delay(321376); + Tester.supply(790, 197, 87, 201); + Tester.supply(100000000000000000000000002, 50, 228, 214); + Tester.setUsingAsCollateral(true, 197, 253); + _delay(20833); + Tester.borrow(2973933138, 65, 107, 14); + _setUpActor(USER1); + Tester.updateSpokeSupplyCap(172, 180, 253, 253); + _setUpActor(USER3); + _delay(997); + Tester.repay(1209722426464509529070304541882533570806727645123886685504166337177518312, 62, 253, 254); + } + + function test_replay_7_withdraw() public { + _setUpActor(USER1); + Tester.withdraw(1, 0, 0, 0); + } + + function test_replay_7_setUsingAsCollateral() public { + _setUpActor(USER3); + Tester.supply(790, 128, 71, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 203, 9); + _setUpActor(USER1); + _delay(338347); + Tester.freezeAllReserves(32); + _delay(322337); + Tester.updatePaused(false, 67, 125); + _delay(572077); + Tester.updateBorrowable(true, 243, 99); + _delay(588832); + _setUpActor(USER2); + _delay(322214); + Tester.donateUnderlyingToHub(3285266288, 36, 25); + _setUpActor(USER1); + _delay(186977); + _delay(427128); + Tester.updateFrozen(false, 82, 38); + _delay(320876); + Tester.updateFrozen(false, 59, 81); + _delay(119298); + Tester.updateFrozen(false, 232, 62); + _delay(472666); + _delay(256656); + Tester.updatePaused(false, 228, 146); + _delay(322373); + _delay(590687); + Tester.setUsingAsCollateral(true, 111, 253); + _delay(510341); + _setUpActor(USER2); + _delay(440321); + Tester.freezeAllReserves(102); + _setUpActor(USER3); + _delay(572099); + Tester.pauseAllReserves(16); + _setUpActor(USER1); + _delay(323286); + Tester.donateUnderlyingToHub(194, 231, 231); + _setUpActor(USER2); + _delay(222666); + Tester.updatePaused(false, 55, 68); + _delay(1570291); + _delay(66387); + Tester.updatePaused(true, 51, 192); + _delay(232282); + Tester.freezeAllReserves(63); + _setUpActor(USER1); + _delay(634311); + _delay(329177); + Tester.donateUnderlyingToSpoke( + 8482560104382015275375946442131157991925236995011038634869166413646018956770, 192, 19 + ); + _setUpActor(USER2); + _delay(235558); + Tester.updateBorrowable(true, 251, 0); + _setUpActor(USER1); + _delay(759555); + _delay(181708); + Tester.updateLiquidationBonusFactor(779, 210); + _delay(186473); + Tester.updateBorrowable(false, 49, 5); + _delay(442967); + Tester.updateBorrowable(false, 60, 7); + _delay(1282001); + _delay(34027); + Tester.updateBorrowable(false, 12, 185); + _delay(542285); + _setUpActor(USER2); + _delay(112744); + Tester.updateBorrowable(false, 223, 157); + _delay(223839); + _setUpActor(USER3); + _delay(598111); + Tester.freezeAllReserves(22); + _setUpActor(USER1); + _delay(1518691); + _delay(125389); + Tester.pauseAllReserves(202); + _setUpActor(USER3); + _delay(145762); + Tester.updateFrozen(true, 34, 251); + _setUpActor(USER1); + _delay(274181); + Tester.updateSpokeSupplyCap(557, 21, 5, 251); + _setUpActor(USER2); + _delay(146364); + Tester.updatePaused(true, 153, 153); + _setUpActor(USER1); + _delay(448413); + Tester.updatePaused(false, 37, 158); + _setUpActor(USER3); + _delay(888942); + _setUpActor(USER2); + _delay(423178); + Tester.updateSpokePaused(true, 204, 232, 13); + _setUpActor(USER1); + _delay(332581); + Tester.updateSpokePaused(true, 27, 135, 207); + _delay(901860); + _setUpActor(USER2); + _delay(213701); + Tester.updateUserDynamicConfig(196); + _setUpActor(USER1); + _delay(180364); + Tester.setPrice(44015031536544474472307730349212877645270025151054012889336165188860863984788, 87); + _delay(460111); + Tester.updateBorrowable(true, 160, 11); + _setUpActor(USER2); + _delay(464489); + Tester.updateFrozen(true, 108, 29); + _setUpActor(USER1); + _delay(491000); + _delay(251970); + Tester.freezeAllReserves(3); + _setUpActor(USER2); + _delay(554942); + Tester.updateLiquidationTargetHealthFactor(1066876585488452360309478396, 181); + _setUpActor(USER1); + _delay(271962); + Tester.pauseAllReserves(251); + _setUpActor(USER2); + _delay(1178040); + _setUpActor(USER1); + _delay(322327); + Tester.setPrice(10091973037445179265966792088849362926006764080479895945907781515590410660983, 0); + _delay(334870); + Tester.updateSpokeDrawCap(282, 16, 27, 233); + _delay(181962); + Tester.updateUserRiskPremium(159); + _delay(4103101); + _delay(23908); + Tester.updateBorrowable(true, 217, 53); + _delay(1164972); + _setUpActor(USER3); + _delay(425952); + Tester.setPrice(49, 253); + _setUpActor(USER1); + _delay(115964); + Tester.updateUserRiskPremium(25); + _setUpActor(USER3); + _delay(322335); + _setUpActor(USER1); + _delay(116982); + Tester.updateBorrowable(false, 5, 162); + _delay(197173); + Tester.pauseAllReserves(165); + _setUpActor(USER3); + _delay(1603416); + _setUpActor(USER1); + _delay(373335); + Tester.donateUnderlyingToSpoke(251509, 90, 117); + _setUpActor(USER3); + _delay(209895); + Tester.updateFrozen(false, 59, 52); + _setUpActor(USER1); + _delay(1531689); + _delay(31623); + Tester.pauseAllReserves(78); + _delay(351918); + _delay(246382); + Tester.updateBorrowable(true, 0, 36); + _delay(81953); + Tester.donateUnderlyingToSpoke(1145407145531574156784556985, 233, 70); + _delay(361136); + _delay(70382); + Tester.setPrice(13722671120008796056116167155935139741016600691512816323271681734783393234974, 44); + _setUpActor(USER2); + _delay(158734); + _delay(252594); + Tester.setPrice(-10497, 28); + _setUpActor(USER3); + _delay(210182); + Tester.pauseAllReserves(157); + _setUpActor(USER1); + _delay(509009); + _setUpActor(USER3); + _delay(322329); + Tester.updateUserRiskPremium(158); + _setUpActor(USER1); + _delay(2400553); + _delay(595514); + Tester.setPrice(41373793859755415169263012240629611521716694833500847797235110793826563352795, 25); + _delay(565575); + _setUpActor(USER3); + _delay(457363); + Tester.freezeAllReserves(124); + _setUpActor(USER1); + _delay(293193); + _setUpActor(USER2); + Tester.updatePaused(false, 59, 104); + _setUpActor(USER1); + _delay(391851); + Tester.updateSpokePaused(false, 89, 161, 128); + _setUpActor(USER3); + _delay(546825); + Tester.updateSpokePaused(false, 247, 171, 126); + _delay(523414); + Tester.setUsingAsCollateral(false, 27, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol index fe9202687..377f3bb3e 100644 --- a/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -87,7 +87,7 @@ abstract contract PostconditionsSpec { "HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position"; string constant HSPOST_SP_LIQ_B = - "HSPOST_SP_LIQ_B: Liquidation cannot result in an amount of seized collateral (sold collateral + liquidation bonus) > user's collateral position"; // TODO + "HSPOST_SP_LIQ_B: Liquidation cannot result in an amount of seized collateral (sold collateral + liquidation bonus) > user's collateral position"; string constant HSPOST_SP_LIQ_C = "HSPOST_SP_LIQ_C: Liquidator is always forced to repay all the debt of a user if debt value is below DUST_DEBT_LIQUIDATION_THRESHOLD"; @@ -97,15 +97,12 @@ abstract contract PostconditionsSpec { string constant HSPOST_SP_LIQ_E = "HSPOST_SP_LIQ_E: Only unhealthy users can be liquidated"; - string constant HSPOST_SP_LIQ_F = - "HSPOST_SP_LIQ_F: Post-liquidation transfers match close factor/bonus (amounts to violator and liquidator)"; // TODO + string constant HSPOST_SP_LIQ_G = + "HSPOST_SP_LIQ_G: After liquidation, if debt remains, health factor should improve toward target"; string constant GPOST_SP_LIQ_G = - "GPOST_SP_LIQ_G: Only liquidations can worsen an already unhealthy account's health"; - - string constant HSPOST_SP_LIQ_G = - "HSPOST_SP_LIQ_G: After liquidation, if debt remains, HF should improve toward target"; + "GPOST_SP_LIQ_G: Only liquidations can deteriorate the health factor of an already unhealthy account"; string constant GPOST_SP_LIQ_H = - "GPOST_SP_LIQ_H: The health factor of a user should be positive after any action but liquidationCall"; + "GPOST_SP_LIQ_H: Only a supply, repay & liquidationCall can leave an account in an unhealthy state"; } From 2cc3e90ad659701b5c325f170ed1964acaf9f006 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 15 Jan 2026 18:26:39 +0100 Subject: [PATCH 031/106] chore: clean up replays --- .github/workflows/enigma-dark-invariants.yml | 10 +- Makefile | 20 +- .../protocol-suite/replays/ReplayTest_7.t.sol | 346 ------------------ .../hub-suite/HandlerAggregator.t.sol | 0 .../hub-suite/Invariants.t.sol | 0 .../hub-suite/README.md | 0 .../hub-suite/Setup.t.sol | 0 .../hub-suite/SpecAggregator.t.sol | 0 .../hub-suite/Tester.t.sol | 0 .../hub-suite/_config/echidna_config.yaml | 2 +- .../hub-suite/_config/echidna_config_ci.yaml | 0 .../hub-suite/base/BaseHandler.t.sol | 0 .../hub-suite/base/BaseHooks.t.sol | 0 .../hub-suite/base/BaseStorage.t.sol | 0 .../hub-suite/base/BaseTest.t.sol | 0 .../hub-suite/base/ProtocolAssertions.t.sol | 0 .../handlers/HubConfiguratorHandler.t.sol | 0 .../hub-suite/handlers/HubHandler.t.sol | 0 .../interfaces/IHubConfiguratorHandler.sol | 0 .../handlers/interfaces/IHubHandler.sol | 0 .../simulators/DonationAttackHandler.t.sol | 0 .../hooks/DefaultBeforeAfterHooks.t.sol | 0 .../hub-suite/hooks/HookAggregator.t.sol | 0 .../hub-suite/invariants/HubInvariants.t.sol | 2 +- .../hub-suite/replays/ReplayTest_1.t.sol | 0 .../hub-suite/replays/ReplayTest_2.t.sol | 0 .../hub-suite/replays/ReplayTest_3.t.sol | 0 .../hub-suite/replays/ReplayTest_4.t.sol | 0 .../hub-suite/replays/ReplayTest_5.t.sol | 0 .../hub-suite/replays/ReplayTest_6.t.sol | 0 .../hub-suite/replays/ReplayTest_7.t.sol | 0 .../hub-suite/replays/ReplayTest_8.t.sol | 0 .../hub-suite/replays/ReplayTest_9.t.sol | 0 .../hub-suite/specs/HubInvariantsSpec.t.sol | 0 .../specs/HubPostconditionsSpec.t.sol | 0 .../hub-suite/utils/PropertiesConstants.sol | 0 .../hub-suite/utils/StdAsserts.sol | 0 .../protocol-suite/HandlerAggregator.t.sol | 0 .../protocol-suite/Invariants.t.sol | 0 .../protocol-suite/README.md | 0 .../protocol-suite/Setup.t.sol | 0 .../protocol-suite/SpecAggregator.t.sol | 0 .../protocol-suite/TODO | 30 +- .../protocol-suite/Tester.t.sol | 0 .../protocol-suite/TesterFoundry.t.sol | 2 +- .../_config/echidna_config.yaml | 2 +- .../_config/echidna_config_ci.yaml | 0 .../protocol-suite/base/BaseHandler.t.sol | 0 .../protocol-suite/base/BaseHooks.t.sol | 0 .../protocol-suite/base/BaseStorage.t.sol | 0 .../protocol-suite/base/BaseTest.t.sol | 0 .../base/ProtocolAssertions.t.sol | 0 .../handlers/hub/HubConfiguratorHandler.t.sol | 0 .../interfaces/IHubConfiguratorHandler.sol | 0 .../handlers/interfaces/IHubHandler.sol | 0 .../interfaces/ISpokeConfiguratorHandler.sol | 0 .../handlers/interfaces/ISpokeHandler.sol | 0 .../interfaces/ITreasurySpokeHandler.sol | 0 .../simulators/DonationAttackHandler.t.sol | 0 .../PriceFeedSimulatorHandler.t.sol | 0 .../spoke/SpokeConfiguratorHandler.t.sol | 0 .../handlers/spoke/SpokeHandler.t.sol | 25 +- .../handlers/spoke/TreasurySpokeHandler.t.sol | 13 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 0 .../protocol-suite/hooks/HookAggregator.t.sol | 0 .../invariants/HubInvariants.t.sol | 3 +- .../invariants/SpokeInvariants.t.sol | 4 +- .../protocol-suite/replays/ReplayTest_1.t.sol | 0 .../protocol-suite/replays/ReplayTest_2.t.sol | 0 .../protocol-suite/replays/ReplayTest_3.t.sol | 0 .../protocol-suite/replays/ReplayTest_4.t.sol | 0 .../protocol-suite/replays/ReplayTest_5.t.sol | 0 .../protocol-suite/replays/ReplayTest_6.t.sol | 0 .../protocol-suite/replays/ReplayTest_7.t.sol | 139 +++++++ .../protocol-suite/specs/InvariantsSpec.t.sol | 0 .../specs/PostconditionsSpec.t.sol | 0 .../shared/mocks/MockPriceFeedSimulator.sol | 0 .../shared/remote/DOCKERFILE | 0 .../shared/utils/Actor.sol | 0 .../shared/utils/ActorsUtils.sol | 0 .../shared/utils/CREATE3.sol | 0 .../shared/utils/DeployPermit2.sol | 0 .../shared/utils/ErrorHandlers.sol | 0 .../shared/utils/PropertiesAsserts.sol | 0 .../shared/utils/PropertiesConstants.sol | 0 .../shared/utils/StdAsserts.sol | 0 86 files changed, 191 insertions(+), 407 deletions(-) delete mode 100644 tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_7.t.sol rename tests/{enigma-dark-invariants => invariants}/hub-suite/HandlerAggregator.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/Invariants.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/README.md (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/Setup.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/SpecAggregator.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/Tester.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/_config/echidna_config.yaml (95%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/_config/echidna_config_ci.yaml (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/base/BaseHandler.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/base/BaseHooks.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/base/BaseStorage.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/base/BaseTest.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/base/ProtocolAssertions.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/handlers/HubConfiguratorHandler.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/handlers/HubHandler.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/handlers/interfaces/IHubHandler.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/handlers/simulators/DonationAttackHandler.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/hooks/HookAggregator.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/invariants/HubInvariants.t.sol (99%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/replays/ReplayTest_1.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/replays/ReplayTest_2.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/replays/ReplayTest_3.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/replays/ReplayTest_4.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/replays/ReplayTest_5.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/replays/ReplayTest_6.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/replays/ReplayTest_7.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/replays/ReplayTest_8.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/replays/ReplayTest_9.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/specs/HubInvariantsSpec.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/specs/HubPostconditionsSpec.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/utils/PropertiesConstants.sol (100%) rename tests/{enigma-dark-invariants => invariants}/hub-suite/utils/StdAsserts.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/HandlerAggregator.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/Invariants.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/README.md (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/Setup.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/SpecAggregator.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/TODO (63%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/Tester.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/TesterFoundry.t.sol (94%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/_config/echidna_config.yaml (95%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/_config/echidna_config_ci.yaml (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/base/BaseHandler.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/base/BaseHooks.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/base/BaseStorage.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/base/BaseTest.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/base/ProtocolAssertions.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/interfaces/IHubHandler.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/interfaces/ISpokeHandler.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/spoke/SpokeHandler.t.sol (94%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol (92%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/hooks/HookAggregator.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/invariants/HubInvariants.t.sol (98%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/invariants/SpokeInvariants.t.sol (94%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/replays/ReplayTest_1.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/replays/ReplayTest_2.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/replays/ReplayTest_3.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/replays/ReplayTest_4.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/replays/ReplayTest_5.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/replays/ReplayTest_6.t.sol (100%) create mode 100644 tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol rename tests/{enigma-dark-invariants => invariants}/protocol-suite/specs/InvariantsSpec.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/protocol-suite/specs/PostconditionsSpec.t.sol (100%) rename tests/{enigma-dark-invariants => invariants}/shared/mocks/MockPriceFeedSimulator.sol (100%) rename tests/{enigma-dark-invariants => invariants}/shared/remote/DOCKERFILE (100%) rename tests/{enigma-dark-invariants => invariants}/shared/utils/Actor.sol (100%) rename tests/{enigma-dark-invariants => invariants}/shared/utils/ActorsUtils.sol (100%) rename tests/{enigma-dark-invariants => invariants}/shared/utils/CREATE3.sol (100%) rename tests/{enigma-dark-invariants => invariants}/shared/utils/DeployPermit2.sol (100%) rename tests/{enigma-dark-invariants => invariants}/shared/utils/ErrorHandlers.sol (100%) rename tests/{enigma-dark-invariants => invariants}/shared/utils/PropertiesAsserts.sol (100%) rename tests/{enigma-dark-invariants => invariants}/shared/utils/PropertiesConstants.sol (100%) rename tests/{enigma-dark-invariants => invariants}/shared/utils/StdAsserts.sol (100%) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index b93cbfca6..5972a5450 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -58,11 +58,11 @@ jobs: path: | out cache - key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/enigma-dark-invariants/**/*.sol') }} + key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/invariants/**/*.sol') }} - name: Build contracts (skip if cache hit) if: steps.restore-build.outputs.cache-hit != 'true' - run: forge build tests/enigma-dark-invariants/Tester.t.sol --build-info + run: forge build tests/invariants/Tester.t.sol --build-info - name: Cache build artifacts if: steps.restore-build.outputs.cache-hit != 'true' @@ -72,7 +72,7 @@ jobs: path: | out cache - key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/enigma-dark-invariants/**/*.sol') }} + key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/invariants/**/*.sol') }} echidna: name: Echidna-Invariants (${{ matrix.mode }}) Enigma-Dark @@ -101,12 +101,12 @@ jobs: path: | out cache - key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/enigma-dark-invariants/**/*.sol') }} + key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/invariants/**/*.sol') }} - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode uses: crytic/echidna-action@v2 with: files: . contract: Tester - config: tests/enigma-dark-invariants/_config/echidna_config_ci.yaml + config: tests/invariants/_config/echidna_config_ci.yaml test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} \ No newline at end of file diff --git a/Makefile b/Makefile index cb4b04d65..2b54cda8c 100644 --- a/Makefile +++ b/Makefile @@ -32,18 +32,18 @@ coverage : # Echidna echidna: - echidna tests/enigma-dark-invariants/protocol-suite/Tester.t.sol --contract Tester --config ./tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml + echidna tests/invariants/protocol-suite/Tester.t.sol --contract Tester --config ./tests/invariants/protocol-suite/_config/echidna_config.yaml echidna-assert: - echidna tests/enigma-dark-invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml + echidna tests/invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/invariants/protocol-suite/_config/echidna_config.yaml echidna-explore: - echidna tests/enigma-dark-invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml + echidna tests/invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/invariants/protocol-suite/_config/echidna_config.yaml echidna-hub: - echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml + echidna tests/invariants/hub-suite/Tester.t.sol --contract Tester --config ./tests/invariants/hub-suite/_config/echidna_config.yaml echidna-hub-assert: - echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml + echidna tests/invariants/hub-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/invariants/hub-suite/_config/echidna_config.yaml echidna-hub-explore: - echidna tests/enigma-dark-invariants/hub-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml + echidna tests/invariants/hub-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/invariants/hub-suite/_config/echidna_config.yaml # Medusa medusa: @@ -56,11 +56,11 @@ foundry-invariants: # Results runes-echidna: - runes convert ./tests/enigma-dark-invariants/protocol-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/enigma-dark-invariants/protocol-suite/replays + runes convert ./tests/invariants/protocol-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/invariants/protocol-suite/replays runes-medusa: - runes convert ./tests/enigma-dark-invariants/protocol-suite/_corpus/medusa/ --output ./tests/enigma-dark-invariants/protocol-suite/replays + runes convert ./tests/invariants/protocol-suite/_corpus/medusa/ --output ./tests/invariants/protocol-suite/replays runes-echidna-hub: - runes convert ./tests/enigma-dark-invariants/hub-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/enigma-dark-invariants/hub-suite/replays + runes convert ./tests/invariants/hub-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/invariants/hub-suite/replays runes-medusa-hub: - runes convert ./tests/enigma-dark-invariants/hub-suite/_corpus/medusa/ --output ./tests/enigma-dark-invariants/hub-suite/replays \ No newline at end of file + runes convert ./tests/invariants/hub-suite/_corpus/medusa/ --output ./tests/invariants/hub-suite/replays \ No newline at end of file diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_7.t.sol b/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_7.t.sol deleted file mode 100644 index 165aa2290..000000000 --- a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_7.t.sol +++ /dev/null @@ -1,346 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; - -// Utils -import {Actor} from "../../shared/utils/Actor.sol"; - -contract ReplayTest7 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest7 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_7_updateUserRiskPremium() public { - _setUpActor(USER3); - Tester.supply(790, 128, 71, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 203, 9); - _setUpActor(USER1); - _delay(1812425); - _delay(320876); - Tester.updateFrozen(false, 59, 85); - _setUpActor(USER2); - _delay(343393); - Tester.updateHealthFactorForMaxBonus(105, 146); - _setUpActor(USER1); - _delay(2272303); - _setUpActor(USER2); - _delay(62189); - Tester.updateUserRiskPremium(101); - _setUpActor(USER3); - _delay(590687); - Tester.setUsingAsCollateral(true, 111, 253); - _delay(323286); - Tester.donateUnderlyingToHub(194, 231, 231); - _delay(505810); - Tester.borrow(56, 77, 155, 21); - _setUpActor(USER2); - _delay(112744); - Tester.updateBorrowable(false, 223, 157); - _setUpActor(USER1); - _delay(1632525); - _setUpActor(USER2); - _delay(128066); - Tester.updateFrozen(false, 52, 208); - _delay(180364); - Tester.setPrice(44015031536544474472307730349212877645270025151054012889336165188860863984788, 87); - _setUpActor(USER3); - _delay(460111); - Tester.updateBorrowable(true, 160, 11); - _setUpActor(USER1); - _delay(898801); - _setUpActor(USER3); - _delay(271962); - Tester.pauseAllReserves(251); - _setUpActor(USER2); - _delay(928317); - _setUpActor(USER1); - _delay(23908); - Tester.updateBorrowable(true, 217, 53); - _setUpActor(USER3); - _delay(582766); - Tester.updateUserRiskPremium(25); - } - - function test_replay_7_repay() public { - _setUpActor(USER3); - _delay(321376); - Tester.supply(790, 197, 87, 201); - Tester.supply(100000000000000000000000002, 50, 228, 214); - Tester.setUsingAsCollateral(true, 197, 253); - _delay(20833); - Tester.borrow(2973933138, 65, 107, 14); - _setUpActor(USER1); - Tester.updateSpokeSupplyCap(172, 180, 253, 253); - _setUpActor(USER3); - _delay(997); - Tester.repay(1209722426464509529070304541882533570806727645123886685504166337177518312, 62, 253, 254); - } - - function test_replay_7_withdraw() public { - _setUpActor(USER1); - Tester.withdraw(1, 0, 0, 0); - } - - function test_replay_7_setUsingAsCollateral() public { - _setUpActor(USER3); - Tester.supply(790, 128, 71, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 203, 9); - _setUpActor(USER1); - _delay(338347); - Tester.freezeAllReserves(32); - _delay(322337); - Tester.updatePaused(false, 67, 125); - _delay(572077); - Tester.updateBorrowable(true, 243, 99); - _delay(588832); - _setUpActor(USER2); - _delay(322214); - Tester.donateUnderlyingToHub(3285266288, 36, 25); - _setUpActor(USER1); - _delay(186977); - _delay(427128); - Tester.updateFrozen(false, 82, 38); - _delay(320876); - Tester.updateFrozen(false, 59, 81); - _delay(119298); - Tester.updateFrozen(false, 232, 62); - _delay(472666); - _delay(256656); - Tester.updatePaused(false, 228, 146); - _delay(322373); - _delay(590687); - Tester.setUsingAsCollateral(true, 111, 253); - _delay(510341); - _setUpActor(USER2); - _delay(440321); - Tester.freezeAllReserves(102); - _setUpActor(USER3); - _delay(572099); - Tester.pauseAllReserves(16); - _setUpActor(USER1); - _delay(323286); - Tester.donateUnderlyingToHub(194, 231, 231); - _setUpActor(USER2); - _delay(222666); - Tester.updatePaused(false, 55, 68); - _delay(1570291); - _delay(66387); - Tester.updatePaused(true, 51, 192); - _delay(232282); - Tester.freezeAllReserves(63); - _setUpActor(USER1); - _delay(634311); - _delay(329177); - Tester.donateUnderlyingToSpoke( - 8482560104382015275375946442131157991925236995011038634869166413646018956770, 192, 19 - ); - _setUpActor(USER2); - _delay(235558); - Tester.updateBorrowable(true, 251, 0); - _setUpActor(USER1); - _delay(759555); - _delay(181708); - Tester.updateLiquidationBonusFactor(779, 210); - _delay(186473); - Tester.updateBorrowable(false, 49, 5); - _delay(442967); - Tester.updateBorrowable(false, 60, 7); - _delay(1282001); - _delay(34027); - Tester.updateBorrowable(false, 12, 185); - _delay(542285); - _setUpActor(USER2); - _delay(112744); - Tester.updateBorrowable(false, 223, 157); - _delay(223839); - _setUpActor(USER3); - _delay(598111); - Tester.freezeAllReserves(22); - _setUpActor(USER1); - _delay(1518691); - _delay(125389); - Tester.pauseAllReserves(202); - _setUpActor(USER3); - _delay(145762); - Tester.updateFrozen(true, 34, 251); - _setUpActor(USER1); - _delay(274181); - Tester.updateSpokeSupplyCap(557, 21, 5, 251); - _setUpActor(USER2); - _delay(146364); - Tester.updatePaused(true, 153, 153); - _setUpActor(USER1); - _delay(448413); - Tester.updatePaused(false, 37, 158); - _setUpActor(USER3); - _delay(888942); - _setUpActor(USER2); - _delay(423178); - Tester.updateSpokePaused(true, 204, 232, 13); - _setUpActor(USER1); - _delay(332581); - Tester.updateSpokePaused(true, 27, 135, 207); - _delay(901860); - _setUpActor(USER2); - _delay(213701); - Tester.updateUserDynamicConfig(196); - _setUpActor(USER1); - _delay(180364); - Tester.setPrice(44015031536544474472307730349212877645270025151054012889336165188860863984788, 87); - _delay(460111); - Tester.updateBorrowable(true, 160, 11); - _setUpActor(USER2); - _delay(464489); - Tester.updateFrozen(true, 108, 29); - _setUpActor(USER1); - _delay(491000); - _delay(251970); - Tester.freezeAllReserves(3); - _setUpActor(USER2); - _delay(554942); - Tester.updateLiquidationTargetHealthFactor(1066876585488452360309478396, 181); - _setUpActor(USER1); - _delay(271962); - Tester.pauseAllReserves(251); - _setUpActor(USER2); - _delay(1178040); - _setUpActor(USER1); - _delay(322327); - Tester.setPrice(10091973037445179265966792088849362926006764080479895945907781515590410660983, 0); - _delay(334870); - Tester.updateSpokeDrawCap(282, 16, 27, 233); - _delay(181962); - Tester.updateUserRiskPremium(159); - _delay(4103101); - _delay(23908); - Tester.updateBorrowable(true, 217, 53); - _delay(1164972); - _setUpActor(USER3); - _delay(425952); - Tester.setPrice(49, 253); - _setUpActor(USER1); - _delay(115964); - Tester.updateUserRiskPremium(25); - _setUpActor(USER3); - _delay(322335); - _setUpActor(USER1); - _delay(116982); - Tester.updateBorrowable(false, 5, 162); - _delay(197173); - Tester.pauseAllReserves(165); - _setUpActor(USER3); - _delay(1603416); - _setUpActor(USER1); - _delay(373335); - Tester.donateUnderlyingToSpoke(251509, 90, 117); - _setUpActor(USER3); - _delay(209895); - Tester.updateFrozen(false, 59, 52); - _setUpActor(USER1); - _delay(1531689); - _delay(31623); - Tester.pauseAllReserves(78); - _delay(351918); - _delay(246382); - Tester.updateBorrowable(true, 0, 36); - _delay(81953); - Tester.donateUnderlyingToSpoke(1145407145531574156784556985, 233, 70); - _delay(361136); - _delay(70382); - Tester.setPrice(13722671120008796056116167155935139741016600691512816323271681734783393234974, 44); - _setUpActor(USER2); - _delay(158734); - _delay(252594); - Tester.setPrice(-10497, 28); - _setUpActor(USER3); - _delay(210182); - Tester.pauseAllReserves(157); - _setUpActor(USER1); - _delay(509009); - _setUpActor(USER3); - _delay(322329); - Tester.updateUserRiskPremium(158); - _setUpActor(USER1); - _delay(2400553); - _delay(595514); - Tester.setPrice(41373793859755415169263012240629611521716694833500847797235110793826563352795, 25); - _delay(565575); - _setUpActor(USER3); - _delay(457363); - Tester.freezeAllReserves(124); - _setUpActor(USER1); - _delay(293193); - _setUpActor(USER2); - Tester.updatePaused(false, 59, 104); - _setUpActor(USER1); - _delay(391851); - Tester.updateSpokePaused(false, 89, 161, 128); - _setUpActor(USER3); - _delay(546825); - Tester.updateSpokePaused(false, 247, 171, 126); - _delay(523414); - Tester.setUsingAsCollateral(false, 27, 0); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } -} diff --git a/tests/enigma-dark-invariants/hub-suite/HandlerAggregator.t.sol b/tests/invariants/hub-suite/HandlerAggregator.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/HandlerAggregator.t.sol rename to tests/invariants/hub-suite/HandlerAggregator.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/Invariants.t.sol b/tests/invariants/hub-suite/Invariants.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/Invariants.t.sol rename to tests/invariants/hub-suite/Invariants.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/README.md b/tests/invariants/hub-suite/README.md similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/README.md rename to tests/invariants/hub-suite/README.md diff --git a/tests/enigma-dark-invariants/hub-suite/Setup.t.sol b/tests/invariants/hub-suite/Setup.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/Setup.t.sol rename to tests/invariants/hub-suite/Setup.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/SpecAggregator.t.sol b/tests/invariants/hub-suite/SpecAggregator.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/SpecAggregator.t.sol rename to tests/invariants/hub-suite/SpecAggregator.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/Tester.t.sol b/tests/invariants/hub-suite/Tester.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/Tester.t.sol rename to tests/invariants/hub-suite/Tester.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml b/tests/invariants/hub-suite/_config/echidna_config.yaml similarity index 95% rename from tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml rename to tests/invariants/hub-suite/_config/echidna_config.yaml index 5d96c72bd..0ecc17a46 100644 --- a/tests/enigma-dark-invariants/hub-suite/_config/echidna_config.yaml +++ b/tests/invariants/hub-suite/_config/echidna_config.yaml @@ -42,7 +42,7 @@ coverage: true coverageFormats: [ "html" ] #directory to save the corpus; by default is disabled -corpusDir: "tests/enigma-dark-invariants/hub-suite/_corpus/echidna/default/_data/corpus" +corpusDir: "tests/invariants/hub-suite/_corpus/echidna/default/_data/corpus" # constants for corpus mutations (for experimentation only) #mutConsts: [100, 1, 1] diff --git a/tests/enigma-dark-invariants/hub-suite/_config/echidna_config_ci.yaml b/tests/invariants/hub-suite/_config/echidna_config_ci.yaml similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/_config/echidna_config_ci.yaml rename to tests/invariants/hub-suite/_config/echidna_config_ci.yaml diff --git a/tests/enigma-dark-invariants/hub-suite/base/BaseHandler.t.sol b/tests/invariants/hub-suite/base/BaseHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/base/BaseHandler.t.sol rename to tests/invariants/hub-suite/base/BaseHandler.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/base/BaseHooks.t.sol b/tests/invariants/hub-suite/base/BaseHooks.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/base/BaseHooks.t.sol rename to tests/invariants/hub-suite/base/BaseHooks.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol b/tests/invariants/hub-suite/base/BaseStorage.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/base/BaseStorage.t.sol rename to tests/invariants/hub-suite/base/BaseStorage.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/base/BaseTest.t.sol b/tests/invariants/hub-suite/base/BaseTest.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/base/BaseTest.t.sol rename to tests/invariants/hub-suite/base/BaseTest.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/base/ProtocolAssertions.t.sol b/tests/invariants/hub-suite/base/ProtocolAssertions.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/base/ProtocolAssertions.t.sol rename to tests/invariants/hub-suite/base/ProtocolAssertions.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol rename to tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol b/tests/invariants/hub-suite/handlers/HubHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/handlers/HubHandler.t.sol rename to tests/invariants/hub-suite/handlers/HubHandler.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/tests/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol rename to tests/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/tests/invariants/hub-suite/handlers/interfaces/IHubHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/handlers/interfaces/IHubHandler.sol rename to tests/invariants/hub-suite/handlers/interfaces/IHubHandler.sol diff --git a/tests/enigma-dark-invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol b/tests/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol rename to tests/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol rename to tests/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol b/tests/invariants/hub-suite/hooks/HookAggregator.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/hooks/HookAggregator.t.sol rename to tests/invariants/hub-suite/hooks/HookAggregator.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol b/tests/invariants/hub-suite/invariants/HubInvariants.t.sol similarity index 99% rename from tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol rename to tests/invariants/hub-suite/invariants/HubInvariants.t.sol index 11beb0592..55d6c6a5c 100644 --- a/tests/enigma-dark-invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/tests/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -75,7 +75,7 @@ abstract contract HubInvariants is HandlerAggregator { uint256 totalDebt = hub.getAssetTotalOwed(assetId); // Checks - assertApproxEqAbs( + assertApproxEqAbs( //TODO check todo file INV_HUB_E totalSuppliedAssets, hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)), hub.previewRemoveByShares(assetId, 1), // tolerance of 1 share for rounding diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_1.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_1.t.sol rename to tests/invariants/hub-suite/replays/ReplayTest_1.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_2.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_2.t.sol rename to tests/invariants/hub-suite/replays/ReplayTest_2.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_3.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_3.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_3.t.sol rename to tests/invariants/hub-suite/replays/ReplayTest_3.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_4.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_4.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_4.t.sol rename to tests/invariants/hub-suite/replays/ReplayTest_4.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_5.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_5.t.sol rename to tests/invariants/hub-suite/replays/ReplayTest_5.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_6.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_6.t.sol rename to tests/invariants/hub-suite/replays/ReplayTest_6.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_7.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_7.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_7.t.sol rename to tests/invariants/hub-suite/replays/ReplayTest_7.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_8.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_8.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_8.t.sol rename to tests/invariants/hub-suite/replays/ReplayTest_8.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_9.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_9.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/replays/ReplayTest_9.t.sol rename to tests/invariants/hub-suite/replays/ReplayTest_9.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/tests/invariants/hub-suite/specs/HubInvariantsSpec.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/specs/HubInvariantsSpec.t.sol rename to tests/invariants/hub-suite/specs/HubInvariantsSpec.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/specs/HubPostconditionsSpec.t.sol b/tests/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/specs/HubPostconditionsSpec.t.sol rename to tests/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol diff --git a/tests/enigma-dark-invariants/hub-suite/utils/PropertiesConstants.sol b/tests/invariants/hub-suite/utils/PropertiesConstants.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/utils/PropertiesConstants.sol rename to tests/invariants/hub-suite/utils/PropertiesConstants.sol diff --git a/tests/enigma-dark-invariants/hub-suite/utils/StdAsserts.sol b/tests/invariants/hub-suite/utils/StdAsserts.sol similarity index 100% rename from tests/enigma-dark-invariants/hub-suite/utils/StdAsserts.sol rename to tests/invariants/hub-suite/utils/StdAsserts.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/HandlerAggregator.t.sol b/tests/invariants/protocol-suite/HandlerAggregator.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/HandlerAggregator.t.sol rename to tests/invariants/protocol-suite/HandlerAggregator.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol b/tests/invariants/protocol-suite/Invariants.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/Invariants.t.sol rename to tests/invariants/protocol-suite/Invariants.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/README.md b/tests/invariants/protocol-suite/README.md similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/README.md rename to tests/invariants/protocol-suite/README.md diff --git a/tests/enigma-dark-invariants/protocol-suite/Setup.t.sol b/tests/invariants/protocol-suite/Setup.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/Setup.t.sol rename to tests/invariants/protocol-suite/Setup.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/SpecAggregator.t.sol b/tests/invariants/protocol-suite/SpecAggregator.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/SpecAggregator.t.sol rename to tests/invariants/protocol-suite/SpecAggregator.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/TODO b/tests/invariants/protocol-suite/TODO similarity index 63% rename from tests/enigma-dark-invariants/protocol-suite/TODO rename to tests/invariants/protocol-suite/TODO index 6a15c78fb..345be048a 100644 --- a/tests/enigma-dark-invariants/protocol-suite/TODO +++ b/tests/invariants/protocol-suite/TODO @@ -1,5 +1,6 @@ # Replay Tests to Implement +# INV_HUB_E ## test_replay_3_add - Status: FAILING - Invariant: `INV_HUB_E` @@ -7,54 +8,37 @@ - Details: Left: 8, Right: 6, Max Delta: 1, Actual Delta: 2 - Context: After supply/borrow with time delay, the addedAssets vs previewRemoveByShares diverges by more than the allowed 1 share precision loss -## test_replay_3_setUsingAsCollateral -- Status: FAILING -- Invariant: `INV_HUB_E` -- Error: `hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss` -- Details: Left: 792, Right: 790, Max Delta: 1, Actual Delta: 2 -- Context: After supply/borrow with time delay, the addedAssets vs previewRemoveByShares diverges by more than the allowed 1 share precision loss - ## test_replay_5_donateUnderlyingToHub - Status: FAILING - Invariant: `INV_HUB_E` - Error: `hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss` - Details: Left: 4, Right: 0, Max Delta: 1, Actual Delta: 4 -## test_replay_6_supply -- Status: FAILING -- Invariant: `INV_SP_F` -- Error: `Sum of user supplied assets on a spoke for a given asset == spoke supplied assets (hub spoke added assets)` -- Details: 1327429017 != 1327429018 (off by 1) -- Context: After multiple supply/borrow operations with time delays, user supplied assets sum doesn't match spoke added assets -- TODO: Write optimization handler for this - ## test_replay_6_freezeAllReserves - Status: FAILING - Invariant: `INV_HUB_E` - Error: `hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss` - Details: Left: 676, Right: 673, Max Delta: 1, Actual Delta: 3 - -## test_replay_7_withdraw -- Status: FAILING -- Invariant: `GPOST_SP_H` -- Error: `if user totalDebt == 0 and withdraw is called, user can withdraw all supplied` -- Details: withdraw(1,0,0,0) reverts with `InvalidAmount()` instead of succeeding -- Context: Postcondition expects withdraw to succeed when user has no debt - + +# GPOST_HUB_E ## test_replay_7_repay - Status: FAILING - Invariant: `GPOST_HUB_E` - Error: `if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)` - Details: 100000000000000000000000977 > 172000000000000000000 (exceeds cap) - Context: After repay, addedAssets exceeds the configured add cap +- TODO: check if totalAssets increasing is expected behaviour on repayment due to rounding +# GPOST_SP_LIQ_H ## test_replay_7_updateUserRiskPremium - Status: FAILING - Invariant: `GPOST_SP_LIQ_H` - Error: `Only a supply, repay & liquidationCall can leave an account in an unhealthy state` - Details: 998420221169036334 < 1000000000000000000 (HF < 1e18) - Context: After updateUserRiskPremium, user's health factor drops below 1 +- TODO: check if updateUserRiskPremium should enforce any health factor constraints +# INV_HUB_ERC4626_C ## test_replay_9_eliminateDeficit (hub-suite) - Status: FAILING - Invariant: `INV_HUB_ERC4626_C` diff --git a/tests/enigma-dark-invariants/protocol-suite/Tester.t.sol b/tests/invariants/protocol-suite/Tester.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/Tester.t.sol rename to tests/invariants/protocol-suite/Tester.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/TesterFoundry.t.sol b/tests/invariants/protocol-suite/TesterFoundry.t.sol similarity index 94% rename from tests/enigma-dark-invariants/protocol-suite/TesterFoundry.t.sol rename to tests/invariants/protocol-suite/TesterFoundry.t.sol index 0ffe52372..722147488 100644 --- a/tests/enigma-dark-invariants/protocol-suite/TesterFoundry.t.sol +++ b/tests/invariants/protocol-suite/TesterFoundry.t.sol @@ -13,7 +13,7 @@ contract TesterFoundry is Invariants, Setup, StdInvariant { /// forge-config: default.invariant.fail-on-revert = false /// forge-config: default.invariant.runs = 1000 /// forge-config: default.invariant.depth = 100 - /// forge-config: default.invariant.corpus-dir = "tests/enigma-dark-invariants/_corpus/foundry" + /// forge-config: default.invariant.corpus-dir = "tests/invariants/_corpus/foundry" /// forge-config: default.invariant.show-solidity = true /// forge-config: default.invariant.show-metrics = true /// forge-config: default.fuzz.seed = '0x1' diff --git a/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml b/tests/invariants/protocol-suite/_config/echidna_config.yaml similarity index 95% rename from tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml rename to tests/invariants/protocol-suite/_config/echidna_config.yaml index 49442d489..b846f8dde 100644 --- a/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config.yaml +++ b/tests/invariants/protocol-suite/_config/echidna_config.yaml @@ -42,7 +42,7 @@ coverage: true coverageFormats: [ "html" ] #directory to save the corpus; by default is disabled -corpusDir: "tests/enigma-dark-invariants/protocol-suite/_corpus/echidna/default/_data/corpus" +corpusDir: "tests/invariants/protocol-suite/_corpus/echidna/default/_data/corpus" # constants for corpus mutations (for experimentation only) #mutConsts: [100, 1, 1] diff --git a/tests/enigma-dark-invariants/protocol-suite/_config/echidna_config_ci.yaml b/tests/invariants/protocol-suite/_config/echidna_config_ci.yaml similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/_config/echidna_config_ci.yaml rename to tests/invariants/protocol-suite/_config/echidna_config_ci.yaml diff --git a/tests/enigma-dark-invariants/protocol-suite/base/BaseHandler.t.sol b/tests/invariants/protocol-suite/base/BaseHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/base/BaseHandler.t.sol rename to tests/invariants/protocol-suite/base/BaseHandler.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/base/BaseHooks.t.sol b/tests/invariants/protocol-suite/base/BaseHooks.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/base/BaseHooks.t.sol rename to tests/invariants/protocol-suite/base/BaseHooks.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol b/tests/invariants/protocol-suite/base/BaseStorage.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/base/BaseStorage.t.sol rename to tests/invariants/protocol-suite/base/BaseStorage.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/base/BaseTest.t.sol b/tests/invariants/protocol-suite/base/BaseTest.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/base/BaseTest.t.sol rename to tests/invariants/protocol-suite/base/BaseTest.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/base/ProtocolAssertions.t.sol b/tests/invariants/protocol-suite/base/ProtocolAssertions.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/base/ProtocolAssertions.t.sol rename to tests/invariants/protocol-suite/base/ProtocolAssertions.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol b/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol rename to tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/tests/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol rename to tests/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubHandler.sol b/tests/invariants/protocol-suite/handlers/interfaces/IHubHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/IHubHandler.sol rename to tests/invariants/protocol-suite/handlers/interfaces/IHubHandler.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol b/tests/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol rename to tests/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol b/tests/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol rename to tests/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol b/tests/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol rename to tests/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol b/tests/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol rename to tests/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol b/tests/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol rename to tests/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol b/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol rename to tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol similarity index 94% rename from tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol rename to tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index b7c987371..41440073e 100644 --- a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -16,6 +16,7 @@ import {BaseHandler} from "../../base/BaseHandler.t.sol"; // Contracts import {Spoke} from "src/spoke/Spoke.sol"; +import {IHub} from "src/hub/interfaces/IHub.sol"; /// @title SpokeHandler /// @notice Handler test contract for a set of actions @@ -69,7 +70,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (success) { _after(); } else { - revert("DefaultHandler: supply failed"); + revert("SpokeHandler: supply failed"); } } @@ -85,6 +86,9 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { // Get one of the reserves IDs randomly uint256 reserveId = _getRandomReserveId(spoke, k); + uint256 userAmount = IHub(_getHubAddress(spoke, reserveId)) + .previewRemoveByShares(reserveId, ISpoke(spoke).getUserSuppliedShares(reserveId, onBehalfOf)); + // Register user to check postconditions _registerUserToCheck(spoke, reserveId, onBehalfOf); @@ -92,14 +96,15 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.withdraw, (reserveId, amount, onBehalfOf))); // Implemented outside the success check to assert success - if (defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt == 0 && amount > 0) { + if (defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt == 0 && (amount > 0 && userAmount != 0)) + { assertTrue(success, GPOST_SP_H); } if (success) { _after(); } else { - revert("DefaultHandler: withdraw failed"); + revert("SpokeHandler: withdraw failed"); } } @@ -131,7 +136,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertTrue(isHealthy, HSPOST_SP_D); } else { - revert("DefaultHandler: borrow failed"); + revert("SpokeHandler: borrow failed"); } } @@ -164,7 +169,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { HSPOST_SP_C ); } else { - revert("DefaultHandler: repay failed"); + revert("SpokeHandler: repay failed"); } } @@ -291,7 +296,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ); } } else { - revert("DefaultHandler: liquidationCall failed"); + revert("SpokeHandler: liquidationCall failed"); } } @@ -307,7 +312,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { (bool isUsingAsCollateral,) = ISpoke(spoke).getUserReserveStatus(reserveId, onBehalfOf); - require(usingAsCollateral != isUsingAsCollateral, "DefaultHandler: usingAsCollateral already set"); + require(usingAsCollateral != isUsingAsCollateral, "SpokeHandler: usingAsCollateral already set"); // Register user to check postconditions /// @dev setUsingAsCollateral(reserveId, FALSE) all reserves in user position should be refreshed, @@ -323,7 +328,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (success) { _after(); } else { - revert("DefaultHandler: setUsingAsCollateral failed"); + revert("SpokeHandler: setUsingAsCollateral failed"); } } @@ -349,7 +354,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ///// HSPOST ///// assertEq(_getTotalDebt(spoke, onBehalfOf), totalDebt, HSPOST_SP_F); } else { - revert("DefaultHandler: updateUserRiskPremium failed"); + revert("SpokeHandler: updateUserRiskPremium failed"); } } @@ -369,7 +374,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (success) { _after(); } else { - revert("DefaultHandler: updateUserDynamicConfig failed"); + revert("SpokeHandler: updateUserDynamicConfig failed"); } } diff --git a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol b/tests/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol similarity index 92% rename from tests/enigma-dark-invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol rename to tests/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol index 5327c5e88..7f48d9e20 100644 --- a/tests/enigma-dark-invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol @@ -33,8 +33,9 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { // OWNER ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - function supply(uint256 amount, uint8 i, uint8 j) external {// TODO fix coverage issues - // Get one of the hub addresses randomly + function supply(uint256 amount, uint8 i, uint8 j) external { + // TODO fix coverage issues + // Get one of the hub addresses randomly address hubAddress = _getRandomHub(i); address treasurySpoke = hubInfo[hubAddress].treasurySpoke; @@ -45,7 +46,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { try ITreasurySpoke(treasurySpoke).supply(reserveId, amount, msg.sender) { _after(); } catch { - revert("DefaultHandler: supply failed"); + revert("TreasurySpokeHandler: supply failed"); } } @@ -53,7 +54,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { // Get one of the hub addresses randomly address hubAddress = _getRandomHub(i); address treasurySpoke = hubInfo[hubAddress].treasurySpoke; - + // Get one of the reserves IDs randomly uint256 reserveId = _getRandomReserveId(treasurySpoke, j); @@ -61,7 +62,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { try ITreasurySpoke(treasurySpoke).withdraw(reserveId, amount, msg.sender) { _after(); } catch { - revert("DefaultHandler: withdraw failed"); + revert("TreasurySpokeHandler: withdraw failed"); } } @@ -79,7 +80,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { try ITreasurySpoke(hubInfo[hubAddress].treasurySpoke).transfer(asset, to, amount) { _after(); } catch { - revert("DefaultHandler: transfer failed"); + revert("TreasurySpokeHandler: transfer failed"); } } diff --git a/tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol rename to tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol b/tests/invariants/protocol-suite/hooks/HookAggregator.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/hooks/HookAggregator.t.sol rename to tests/invariants/protocol-suite/hooks/HookAggregator.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol b/tests/invariants/protocol-suite/invariants/HubInvariants.t.sol similarity index 98% rename from tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol rename to tests/invariants/protocol-suite/invariants/HubInvariants.t.sol index d4c591d81..9748eba71 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/tests/invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -76,8 +76,7 @@ abstract contract HubInvariants is HandlerAggregator { uint256 totalDebt = IHub(hubAddress).getAssetTotalOwed(assetId); uint256 accruedFees = IHub(hubAddress).getAssetAccruedFees(assetId); - // Checks - // Note: tolerance increased to 2 shares due to premium rounding accumulation + // Checks //TODO check todo file INV_HUB_E assertApproxEqAbs( totalSuppliedAssets, IHub(hubAddress).previewRemoveByShares(assetId, IHub(hubAddress).getAddedShares(assetId)), diff --git a/tests/enigma-dark-invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol similarity index 94% rename from tests/enigma-dark-invariants/protocol-suite/invariants/SpokeInvariants.t.sol rename to tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol index 4bca3cdd0..04855d780 100644 --- a/tests/enigma-dark-invariants/protocol-suite/invariants/SpokeInvariants.t.sol +++ b/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol @@ -90,6 +90,8 @@ abstract contract SpokeInvariants is HandlerAggregator { for (uint256 i; i < actorAddresses.length; i++) { sumUserAssets += ISpokeBase(spoke).getUserSuppliedAssets(reserveId, actorAddresses[i]); } - assertEq(sumUserAssets, ISpokeBase(spoke).getReserveSuppliedAssets(reserveId), INV_SP_F); + uint256 reserveSuppliedAssets = ISpokeBase(spoke).getReserveSuppliedAssets(reserveId); + assertLe(sumUserAssets, reserveSuppliedAssets, INV_SP_F); + assertApproxEqAbs(sumUserAssets, reserveSuppliedAssets, NUMBER_OF_ACTORS, INV_SP_F); } } diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_1.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_1.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_1.t.sol rename to tests/invariants/protocol-suite/replays/ReplayTest_1.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_2.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_2.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_2.t.sol rename to tests/invariants/protocol-suite/replays/ReplayTest_2.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_3.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_3.t.sol rename to tests/invariants/protocol-suite/replays/ReplayTest_3.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_4.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_4.t.sol rename to tests/invariants/protocol-suite/replays/ReplayTest_4.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_5.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_5.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_5.t.sol rename to tests/invariants/protocol-suite/replays/ReplayTest_5.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_6.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_6.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/replays/ReplayTest_6.t.sol rename to tests/invariants/protocol-suite/replays/ReplayTest_6.t.sol diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol new file mode 100644 index 000000000..8993c0ca3 --- /dev/null +++ b/tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Contracts +import {Invariants} from "../Invariants.t.sol"; +import {Setup} from "../Setup.t.sol"; + +// Utils +import {Actor} from "../../shared/utils/Actor.sol"; + +contract ReplayTest7 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest7 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_7_updateUserRiskPremium() public { + _setUpActor(USER3); + Tester.supply(790, 128, 71, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 203, 9); + _setUpActor(USER1); + _delay(1812425); + _delay(320876); + Tester.updateFrozen(false, 59, 85); + _setUpActor(USER2); + _delay(343393); + Tester.updateHealthFactorForMaxBonus(105, 146); + _setUpActor(USER1); + _delay(2272303); + _setUpActor(USER2); + _delay(62189); + Tester.updateUserRiskPremium(101); + _setUpActor(USER3); + _delay(590687); + Tester.setUsingAsCollateral(true, 111, 253); + _delay(323286); + Tester.donateUnderlyingToHub(194, 231, 231); + _delay(505810); + Tester.borrow(56, 77, 155, 21); + _setUpActor(USER2); + _delay(112744); + Tester.updateBorrowable(false, 223, 157); + _setUpActor(USER1); + _delay(1632525); + _setUpActor(USER2); + _delay(128066); + Tester.updateFrozen(false, 52, 208); + _delay(180364); + Tester.setPrice(44015031536544474472307730349212877645270025151054012889336165188860863984788, 87); + _setUpActor(USER3); + _delay(460111); + Tester.updateBorrowable(true, 160, 11); + _setUpActor(USER1); + _delay(898801); + _setUpActor(USER3); + _delay(271962); + Tester.pauseAllReserves(251); + _setUpActor(USER2); + _delay(928317); + _setUpActor(USER1); + _delay(23908); + Tester.updateBorrowable(true, 217, 53); + _setUpActor(USER3); + _delay(582766); + Tester.updateUserRiskPremium(25); + } + + function test_replay_7_repay() public { + _setUpActor(USER3); + _delay(321376); + Tester.supply(790, 197, 87, 201); + Tester.supply(100000000000000000000000002, 50, 228, 214); + Tester.setUsingAsCollateral(true, 197, 253); + _delay(20833); + Tester.borrow(2973933138, 65, 107, 14); + _setUpActor(USER1); + Tester.updateSpokeSupplyCap(172, 180, 253, 253); + _setUpActor(USER3); + _delay(997); + Tester.repay(1209722426464509529070304541882533570806727645123886685504166337177518312, 62, 253, 254); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol b/tests/invariants/protocol-suite/specs/InvariantsSpec.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/specs/InvariantsSpec.t.sol rename to tests/invariants/protocol-suite/specs/InvariantsSpec.t.sol diff --git a/tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/tests/invariants/protocol-suite/specs/PostconditionsSpec.t.sol similarity index 100% rename from tests/enigma-dark-invariants/protocol-suite/specs/PostconditionsSpec.t.sol rename to tests/invariants/protocol-suite/specs/PostconditionsSpec.t.sol diff --git a/tests/enigma-dark-invariants/shared/mocks/MockPriceFeedSimulator.sol b/tests/invariants/shared/mocks/MockPriceFeedSimulator.sol similarity index 100% rename from tests/enigma-dark-invariants/shared/mocks/MockPriceFeedSimulator.sol rename to tests/invariants/shared/mocks/MockPriceFeedSimulator.sol diff --git a/tests/enigma-dark-invariants/shared/remote/DOCKERFILE b/tests/invariants/shared/remote/DOCKERFILE similarity index 100% rename from tests/enigma-dark-invariants/shared/remote/DOCKERFILE rename to tests/invariants/shared/remote/DOCKERFILE diff --git a/tests/enigma-dark-invariants/shared/utils/Actor.sol b/tests/invariants/shared/utils/Actor.sol similarity index 100% rename from tests/enigma-dark-invariants/shared/utils/Actor.sol rename to tests/invariants/shared/utils/Actor.sol diff --git a/tests/enigma-dark-invariants/shared/utils/ActorsUtils.sol b/tests/invariants/shared/utils/ActorsUtils.sol similarity index 100% rename from tests/enigma-dark-invariants/shared/utils/ActorsUtils.sol rename to tests/invariants/shared/utils/ActorsUtils.sol diff --git a/tests/enigma-dark-invariants/shared/utils/CREATE3.sol b/tests/invariants/shared/utils/CREATE3.sol similarity index 100% rename from tests/enigma-dark-invariants/shared/utils/CREATE3.sol rename to tests/invariants/shared/utils/CREATE3.sol diff --git a/tests/enigma-dark-invariants/shared/utils/DeployPermit2.sol b/tests/invariants/shared/utils/DeployPermit2.sol similarity index 100% rename from tests/enigma-dark-invariants/shared/utils/DeployPermit2.sol rename to tests/invariants/shared/utils/DeployPermit2.sol diff --git a/tests/enigma-dark-invariants/shared/utils/ErrorHandlers.sol b/tests/invariants/shared/utils/ErrorHandlers.sol similarity index 100% rename from tests/enigma-dark-invariants/shared/utils/ErrorHandlers.sol rename to tests/invariants/shared/utils/ErrorHandlers.sol diff --git a/tests/enigma-dark-invariants/shared/utils/PropertiesAsserts.sol b/tests/invariants/shared/utils/PropertiesAsserts.sol similarity index 100% rename from tests/enigma-dark-invariants/shared/utils/PropertiesAsserts.sol rename to tests/invariants/shared/utils/PropertiesAsserts.sol diff --git a/tests/enigma-dark-invariants/shared/utils/PropertiesConstants.sol b/tests/invariants/shared/utils/PropertiesConstants.sol similarity index 100% rename from tests/enigma-dark-invariants/shared/utils/PropertiesConstants.sol rename to tests/invariants/shared/utils/PropertiesConstants.sol diff --git a/tests/enigma-dark-invariants/shared/utils/StdAsserts.sol b/tests/invariants/shared/utils/StdAsserts.sol similarity index 100% rename from tests/enigma-dark-invariants/shared/utils/StdAsserts.sol rename to tests/invariants/shared/utils/StdAsserts.sol From 66ec5573d09373ec3478a4a367347e85d251433d Mon Sep 17 00:00:00 2001 From: Elpacos Date: Thu, 15 Jan 2026 18:46:52 +0100 Subject: [PATCH 032/106] chore: improve test_replay_7_repay description --- tests/invariants/protocol-suite/TODO | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/invariants/protocol-suite/TODO b/tests/invariants/protocol-suite/TODO index 345be048a..989e2f02c 100644 --- a/tests/invariants/protocol-suite/TODO +++ b/tests/invariants/protocol-suite/TODO @@ -27,7 +27,7 @@ - Error: `if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)` - Details: 100000000000000000000000977 > 172000000000000000000 (exceeds cap) - Context: After repay, addedAssets exceeds the configured add cap -- TODO: check if totalAssets increasing is expected behaviour on repayment due to rounding +- TODO: check if totalAssets increasing is expected behaviour on repayment due to round up on repay amount, before: 100000000000000000000000976, after: 100000000000000000000000977, 1 wei difference # GPOST_SP_LIQ_H ## test_replay_7_updateUserRiskPremium From d0012beefc93f23a524a021290d2d7749c6af59b Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:27:09 +0530 Subject: [PATCH 033/106] chore: lint --- .github/workflows/enigma-dark-invariants.yml | 6 +- medusa.hub.json | 177 ++- medusa.protocol.json | 177 ++- .../hub-suite/HandlerAggregator.t.sol | 8 +- tests/invariants/hub-suite/Invariants.t.sol | 82 +- tests/invariants/hub-suite/README.md | 5 + tests/invariants/hub-suite/Setup.t.sol | 604 ++++----- .../invariants/hub-suite/SpecAggregator.t.sol | 4 +- tests/invariants/hub-suite/Tester.t.sol | 16 +- .../hub-suite/_config/echidna_config.yaml | 8 +- .../hub-suite/_config/echidna_config_ci.yaml | 8 +- .../hub-suite/base/BaseHandler.t.sol | 118 +- .../invariants/hub-suite/base/BaseHooks.t.sol | 10 +- .../hub-suite/base/BaseStorage.t.sol | 150 +-- .../invariants/hub-suite/base/BaseTest.t.sol | 220 ++-- .../hub-suite/base/ProtocolAssertions.t.sol | 4 +- .../handlers/HubConfiguratorHandler.t.sol | 93 +- .../hub-suite/handlers/HubHandler.t.sol | 673 +++++----- .../handlers/interfaces/IHubHandler.sol | 34 +- .../simulators/DonationAttackHandler.t.sol | 36 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 434 ++++--- .../hub-suite/hooks/HookAggregator.t.sol | 141 +- .../hub-suite/invariants/HubInvariants.t.sol | 475 ++++--- .../hub-suite/replays/ReplayTest_1.t.sol | 140 +- .../hub-suite/replays/ReplayTest_2.t.sol | 242 ++-- .../hub-suite/replays/ReplayTest_3.t.sol | 142 +- .../hub-suite/replays/ReplayTest_4.t.sol | 164 +-- .../hub-suite/replays/ReplayTest_5.t.sol | 138 +- .../hub-suite/replays/ReplayTest_6.t.sol | 232 ++-- .../hub-suite/replays/ReplayTest_7.t.sol | 138 +- .../hub-suite/replays/ReplayTest_8.t.sol | 136 +- .../hub-suite/replays/ReplayTest_9.t.sol | 138 +- .../hub-suite/specs/HubInvariantsSpec.t.sol | 136 +- .../specs/HubPostconditionsSpec.t.sol | 7 +- .../hub-suite/utils/PropertiesConstants.sol | 40 +- .../invariants/hub-suite/utils/StdAsserts.sol | 86 +- .../protocol-suite/HandlerAggregator.t.sol | 28 +- .../protocol-suite/Invariants.t.sol | 98 +- tests/invariants/protocol-suite/README.md | 10 +- tests/invariants/protocol-suite/Setup.t.sol | 1151 +++++++++-------- .../protocol-suite/SpecAggregator.t.sol | 56 +- tests/invariants/protocol-suite/Tester.t.sol | 22 +- .../protocol-suite/TesterFoundry.t.sol | 60 +- .../_config/echidna_config.yaml | 8 +- .../_config/echidna_config_ci.yaml | 8 +- .../protocol-suite/base/BaseHandler.t.sol | 120 +- .../protocol-suite/base/BaseHooks.t.sol | 10 +- .../protocol-suite/base/BaseStorage.t.sol | 292 ++--- .../protocol-suite/base/BaseTest.t.sol | 293 +++-- .../base/ProtocolAssertions.t.sol | 4 +- .../handlers/hub/HubConfiguratorHandler.t.sol | 97 +- .../handlers/interfaces/ISpokeHandler.sol | 23 +- .../simulators/DonationAttackHandler.t.sol | 70 +- .../PriceFeedSimulatorHandler.t.sol | 40 +- .../spoke/SpokeConfiguratorHandler.t.sol | 104 +- .../handlers/spoke/SpokeHandler.t.sol | 701 +++++----- .../handlers/spoke/TreasurySpokeHandler.t.sol | 112 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 625 ++++----- .../protocol-suite/hooks/HookAggregator.t.sol | 207 ++- .../invariants/HubInvariants.t.sol | 342 ++--- .../invariants/SpokeInvariants.t.sol | 149 ++- .../protocol-suite/replays/ReplayTest_1.t.sol | 170 +-- .../protocol-suite/replays/ReplayTest_2.t.sol | 152 +-- .../protocol-suite/replays/ReplayTest_3.t.sol | 144 +-- .../protocol-suite/replays/ReplayTest_4.t.sol | 142 +- .../protocol-suite/replays/ReplayTest_5.t.sol | 122 +- .../protocol-suite/replays/ReplayTest_6.t.sol | 166 +-- .../protocol-suite/replays/ReplayTest_7.t.sol | 264 ++-- .../protocol-suite/specs/InvariantsSpec.t.sol | 85 +- .../specs/PostconditionsSpec.t.sol | 117 +- .../shared/mocks/MockPriceFeedSimulator.sol | 86 +- tests/invariants/shared/utils/Actor.sol | 92 +- tests/invariants/shared/utils/ActorsUtils.sol | 76 +- tests/invariants/shared/utils/CREATE3.sol | 214 +-- .../invariants/shared/utils/DeployPermit2.sol | 22 +- .../invariants/shared/utils/ErrorHandlers.sol | 84 +- .../shared/utils/PropertiesAsserts.sol | 929 +++++++------ .../shared/utils/PropertiesConstants.sol | 52 +- tests/invariants/shared/utils/StdAsserts.sol | 917 ++++++------- 79 files changed, 7066 insertions(+), 6620 deletions(-) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index 5972a5450..b92d7f059 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -39,7 +39,7 @@ jobs: run: | git clone https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/aave/aave-v4-invariants-corpus.git corpus echo "sha=$(git -C corpus rev-parse HEAD)" >> $GITHUB_OUTPUT - + - name: Cache corpus uses: actions/cache@v4 with: @@ -63,7 +63,7 @@ jobs: - name: Build contracts (skip if cache hit) if: steps.restore-build.outputs.cache-hit != 'true' run: forge build tests/invariants/Tester.t.sol --build-info - + - name: Cache build artifacts if: steps.restore-build.outputs.cache-hit != 'true' id: save-build @@ -109,4 +109,4 @@ jobs: files: . contract: Tester config: tests/invariants/_config/echidna_config_ci.yaml - test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} \ No newline at end of file + test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} diff --git a/medusa.hub.json b/medusa.hub.json index b969efe22..cf8158f47 100644 --- a/medusa.hub.json +++ b/medusa.hub.json @@ -1,97 +1,82 @@ { - "fuzzing": { - "workers": 10, - "workerResetLimit": 50, - "timeout": 0, - "testLimit": 0, - "callSequenceLength": 100, - "corpusDirectory": "tests/enigma-dark-invariants/hub-suite/_corpus/medusa", - "coverageEnabled": true, - "deploymentOrder": [ - "Tester" - ], - "targetContracts": [ - "Tester" - ], - "targetContractsBalances": [ - "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" - ], - "predeployedContracts": { - "LiquidationLogic": "0xf01" - }, - "constructorArgs": {}, - "deployerAddress": "0x30000", - "senderAddresses": [ - "0x10000", - "0x20000", - "0x30000" - ], - "blockNumberDelayMax": 60480, - "blockTimestampDelayMax": 604800, - "blockGasLimit": 12500000000, - "transactionGasLimit": 1250000000, - "testing": { - "stopOnFailedTest": false, - "stopOnFailedContractMatching": false, - "stopOnNoTests": true, - "testAllContracts": false, - "traceAll": false, - "assertionTesting": { - "enabled": true, - "testViewMethods": true, - "assertionModes": { - "failOnCompilerInsertedPanic": false, - "failOnAssertion": true, - "failOnArithmeticUnderflow": false, - "failOnDivideByZero": false, - "failOnEnumTypeConversionOutOfBounds": false, - "failOnIncorrectStorageAccess": false, - "failOnPopEmptyArray": false, - "failOnOutOfBoundsArrayAccess": false, - "failOnAllocateTooMuchMemory": false, - "failOnCallUninitializedVariable": false - } - }, - "propertyTesting": { - "enabled": true, - "testPrefixes": [ - "fuzz_", - "invariant_" - ] - }, - "optimizationTesting": { - "enabled": false, - "testPrefixes": [ - "optimize_" - ] - }, - "excludeFunctionSignatures": [ - "Tester.checkPostConditions()" - ] - }, - "chainConfig": { - "codeSizeCheckDisabled": true, - "cheatCodes": { - "cheatCodesEnabled": true, - "enableFFI": false - } - } - }, - "compilation": { - "platform": "crytic-compile", - "platformConfig": { - "target": "tests/enigma-dark-invariants/hub-suite/Tester.t.sol", - "solcVersion": "", - "exportDirectory": "", - "args": [ - "--solc-remaps", - "forge-std/=../../../lib/forge-std/src/", - "--compile-libraries=(LiquidationLogic,0xf01)" - ] - } - }, - "logging": { - "level": "info", - "logDirectory": "" - } -} \ No newline at end of file + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "callSequenceLength": 100, + "corpusDirectory": "tests/invariants/hub-suite/_corpus/medusa", + "coverageEnabled": true, + "deploymentOrder": ["Tester"], + "targetContracts": ["Tester"], + "targetContractsBalances": [ + "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" + ], + "predeployedContracts": { + "LiquidationLogic": "0xf01" + }, + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": ["0x10000", "0x20000", "0x30000"], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 12500000000, + "transactionGasLimit": 1250000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "assertionModes": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": ["fuzz_", "invariant_"] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": ["optimize_"] + }, + "excludeFunctionSignatures": ["Tester.checkPostConditions()"] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": "tests/invariants/hub-suite/Tester.t.sol", + "solcVersion": "", + "exportDirectory": "", + "args": [ + "--solc-remaps", + "forge-std/=../../../lib/forge-std/src/", + "--compile-libraries=(LiquidationLogic,0xf01)" + ] + } + }, + "logging": { + "level": "info", + "logDirectory": "" + } +} diff --git a/medusa.protocol.json b/medusa.protocol.json index 05a083bab..3e075f680 100644 --- a/medusa.protocol.json +++ b/medusa.protocol.json @@ -1,97 +1,82 @@ { - "fuzzing": { - "workers": 10, - "workerResetLimit": 50, - "timeout": 0, - "testLimit": 0, - "callSequenceLength": 100, - "corpusDirectory": "tests/enigma-dark-invariants/protocol-suite/_corpus/medusa", - "coverageEnabled": true, - "deploymentOrder": [ - "Tester" - ], - "targetContracts": [ - "Tester" - ], - "targetContractsBalances": [ - "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" - ], - "predeployedContracts": { - "LiquidationLogic": "0xf01" - }, - "constructorArgs": {}, - "deployerAddress": "0x30000", - "senderAddresses": [ - "0x10000", - "0x20000", - "0x30000" - ], - "blockNumberDelayMax": 60480, - "blockTimestampDelayMax": 604800, - "blockGasLimit": 12500000000, - "transactionGasLimit": 1250000000, - "testing": { - "stopOnFailedTest": false, - "stopOnFailedContractMatching": false, - "stopOnNoTests": true, - "testAllContracts": false, - "traceAll": false, - "assertionTesting": { - "enabled": true, - "testViewMethods": true, - "assertionModes": { - "failOnCompilerInsertedPanic": false, - "failOnAssertion": true, - "failOnArithmeticUnderflow": false, - "failOnDivideByZero": false, - "failOnEnumTypeConversionOutOfBounds": false, - "failOnIncorrectStorageAccess": false, - "failOnPopEmptyArray": false, - "failOnOutOfBoundsArrayAccess": false, - "failOnAllocateTooMuchMemory": false, - "failOnCallUninitializedVariable": false - } - }, - "propertyTesting": { - "enabled": true, - "testPrefixes": [ - "fuzz_", - "invariant_" - ] - }, - "optimizationTesting": { - "enabled": false, - "testPrefixes": [ - "optimize_" - ] - }, - "excludeFunctionSignatures": [ - "Tester.checkPostConditions()" - ] - }, - "chainConfig": { - "codeSizeCheckDisabled": true, - "cheatCodes": { - "cheatCodesEnabled": true, - "enableFFI": false - } - } - }, - "compilation": { - "platform": "crytic-compile", - "platformConfig": { - "target": "tests/enigma-dark-invariants/protocol-suite/Tester.t.sol", - "solcVersion": "", - "exportDirectory": "", - "args": [ - "--solc-remaps", - "forge-std/=../../../lib/forge-std/src/", - "--compile-libraries=(LiquidationLogic,0xf01)" - ] - } - }, - "logging": { - "level": "info", - "logDirectory": "" - } -} \ No newline at end of file + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "callSequenceLength": 100, + "corpusDirectory": "tests/enigma-dark-invariants/protocol-suite/_corpus/medusa", + "coverageEnabled": true, + "deploymentOrder": ["Tester"], + "targetContracts": ["Tester"], + "targetContractsBalances": [ + "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" + ], + "predeployedContracts": { + "LiquidationLogic": "0xf01" + }, + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": ["0x10000", "0x20000", "0x30000"], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 12500000000, + "transactionGasLimit": 1250000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "assertionModes": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": ["fuzz_", "invariant_"] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": ["optimize_"] + }, + "excludeFunctionSignatures": ["Tester.checkPostConditions()"] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": "tests/enigma-dark-invariants/protocol-suite/Tester.t.sol", + "solcVersion": "", + "exportDirectory": "", + "args": [ + "--solc-remaps", + "forge-std/=../../../lib/forge-std/src/", + "--compile-libraries=(LiquidationLogic,0xf01)" + ] + } + }, + "logging": { + "level": "info", + "logDirectory": "" + } +} diff --git a/tests/invariants/hub-suite/HandlerAggregator.t.sol b/tests/invariants/hub-suite/HandlerAggregator.t.sol index a653313fa..7fd465e18 100644 --- a/tests/invariants/hub-suite/HandlerAggregator.t.sol +++ b/tests/invariants/hub-suite/HandlerAggregator.t.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.19; // Handler contracts -import {HubHandler} from "./handlers/HubHandler.t.sol"; -import {HubConfiguratorHandler} from "./handlers/HubConfiguratorHandler.t.sol"; -import {DonationAttackHandler} from "./handlers/simulators/DonationAttackHandler.t.sol"; +import {HubHandler} from './handlers/HubHandler.t.sol'; +import {HubConfiguratorHandler} from './handlers/HubConfiguratorHandler.t.sol'; +import {DonationAttackHandler} from './handlers/simulators/DonationAttackHandler.t.sol'; /// @notice Helper contract to aggregate all handler contracts for hub suite abstract contract HandlerAggregator is HubHandler, HubConfiguratorHandler, DonationAttackHandler { - function _setUpHandlers() internal {} + function _setUpHandlers() internal {} } diff --git a/tests/invariants/hub-suite/Invariants.t.sol b/tests/invariants/hub-suite/Invariants.t.sol index 6d738d0b7..3c2129b0c 100644 --- a/tests/invariants/hub-suite/Invariants.t.sol +++ b/tests/invariants/hub-suite/Invariants.t.sol @@ -1,54 +1,54 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {HubInvariants} from "./invariants/HubInvariants.t.sol"; +import {HubInvariants} from './invariants/HubInvariants.t.sol'; /// @title Invariants /// @notice Aggregator for hub invariants abstract contract Invariants is HubInvariants { - function invariant_INV_HUB() public returns (bool) { - uint256 assetCount = hub.getAssetCount(); + function invariant_INV_HUB() public returns (bool) { + uint256 assetCount = hub.getAssetCount(); - for (uint256 i; i < assetCount; i++) { - // HUB - assert_INV_HUB_A(i); - assert_INV_HUB_B(i); - assert_INV_HUB_C(i); - assert_INV_HUB_EF(i); - assert_INV_HUB_GH(i); - assert_INV_HUB_I(i); - assert_INV_HUB_K(i); - assert_INV_HUB_L(i); - assert_INV_HUB_O(i); - assert_INV_HUB_P(i); - assert_INV_HUB_N(i); - for (uint256 j; j < NUMBER_OF_ACTORS; j++) { - assert_INV_HUB_ERC4626_A(i, actorAddresses[j]); - assert_INV_HUB_ERC4626_B(i, actorAddresses[j]); - } - assert_INV_HUB_ERC4626_C(i); - assert_INV_HUB_ERC4626_D(i); - } - - return true; + for (uint256 i; i < assetCount; i++) { + // HUB + assert_INV_HUB_A(i); + assert_INV_HUB_B(i); + assert_INV_HUB_C(i); + assert_INV_HUB_EF(i); + assert_INV_HUB_GH(i); + assert_INV_HUB_I(i); + assert_INV_HUB_K(i); + assert_INV_HUB_L(i); + assert_INV_HUB_O(i); + assert_INV_HUB_P(i); + assert_INV_HUB_N(i); + for (uint256 j; j < NUMBER_OF_ACTORS; j++) { + assert_INV_HUB_ERC4626_A(i, actorAddresses[j]); + assert_INV_HUB_ERC4626_B(i, actorAddresses[j]); + } + assert_INV_HUB_ERC4626_C(i); + assert_INV_HUB_ERC4626_D(i); } - function invariant_INV_HUB_AVAILABILITY() public returns (bool) { - uint256 assetCount = hub.getAssetCount(); - for (uint256 i; i < assetCount; i++) { - assert_INV_HUB_AVAILABILITY_A(i); - assert_INV_HUB_AVAILABILITY_B(i); - assert_INV_HUB_AVAILABILITY_C(i); - assert_INV_HUB_AVAILABILITY_D(i); - assert_INV_HUB_AVAILABILITY_E(i); - for (uint256 j; j < NUMBER_OF_ACTORS; j++) { - assert_INV_HUB_AVAILABILITY_F(i, actorAddresses[j]); - assert_INV_HUB_AVAILABILITY_G(i, actorAddresses[j]); - assert_INV_HUB_AVAILABILITY_H(i, actorAddresses[j]); - assert_INV_HUB_AVAILABILITY_I(i, actorAddresses[j]); - } - } + return true; + } - return true; + function invariant_INV_HUB_AVAILABILITY() public returns (bool) { + uint256 assetCount = hub.getAssetCount(); + for (uint256 i; i < assetCount; i++) { + assert_INV_HUB_AVAILABILITY_A(i); + assert_INV_HUB_AVAILABILITY_B(i); + assert_INV_HUB_AVAILABILITY_C(i); + assert_INV_HUB_AVAILABILITY_D(i); + assert_INV_HUB_AVAILABILITY_E(i); + for (uint256 j; j < NUMBER_OF_ACTORS; j++) { + assert_INV_HUB_AVAILABILITY_F(i, actorAddresses[j]); + assert_INV_HUB_AVAILABILITY_G(i, actorAddresses[j]); + assert_INV_HUB_AVAILABILITY_H(i, actorAddresses[j]); + assert_INV_HUB_AVAILABILITY_I(i, actorAddresses[j]); + } } + + return true; + } } diff --git a/tests/invariants/hub-suite/README.md b/tests/invariants/hub-suite/README.md index 2b385617a..edc1ae221 100644 --- a/tests/invariants/hub-suite/README.md +++ b/tests/invariants/hub-suite/README.md @@ -5,6 +5,7 @@ A simplified handler-based invariant testing suite focused exclusively on the ** ## Overview The suite tests a simplified hub-centric deployment with: + - **1 Hub** with a single interest rate strategy - **Multiple Actors** simulating spoke behavior (registered as spokes in the hub) - **2 Base Assets** (USDC, WETH) to keep the asset surface simple and performant @@ -18,22 +19,26 @@ All protocol actions are monitored by hooks that snapshot state and verify postc ### Core Components **Setup Layer** (`Setup.t.sol`, `base/`) + - Deploys a single Hub with a deterministic interest rate strategy - Configures 2 base assets (USDC, WETH) for simplicity and performance - Initializes multiple actors with spoke permissions registered on the hub - Simplified configuration compared to the full multi-hub, multi-spoke suite **Handler Layer** (`handlers/`) + - `HubHandler` – hub liquidity operations (supply, draw, repay) through actor-spokes - `HubConfiguratorHandler` – admin operations (spoke cap updates, risk parameter changes) - Handlers expose hub interface to actors registered as spokes **Verification Layer** (`hooks/`, `invariants/`) + - Before/after hooks with state snapshots - Global and handler-specific postcondition assertions - Hub invariants (liquidity accounting, share calculations, interest accrual) **Utilities** (`utils/`) + - Constants and assertion helpers - Random value generation for fuzzing inputs diff --git a/tests/invariants/hub-suite/Setup.t.sol b/tests/invariants/hub-suite/Setup.t.sol index 81fe14c60..955e48733 100644 --- a/tests/invariants/hub-suite/Setup.t.sol +++ b/tests/invariants/hub-suite/Setup.t.sol @@ -2,307 +2,325 @@ pragma solidity ^0.8.19; // Libraries -import {ActorsUtils} from "../shared/utils/ActorsUtils.sol"; -import {Constants} from "tests/Constants.sol"; -import {Roles} from "src/libraries/types/Roles.sol"; -import {Actor} from "../shared/utils/Actor.sol"; -import "forge-std/console.sol"; +import {ActorsUtils} from '../shared/utils/ActorsUtils.sol'; +import {Constants} from 'tests/Constants.sol'; +import {Roles} from 'src/libraries/types/Roles.sol'; +import {Actor} from '../shared/utils/Actor.sol'; +import 'forge-std/console.sol'; // Interfaces -import {IHub} from "src/hub/Hub.sol"; +import {IHub} from 'src/hub/Hub.sol'; // Test Contracts -import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; // Contracts -import {BaseTest} from "./base/BaseTest.t.sol"; -import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; -import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; -import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; -import {Hub} from "src/hub/Hub.sol"; -import {Spoke} from "src/spoke/Spoke.sol"; -import {HubConfigurator} from "src/hub/HubConfigurator.sol"; +import {BaseTest} from './base/BaseTest.t.sol'; +import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; +import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; +import {Hub} from 'src/hub/Hub.sol'; +import {Spoke} from 'src/spoke/Spoke.sol'; +import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; /// @notice Setup contract for the invariant test Suite, inherited by Tester contract Setup is BaseTest { - /// @notice Number of actors to deploy - function _setUp() internal { - // Deploy the suite assets - _deployAssets(); - - // Deploy protocol contracts and protocol actors - _deployProtocolCore(); - - // Deploy actors - _setUpActors(); - - // Configure the token list on the protocol - _configureTokenList(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // ASSETS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Deploy the suite assets - function _deployAssets() internal { - usdc = new TestnetERC20("USDC", "USDC", 6); - weth = new TestnetERC20("WETH", "WETH", 18); - wbtc = new TestnetERC20("WBTC", "WBTC", 8); - - baseAssets.push(AssetInfo({underlying: address(usdc), decimals: 6})); - baseAssets.push(AssetInfo({underlying: address(weth), decimals: 18})); - baseAssets.push(AssetInfo({underlying: address(wbtc), decimals: 8})); - - vm.label(address(usdc), "usdc"); - vm.label(address(weth), "weth"); - vm.label(address(wbtc), "wbtc"); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CORE // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Deploy protocol core contracts - function _deployProtocolCore() internal { - // Access manager - accessManager = new AccessManager(admin); - - // Hub 1 - hub = new Hub(address(accessManager)); - irStrategy = new AssetInterestRateStrategy(address(hub)); - - // Configurators - hubConfigurator = new HubConfigurator(admin); - _setUpConfiguratorRoles(); - - vm.label(address(accessManager), "accessManager"); - vm.label(address(hub), "hub"); - vm.label(address(hubConfigurator), "hubConfigurator"); - vm.label(address(irStrategy), "irStrategy"); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CONFIGS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _configureTokenList() internal { - // Configure hubs - _configureHubs(); - - // Configure spokes - _configureSpokes(); - } - - /// @notice Configure the hubs - function _configureHubs() internal { - // HUB 1 - bytes memory encodedIrData = abi.encode( - IAssetInterestRateStrategy.InterestRateData({ - optimalUsageRatio: OPTIMAL_USAGE_RATIO_IR1, - baseVariableBorrowRate: BASE_VARIABLE_BORROW_RATE_IR1, - variableRateSlope1: VARIABLE_RATE_SLOPE_1_IR1, - variableRateSlope2: VARIABLE_RATE_SLOPE_2_IR1 - }) - ); - - // Add USDC - usdcAssetId = hub.addAsset(address(usdc), usdc.decimals(), address(this), address(irStrategy), encodedIrData); - hub.updateAssetConfig( - usdcAssetId, - IHub.AssetConfig({ - liquidityFee: 5_00, - feeReceiver: address(this), - irStrategy: address(irStrategy), - reinvestmentController: address(0) - }), - new bytes(0) - ); - hubAssetIds.push(usdcAssetId); - assetIdToUnderlying[usdcAssetId] = address(usdc); - underlyingToAssetId[address(usdc)] = usdcAssetId; - - // Add WETH - wethAssetId = hub.addAsset(address(weth), weth.decimals(), address(this), address(irStrategy), encodedIrData); - hub.updateAssetConfig( - wethAssetId, - IHub.AssetConfig({ - liquidityFee: 10_00, - feeReceiver: address(this), - irStrategy: address(irStrategy), - reinvestmentController: address(0) - }), - new bytes(0) - ); - hubAssetIds.push(wethAssetId); - assetIdToUnderlying[wethAssetId] = address(weth); - underlyingToAssetId[address(weth)] = wethAssetId; - - // Add WBTC - wbtcAssetId = hub.addAsset(address(wbtc), wbtc.decimals(), address(this), address(irStrategy), encodedIrData); - hub.updateAssetConfig( - wbtcAssetId, - IHub.AssetConfig({ - liquidityFee: 5_00, - feeReceiver: address(this), - irStrategy: address(irStrategy), - reinvestmentController: address(0) - }), - new bytes(0) - ); - hubAssetIds.push(wbtcAssetId); - assetIdToUnderlying[wbtcAssetId] = address(wbtc); - underlyingToAssetId[address(wbtc)] = wbtcAssetId; - } - - function _configureSpokes() internal { - // Spoke 1: usdc, weth and wbtc - // Spoke 2: weth and wbtc - // Spoke 3: usdc, weth and wbtc - - // Add SPOKE 1 assets to hub - hub.addSpoke( - usdcAssetId, - address(actors[USER1]), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub.addSpoke( - wethAssetId, - address(actors[USER1]), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub.addSpoke( - wbtcAssetId, - address(actors[USER1]), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - - // Add SPOKE 2 assets to hub - hub.addSpoke( - wethAssetId, - address(actors[USER2]), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub.addSpoke( - wbtcAssetId, - address(actors[USER2]), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - - // Add SPOKE 3 assets to hub - hub.addSpoke( - usdcAssetId, - address(actors[USER3]), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub.addSpoke( - wethAssetId, - address(actors[USER3]), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub.addSpoke( - wbtcAssetId, - address(actors[USER3]), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - - // Set reinvestment controller - hubConfigurator.updateReinvestmentController(address(hub), usdcAssetId, address(this)); - hubConfigurator.updateReinvestmentController(address(hub), wethAssetId, address(this)); - hubConfigurator.updateReinvestmentController(address(hub), wbtcAssetId, address(this)); - - usdc.approve(address(hub), type(uint256).max); - weth.approve(address(hub), type(uint256).max); - wbtc.approve(address(hub), type(uint256).max); - } - - /// @notice Set up roles for the configurators - function _setUpConfiguratorRoles() internal virtual { - // Grant roles to configurators - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(this), 0); - // Grant responsibilities on hubs - { - bytes4[] memory selectors = new bytes4[](3); - selectors[0] = IHub.updateSpokeConfig.selector; - selectors[1] = IHub.setInterestRateData.selector; - selectors[2] = IHub.updateAssetConfig.selector; - accessManager.setTargetFunctionRole(address(hub), selectors, Roles.HUB_ADMIN_ROLE); - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTORS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Deploy protocol actors and initialize their balances - function _setUpActors() internal { - // Initialize the three actors of the fuzzers - address[] memory addresses = new address[](3); - addresses[0] = USER1; - addresses[1] = USER2; - addresses[2] = USER3; - - // Initialize the tokens array - address[] memory tokens = new address[](3); - tokens[0] = address(usdc); - tokens[1] = address(weth); - tokens[2] = address(wbtc); - - address[] memory contracts = new address[](1); - contracts[0] = address(hub); - - actorAddresses = ActorsUtils.setUpActors(addresses, tokens, contracts); - actors[USER1] = Actor(payable(actorAddresses[0])); - actors[USER2] = Actor(payable(actorAddresses[1])); - actors[USER3] = Actor(payable(actorAddresses[2])); + /// @notice Number of actors to deploy + function _setUp() internal { + // Deploy the suite assets + _deployAssets(); + + // Deploy protocol contracts and protocol actors + _deployProtocolCore(); + + // Deploy actors + _setUpActors(); + + // Configure the token list on the protocol + _configureTokenList(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSETS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy the suite assets + function _deployAssets() internal { + usdc = new TestnetERC20('USDC', 'USDC', 6); + weth = new TestnetERC20('WETH', 'WETH', 18); + wbtc = new TestnetERC20('WBTC', 'WBTC', 8); + + baseAssets.push(AssetInfo({underlying: address(usdc), decimals: 6})); + baseAssets.push(AssetInfo({underlying: address(weth), decimals: 18})); + baseAssets.push(AssetInfo({underlying: address(wbtc), decimals: 8})); + + vm.label(address(usdc), 'usdc'); + vm.label(address(weth), 'weth'); + vm.label(address(wbtc), 'wbtc'); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CORE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy protocol core contracts + function _deployProtocolCore() internal { + // Access manager + accessManager = new AccessManager(admin); + + // Hub 1 + hub = new Hub(address(accessManager)); + irStrategy = new AssetInterestRateStrategy(address(hub)); + + // Configurators + hubConfigurator = new HubConfigurator(admin); + _setUpConfiguratorRoles(); + + vm.label(address(accessManager), 'accessManager'); + vm.label(address(hub), 'hub'); + vm.label(address(hubConfigurator), 'hubConfigurator'); + vm.label(address(irStrategy), 'irStrategy'); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CONFIGS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _configureTokenList() internal { + // Configure hubs + _configureHubs(); + + // Configure spokes + _configureSpokes(); + } + + /// @notice Configure the hubs + function _configureHubs() internal { + // HUB 1 + bytes memory encodedIrData = abi.encode( + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: OPTIMAL_USAGE_RATIO_IR1, + baseVariableBorrowRate: BASE_VARIABLE_BORROW_RATE_IR1, + variableRateSlope1: VARIABLE_RATE_SLOPE_1_IR1, + variableRateSlope2: VARIABLE_RATE_SLOPE_2_IR1 + }) + ); + + // Add USDC + usdcAssetId = hub.addAsset( + address(usdc), + usdc.decimals(), + address(this), + address(irStrategy), + encodedIrData + ); + hub.updateAssetConfig( + usdcAssetId, + IHub.AssetConfig({ + liquidityFee: 5_00, + feeReceiver: address(this), + irStrategy: address(irStrategy), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds.push(usdcAssetId); + assetIdToUnderlying[usdcAssetId] = address(usdc); + underlyingToAssetId[address(usdc)] = usdcAssetId; + + // Add WETH + wethAssetId = hub.addAsset( + address(weth), + weth.decimals(), + address(this), + address(irStrategy), + encodedIrData + ); + hub.updateAssetConfig( + wethAssetId, + IHub.AssetConfig({ + liquidityFee: 10_00, + feeReceiver: address(this), + irStrategy: address(irStrategy), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds.push(wethAssetId); + assetIdToUnderlying[wethAssetId] = address(weth); + underlyingToAssetId[address(weth)] = wethAssetId; + + // Add WBTC + wbtcAssetId = hub.addAsset( + address(wbtc), + wbtc.decimals(), + address(this), + address(irStrategy), + encodedIrData + ); + hub.updateAssetConfig( + wbtcAssetId, + IHub.AssetConfig({ + liquidityFee: 5_00, + feeReceiver: address(this), + irStrategy: address(irStrategy), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds.push(wbtcAssetId); + assetIdToUnderlying[wbtcAssetId] = address(wbtc); + underlyingToAssetId[address(wbtc)] = wbtcAssetId; + } + + function _configureSpokes() internal { + // Spoke 1: usdc, weth and wbtc + // Spoke 2: weth and wbtc + // Spoke 3: usdc, weth and wbtc + + // Add SPOKE 1 assets to hub + hub.addSpoke( + usdcAssetId, + address(actors[USER1]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub.addSpoke( + wethAssetId, + address(actors[USER1]), + IHub.SpokeConfig({ + addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, + drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub.addSpoke( + wbtcAssetId, + address(actors[USER1]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + + // Add SPOKE 2 assets to hub + hub.addSpoke( + wethAssetId, + address(actors[USER2]), + IHub.SpokeConfig({ + addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, + drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub.addSpoke( + wbtcAssetId, + address(actors[USER2]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + + // Add SPOKE 3 assets to hub + hub.addSpoke( + usdcAssetId, + address(actors[USER3]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub.addSpoke( + wethAssetId, + address(actors[USER3]), + IHub.SpokeConfig({ + addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, + drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub.addSpoke( + wbtcAssetId, + address(actors[USER3]), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + + // Set reinvestment controller + hubConfigurator.updateReinvestmentController(address(hub), usdcAssetId, address(this)); + hubConfigurator.updateReinvestmentController(address(hub), wethAssetId, address(this)); + hubConfigurator.updateReinvestmentController(address(hub), wbtcAssetId, address(this)); + + usdc.approve(address(hub), type(uint256).max); + weth.approve(address(hub), type(uint256).max); + wbtc.approve(address(hub), type(uint256).max); + } + + /// @notice Set up roles for the configurators + function _setUpConfiguratorRoles() internal virtual { + // Grant roles to configurators + accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); + accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(this), 0); + // Grant responsibilities on hubs + { + bytes4[] memory selectors = new bytes4[](3); + selectors[0] = IHub.updateSpokeConfig.selector; + selectors[1] = IHub.setInterestRateData.selector; + selectors[2] = IHub.updateAssetConfig.selector; + accessManager.setTargetFunctionRole(address(hub), selectors, Roles.HUB_ADMIN_ROLE); } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy protocol actors and initialize their balances + function _setUpActors() internal { + // Initialize the three actors of the fuzzers + address[] memory addresses = new address[](3); + addresses[0] = USER1; + addresses[1] = USER2; + addresses[2] = USER3; + + // Initialize the tokens array + address[] memory tokens = new address[](3); + tokens[0] = address(usdc); + tokens[1] = address(weth); + tokens[2] = address(wbtc); + + address[] memory contracts = new address[](1); + contracts[0] = address(hub); + + actorAddresses = ActorsUtils.setUpActors(addresses, tokens, contracts); + actors[USER1] = Actor(payable(actorAddresses[0])); + actors[USER2] = Actor(payable(actorAddresses[1])); + actors[USER3] = Actor(payable(actorAddresses[2])); + } } diff --git a/tests/invariants/hub-suite/SpecAggregator.t.sol b/tests/invariants/hub-suite/SpecAggregator.t.sol index 1727f4ac4..dd160ffd3 100644 --- a/tests/invariants/hub-suite/SpecAggregator.t.sol +++ b/tests/invariants/hub-suite/SpecAggregator.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.19; // Test Contracts -import {HubInvariantsSpec} from "./specs/HubInvariantsSpec.t.sol"; -import {HubPostconditionsSpec} from "./specs/HubPostconditionsSpec.t.sol"; +import {HubInvariantsSpec} from './specs/HubInvariantsSpec.t.sol'; +import {HubPostconditionsSpec} from './specs/HubPostconditionsSpec.t.sol'; /// @title SpecAggregator /// @notice Helper contract to aggregate all spec contracts, inherited in BaseHooks diff --git a/tests/invariants/hub-suite/Tester.t.sol b/tests/invariants/hub-suite/Tester.t.sol index 948500e86..ce2a71ffa 100644 --- a/tests/invariants/hub-suite/Tester.t.sol +++ b/tests/invariants/hub-suite/Tester.t.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {Invariants} from "./Invariants.t.sol"; -import {Setup} from "./Setup.t.sol"; +import {Invariants} from './Invariants.t.sol'; +import {Setup} from './Setup.t.sol'; /// @title Tester /// @notice Entry point for hub invariant testing contract Tester is Invariants, Setup { - constructor() payable { - setUp(); - } + constructor() payable { + setUp(); + } - function setUp() internal { - _setUp(); - } + function setUp() internal { + _setUp(); + } } diff --git a/tests/invariants/hub-suite/_config/echidna_config.yaml b/tests/invariants/hub-suite/_config/echidna_config.yaml index 0ecc17a46..8a0ec2b68 100644 --- a/tests/invariants/hub-suite/_config/echidna_config.yaml +++ b/tests/invariants/hub-suite/_config/echidna_config.yaml @@ -26,7 +26,7 @@ propMaxGas: 1000000000 testMaxGas: 1000000000 # list of methods to filter -filterFunctions: [ "Tester.checkPostConditions()" ] +filterFunctions: ["Tester.checkPostConditions()"] # by default, blacklist methods in filterFunctions #filterBlacklist: false @@ -39,7 +39,7 @@ stopOnFail: false coverage: true # list of file formats to save coverage reports in; default is all possible formats -coverageFormats: [ "html" ] +coverageFormats: ["html"] #directory to save the corpus; by default is disabled corpusDir: "tests/invariants/hub-suite/_corpus/echidna/default/_data/corpus" @@ -47,9 +47,9 @@ corpusDir: "tests/invariants/hub-suite/_corpus/echidna/default/_data/corpus" #mutConsts: [100, 1, 1] #remappings -cryticArgs: [ "--compile-libraries=(LiquidationLogic,0xf01)" ] +cryticArgs: ["--compile-libraries=(LiquidationLogic,0xf01)"] -deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] +deployContracts: [["0xf01", "LiquidationLogic"]] # maximum value to send to payable functions maxValue: 1e+30 # 100000000000 eth diff --git a/tests/invariants/hub-suite/_config/echidna_config_ci.yaml b/tests/invariants/hub-suite/_config/echidna_config_ci.yaml index 056c9bc11..11027a560 100644 --- a/tests/invariants/hub-suite/_config/echidna_config_ci.yaml +++ b/tests/invariants/hub-suite/_config/echidna_config_ci.yaml @@ -26,7 +26,7 @@ propMaxGas: 1000000000 testMaxGas: 1000000000 # list of methods to filter -filterFunctions: [ "Tester.checkPostConditions()" ] +filterFunctions: ["Tester.checkPostConditions()"] # by default, blacklist methods in filterFunctions #filterBlacklist: false @@ -39,7 +39,7 @@ stopOnFail: false coverage: true # list of file formats to save coverage reports in; default is all possible formats -coverageFormats: [ "html" ] +coverageFormats: ["html"] #directory to save the corpus; by default is disabled corpusDir: "corpus" @@ -47,9 +47,9 @@ corpusDir: "corpus" #mutConsts: [100, 1, 1] #remappings -cryticArgs: [ "--ignore-compile", "--compile-libraries=(LiquidationLogic,0xf01)" ] +cryticArgs: ["--ignore-compile", "--compile-libraries=(LiquidationLogic,0xf01)"] -deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] +deployContracts: [["0xf01", "LiquidationLogic"]] # maximum value to send to payable functions maxValue: 1e+30 # 100000000000 eth diff --git a/tests/invariants/hub-suite/base/BaseHandler.t.sol b/tests/invariants/hub-suite/base/BaseHandler.t.sol index 551f54df8..a5e422751 100644 --- a/tests/invariants/hub-suite/base/BaseHandler.t.sol +++ b/tests/invariants/hub-suite/base/BaseHandler.t.sol @@ -2,80 +2,86 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; // Libraries import {MockERC20} from 'tests/mocks/MockERC20.sol'; // Contracts -import {Actor} from "../../shared/utils/Actor.sol"; -import {HookAggregator} from "../hooks/HookAggregator.t.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; +import {HookAggregator} from '../hooks/HookAggregator.t.sol'; /// @title BaseHandler /// @notice Contains common logic for all handlers /// @dev inherits all suite assertions since per action assertions are implmenteds in the handlers contract BaseHandler is HookAggregator { - /////////////////////////////////////////////////////////////////////////////////////////////// - // MODIFIERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // MODIFIERS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // SHARED VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // SHARED VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice Helper function to randomize a uint256 seed with a string salt - function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { - return uint256(keccak256(abi.encodePacked(seed, salt))); - } + /// @notice Helper function to randomize a uint256 seed with a string salt + function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(seed, salt))); + } - /// @notice Helper function to get a random value - function _getRandomValue(uint256 modulus) internal view returns (uint256) { - uint256 randomNumber = uint256(keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender))); - return randomNumber % modulus; // Adjust the modulus to the desired range - } + /// @notice Helper function to get a random value + function _getRandomValue(uint256 modulus) internal view returns (uint256) { + uint256 randomNumber = uint256( + keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender)) + ); + return randomNumber % modulus; // Adjust the modulus to the desired range + } - /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor - function _approve(address token, Actor actor_, address spender, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor_.proxy(token, abi.encodeWithSelector(0x095ea7b3, spender, amount)); - require(success, string(returnData)); - } + /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor + function _approve(address token, Actor actor_, address spender, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor_.proxy( + token, + abi.encodeWithSelector(0x095ea7b3, spender, amount) + ); + require(success, string(returnData)); + } - /// @notice Helper function to safely approve an amount of tokens to a spender + /// @notice Helper function to safely approve an amount of tokens to a spender - function _approve(address token, address owner, address spender, uint256 amount) internal { - vm.prank(owner); - _safeApprove(token, spender, 0); - vm.prank(owner); - _safeApprove(token, spender, amount); - } + function _approve(address token, address owner, address spender, uint256 amount) internal { + vm.prank(owner); + _safeApprove(token, spender, 0); + vm.prank(owner); + _safeApprove(token, spender, amount); + } - /// @notice Helper function to safely approve an amount of tokens to a spender - /// @dev This function is used to revert on failed approvals - function _safeApprove(address token, address spender, uint256 amount) internal { - (bool success, bytes memory retdata) = - token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); - assert(success); - if (retdata.length > 0) assert(abi.decode(retdata, (bool))); - } + /// @notice Helper function to safely approve an amount of tokens to a spender + /// @dev This function is used to revert on failed approvals + function _safeApprove(address token, address spender, uint256 amount) internal { + (bool success, bytes memory retdata) = token.call( + abi.encodeWithSelector(IERC20.approve.selector, spender, amount) + ); + assert(success); + if (retdata.length > 0) assert(abi.decode(retdata, (bool))); + } - /// @notice Helper function to mint an amount of tokens to an address - function _mint(address token, address receiver, uint256 amount) internal { - MockERC20(token).mint(receiver, amount); - } + /// @notice Helper function to mint an amount of tokens to an address + function _mint(address token, address receiver, uint256 amount) internal { + MockERC20(token).mint(receiver, amount); + } - /// @notice Helper function to mint an amount of tokens to an address and approve them to a spender - /// @param token Address of the token to mint - /// @param owner Address of the new owner of the tokens - /// @param spender Address of the spender to approve the tokens to - /// @param amount Amount of tokens to mint and approve - function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { - _mint(token, owner, amount); - _approve(token, owner, spender, amount); - } + /// @notice Helper function to mint an amount of tokens to an address and approve them to a spender + /// @param token Address of the token to mint + /// @param owner Address of the new owner of the tokens + /// @param spender Address of the spender to approve the tokens to + /// @param amount Amount of tokens to mint and approve + function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { + _mint(token, owner, amount); + _approve(token, owner, spender, amount); + } } diff --git a/tests/invariants/hub-suite/base/BaseHooks.t.sol b/tests/invariants/hub-suite/base/BaseHooks.t.sol index 68e4ef887..e5015ea22 100644 --- a/tests/invariants/hub-suite/base/BaseHooks.t.sol +++ b/tests/invariants/hub-suite/base/BaseHooks.t.sol @@ -2,17 +2,17 @@ pragma solidity ^0.8.19; // Contracts -import {ProtocolAssertions} from "./ProtocolAssertions.t.sol"; +import {ProtocolAssertions} from './ProtocolAssertions.t.sol'; // Test Contracts -import {SpecAggregator} from "../SpecAggregator.t.sol"; +import {SpecAggregator} from '../SpecAggregator.t.sol'; /// @title BaseHooks /// @notice Contains common logic for all hooks /// @dev inherits all suite assertions since per-action assertions are implemented in the handlers /// @dev inherits SpecAggregator contract BaseHooks is ProtocolAssertions, SpecAggregator { -/////////////////////////////////////////////////////////////////////////////////////////////// -// HELPERS // -/////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/invariants/hub-suite/base/BaseStorage.t.sol b/tests/invariants/hub-suite/base/BaseStorage.t.sol index ec1dbe711..87892350f 100644 --- a/tests/invariants/hub-suite/base/BaseStorage.t.sol +++ b/tests/invariants/hub-suite/base/BaseStorage.t.sol @@ -2,108 +2,108 @@ pragma solidity ^0.8.19; // Contracts -import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; -import {IHub} from "src/hub/Hub.sol"; -import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; -import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; -import {HubConfigurator} from "src/hub/HubConfigurator.sol"; -import {IAaveOracle} from "src/spoke/interfaces/IAaveOracle.sol"; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; +import {IHub} from 'src/hub/Hub.sol'; +import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; +import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; +import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; /// @notice BaseStorage contract for hub-focused test suite abstract contract BaseStorage { - /////////////////////////////////////////////////////////////////////////////////////////////// - // CONSTANTS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // CONSTANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// - uint256 constant MAX_TOKEN_AMOUNT = 1e29; + uint256 constant MAX_TOKEN_AMOUNT = 1e29; - uint256 constant ONE_DAY = 1 days; - uint256 constant ONE_MONTH = ONE_YEAR / 12; - uint256 constant ONE_YEAR = 365 days; + uint256 constant ONE_DAY = 1 days; + uint256 constant ONE_MONTH = ONE_YEAR / 12; + uint256 constant ONE_YEAR = 365 days; - uint256 internal constant NUMBER_OF_ACTORS = 3; - uint256 internal constant INITIAL_ETH_BALANCE = 1e26; - uint256 internal constant INITIAL_COLL_BALANCE = 1e21; + uint256 internal constant NUMBER_OF_ACTORS = 3; + uint256 internal constant INITIAL_ETH_BALANCE = 1e26; + uint256 internal constant INITIAL_COLL_BALANCE = 1e21; - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTORS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice The address of the admin, the tester itself - address internal admin = address(this); + /// @notice The address of the admin, the tester itself + address internal admin = address(this); - /// @notice Stores the actor during a handler call - Actor internal actor; + /// @notice Stores the actor during a handler call + Actor internal actor; - /// @notice Mapping of fuzzer user addresses to actors - mapping(address => Actor) internal actors; + /// @notice Mapping of fuzzer user addresses to actors + mapping(address => Actor) internal actors; - /// @notice Array of all actor addresses (simulating spokes) - address[] internal actorAddresses; + /// @notice Array of all actor addresses (simulating spokes) + address[] internal actorAddresses; - /// @notice The signature of the action that is being executed - bytes4 internal currentActionSignature; + /// @notice The signature of the action that is being executed + bytes4 internal currentActionSignature; - /////////////////////////////////////////////////////////////////////////////////////////////// - // ASSETS STORAGE // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSETS STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice The USDC token - TestnetERC20 internal usdc; - /// @notice The WETH token - TestnetERC20 internal weth; - /// @notice The WBTC token - TestnetERC20 internal wbtc; + /// @notice The USDC token + TestnetERC20 internal usdc; + /// @notice The WETH token + TestnetERC20 internal weth; + /// @notice The WBTC token + TestnetERC20 internal wbtc; - uint256 internal targetAssetId; + uint256 internal targetAssetId; - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB STORAGE // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice Single Hub instance - IHub internal hub; + /// @notice Single Hub instance + IHub internal hub; - /// @notice Interest rate strategy for the hub - AssetInterestRateStrategy internal irStrategy; + /// @notice Interest rate strategy for the hub + AssetInterestRateStrategy internal irStrategy; - /// @notice Hub Configurator - HubConfigurator internal hubConfigurator; + /// @notice Hub Configurator + HubConfigurator internal hubConfigurator; - /// @notice Access Manager - AccessManager internal accessManager; + /// @notice Access Manager + AccessManager internal accessManager; - // PRICE FEEDS - address[] internal priceFeeds; + // PRICE FEEDS + address[] internal priceFeeds; - /////////////////////////////////////////////////////////////////////////////////////////////// - // ASSET CONFIG // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSET CONFIG // + /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice Asset info struct - struct AssetInfo { - address underlying; - uint8 decimals; - } + /// @notice Asset info struct + struct AssetInfo { + address underlying; + uint8 decimals; + } - /// @notice Array of base assets for the hub - AssetInfo[] internal baseAssets; + /// @notice Array of base assets for the hub + AssetInfo[] internal baseAssets; - /// @notice Hub asset IDs - uint256 internal wethAssetId; - uint256 internal usdcAssetId; - uint256 internal wbtcAssetId; - uint256[] internal hubAssetIds; - mapping(uint256 => address) internal assetIdToUnderlying; - mapping(address => uint256) internal underlyingToAssetId; + /// @notice Hub asset IDs + uint256 internal wethAssetId; + uint256 internal usdcAssetId; + uint256 internal wbtcAssetId; + uint256[] internal hubAssetIds; + mapping(uint256 => address) internal assetIdToUnderlying; + mapping(address => uint256) internal underlyingToAssetId; - /////////////////////////////////////////////////////////////////////////////////////////////// - // SPOKE CONFIG // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE CONFIG // + /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice Array of spoke addresses (actors acting as spokes) - address[] internal spokeAddresses; + /// @notice Array of spoke addresses (actors acting as spokes) + address[] internal spokeAddresses; } diff --git a/tests/invariants/hub-suite/base/BaseTest.t.sol b/tests/invariants/hub-suite/base/BaseTest.t.sol index 8961e08f3..53311db8c 100644 --- a/tests/invariants/hub-suite/base/BaseTest.t.sol +++ b/tests/invariants/hub-suite/base/BaseTest.t.sol @@ -2,122 +2,128 @@ pragma solidity ^0.8.19; // Libraries -import {Vm} from "forge-std/Base.sol"; -import {StdUtils} from "forge-std/StdUtils.sol"; -import "forge-std/console.sol"; +import {Vm} from 'forge-std/Base.sol'; +import {StdUtils} from 'forge-std/StdUtils.sol'; +import 'forge-std/console.sol'; // Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; -import {PropertiesConstants} from "../../shared/utils/PropertiesConstants.sol"; -import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; +import {PropertiesConstants} from '../../shared/utils/PropertiesConstants.sol'; +import {StdAsserts} from '../../shared/utils/StdAsserts.sol'; // Base -import {BaseStorage} from "./BaseStorage.t.sol"; +import {BaseStorage} from './BaseStorage.t.sol'; /// @notice Base contract for all test contracts extends BaseStorage /// @dev Provides setup modifier and cheat code setup /// @dev inherits Storage, Testing constants assertions and utils needed for testing abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdUtils { - bool internal IS_TEST = true; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTOR PROXY MECHANISM // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @dev Actor proxy mechanism - modifier setup() virtual { - actor = actors[msg.sender]; - _; - delete actor; - } - - /// @dev Solves medusa backward time warp issue - modifier monotonicTimestamp() virtual { - /// @dev: Implement monotonic timestamp if needed - _; - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CALLBACKS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - receive() external payable {} - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CHEAT CODE SETUP // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @dev Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. - address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - - /// @dev Virtual machine instance - Vm internal constant vm = Vm(VM_ADDRESS); - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS: RANDOM GETTERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Get a random actor proxy address - function _getRandomActor(uint256 _i) internal view returns (address) { - uint256 _actorIndex = _i % NUMBER_OF_ACTORS; - return actorAddresses[_actorIndex]; - } - - /// @notice Helper function to get a random base asset - function _getRandomBaseAsset(uint256 i) internal view returns (address) { - uint256 _assetIndex = i % baseAssets.length; - return baseAssets[_assetIndex].underlying; - } - - /// @notice Helper function to get random base asset full info - function _getRandomBaseAssetId(uint256 i) internal view returns (uint256) { - uint256 _assetIndex = i % hubAssetIds.length; - return hubAssetIds[_assetIndex]; - } - - /// @notice Helper function to get random base asset full info - function _getRandomBaseAssetFullInfo(uint256 i) internal view returns (AssetInfo memory) { - uint256 _assetIndex = i % baseAssets.length; - return baseAssets[_assetIndex]; - } - - /// @notice Helper function to get a random price feed address - function _getRandomPriceFeed(uint256 i) internal view returns (address) { - uint256 _priceFeedIndex = i % priceFeeds.length; - return priceFeeds[_priceFeedIndex]; - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS: GETTERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Get a random address - function _makeAddr(string memory name) internal pure returns (address addr) { - uint256 privateKey = uint256(keccak256(abi.encodePacked(name))); - addr = vm.addr(privateKey); - } - - /// @notice Helper function to transfer tokens by actor - function _transferByActor(address token, address to, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor.proxy(token, abi.encodeWithSelector(IERC20.transfer.selector, to, amount)); - require(success, string(returnData)); - } - - /// @notice Helper function to approve tokens by actor - function _approveByActor(address token, address spender, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor.proxy(token, abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); - require(success, string(returnData)); - } + bool internal IS_TEST = true; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTOR PROXY MECHANISM // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Actor proxy mechanism + modifier setup() virtual { + actor = actors[msg.sender]; + _; + delete actor; + } + + /// @dev Solves medusa backward time warp issue + modifier monotonicTimestamp() virtual { + /// @dev: Implement monotonic timestamp if needed + _; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CALLBACKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + receive() external payable {} + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CHEAT CODE SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. + address internal constant VM_ADDRESS = address(uint160(uint256(keccak256('hevm cheat code')))); + + /// @dev Virtual machine instance + Vm internal constant vm = Vm(VM_ADDRESS); + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS: RANDOM GETTERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Get a random actor proxy address + function _getRandomActor(uint256 _i) internal view returns (address) { + uint256 _actorIndex = _i % NUMBER_OF_ACTORS; + return actorAddresses[_actorIndex]; + } + + /// @notice Helper function to get a random base asset + function _getRandomBaseAsset(uint256 i) internal view returns (address) { + uint256 _assetIndex = i % baseAssets.length; + return baseAssets[_assetIndex].underlying; + } + + /// @notice Helper function to get random base asset full info + function _getRandomBaseAssetId(uint256 i) internal view returns (uint256) { + uint256 _assetIndex = i % hubAssetIds.length; + return hubAssetIds[_assetIndex]; + } + + /// @notice Helper function to get random base asset full info + function _getRandomBaseAssetFullInfo(uint256 i) internal view returns (AssetInfo memory) { + uint256 _assetIndex = i % baseAssets.length; + return baseAssets[_assetIndex]; + } + + /// @notice Helper function to get a random price feed address + function _getRandomPriceFeed(uint256 i) internal view returns (address) { + uint256 _priceFeedIndex = i % priceFeeds.length; + return priceFeeds[_priceFeedIndex]; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS: GETTERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Get a random address + function _makeAddr(string memory name) internal pure returns (address addr) { + uint256 privateKey = uint256(keccak256(abi.encodePacked(name))); + addr = vm.addr(privateKey); + } + + /// @notice Helper function to transfer tokens by actor + function _transferByActor(address token, address to, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor.proxy( + token, + abi.encodeWithSelector(IERC20.transfer.selector, to, amount) + ); + require(success, string(returnData)); + } + + /// @notice Helper function to approve tokens by actor + function _approveByActor(address token, address spender, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor.proxy( + token, + abi.encodeWithSelector(IERC20.approve.selector, spender, amount) + ); + require(success, string(returnData)); + } } diff --git a/tests/invariants/hub-suite/base/ProtocolAssertions.t.sol b/tests/invariants/hub-suite/base/ProtocolAssertions.t.sol index 18434085f..30b41eb54 100644 --- a/tests/invariants/hub-suite/base/ProtocolAssertions.t.sol +++ b/tests/invariants/hub-suite/base/ProtocolAssertions.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.19; // Base -import {BaseTest} from "./BaseTest.t.sol"; -import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; +import {BaseTest} from './BaseTest.t.sol'; +import {StdAsserts} from '../../shared/utils/StdAsserts.sol'; /// @title ProtocolAssertions /// @notice Helper contract for protocol specific assertions diff --git a/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol index 2ce002184..3b3afc936 100644 --- a/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol +++ b/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol @@ -2,54 +2,63 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {IHubConfiguratorHandler} from "./interfaces/IHubConfiguratorHandler.sol"; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IHubConfiguratorHandler} from './interfaces/IHubConfiguratorHandler.sol'; // Libraries -import "forge-std/console.sol"; +import 'forge-std/console.sol'; // Test Contracts -import {Actor} from "../../shared/utils/Actor.sol"; -import {BaseHandler} from "../base/BaseHandler.t.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; +import {BaseHandler} from '../base/BaseHandler.t.sol'; /// @title HubConfiguratorHandler /// @notice Handler test contract for a set of actions contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STATE VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j) external setup { - uint256 assetId = _getRandomBaseAssetId(i); - address spoke = _getRandomActor(j); - hubConfigurator.updateSpokeSupplyCap(address(hub), assetId, spoke, addCap); - } - - function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j) external setup { - uint256 assetId = _getRandomBaseAssetId(i); - address spoke = _getRandomActor(j); - hubConfigurator.updateSpokeDrawCap(address(hub), assetId, spoke, drawCap); - } - - function updateSpokeRiskPremiumThreshold(uint256 riskPremiumThreshold, uint8 i, uint8 j) external setup { - uint256 assetId = _getRandomBaseAssetId(i); - address spoke = _getRandomActor(j); - hubConfigurator.updateSpokeRiskPremiumThreshold(address(hub), assetId, spoke, riskPremiumThreshold); - } - - function updateSpokePaused(bool paused, uint8 i, uint8 j) external setup { - uint256 assetId = _getRandomBaseAssetId(i); - address spoke = _getRandomActor(j); - hubConfigurator.updateSpokePaused(address(hub), assetId, spoke, paused); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j) external setup { + uint256 assetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + hubConfigurator.updateSpokeSupplyCap(address(hub), assetId, spoke, addCap); + } + + function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j) external setup { + uint256 assetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + hubConfigurator.updateSpokeDrawCap(address(hub), assetId, spoke, drawCap); + } + + function updateSpokeRiskPremiumThreshold( + uint256 riskPremiumThreshold, + uint8 i, + uint8 j + ) external setup { + uint256 assetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + hubConfigurator.updateSpokeRiskPremiumThreshold( + address(hub), + assetId, + spoke, + riskPremiumThreshold + ); + } + + function updateSpokePaused(bool paused, uint8 i, uint8 j) external setup { + uint256 assetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + hubConfigurator.updateSpokePaused(address(hub), assetId, spoke, paused); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/invariants/hub-suite/handlers/HubHandler.t.sol b/tests/invariants/hub-suite/handlers/HubHandler.t.sol index 2d1846e52..377e766af 100644 --- a/tests/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/tests/invariants/hub-suite/handlers/HubHandler.t.sol @@ -2,351 +2,394 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {IHub, IHubBase} from "src/hub/Hub.sol"; -import {IHubHandler} from "./interfaces/IHubHandler.sol"; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IHub, IHubBase} from 'src/hub/Hub.sol'; +import {IHubHandler} from './interfaces/IHubHandler.sol'; // Libraries -import {WadRayMath} from "src/libraries/math/WadRayMath.sol"; -import "forge-std/console.sol"; +import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; +import 'forge-std/console.sol'; // Test Contracts -import {Actor} from "../../shared/utils/Actor.sol"; -import {BaseHandler} from "../base/BaseHandler.t.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; +import {BaseHandler} from '../base/BaseHandler.t.sol'; /// @title HubHandler /// @notice Handler for hub-level operations through actor-spokes contract HubHandler is BaseHandler, IHubHandler { - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function add(uint256 amount, uint8 i) public setup returns (uint256 addedShares) { - bool success; - bytes memory returnData; - uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); - address underlying = assetIdToUnderlying[cachedTargetAssetId]; - - uint256 previewAddedShares = hub.previewAddByAssets(cachedTargetAssetId, amount); - - uint256 assetsBefore = hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)); - uint256 sharesBefore = hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)); - - _before(); - vm.prank(address(actor)); - IERC20(underlying).transfer(address(hub), amount); - (success, returnData) = actor.proxy(address(hub), abi.encodeCall(IHubBase.add, (targetAssetId, amount))); - - if (success) { - _after(); - - addedShares = uint256(abi.decode(returnData, (uint256))); - - assertGe( - assetsBefore + amount, - hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)), - HSPOST_HUB_ERC4626_ADD_A - ); - assertEq( - sharesBefore + addedShares, - hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)), - HSPOST_HUB_ERC4626_ADD_B - ); - - assertLe(previewAddedShares, addedShares, HSPOST_HUB_ERC4626_ADD_C); - } else { - revert("HubHandler: add failed"); - } + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function add(uint256 amount, uint8 i) public setup returns (uint256 addedShares) { + bool success; + bytes memory returnData; + uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); + address underlying = assetIdToUnderlying[cachedTargetAssetId]; + + uint256 previewAddedShares = hub.previewAddByAssets(cachedTargetAssetId, amount); + + uint256 assetsBefore = hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)); + uint256 sharesBefore = hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)); + + _before(); + vm.prank(address(actor)); + IERC20(underlying).transfer(address(hub), amount); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHubBase.add, (targetAssetId, amount)) + ); + + if (success) { + _after(); + + addedShares = uint256(abi.decode(returnData, (uint256))); + + assertGe( + assetsBefore + amount, + hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)), + HSPOST_HUB_ERC4626_ADD_A + ); + assertEq( + sharesBefore + addedShares, + hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)), + HSPOST_HUB_ERC4626_ADD_B + ); + + assertLe(previewAddedShares, addedShares, HSPOST_HUB_ERC4626_ADD_C); + } else { + revert('HubHandler: add failed'); } - - function remove(uint256 amount, uint8 i) public setup returns (uint256 removedShares) { - bool success; - bytes memory returnData; - uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); - - uint256 previewRemovedShares = hub.previewRemoveByAssets(cachedTargetAssetId, amount); - - uint256 assetsBefore = hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)); - uint256 sharesBefore = hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)); - - _before(); - (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.remove, (cachedTargetAssetId, amount, address(actor)))); - - if (success) { - _after(); - - removedShares = uint256(abi.decode(returnData, (uint256))); - - assertGe( - assetsBefore, - hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)) + amount, - HSPOST_HUB_ERC4626_REMOVE_A - ); - assertEq( - sharesBefore, - hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)) + removedShares, - HSPOST_HUB_ERC4626_REMOVE_B - ); - - assertGe(previewRemovedShares, removedShares, HSPOST_HUB_ERC4626_REMOVE_C); - } else { - revert("HubHandler: remove failed"); - } + } + + function remove(uint256 amount, uint8 i) public setup returns (uint256 removedShares) { + bool success; + bytes memory returnData; + uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); + + uint256 previewRemovedShares = hub.previewRemoveByAssets(cachedTargetAssetId, amount); + + uint256 assetsBefore = hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)); + uint256 sharesBefore = hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)); + + _before(); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHubBase.remove, (cachedTargetAssetId, amount, address(actor))) + ); + + if (success) { + _after(); + + removedShares = uint256(abi.decode(returnData, (uint256))); + + assertGe( + assetsBefore, + hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)) + amount, + HSPOST_HUB_ERC4626_REMOVE_A + ); + assertEq( + sharesBefore, + hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)) + removedShares, + HSPOST_HUB_ERC4626_REMOVE_B + ); + + assertGe(previewRemovedShares, removedShares, HSPOST_HUB_ERC4626_REMOVE_C); + } else { + revert('HubHandler: remove failed'); } + } - function draw(uint256 amount, uint8 i) public setup returns (uint256 drawnShares) { - bool success; - bytes memory returnData; - uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); + function draw(uint256 amount, uint8 i) public setup returns (uint256 drawnShares) { + bool success; + bytes memory returnData; + uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); - uint256 previewDrawnShares = hub.previewDrawByAssets(cachedTargetAssetId, amount); + uint256 previewDrawnShares = hub.previewDrawByAssets(cachedTargetAssetId, amount); - (uint256 drawnBefore,) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); - uint256 sharesBefore = hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)); + (uint256 drawnBefore, ) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); + uint256 sharesBefore = hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)); - _before(); - (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.draw, (cachedTargetAssetId, amount, address(actor)))); + _before(); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHubBase.draw, (cachedTargetAssetId, amount, address(actor))) + ); - if (success) { - _after(); + if (success) { + _after(); - drawnShares = uint256(abi.decode(returnData, (uint256))); + drawnShares = uint256(abi.decode(returnData, (uint256))); - (uint256 drawnAfter,) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); + (uint256 drawnAfter, ) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); - assertLe(drawnBefore + amount, drawnAfter, HSPOST_HUB_ERC4626_DRAW_A); - assertEq( - sharesBefore + drawnShares, - hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)), - HSPOST_HUB_ERC4626_DRAW_B - ); + assertLe(drawnBefore + amount, drawnAfter, HSPOST_HUB_ERC4626_DRAW_A); + assertEq( + sharesBefore + drawnShares, + hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)), + HSPOST_HUB_ERC4626_DRAW_B + ); - assertGe(previewDrawnShares, drawnShares, HSPOST_HUB_ERC4626_DRAW_C); - } else { - revert("HubHandler: draw failed"); - } + assertGe(previewDrawnShares, drawnShares, HSPOST_HUB_ERC4626_DRAW_C); + } else { + revert('HubHandler: draw failed'); } - - function restore(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) - public - setup - returns (uint256 restoredDrawnShares) - { - bool success; - bytes memory returnData; - uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); - - uint256 previewRestoredShares = hub.previewRestoreByAssets(cachedTargetAssetId, drawnAmount); - - (uint256 drawnBefore,) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); - uint256 drawnSharesBefore = hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)); - - IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta(sharesDelta, premiumAmount, targetAssetId); - - _before(); - vm.prank(address(actor)); - IERC20(assetIdToUnderlying[cachedTargetAssetId]).transfer(address(hub), drawnAmount + premiumAmount); - (success, returnData) = actor.proxy( - address(hub), abi.encodeCall(IHubBase.restore, (cachedTargetAssetId, drawnAmount, premiumDelta)) - ); - - if (success) { - _after(); - - restoredDrawnShares = uint256(abi.decode(returnData, (uint256))); - - (uint256 drawnAfter,) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); - - assertEq(drawnBefore, drawnAfter + drawnAmount, HSPOST_HUB_ERC4626_RESTORE_A); - assertEq( - drawnSharesBefore, - hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)) + restoredDrawnShares, - HSPOST_HUB_ERC4626_RESTORE_B - ); - - assertLe(previewRestoredShares, restoredDrawnShares, HSPOST_HUB_ERC4626_RESTORE_C); - } else { - revert("HubHandler: restore failed"); - } + } + + function restore( + uint256 drawnAmount, + uint256 premiumAmount, + int256 sharesDelta, + uint8 i + ) public setup returns (uint256 restoredDrawnShares) { + bool success; + bytes memory returnData; + uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); + + uint256 previewRestoredShares = hub.previewRestoreByAssets(cachedTargetAssetId, drawnAmount); + + (uint256 drawnBefore, ) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); + uint256 drawnSharesBefore = hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)); + + IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta( + sharesDelta, + premiumAmount, + targetAssetId + ); + + _before(); + vm.prank(address(actor)); + IERC20(assetIdToUnderlying[cachedTargetAssetId]).transfer( + address(hub), + drawnAmount + premiumAmount + ); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHubBase.restore, (cachedTargetAssetId, drawnAmount, premiumDelta)) + ); + + if (success) { + _after(); + + restoredDrawnShares = uint256(abi.decode(returnData, (uint256))); + + (uint256 drawnAfter, ) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); + + assertEq(drawnBefore, drawnAfter + drawnAmount, HSPOST_HUB_ERC4626_RESTORE_A); + assertEq( + drawnSharesBefore, + hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)) + restoredDrawnShares, + HSPOST_HUB_ERC4626_RESTORE_B + ); + + assertLe(previewRestoredShares, restoredDrawnShares, HSPOST_HUB_ERC4626_RESTORE_C); + } else { + revert('HubHandler: restore failed'); } - - function reportDeficit(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) external setup { - bool success; - bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); - - IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta(sharesDelta, premiumAmount, targetAssetId); - - _before(); - (success, returnData) = actor.proxy( - address(hub), abi.encodeCall(IHubBase.reportDeficit, (targetAssetId, drawnAmount, premiumDelta)) - ); - - if (success) { - _after(); - } else { - revert("HubHandler: reportDeficit failed"); - } + } + + function reportDeficit( + uint256 drawnAmount, + uint256 premiumAmount, + int256 sharesDelta, + uint8 i + ) external setup { + bool success; + bytes memory returnData; + targetAssetId = _getRandomBaseAssetId(i); + + IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta( + sharesDelta, + premiumAmount, + targetAssetId + ); + + _before(); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHubBase.reportDeficit, (targetAssetId, drawnAmount, premiumDelta)) + ); + + if (success) { + _after(); + } else { + revert('HubHandler: reportDeficit failed'); } - - function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external setup { - bool success; - bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); - address spoke = _getRandomActor(j); - - _before(); - (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHub.eliminateDeficit, (targetAssetId, amount, spoke))); - - if (success) { - _after(); - } else { - revert("HubHandler: eliminateDeficit failed"); - } + } + + function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external setup { + bool success; + bytes memory returnData; + targetAssetId = _getRandomBaseAssetId(i); + address spoke = _getRandomActor(j); + + _before(); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHub.eliminateDeficit, (targetAssetId, amount, spoke)) + ); + + if (success) { + _after(); + } else { + revert('HubHandler: eliminateDeficit failed'); } - - function refreshPremium(int256 sharesDelta, uint8 i) external setup { - bool success; - bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); - - int256 offsetRayDelta = sharesDelta * int256(hub.getAssetDrawnIndex(targetAssetId)); - IHubBase.PremiumDelta memory premiumDelta = - IHubBase.PremiumDelta({sharesDelta: sharesDelta, offsetRayDelta: offsetRayDelta, restoredPremiumRay: 0}); - - _before(); - (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.refreshPremium, (targetAssetId, premiumDelta))); - - if (success) { - _after(); - - // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) - assertEq( - defaultVarsAfter.assetVars[targetAssetId].premium, - defaultVarsBefore.assetVars[targetAssetId].premium, - HSPOST_HUB_M - ); - } else { - revert("HubHandler: refreshPremium failed"); - } + } + + function refreshPremium(int256 sharesDelta, uint8 i) external setup { + bool success; + bytes memory returnData; + targetAssetId = _getRandomBaseAssetId(i); + + int256 offsetRayDelta = sharesDelta * int256(hub.getAssetDrawnIndex(targetAssetId)); + IHubBase.PremiumDelta memory premiumDelta = IHubBase.PremiumDelta({ + sharesDelta: sharesDelta, + offsetRayDelta: offsetRayDelta, + restoredPremiumRay: 0 + }); + + _before(); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHubBase.refreshPremium, (targetAssetId, premiumDelta)) + ); + + if (success) { + _after(); + + // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) + assertEq( + defaultVarsAfter.assetVars[targetAssetId].premium, + defaultVarsBefore.assetVars[targetAssetId].premium, + HSPOST_HUB_M + ); + } else { + revert('HubHandler: refreshPremium failed'); } - - function payFeeShares(uint256 shares, uint8 i) external setup { - bool success; - bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); - - _before(); - (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHubBase.payFeeShares, (targetAssetId, shares))); - if (success) { - _after(); - } else { - revert("HubHandler: payFeeShares failed"); - } - } - - function transferShares(uint256 shares, uint8 i, uint8 j) external setup { - bool success; - bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); - address toSpoke = _getRandomActor(j); - - _before(); - (success, returnData) = - actor.proxy(address(hub), abi.encodeCall(IHub.transferShares, (targetAssetId, shares, toSpoke))); - - if (success) { - _after(); - } else { - revert("HubHandler: transferShares failed"); - } - } - - function sweep(uint256 amount, uint8 i) external { - targetAssetId = _getRandomBaseAssetId(i); - - _before(); - try hub.sweep(targetAssetId, amount) { - _after(); - } catch { - revert("HubHandler: sweep failed"); - } + } + + function payFeeShares(uint256 shares, uint8 i) external setup { + bool success; + bytes memory returnData; + targetAssetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHubBase.payFeeShares, (targetAssetId, shares)) + ); + if (success) { + _after(); + } else { + revert('HubHandler: payFeeShares failed'); } - - function reclaim(uint256 amount, uint8 i) external { - targetAssetId = _getRandomBaseAssetId(i); - - _before(); - try hub.reclaim(targetAssetId, amount) { - _after(); - } catch { - revert("HubHandler: reclaim failed"); - } + } + + function transferShares(uint256 shares, uint8 i, uint8 j) external setup { + bool success; + bytes memory returnData; + targetAssetId = _getRandomBaseAssetId(i); + address toSpoke = _getRandomActor(j); + + _before(); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHub.transferShares, (targetAssetId, shares, toSpoke)) + ); + + if (success) { + _after(); + } else { + revert('HubHandler: transferShares failed'); } + } - /////////////////////////////////////////////////////////////////////////////////////////////// - // ROUNDTRIP // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function roundtrip_ERC4626_RT_A(uint256 amount, uint8 i) external { - uint256 assetId = _getRandomBaseAssetId(i); + function sweep(uint256 amount, uint8 i) external { + targetAssetId = _getRandomBaseAssetId(i); - uint256 previewSharesToAdd = hub.previewAddByAssets(assetId, amount); - uint256 previewAssetsToRemove = hub.previewRemoveByShares(assetId, previewSharesToAdd); - - assertLe(previewAssetsToRemove, amount, HSPOST_HUB_ERC4626_RT_A); + _before(); + try hub.sweep(targetAssetId, amount) { + _after(); + } catch { + revert('HubHandler: sweep failed'); } + } - function roundtrip_ERC4626_RT_B(uint256 amount, uint8 i) external { - uint256 sharesAdded = add(amount, i); - uint256 previewAssetsToRemove = hub.previewRemoveByShares(_getRandomBaseAssetId(i), sharesAdded); - uint256 sharesRemoved = remove(previewAssetsToRemove, i); - - assertGe(sharesRemoved, sharesAdded, HSPOST_HUB_ERC4626_RT_B); - } - - function roundtrip_ERC4626_RT_C(uint256 shares, uint8 i) external { - uint256 assetId = _getRandomBaseAssetId(i); - - uint256 previewAssetsToRemove = hub.previewRemoveByShares(assetId, shares); - uint256 previewShares = hub.previewAddByAssets(assetId, previewAssetsToRemove); - - assertLe(previewShares, shares, HSPOST_HUB_ERC4626_RT_C); - } - - function roundtrip_ERC4626_RT_D(uint256 amount, uint8 i) external { - uint256 sharesRemoved = remove(amount, i); - uint256 previewAssetsToAdd = hub.previewAddByShares(_getRandomBaseAssetId(i), sharesRemoved); - uint256 sharesAdded = add(previewAssetsToAdd, i); - - assertLe(sharesAdded, sharesRemoved, HSPOST_HUB_ERC4626_RT_D); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _calculatePremiumDelta(int256 sharesDelta, uint256 premiumAmount, uint256 assetId) - internal - view - returns (IHubBase.PremiumDelta memory) - { - uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); - - // Calculate restoredPremiumRay from premiumAmount - uint256 restoredPremiumRay = premiumAmount * WadRayMath.RAY; - - // Calculate offsetRayDelta to satisfy: (sharesDelta * drawnIndex) - offsetRayDelta + restoredPremiumRay == 0 - // Therefore: offsetRayDelta = (sharesDelta * drawnIndex) + restoredPremiumRay - int256 offsetRayDelta = (sharesDelta * int256(drawnIndex)) + int256(restoredPremiumRay); + function reclaim(uint256 amount, uint8 i) external { + targetAssetId = _getRandomBaseAssetId(i); - return IHubBase.PremiumDelta({ - sharesDelta: sharesDelta, offsetRayDelta: offsetRayDelta, restoredPremiumRay: restoredPremiumRay - }); + _before(); + try hub.reclaim(targetAssetId, amount) { + _after(); + } catch { + revert('HubHandler: reclaim failed'); } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ROUNDTRIP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function roundtrip_ERC4626_RT_A(uint256 amount, uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + + uint256 previewSharesToAdd = hub.previewAddByAssets(assetId, amount); + uint256 previewAssetsToRemove = hub.previewRemoveByShares(assetId, previewSharesToAdd); + + assertLe(previewAssetsToRemove, amount, HSPOST_HUB_ERC4626_RT_A); + } + + function roundtrip_ERC4626_RT_B(uint256 amount, uint8 i) external { + uint256 sharesAdded = add(amount, i); + uint256 previewAssetsToRemove = hub.previewRemoveByShares( + _getRandomBaseAssetId(i), + sharesAdded + ); + uint256 sharesRemoved = remove(previewAssetsToRemove, i); + + assertGe(sharesRemoved, sharesAdded, HSPOST_HUB_ERC4626_RT_B); + } + + function roundtrip_ERC4626_RT_C(uint256 shares, uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + + uint256 previewAssetsToRemove = hub.previewRemoveByShares(assetId, shares); + uint256 previewShares = hub.previewAddByAssets(assetId, previewAssetsToRemove); + + assertLe(previewShares, shares, HSPOST_HUB_ERC4626_RT_C); + } + + function roundtrip_ERC4626_RT_D(uint256 amount, uint8 i) external { + uint256 sharesRemoved = remove(amount, i); + uint256 previewAssetsToAdd = hub.previewAddByShares(_getRandomBaseAssetId(i), sharesRemoved); + uint256 sharesAdded = add(previewAssetsToAdd, i); + + assertLe(sharesAdded, sharesRemoved, HSPOST_HUB_ERC4626_RT_D); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _calculatePremiumDelta( + int256 sharesDelta, + uint256 premiumAmount, + uint256 assetId + ) internal view returns (IHubBase.PremiumDelta memory) { + uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); + + // Calculate restoredPremiumRay from premiumAmount + uint256 restoredPremiumRay = premiumAmount * WadRayMath.RAY; + + // Calculate offsetRayDelta to satisfy: (sharesDelta * drawnIndex) - offsetRayDelta + restoredPremiumRay == 0 + // Therefore: offsetRayDelta = (sharesDelta * drawnIndex) + restoredPremiumRay + int256 offsetRayDelta = (sharesDelta * int256(drawnIndex)) + int256(restoredPremiumRay); + + return + IHubBase.PremiumDelta({ + sharesDelta: sharesDelta, + offsetRayDelta: offsetRayDelta, + restoredPremiumRay: restoredPremiumRay + }); + } } diff --git a/tests/invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/tests/invariants/hub-suite/handlers/interfaces/IHubHandler.sol index 962647c47..f718c4a44 100644 --- a/tests/invariants/hub-suite/handlers/interfaces/IHubHandler.sol +++ b/tests/invariants/hub-suite/handlers/interfaces/IHubHandler.sol @@ -4,17 +4,25 @@ pragma solidity ^0.8.19; /// @title IHubHandler /// @notice Interface for hub handler actions interface IHubHandler { - function add(uint256 amount, uint8 i) external returns (uint256 addedShares); - function remove(uint256 amount, uint8 i) external returns (uint256 removedShares); - function draw(uint256 amount, uint8 i) external returns (uint256 drawnShares); - function restore(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) - external - returns (uint256 restoredDrawnShares); - function reportDeficit(uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i) external; - function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external; - function refreshPremium(int256 sharesDelta, uint8 i) external; - function payFeeShares(uint256 shares, uint8 i) external; - function transferShares(uint256 shares, uint8 i, uint8 j) external; - function sweep(uint256 amount, uint8 i) external; - function reclaim(uint256 amount, uint8 i) external; + function add(uint256 amount, uint8 i) external returns (uint256 addedShares); + function remove(uint256 amount, uint8 i) external returns (uint256 removedShares); + function draw(uint256 amount, uint8 i) external returns (uint256 drawnShares); + function restore( + uint256 drawnAmount, + uint256 premiumAmount, + int256 sharesDelta, + uint8 i + ) external returns (uint256 restoredDrawnShares); + function reportDeficit( + uint256 drawnAmount, + uint256 premiumAmount, + int256 sharesDelta, + uint8 i + ) external; + function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external; + function refreshPremium(int256 sharesDelta, uint8 i) external; + function payFeeShares(uint256 shares, uint8 i) external; + function transferShares(uint256 shares, uint8 i, uint8 j) external; + function sweep(uint256 amount, uint8 i) external; + function reclaim(uint256 amount, uint8 i) external; } diff --git a/tests/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol b/tests/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol index 3ee317cf9..1af52dcd9 100644 --- a/tests/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol +++ b/tests/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol @@ -2,31 +2,31 @@ pragma solidity ^0.8.19; // Test Contracts -import {BaseHandler} from "../../base/BaseHandler.t.sol"; -import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; +import {BaseHandler} from '../../base/BaseHandler.t.sol'; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; /// @title DonationAttackHandler /// @notice Handler test contract for a set of actions contract DonationAttackHandler is BaseHandler { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STATE VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// - function donateUnderlyingToHub(uint256 amount, uint8 i) external { - address underlying = _getRandomBaseAsset(i); + function donateUnderlyingToHub(uint256 amount, uint8 i) external { + address underlying = _getRandomBaseAsset(i); - TestnetERC20(underlying).mint(address(hub), amount); - } + TestnetERC20(underlying).mint(address(hub), amount); + } - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index f0fb97ffd..8a3e88e52 100644 --- a/tests/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -2,235 +2,245 @@ pragma solidity ^0.8.19; // Libraries -import {MathUtils} from "src/libraries/math/MathUtils.sol"; -import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; -import "forge-std/console.sol"; +import {MathUtils} from 'src/libraries/math/MathUtils.sol'; +import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; +import 'forge-std/console.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; -import {PropertiesConstants} from "../../shared/utils/PropertiesConstants.sol"; -import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; +import {PropertiesConstants} from '../../shared/utils/PropertiesConstants.sol'; +import {StdAsserts} from '../../shared/utils/StdAsserts.sol'; // Interfaces -import {IHub} from "src/hub/interfaces/IHub.sol"; -import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; -import {IHubHandler} from "../handlers/interfaces/IHubHandler.sol"; +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; +import {IHubHandler} from '../handlers/interfaces/IHubHandler.sol'; // Contracts -import {BaseHooks} from "../base/BaseHooks.t.sol"; +import {BaseHooks} from '../base/BaseHooks.t.sol'; /// @title DefaultBeforeAfterHooks /// @notice Helper contract for before and after hooks, state variable caching and postconditions /// @dev This contract is inherited by handlers abstract contract DefaultBeforeAfterHooks is BaseHooks { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STRUCTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - struct AssetVars { - uint256 drawnIndex; - uint256 totalAssets; - uint256 totalShares; - uint256 drawn; - uint256 premium; - uint256 liquidity; - uint256 deficitRay; - uint256 swept; - uint256 lastUpdateTimestamp; - uint256 drawnRate; + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct AssetVars { + uint256 drawnIndex; + uint256 totalAssets; + uint256 totalShares; + uint256 drawn; + uint256 premium; + uint256 liquidity; + uint256 deficitRay; + uint256 swept; + uint256 lastUpdateTimestamp; + uint256 drawnRate; + } + + struct SpokeDataVars { + uint256 addedAssets; + uint256 addedShares; + uint256 drawnShares; + uint256 premiumShares; + int256 premiumOffsetRay; + uint256 deficitRay; + uint256 drawn; + uint256 premium; + uint256 owed; + } + + struct DefaultVars { + mapping(uint256 assetId => AssetVars) assetVars; + mapping(uint256 assetId => mapping(address spoke => SpokeDataVars)) spokeDataVars; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Default variables before and after + DefaultVars defaultVarsBefore; + DefaultVars defaultVarsAfter; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Default hooks setup + function _setUpDefaultHooks() internal {} + + /// @notice Helper to initialize storage arrays of default vars + function _setUpDefaultVars(DefaultVars storage _defaultVars) internal {} + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _defaultHooksBefore() internal { + // Asset values + _setAssetValues(defaultVarsBefore); + // Spoke data values + _setSpokeDataValues(defaultVarsBefore); + } + + function _defaultHooksAfter() internal { + // Asset values + _setAssetValues(defaultVarsAfter); + // Spoke data values + _setSpokeDataValues(defaultVarsAfter); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _setAssetValues(DefaultVars storage _defaultVars) internal { + uint256 assetCount = hub.getAssetCount(); + for (uint256 j; j < assetCount; j++) { + _defaultVars.assetVars[j].drawnIndex = hub.getAssetDrawnIndex(j); + _defaultVars.assetVars[j].totalAssets = hub.getAddedAssets(j); + _defaultVars.assetVars[j].totalShares = hub.getAddedShares(j); + (_defaultVars.assetVars[j].drawn, _defaultVars.assetVars[j].premium) = hub.getAssetOwed(j); + _defaultVars.assetVars[j].lastUpdateTimestamp = hub.getAsset(j).lastUpdateTimestamp; + _defaultVars.assetVars[j].drawnRate = hub.getAssetDrawnRate(j); + _defaultVars.assetVars[j].liquidity = hub.getAssetLiquidity(j); + _defaultVars.assetVars[j].deficitRay = hub.getAssetDeficitRay(j); + _defaultVars.assetVars[j].swept = hub.getAssetSwept(j); } - - struct SpokeDataVars { - uint256 addedAssets; - uint256 addedShares; - uint256 drawnShares; - uint256 premiumShares; - int256 premiumOffsetRay; - uint256 deficitRay; - uint256 drawn; - uint256 premium; - uint256 owed; + } + + function _setSpokeDataValues(DefaultVars storage _defaultVars) internal { + uint256 assetCount = hub.getAssetCount(); + + for (uint256 i; i < assetCount; i++) { + for (uint256 j; j < NUMBER_OF_ACTORS; j++) { + address spoke = actorAddresses[j]; + + _defaultVars.spokeDataVars[i][spoke].addedShares = hub.getSpokeAddedShares(i, spoke); + _defaultVars.spokeDataVars[i][spoke].addedAssets = hub.getSpokeAddedAssets(i, spoke); + _defaultVars.spokeDataVars[i][spoke].drawnShares = hub.getSpokeDrawnShares(i, spoke); + ( + _defaultVars.spokeDataVars[i][spoke].premiumShares, + _defaultVars.spokeDataVars[i][spoke].premiumOffsetRay + ) = hub.getSpokePremiumData(i, spoke); + _defaultVars.spokeDataVars[i][spoke].deficitRay = hub.getSpokeDeficitRay(i, spoke); + ( + _defaultVars.spokeDataVars[i][spoke].drawn, + _defaultVars.spokeDataVars[i][spoke].premium + ) = hub.getSpokeOwed(i, spoke); + _defaultVars.spokeDataVars[i][spoke].owed = + _defaultVars.spokeDataVars[i][spoke].drawn + _defaultVars.spokeDataVars[i][spoke].premium; + } } - - struct DefaultVars { - mapping(uint256 assetId => AssetVars) assetVars; - mapping(uint256 assetId => mapping(address spoke => SpokeDataVars)) spokeDataVars; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POST CONDITIONS: HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_GPOST_HUB_A(uint256 assetId) internal { + assertGe( + defaultVarsAfter.assetVars[assetId].drawnIndex, + defaultVarsBefore.assetVars[assetId].drawnIndex, + GPOST_HUB_A + ); + } + + function assert_GPOST_HUB_B(uint256 assetId) internal { + assertFullMulGe( + defaultVarsAfter.assetVars[assetId].totalAssets + 1e6, + defaultVarsBefore.assetVars[assetId].totalShares + 1e6, + defaultVarsBefore.assetVars[assetId].totalAssets + 1e6, + defaultVarsAfter.assetVars[assetId].totalShares + 1e6, + GPOST_HUB_B + ); + } + + function assert_GPOST_HUB_C(uint256 assetId) internal { + // Read the cached signature of the current action + bytes4 signature = currentActionSignature; + if ( + signature == IHubHandler.add.selector || + signature == IHubHandler.remove.selector || + signature == IHubHandler.draw.selector || + signature == IHubHandler.restore.selector || + signature == IHubHandler.reportDeficit.selector || + signature == IHubHandler.sweep.selector || + signature == IHubHandler.reclaim.selector || + signature == IHubHandler.eliminateDeficit.selector || + signature == IHubHandler.refreshPremium.selector || + signature == IHubHandler.payFeeShares.selector || + signature == IHubHandler.transferShares.selector + ) { + assertEq( + hub.getAssetDrawnRate(assetId), + IAssetInterestRateStrategy(irStrategy).calculateInterestRate( + assetId, + hub.getAssetLiquidity(assetId), + defaultVarsAfter.assetVars[assetId].drawn, + 0, // Unused in the interest rate calculation + hub.getAssetSwept(assetId) + ), + GPOST_HUB_C + ); } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HOOKS STORAGE // - /////////////////////////////////////////////////////////////////////////////////////////////// - - // Default variables before and after - DefaultVars defaultVarsBefore; - DefaultVars defaultVarsAfter; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // SETUP // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Default hooks setup - function _setUpDefaultHooks() internal {} - - /// @notice Helper to initialize storage arrays of default vars - function _setUpDefaultVars(DefaultVars storage _defaultVars) internal {} - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HOOKS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _defaultHooksBefore() internal { - // Asset values - _setAssetValues(defaultVarsBefore); - // Spoke data values - _setSpokeDataValues(defaultVarsBefore); - } - - function _defaultHooksAfter() internal { - // Asset values - _setAssetValues(defaultVarsAfter); - // Spoke data values - _setSpokeDataValues(defaultVarsAfter); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _setAssetValues(DefaultVars storage _defaultVars) internal { - uint256 assetCount = hub.getAssetCount(); - for (uint256 j; j < assetCount; j++) { - _defaultVars.assetVars[j].drawnIndex = hub.getAssetDrawnIndex(j); - _defaultVars.assetVars[j].totalAssets = hub.getAddedAssets(j); - _defaultVars.assetVars[j].totalShares = hub.getAddedShares(j); - (_defaultVars.assetVars[j].drawn, _defaultVars.assetVars[j].premium) = hub.getAssetOwed(j); - _defaultVars.assetVars[j].lastUpdateTimestamp = hub.getAsset(j).lastUpdateTimestamp; - _defaultVars.assetVars[j].drawnRate = hub.getAssetDrawnRate(j); - _defaultVars.assetVars[j].liquidity = hub.getAssetLiquidity(j); - _defaultVars.assetVars[j].deficitRay = hub.getAssetDeficitRay(j); - _defaultVars.assetVars[j].swept = hub.getAssetSwept(j); - } - } - - function _setSpokeDataValues(DefaultVars storage _defaultVars) internal { - uint256 assetCount = hub.getAssetCount(); - - for (uint256 i; i < assetCount; i++) { - for (uint256 j; j < NUMBER_OF_ACTORS; j++) { - address spoke = actorAddresses[j]; - - _defaultVars.spokeDataVars[i][spoke].addedShares = hub.getSpokeAddedShares(i, spoke); - _defaultVars.spokeDataVars[i][spoke].addedAssets = hub.getSpokeAddedAssets(i, spoke); - _defaultVars.spokeDataVars[i][spoke].drawnShares = hub.getSpokeDrawnShares(i, spoke); - ( - _defaultVars.spokeDataVars[i][spoke].premiumShares, - _defaultVars.spokeDataVars[i][spoke].premiumOffsetRay - ) = hub.getSpokePremiumData(i, spoke); - _defaultVars.spokeDataVars[i][spoke].deficitRay = hub.getSpokeDeficitRay(i, spoke); - (_defaultVars.spokeDataVars[i][spoke].drawn, _defaultVars.spokeDataVars[i][spoke].premium) = - hub.getSpokeOwed(i, spoke); - _defaultVars.spokeDataVars[i][spoke].owed = - _defaultVars.spokeDataVars[i][spoke].drawn + _defaultVars.spokeDataVars[i][spoke].premium; - } - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // POST CONDITIONS: HUB // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function assert_GPOST_HUB_A(uint256 assetId) internal { - assertGe( - defaultVarsAfter.assetVars[assetId].drawnIndex, defaultVarsBefore.assetVars[assetId].drawnIndex, GPOST_HUB_A + } + + function assert_GPOST_HUB_D(uint256 assetId) internal { + assertLe(defaultVarsAfter.assetVars[assetId].lastUpdateTimestamp, block.timestamp, GPOST_HUB_D); + } + + function assert_GPOST_HUB_EF(uint256 assetId, address spoke) internal { + // Get the spoke config + IHub.SpokeConfig memory spokeConfig = hub.getSpokeConfig(assetId, spoke); + (, uint8 decimals) = hub.getAssetUnderlyingAndDecimals(assetId); + + // GPOST_HUB_E + if ( + defaultVarsAfter.spokeDataVars[assetId][spoke].addedAssets > + defaultVarsBefore.spokeDataVars[assetId][spoke].addedAssets && + defaultVarsAfter.spokeDataVars[assetId][spoke].addedShares != + defaultVarsBefore.spokeDataVars[assetId][spoke].addedShares /// @dev required to avoid interest accrual detection + ) { + if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.spokeDataVars[assetId][spoke].addedAssets, + spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_E ); + } } - function assert_GPOST_HUB_B(uint256 assetId) internal { - assertFullMulGe( - defaultVarsAfter.assetVars[assetId].totalAssets + 1e6, - defaultVarsBefore.assetVars[assetId].totalShares + 1e6, - defaultVarsBefore.assetVars[assetId].totalAssets + 1e6, - defaultVarsAfter.assetVars[assetId].totalShares + 1e6, - GPOST_HUB_B - ); - } - - function assert_GPOST_HUB_C(uint256 assetId) internal { - // Read the cached signature of the current action - bytes4 signature = currentActionSignature; - if ( - signature == IHubHandler.add.selector || signature == IHubHandler.remove.selector - || signature == IHubHandler.draw.selector || signature == IHubHandler.restore.selector - || signature == IHubHandler.reportDeficit.selector || signature == IHubHandler.sweep.selector - || signature == IHubHandler.reclaim.selector || signature == IHubHandler.eliminateDeficit.selector - || signature == IHubHandler.refreshPremium.selector || signature == IHubHandler.payFeeShares.selector - || signature == IHubHandler.transferShares.selector - ) { - assertEq( - hub.getAssetDrawnRate(assetId), - IAssetInterestRateStrategy(irStrategy) - .calculateInterestRate( - assetId, - hub.getAssetLiquidity(assetId), - defaultVarsAfter.assetVars[assetId].drawn, - 0, // Unused in the interest rate calculation - hub.getAssetSwept(assetId) - ), - GPOST_HUB_C - ); - } - } - - function assert_GPOST_HUB_D(uint256 assetId) internal { - assertLe(defaultVarsAfter.assetVars[assetId].lastUpdateTimestamp, block.timestamp, GPOST_HUB_D); - } - - function assert_GPOST_HUB_EF(uint256 assetId, address spoke) internal { - // Get the spoke config - IHub.SpokeConfig memory spokeConfig = hub.getSpokeConfig(assetId, spoke); - (, uint8 decimals) = hub.getAssetUnderlyingAndDecimals(assetId); - - // GPOST_HUB_E - if ( - defaultVarsAfter.spokeDataVars[assetId][spoke].addedAssets - > defaultVarsBefore.spokeDataVars[assetId][spoke].addedAssets - && defaultVarsAfter.spokeDataVars[assetId][spoke].addedShares - != defaultVarsBefore.spokeDataVars[assetId][spoke].addedShares /// @dev required to avoid interest accrual detection - ) { - if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { - assertLe( - defaultVarsAfter.spokeDataVars[assetId][spoke].addedAssets, - spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), - GPOST_HUB_E - ); - } - } - - // GPOST_HUB_F - if (defaultVarsAfter.spokeDataVars[assetId][spoke].owed > defaultVarsBefore.spokeDataVars[assetId][spoke].owed) - { - if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { - assertLe( - defaultVarsAfter.spokeDataVars[assetId][spoke].owed, - spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), - GPOST_HUB_F - ); - } - } - } - - function assert_GPOST_HUB_G(uint256 assetId) internal { - assertGe( - defaultVarsAfter.assetVars[assetId].lastUpdateTimestamp, - defaultVarsBefore.assetVars[assetId].lastUpdateTimestamp, - GPOST_HUB_G + // GPOST_HUB_F + if ( + defaultVarsAfter.spokeDataVars[assetId][spoke].owed > + defaultVarsBefore.spokeDataVars[assetId][spoke].owed + ) { + if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.spokeDataVars[assetId][spoke].owed, + spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_F ); + } } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - function _cacheCurrentActionSignature() internal { - currentActionSignature = bytes4(msg.sig); - } + } + + function assert_GPOST_HUB_G(uint256 assetId) internal { + assertGe( + defaultVarsAfter.assetVars[assetId].lastUpdateTimestamp, + defaultVarsBefore.assetVars[assetId].lastUpdateTimestamp, + GPOST_HUB_G + ); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + function _cacheCurrentActionSignature() internal { + currentActionSignature = bytes4(msg.sig); + } } diff --git a/tests/invariants/hub-suite/hooks/HookAggregator.t.sol b/tests/invariants/hub-suite/hooks/HookAggregator.t.sol index 3615b75dd..b8e5b9ae0 100644 --- a/tests/invariants/hub-suite/hooks/HookAggregator.t.sol +++ b/tests/invariants/hub-suite/hooks/HookAggregator.t.sol @@ -2,88 +2,87 @@ pragma solidity ^0.8.19; // Hook Contracts -import {DefaultBeforeAfterHooks} from "./DefaultBeforeAfterHooks.t.sol"; +import {DefaultBeforeAfterHooks} from './DefaultBeforeAfterHooks.t.sol'; // Utils -import {ErrorHandlers} from "../../shared/utils/ErrorHandlers.sol"; -import "forge-std/console.sol"; +import {ErrorHandlers} from '../../shared/utils/ErrorHandlers.sol'; +import 'forge-std/console.sol'; /// @title HookAggregator /// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler abstract contract HookAggregator is DefaultBeforeAfterHooks { - /////////////////////////////////////////////////////////////////////////////////////////////// - // SETUP // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Initializer for the hooks - function _setUpHooks() internal { - _setUpDefaultHooks(); + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Initializer for the hooks + function _setUpHooks() internal { + _setUpDefaultHooks(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Before hook for the handlers + function _before() internal { + _defaultHooksBefore(); + } + + /// @notice After hook for the handlers + function _after() internal { + _defaultHooksAfter(); + + // POST-CONDITIONS + _checkPostConditions(); + + // Reset the state + _resetState(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POSTCONDITION CHECKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Postconditions for the handlers + function _checkPostConditions() internal { + // Store the message signature to avoid losing it inside the checkPostConditions call context + _cacheCurrentActionSignature(); + + try this.checkPostConditions() {} catch (bytes memory returnData) { + ErrorHandlers.handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); } + } - /////////////////////////////////////////////////////////////////////////////////////////////// - // HOOKS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Before hook for the handlers - function _before() internal { - _defaultHooksBefore(); + /// @dev postconditions checks entrypoint, should be self-called + function checkPostConditions() external { + // Target asset postconditions + if (targetAssetId != 0) { + assert_GPOST_HUB_C(targetAssetId); } - /// @notice After hook for the handlers - function _after() internal { - _defaultHooksAfter(); - - // POST-CONDITIONS - _checkPostConditions(); - - // Reset the state - _resetState(); + // Protocol-wide postconditions + uint256 assetCount = hub.getAssetCount(); + for (uint256 i; i < assetCount; i++) { + assert_GPOST_HUB_A(i); + assert_GPOST_HUB_B(i); + assert_GPOST_HUB_D(i); + assert_GPOST_HUB_G(i); + + for (uint256 j; j < NUMBER_OF_ACTORS; j++) { + address spoke = actorAddresses[j]; + assert_GPOST_HUB_EF(i, spoke); + } } + } - /////////////////////////////////////////////////////////////////////////////////////////////// - // POSTCONDITION CHECKS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Postconditions for the handlers - function _checkPostConditions() internal { - // Store the message signature to avoid losing it inside the checkPostConditions call context - _cacheCurrentActionSignature(); + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// - try this.checkPostConditions() {} - catch (bytes memory returnData) { - ErrorHandlers.handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); - } - } - - /// @dev postconditions checks entrypoint, should be self-called - function checkPostConditions() external { - // Target asset postconditions - if (targetAssetId != 0) { - assert_GPOST_HUB_C(targetAssetId); - } - - // Protocol-wide postconditions - uint256 assetCount = hub.getAssetCount(); - for (uint256 i; i < assetCount; i++) { - assert_GPOST_HUB_A(i); - assert_GPOST_HUB_B(i); - assert_GPOST_HUB_D(i); - assert_GPOST_HUB_G(i); - - for (uint256 j; j < NUMBER_OF_ACTORS; j++) { - address spoke = actorAddresses[j]; - assert_GPOST_HUB_EF(i, spoke); - } - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Resets the state of the handlers - function _resetState() internal { - delete currentActionSignature; - delete targetAssetId; - } + /// @notice Resets the state of the handlers + function _resetState() internal { + delete currentActionSignature; + delete targetAssetId; + } } diff --git a/tests/invariants/hub-suite/invariants/HubInvariants.t.sol b/tests/invariants/hub-suite/invariants/HubInvariants.t.sol index 55d6c6a5c..e7dd9c175 100644 --- a/tests/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/tests/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -2,279 +2,278 @@ pragma solidity ^0.8.19; // Libraries -import {WadRayMath} from "src/libraries/math/WadRayMath.sol"; -import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; -import "forge-std/console.sol"; +import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; +import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; +import 'forge-std/console.sol'; // Interfaces -import {IHub} from "src/hub/interfaces/IHub.sol"; -import {IERC20} from "src/dependencies/openzeppelin/IERC20.sol"; +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; // Contracts -import {HandlerAggregator} from "../HandlerAggregator.t.sol"; +import {HandlerAggregator} from '../HandlerAggregator.t.sol'; /// @title HubInvariants /// @notice Implements Hub Invariants for the protocol /// @dev Inherits HandlerAggregator to check actions in assertion testing mode abstract contract HubInvariants is HandlerAggregator { - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // - /////////////////////////////////////////////////////////////////////////////////////////////// - function assert_INV_HUB_A(uint256 assetId) internal { - uint256 assets = hub.getAddedAssets(assetId); - - if (assets == 0) { - assertEq(hub.getAddedShares(assetId), 0, INV_HUB_A2); - } + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + function assert_INV_HUB_A(uint256 assetId) internal { + uint256 assets = hub.getAddedAssets(assetId); + + if (assets == 0) { + assertEq(hub.getAddedShares(assetId), 0, INV_HUB_A2); } + } - function assert_INV_HUB_B(uint256 assetId) internal { - // Sum per-spoke values - uint256 spokeCount = NUMBER_OF_ACTORS; - uint256 sumDebt; + function assert_INV_HUB_B(uint256 assetId) internal { + // Sum per-spoke values + uint256 spokeCount = NUMBER_OF_ACTORS; + uint256 sumDebt; - for (uint256 i; i < spokeCount; i++) { - (uint256 d, uint256 p) = hub.getSpokeOwed(assetId, actorAddresses[i]); - sumDebt += d + p; - } - - uint256 assetTotal = hub.getAssetTotalOwed(assetId); // drawn + premium - assertGe(sumDebt, assetTotal, INV_HUB_B); + for (uint256 i; i < spokeCount; i++) { + (uint256 d, uint256 p) = hub.getSpokeOwed(assetId, actorAddresses[i]); + sumDebt += d + p; } - function assert_INV_HUB_C(uint256 assetId) internal { - // Sum per-spoke values - uint256 spokeCount = NUMBER_OF_ACTORS; - - uint256 sumDrawnShares; - uint256 sumPremDrawnShares; - int256 sumPremOffsetRay; - - for (uint256 i; i < spokeCount; i++) { - address spoke = actorAddresses[i]; - sumDrawnShares += hub.getSpokeDrawnShares(assetId, spoke); - (uint256 premiumDrawnShares, int256 premiumOffsetRay) = hub.getSpokePremiumData(assetId, spoke); - sumPremDrawnShares += premiumDrawnShares; - sumPremOffsetRay += premiumOffsetRay; - } - - // Asset totals - IHub.Asset memory a = hub.getAsset(assetId); - - // Checks - assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); - assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); - assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); + uint256 assetTotal = hub.getAssetTotalOwed(assetId); // drawn + premium + assertGe(sumDebt, assetTotal, INV_HUB_B); + } + + function assert_INV_HUB_C(uint256 assetId) internal { + // Sum per-spoke values + uint256 spokeCount = NUMBER_OF_ACTORS; + + uint256 sumDrawnShares; + uint256 sumPremDrawnShares; + int256 sumPremOffsetRay; + + for (uint256 i; i < spokeCount; i++) { + address spoke = actorAddresses[i]; + sumDrawnShares += hub.getSpokeDrawnShares(assetId, spoke); + (uint256 premiumDrawnShares, int256 premiumOffsetRay) = hub.getSpokePremiumData( + assetId, + spoke + ); + sumPremDrawnShares += premiumDrawnShares; + sumPremOffsetRay += premiumOffsetRay; } - function assert_INV_HUB_EF(uint256 assetId) internal { - // Total amounts - uint256 totalSuppliedAssets = hub.getAddedAssets(assetId); - - IHub.Asset memory asset = hub.getAsset(assetId); - uint256 totalDebt = hub.getAssetTotalOwed(assetId); - - // Checks - assertApproxEqAbs( //TODO check todo file INV_HUB_E - totalSuppliedAssets, - hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)), - hub.previewRemoveByShares(assetId, 1), // tolerance of 1 share for rounding - INV_HUB_E - ); - - assertEq( - totalSuppliedAssets * WadRayMath.RAY, - asset.liquidity * WadRayMath.RAY + totalDebt * WadRayMath.RAY + asset.deficitRay + asset.swept - * WadRayMath.RAY, - INV_HUB_F - ); + // Asset totals + IHub.Asset memory a = hub.getAsset(assetId); + + // Checks + assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); + assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); + assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); + } + + function assert_INV_HUB_EF(uint256 assetId) internal { + // Total amounts + uint256 totalSuppliedAssets = hub.getAddedAssets(assetId); + + IHub.Asset memory asset = hub.getAsset(assetId); + uint256 totalDebt = hub.getAssetTotalOwed(assetId); + + // Checks + assertApproxEqAbs( + //TODO check todo file INV_HUB_E + totalSuppliedAssets, + hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)), + hub.previewRemoveByShares(assetId, 1), // tolerance of 1 share for rounding + INV_HUB_E + ); + + assertEq( + totalSuppliedAssets * WadRayMath.RAY, + asset.liquidity * WadRayMath.RAY + + totalDebt * WadRayMath.RAY + + asset.deficitRay + + asset.swept * WadRayMath.RAY, + INV_HUB_F + ); + } + + function assert_INV_HUB_GH(uint256 assetId) internal { + uint256 spokeCount = NUMBER_OF_ACTORS; + + // Sum per-spoke values + uint256 totalAddedAssets; + uint256 totalAddedShares; + for (uint256 i; i < spokeCount; i++) { + totalAddedAssets += hub.getSpokeAddedAssets(assetId, actorAddresses[i]); + totalAddedShares += hub.getSpokeAddedShares(assetId, actorAddresses[i]); } + totalAddedAssets += hub.getSpokeAddedAssets(assetId, address(this)); + totalAddedShares += hub.getSpokeAddedShares(assetId, address(this)); - function assert_INV_HUB_GH(uint256 assetId) internal { - uint256 spokeCount = NUMBER_OF_ACTORS; - - // Sum per-spoke values - uint256 totalAddedAssets; - uint256 totalAddedShares; - for (uint256 i; i < spokeCount; i++) { - totalAddedAssets += hub.getSpokeAddedAssets(assetId, actorAddresses[i]); - totalAddedShares += hub.getSpokeAddedShares(assetId, actorAddresses[i]); - } - totalAddedAssets += hub.getSpokeAddedAssets(assetId, address(this)); - totalAddedShares += hub.getSpokeAddedShares(assetId, address(this)); - - // Checks - uint256 addedShares = hub.getAddedShares(assetId); - if (addedShares > 0) { - assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); // TODO check if tolerance is correct - } - assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); + // Checks + uint256 addedShares = hub.getAddedShares(assetId); + if (addedShares > 0) { + assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); // TODO check if tolerance is correct } - - function assert_INV_HUB_I(uint256 assetId) internal { - // Get underlying from assetId - (address underlying,) = hub.getAssetUnderlyingAndDecimals(assetId); - - // Query values - uint256 liquidity = hub.getAssetLiquidity(assetId); - uint256 swept = hub.getAssetSwept(assetId); - uint256 underlyingBalance = IERC20(underlying).balanceOf(address(hub)); - - // Checks - assertGe(underlyingBalance + swept, liquidity, INV_HUB_I); + assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); + } + + function assert_INV_HUB_I(uint256 assetId) internal { + // Get underlying from assetId + (address underlying, ) = hub.getAssetUnderlyingAndDecimals(assetId); + + // Query values + uint256 liquidity = hub.getAssetLiquidity(assetId); + uint256 swept = hub.getAssetSwept(assetId); + uint256 underlyingBalance = IERC20(underlying).balanceOf(address(hub)); + + // Checks + assertGe(underlyingBalance + swept, liquidity, INV_HUB_I); + } + + function assert_INV_HUB_K(uint256 assetId) internal { + /// @dev for this check to be meaningful, strategy configuration operations have to be integrated + IHub.AssetConfig memory assetConfig = hub.getAssetConfig(assetId); + + // Checks + assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); + } + + function assert_INV_HUB_L(uint256 assetId) internal { + // Get premium data + (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); + + // Check + assertGe( + int256(hub.previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), + premiumOffsetRay, + INV_HUB_L + ); + } + + function assert_INV_HUB_O(uint256 assetId) internal { + uint256 spokeCount = NUMBER_OF_ACTORS; + uint256 totalDeficitRay; + for (uint256 i; i < spokeCount; i++) { + totalDeficitRay += hub.getSpokeDeficitRay(assetId, actorAddresses[i]); } - - function assert_INV_HUB_K(uint256 assetId) internal { - /// @dev for this check to be meaningful, strategy configuration operations have to be integrated - IHub.AssetConfig memory assetConfig = hub.getAssetConfig(assetId); - - // Checks - assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); - } - - function assert_INV_HUB_L(uint256 assetId) internal { - // Get premium data - (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); - - // Check - assertGe( - int256(hub.previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), premiumOffsetRay, INV_HUB_L - ); - } - - function assert_INV_HUB_O(uint256 assetId) internal { - uint256 spokeCount = NUMBER_OF_ACTORS; - uint256 totalDeficitRay; - for (uint256 i; i < spokeCount; i++) { - totalDeficitRay += hub.getSpokeDeficitRay(assetId, actorAddresses[i]); - } - assertEq(totalDeficitRay, hub.getAssetDeficitRay(assetId), INV_HUB_O); - } - - function assert_INV_HUB_P(uint256 assetId) internal { - (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); - uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); - assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); - } - - function assert_INV_HUB_N(uint256 assetId) internal { - IHub.Asset memory asset = hub.getAsset(assetId); - - // Skip if no debt (no interest can accrue) - if (asset.drawnShares == 0 && asset.premiumShares == 0) return; - - // Get current index (includes unrealized interest) vs stored index - uint256 currentIndex = hub.getAssetDrawnIndex(assetId); - uint256 storedIndex = asset.drawnIndex; - - // Skip if no index growth (no interest accrued) - if (currentIndex == storedIndex) return; - - // Calculate accrued interest from index growth - uint256 indexGrowth = currentIndex - storedIndex; - uint256 totalDebtShares = uint256(asset.drawnShares) + uint256(asset.premiumShares); - uint256 accruedInterestRay = (totalDebtShares * indexGrowth); - - // Get unrealized fees - uint256 unrealizedFees = hub.getAssetAccruedFees(assetId) - asset.realizedFees; - - // Invariant: fees × PERCENTAGE_FACTOR × RAY ≈ accruedInterestRay × liquidityFee - uint256 lhs = unrealizedFees * PercentageMath.PERCENTAGE_FACTOR * WadRayMath.RAY; - uint256 rhs = accruedInterestRay * asset.liquidityFee; - - // Tolerance: 1 wei rounding scaled up - uint256 tolerance = WadRayMath.RAY * PercentageMath.PERCENTAGE_FACTOR; - assertApproxEqAbs(lhs, rhs, tolerance, INV_HUB_N); - } - - function assert_INV_HUB_ERC4626_A(uint256 assetId, address spoke) internal { - uint256 addedAssets = hub.getSpokeAddedAssets(assetId, spoke); - uint256 addedShares = hub.getSpokeAddedShares(assetId, spoke); - if (addedAssets != 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_A); - } - - function assert_INV_HUB_ERC4626_B(uint256 assetId, address spoke) internal { - (uint256 drawnAssets,) = hub.getSpokeOwed(assetId, spoke); - uint256 drawnShares = hub.getSpokeDrawnShares(assetId, spoke); - if (drawnAssets != 0) assertTrue(drawnShares != 0, INV_HUB_ERC4626_B); - } - - function assert_INV_HUB_ERC4626_C(uint256 assetId) internal { - uint256 addedAssets = hub.getAddedAssets(assetId); - uint256 addedShares = hub.getAddedShares(assetId); - if (addedAssets != 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_C); - } - - function assert_INV_HUB_ERC4626_D(uint256 assetId) internal { - (uint256 drawnAssets,) = hub.getAssetOwed(assetId); - uint256 drawnShares = hub.getAssetDrawnShares(assetId); - if (drawnAssets != 0) assertTrue(drawnShares != 0, INV_HUB_ERC4626_D); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB: AVAILABILITY // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function assert_INV_HUB_AVAILABILITY_A(uint256 assetId) internal { - try hub.getAddedAssets(assetId) {} - catch { - assertTrue(false, INV_HUB_AVAILABILITY_A); - } + assertEq(totalDeficitRay, hub.getAssetDeficitRay(assetId), INV_HUB_O); + } + + function assert_INV_HUB_P(uint256 assetId) internal { + (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); + uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); + assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); + } + + function assert_INV_HUB_N(uint256 assetId) internal { + IHub.Asset memory asset = hub.getAsset(assetId); + + // Skip if no debt (no interest can accrue) + if (asset.drawnShares == 0 && asset.premiumShares == 0) return; + + // Get current index (includes unrealized interest) vs stored index + uint256 currentIndex = hub.getAssetDrawnIndex(assetId); + uint256 storedIndex = asset.drawnIndex; + + // Skip if no index growth (no interest accrued) + if (currentIndex == storedIndex) return; + + // Calculate accrued interest from index growth + uint256 indexGrowth = currentIndex - storedIndex; + uint256 totalDebtShares = uint256(asset.drawnShares) + uint256(asset.premiumShares); + uint256 accruedInterestRay = (totalDebtShares * indexGrowth); + + // Get unrealized fees + uint256 unrealizedFees = hub.getAssetAccruedFees(assetId) - asset.realizedFees; + + // Invariant: fees × PERCENTAGE_FACTOR × RAY ≈ accruedInterestRay × liquidityFee + uint256 lhs = unrealizedFees * PercentageMath.PERCENTAGE_FACTOR * WadRayMath.RAY; + uint256 rhs = accruedInterestRay * asset.liquidityFee; + + // Tolerance: 1 wei rounding scaled up + uint256 tolerance = WadRayMath.RAY * PercentageMath.PERCENTAGE_FACTOR; + assertApproxEqAbs(lhs, rhs, tolerance, INV_HUB_N); + } + + function assert_INV_HUB_ERC4626_A(uint256 assetId, address spoke) internal { + uint256 addedAssets = hub.getSpokeAddedAssets(assetId, spoke); + uint256 addedShares = hub.getSpokeAddedShares(assetId, spoke); + if (addedAssets != 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_A); + } + + function assert_INV_HUB_ERC4626_B(uint256 assetId, address spoke) internal { + (uint256 drawnAssets, ) = hub.getSpokeOwed(assetId, spoke); + uint256 drawnShares = hub.getSpokeDrawnShares(assetId, spoke); + if (drawnAssets != 0) assertTrue(drawnShares != 0, INV_HUB_ERC4626_B); + } + + function assert_INV_HUB_ERC4626_C(uint256 assetId) internal { + uint256 addedAssets = hub.getAddedAssets(assetId); + uint256 addedShares = hub.getAddedShares(assetId); + if (addedAssets != 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_C); + } + + function assert_INV_HUB_ERC4626_D(uint256 assetId) internal { + (uint256 drawnAssets, ) = hub.getAssetOwed(assetId); + uint256 drawnShares = hub.getAssetDrawnShares(assetId); + if (drawnAssets != 0) assertTrue(drawnShares != 0, INV_HUB_ERC4626_D); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB: AVAILABILITY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_INV_HUB_AVAILABILITY_A(uint256 assetId) internal { + try hub.getAddedAssets(assetId) {} catch { + assertTrue(false, INV_HUB_AVAILABILITY_A); } + } - function assert_INV_HUB_AVAILABILITY_B(uint256 assetId) internal { - try hub.getAssetOwed(assetId) {} - catch { - assertTrue(false, INV_HUB_AVAILABILITY_B); - } + function assert_INV_HUB_AVAILABILITY_B(uint256 assetId) internal { + try hub.getAssetOwed(assetId) {} catch { + assertTrue(false, INV_HUB_AVAILABILITY_B); } + } - function assert_INV_HUB_AVAILABILITY_C(uint256 assetId) internal { - try hub.getAssetTotalOwed(assetId) {} - catch { - assertTrue(false, INV_HUB_AVAILABILITY_C); - } + function assert_INV_HUB_AVAILABILITY_C(uint256 assetId) internal { + try hub.getAssetTotalOwed(assetId) {} catch { + assertTrue(false, INV_HUB_AVAILABILITY_C); } + } - function assert_INV_HUB_AVAILABILITY_D(uint256 assetId) internal { - try hub.getAssetPremiumRay(assetId) {} - catch { - assertTrue(false, INV_HUB_AVAILABILITY_D); - } + function assert_INV_HUB_AVAILABILITY_D(uint256 assetId) internal { + try hub.getAssetPremiumRay(assetId) {} catch { + assertTrue(false, INV_HUB_AVAILABILITY_D); } + } - function assert_INV_HUB_AVAILABILITY_E(uint256 assetId) internal { - try hub.getAssetAccruedFees(assetId) {} - catch { - assertTrue(false, INV_HUB_AVAILABILITY_E); - } + function assert_INV_HUB_AVAILABILITY_E(uint256 assetId) internal { + try hub.getAssetAccruedFees(assetId) {} catch { + assertTrue(false, INV_HUB_AVAILABILITY_E); } + } - function assert_INV_HUB_AVAILABILITY_F(uint256 assetId, address spoke) internal { - try hub.getSpokeAddedAssets(assetId, spoke) {} - catch { - assertTrue(false, INV_HUB_AVAILABILITY_F); - } + function assert_INV_HUB_AVAILABILITY_F(uint256 assetId, address spoke) internal { + try hub.getSpokeAddedAssets(assetId, spoke) {} catch { + assertTrue(false, INV_HUB_AVAILABILITY_F); } + } - function assert_INV_HUB_AVAILABILITY_G(uint256 assetId, address spoke) internal { - try hub.getSpokeOwed(assetId, spoke) {} - catch { - assertTrue(false, INV_HUB_AVAILABILITY_G); - } + function assert_INV_HUB_AVAILABILITY_G(uint256 assetId, address spoke) internal { + try hub.getSpokeOwed(assetId, spoke) {} catch { + assertTrue(false, INV_HUB_AVAILABILITY_G); } + } - function assert_INV_HUB_AVAILABILITY_H(uint256 assetId, address spoke) internal { - try hub.getSpokeTotalOwed(assetId, spoke) {} - catch { - assertTrue(false, INV_HUB_AVAILABILITY_H); - } + function assert_INV_HUB_AVAILABILITY_H(uint256 assetId, address spoke) internal { + try hub.getSpokeTotalOwed(assetId, spoke) {} catch { + assertTrue(false, INV_HUB_AVAILABILITY_H); } + } - function assert_INV_HUB_AVAILABILITY_I(uint256 assetId, address spoke) internal { - try hub.getSpokePremiumRay(assetId, spoke) {} - catch { - assertTrue(false, INV_HUB_AVAILABILITY_I); - } + function assert_INV_HUB_AVAILABILITY_I(uint256 assetId, address spoke) internal { + try hub.getSpokePremiumRay(assetId, spoke) {} catch { + assertTrue(false, INV_HUB_AVAILABILITY_I); } + } } diff --git a/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol index bb5de425a..e6a652c4a 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol @@ -2,80 +2,80 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest1 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest1 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @dev PASS - function test_replay_1_add() public { - _setUpActor(USER3); - _delay(67960); - Tester.updateSpokePaused(true, 26, 105); - _delay(289607); - Tester.add(1524785992, 171); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest1 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev PASS + function test_replay_1_add() public { + _setUpActor(USER3); + _delay(67960); + Tester.updateSpokePaused(true, 26, 105); + _delay(289607); + Tester.add(1524785992, 171); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/hub-suite/replays/ReplayTest_2.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_2.t.sol index a3dac3bf8..aff11b4e9 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_2.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_2.t.sol @@ -2,131 +2,131 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest2Hub is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest2Hub Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @dev Replays a low-liquidity scenario where virtual assets/shares skew exchange rate. - /// Early redeemers get more assets per share than late redeemers - accepted by design. - function test_replay_2_add() public { - _setUpActor(USER1); - Tester.add(1, 0); - Tester.draw(1, 0); - _delay(1); - Tester.add(3, 0); - } - - /// @dev PASS - function test_replay_2_payFeeShares() public { - _setUpActor(USER1); - Tester.add(1, 0); - Tester.add(2, 1); - Tester.draw(1, 1); - _delay(1); - Tester.payFeeShares(1, 0); - invariant_INV_HUB(); - } - - /// @dev PASS - function test_replay_2_eliminateDeficit() public { - _setUpActor(USER1); - Tester.add(2, 1); - Tester.draw(1, 1); - _delay(1); - Tester.eliminateDeficit(1974577205127400860, 0, 0); - } - - /// @dev PASS - function test_replay_2_remove() public { - _setUpActor(USER1); - Tester.add(2, 1); - Tester.add(1, 0); - Tester.draw(1, 1); - _delay(1); - Tester.remove(1, 0); - } - - /// @dev PASS - function test_replay_2_transferShares() public { - _setUpActor(USER1); - Tester.add(1, 1); - Tester.add(2, 0); - Tester.draw(1, 0); - _delay(1); - Tester.transferShares(1, 1, 0); - } - - /// @dev PASS - function test_replay_2_draw() public { - _setUpActor(USER1); - Tester.add(1, 0); - Tester.add(2, 1); - Tester.draw(1, 1); - _delay(1); - Tester.draw(1, 0); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest2Hub Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Replays a low-liquidity scenario where virtual assets/shares skew exchange rate. + /// Early redeemers get more assets per share than late redeemers - accepted by design. + function test_replay_2_add() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.draw(1, 0); + _delay(1); + Tester.add(3, 0); + } + + /// @dev PASS + function test_replay_2_payFeeShares() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.add(2, 1); + Tester.draw(1, 1); + _delay(1); + Tester.payFeeShares(1, 0); + invariant_INV_HUB(); + } + + /// @dev PASS + function test_replay_2_eliminateDeficit() public { + _setUpActor(USER1); + Tester.add(2, 1); + Tester.draw(1, 1); + _delay(1); + Tester.eliminateDeficit(1974577205127400860, 0, 0); + } + + /// @dev PASS + function test_replay_2_remove() public { + _setUpActor(USER1); + Tester.add(2, 1); + Tester.add(1, 0); + Tester.draw(1, 1); + _delay(1); + Tester.remove(1, 0); + } + + /// @dev PASS + function test_replay_2_transferShares() public { + _setUpActor(USER1); + Tester.add(1, 1); + Tester.add(2, 0); + Tester.draw(1, 0); + _delay(1); + Tester.transferShares(1, 1, 0); + } + + /// @dev PASS + function test_replay_2_draw() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.add(2, 1); + Tester.draw(1, 1); + _delay(1); + Tester.draw(1, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/hub-suite/replays/ReplayTest_3.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_3.t.sol index 3d6949dca..3218f73fb 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_3.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_3.t.sol @@ -2,81 +2,81 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest3Hub is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest3Hub Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @dev FAIL - function test_replay_3_add() public { - _setUpActor(USER1); - Tester.add(1, 0); - Tester.draw(1, 0); - _delay(6343); - Tester.add(6, 0); - invariant_INV_HUB(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest3Hub Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev FAIL + function test_replay_3_add() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.draw(1, 0); + _delay(6343); + Tester.add(6, 0); + invariant_INV_HUB(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/hub-suite/replays/ReplayTest_4.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_4.t.sol index c279a98f7..42378d9c1 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_4.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_4.t.sol @@ -2,92 +2,92 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest4 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest4 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @dev PASS - function test_replay_4_add() public { - _setUpActor(USER3); - Tester.add(1012156, 0); - _setUpActor(USER1); - Tester.draw(1, 0); - _delay(150); - Tester.updateSpokeSupplyCap(0, 9, 50); - Tester.add(9, 0); - } - - /// @dev PASS - function test_replay_4_draw() public { - _setUpActor(USER1); - Tester.add(926436254396264375725066, 1); - Tester.updateSpokeSupplyCap(0, 1, 0); - Tester.draw(68, 1); - _delay(659719); - Tester.draw(403197591059359279380077, 1); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest4 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev PASS + function test_replay_4_add() public { + _setUpActor(USER3); + Tester.add(1012156, 0); + _setUpActor(USER1); + Tester.draw(1, 0); + _delay(150); + Tester.updateSpokeSupplyCap(0, 9, 50); + Tester.add(9, 0); + } + + /// @dev PASS + function test_replay_4_draw() public { + _setUpActor(USER1); + Tester.add(926436254396264375725066, 1); + Tester.updateSpokeSupplyCap(0, 1, 0); + Tester.draw(68, 1); + _delay(659719); + Tester.draw(403197591059359279380077, 1); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/hub-suite/replays/ReplayTest_5.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_5.t.sol index 2f2c6c6e8..191e6f9a2 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_5.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_5.t.sol @@ -2,79 +2,79 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; +import 'forge-std/Test.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest5Hub is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest5Hub Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice BUG: refreshPremium can be called without drawnShares, creating phantom premium - /// that accrues over time and breaks INV_HUB_ERC4626_C (assets > 0 with shares == 0) - function test_replay_5_donateUnderlyingToHub() public { - _setUpActor(USER1); - Tester.refreshPremium(9472849991, 0); - _delay(1); - invariant_INV_HUB(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest5Hub Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice BUG: refreshPremium can be called without drawnShares, creating phantom premium + /// that accrues over time and breaks INV_HUB_ERC4626_C (assets > 0 with shares == 0) + function test_replay_5_donateUnderlyingToHub() public { + _setUpActor(USER1); + Tester.refreshPremium(9472849991, 0); + _delay(1); + invariant_INV_HUB(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/hub-suite/replays/ReplayTest_6.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_6.t.sol index 7fb5b4d23..35b93dd3b 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_6.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_6.t.sol @@ -2,126 +2,126 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest6 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest6 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @dev PASS - function test_replay_6_restore() public { - _setUpActor(USER1); - Tester.refreshPremium(1310678, 2); - _delay(4813); - Tester.restore(0, 2, 0, 2); - } - - /// @dev PASS - function test_replay_6_draw() public { - _setUpActor(USER1); - Tester.add(1, 0); - Tester.refreshPremium(1, 0); - _delay(1); - Tester.draw(1, 0); - } - - /// @dev PASS - function test_replay_6_roundtrip_ERC4626_RT_D() public { - _setUpActor(USER1); - Tester.add(2, 0); - Tester.draw(1, 0); - _delay(1); - Tester.roundtrip_ERC4626_RT_D(1, 0); - } - - /// @dev PASS - function test_replay_6_roundtrip_ERC4626_RT_B() public { - _setUpActor(USER1); - Tester.add(1, 1); - } - - /// @dev PASS - function test_replay_6_roundtrip_ERC4626_RT_C() public { - _setUpActor(USER1); - Tester.refreshPremium(1, 0); - _delay(1); - Tester.roundtrip_ERC4626_RT_C(1, 0); - } - - /// @dev PASS - function test_replay_6_remove() public { - _setUpActor(USER1); - Tester.add(563, 0); - Tester.refreshPremium(34015034, 0); - _delay(174592); - Tester.remove(5, 0); - } - - /// @dev PASS - function test_replay_6_add() public { - _setUpActor(USER1); - Tester.add(1, 1); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest6 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev PASS + function test_replay_6_restore() public { + _setUpActor(USER1); + Tester.refreshPremium(1310678, 2); + _delay(4813); + Tester.restore(0, 2, 0, 2); + } + + /// @dev PASS + function test_replay_6_draw() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.draw(1, 0); + } + + /// @dev PASS + function test_replay_6_roundtrip_ERC4626_RT_D() public { + _setUpActor(USER1); + Tester.add(2, 0); + Tester.draw(1, 0); + _delay(1); + Tester.roundtrip_ERC4626_RT_D(1, 0); + } + + /// @dev PASS + function test_replay_6_roundtrip_ERC4626_RT_B() public { + _setUpActor(USER1); + Tester.add(1, 1); + } + + /// @dev PASS + function test_replay_6_roundtrip_ERC4626_RT_C() public { + _setUpActor(USER1); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.roundtrip_ERC4626_RT_C(1, 0); + } + + /// @dev PASS + function test_replay_6_remove() public { + _setUpActor(USER1); + Tester.add(563, 0); + Tester.refreshPremium(34015034, 0); + _delay(174592); + Tester.remove(5, 0); + } + + /// @dev PASS + function test_replay_6_add() public { + _setUpActor(USER1); + Tester.add(1, 1); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/hub-suite/replays/ReplayTest_7.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_7.t.sol index 035152e35..2a53db6eb 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_7.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_7.t.sol @@ -2,79 +2,79 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest7 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest7 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_7_draw() public { - _setUpActor(USER1); - Tester.add(1, 0); - Tester.refreshPremium(1, 0); - _delay(1); - Tester.draw(1, 0); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest7 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_7_draw() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.draw(1, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/hub-suite/replays/ReplayTest_8.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_8.t.sol index e7aee3856..7446384c5 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_8.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_8.t.sol @@ -2,78 +2,78 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest8 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest8 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_8_eliminateDeficit() public { - _setUpActor(USER1); - Tester.refreshPremium(1, 0); - _delay(1); - Tester.eliminateDeficit(1, 0, 0); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest8 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_8_eliminateDeficit() public { + _setUpActor(USER1); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.eliminateDeficit(1, 0, 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/hub-suite/replays/ReplayTest_9.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_9.t.sol index 472b9ca5f..8efe42615 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_9.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_9.t.sol @@ -2,79 +2,79 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest9 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest9 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_9_eliminateDeficit() public { - _setUpActor(USER1); - Tester.refreshPremium(1, 0); - _delay(1); - Tester.eliminateDeficit(1, 0, 0); - invariant_INV_HUB(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest9 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_9_eliminateDeficit() public { + _setUpActor(USER1); + Tester.refreshPremium(1, 0); + _delay(1); + Tester.eliminateDeficit(1, 0, 0); + invariant_INV_HUB(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/tests/invariants/hub-suite/specs/HubInvariantsSpec.t.sol index fc8efbb63..1468dadce 100644 --- a/tests/invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/tests/invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -1,107 +1,113 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {InvariantsSpec} from "../../protocol-suite/specs/InvariantsSpec.t.sol"; +import {InvariantsSpec} from '../../protocol-suite/specs/InvariantsSpec.t.sol'; /// @title InvariantsSpec /// @notice Invariants specification for the protocol /// @dev Contains pseudo code and description for the invariant properties in the protocol abstract contract HubInvariantsSpec is InvariantsSpec { - /////////////////////////////////////////////////////////////////////////////////////////////*/ + /////////////////////////////////////////////////////////////////////////////////////////////*/ - /////////////////////////////////////////////////////////////////////////////////////////////// - // ERC4626 // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ERC4626 // + /////////////////////////////////////////////////////////////////////////////////////////////// - // Add - string constant HSPOST_HUB_ERC4626_ADD_A = - "HSPOST_HUB_ERC4626_ADD_A: After add, spoke addedAssets must increase by at at most addedAmount"; + // Add + string constant HSPOST_HUB_ERC4626_ADD_A = + 'HSPOST_HUB_ERC4626_ADD_A: After add, spoke addedAssets must increase by at at most addedAmount'; - string constant HSPOST_HUB_ERC4626_ADD_B = - "HSPOST_HUB_ERC4626_ADD_B: After add, spoke addedShares must increase by addedSharesAmount"; + string constant HSPOST_HUB_ERC4626_ADD_B = + 'HSPOST_HUB_ERC4626_ADD_B: After add, spoke addedShares must increase by addedSharesAmount'; - string constant HSPOST_HUB_ERC4626_ADD_C = - "HSPOST_HUB_ERC4626_ADD_C: previewAddedShares must be less than or equal to the added shares after the action"; + string constant HSPOST_HUB_ERC4626_ADD_C = + 'HSPOST_HUB_ERC4626_ADD_C: previewAddedShares must be less than or equal to the added shares after the action'; - // Remove - string constant HSPOST_HUB_ERC4626_REMOVE_A = - "HSPOST_HUB_ERC4626_REMOVE_A: After remove, spoke addedAssets must decrease by at least removedAmount"; + // Remove + string constant HSPOST_HUB_ERC4626_REMOVE_A = + 'HSPOST_HUB_ERC4626_REMOVE_A: After remove, spoke addedAssets must decrease by at least removedAmount'; - string constant HSPOST_HUB_ERC4626_REMOVE_B = - "HSPOST_HUB_ERC4626_REMOVE_B: After remove, spoke addedShares must decrease by removedSharesAmount"; + string constant HSPOST_HUB_ERC4626_REMOVE_B = + 'HSPOST_HUB_ERC4626_REMOVE_B: After remove, spoke addedShares must decrease by removedSharesAmount'; - string constant HSPOST_HUB_ERC4626_REMOVE_C = - "HSPOST_HUB_ERC4626_REMOVE_C: previewRemovedShares must be greater than or equal to the removed shares after the action"; + string constant HSPOST_HUB_ERC4626_REMOVE_C = + 'HSPOST_HUB_ERC4626_REMOVE_C: previewRemovedShares must be greater than or equal to the removed shares after the action'; - // Draw - string constant HSPOST_HUB_ERC4626_DRAW_A = - "HSPOST_HUB_ERC4626_DRAW_A: After draw, spoke drawnAssets must increase by at least drawnAmount"; + // Draw + string constant HSPOST_HUB_ERC4626_DRAW_A = + 'HSPOST_HUB_ERC4626_DRAW_A: After draw, spoke drawnAssets must increase by at least drawnAmount'; - string constant HSPOST_HUB_ERC4626_DRAW_B = - "HSPOST_HUB_ERC4626_DRAW_B: After draw, spoke drawnShares must increase by drawnSharesAmount"; + string constant HSPOST_HUB_ERC4626_DRAW_B = + 'HSPOST_HUB_ERC4626_DRAW_B: After draw, spoke drawnShares must increase by drawnSharesAmount'; - string constant HSPOST_HUB_ERC4626_DRAW_C = - "HSPOST_HUB_ERC4626_DRAW_C: previewDrawnShares must be greater than or equal to the drawn shares after the action"; + string constant HSPOST_HUB_ERC4626_DRAW_C = + 'HSPOST_HUB_ERC4626_DRAW_C: previewDrawnShares must be greater than or equal to the drawn shares after the action'; - // Restore - string constant HSPOST_HUB_ERC4626_RESTORE_A = - "HSPOST_HUB_ERC4626_RESTORE_A: After restore, spoke drawnAssets must increase by at most drawnAmount"; + // Restore + string constant HSPOST_HUB_ERC4626_RESTORE_A = + 'HSPOST_HUB_ERC4626_RESTORE_A: After restore, spoke drawnAssets must increase by at most drawnAmount'; - string constant HSPOST_HUB_ERC4626_RESTORE_B = - "HSPOST_HUB_ERC4626_RESTORE_B: After restore, spoke drawnShares must decrease by restoredSharesAmount"; + string constant HSPOST_HUB_ERC4626_RESTORE_B = + 'HSPOST_HUB_ERC4626_RESTORE_B: After restore, spoke drawnShares must decrease by restoredSharesAmount'; - string constant HSPOST_HUB_ERC4626_RESTORE_C = - "HSPOST_HUB_SP_RESTORE_C: previewRestoredShares must be less than or equal to the restored shares after the action"; + string constant HSPOST_HUB_ERC4626_RESTORE_C = + 'HSPOST_HUB_SP_RESTORE_C: previewRestoredShares must be less than or equal to the restored shares after the action'; - // GENERIC + // GENERIC - string constant INV_HUB_ERC4626_A = - "INV_HUB_ERC4626_A: Spoke cannot have non-zero assets and zero shares in add side"; + string constant INV_HUB_ERC4626_A = + 'INV_HUB_ERC4626_A: Spoke cannot have non-zero assets and zero shares in add side'; - string constant INV_HUB_ERC4626_B = - "INV_HUB_ERC4626_B: Spoke cannot have non-zero assets and zero shares in draw side"; + string constant INV_HUB_ERC4626_B = + 'INV_HUB_ERC4626_B: Spoke cannot have non-zero assets and zero shares in draw side'; - string constant INV_HUB_ERC4626_C = - "INV_HUB_ERC4626_C: Asset cannot have non-zero assets and zero shares in add side"; + string constant INV_HUB_ERC4626_C = + 'INV_HUB_ERC4626_C: Asset cannot have non-zero assets and zero shares in add side'; - string constant INV_HUB_ERC4626_D = - "INV_HUB_ERC4626_D: Asset cannot have non-zero assets and zero shares in draw side"; + string constant INV_HUB_ERC4626_D = + 'INV_HUB_ERC4626_D: Asset cannot have non-zero assets and zero shares in draw side'; - /////////////////////////////////////////////////////////////////////////////////////////////// - // ERC4626 ROUNDTRIP // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ERC4626 ROUNDTRIP // + /////////////////////////////////////////////////////////////////////////////////////////////// - string constant HSPOST_HUB_ERC4626_RT_A = - "HSPOST_HUB_SP_ERC4626_RT_A: previewRemoveByShares(previewAddByAssets(amount)) <= shares added"; + string constant HSPOST_HUB_ERC4626_RT_A = + 'HSPOST_HUB_SP_ERC4626_RT_A: previewRemoveByShares(previewAddByAssets(amount)) <= shares added'; - string constant HSPOST_HUB_ERC4626_RT_B = - "HSPOST_HUB_SP_ERC4626_RT_B: remove(previewRemoveByShares(add(amount))) >= add(amount)"; + string constant HSPOST_HUB_ERC4626_RT_B = + 'HSPOST_HUB_SP_ERC4626_RT_B: remove(previewRemoveByShares(add(amount))) >= add(amount)'; - string constant HSPOST_HUB_ERC4626_RT_C = - "HSPOST_HUB_SP_ERC4626_RT_C: previewAddByAssets(previewRemoveByShares(shares)) >= shares"; + string constant HSPOST_HUB_ERC4626_RT_C = + 'HSPOST_HUB_SP_ERC4626_RT_C: previewAddByAssets(previewRemoveByShares(shares)) >= shares'; - string constant HSPOST_HUB_ERC4626_RT_D = - "HSPOST_HUB_SP_ERC4626_RT_D: add(previewAddByShares(remove(amount))) <= shares removed"; + string constant HSPOST_HUB_ERC4626_RT_D = + 'HSPOST_HUB_SP_ERC4626_RT_D: add(previewAddByShares(remove(amount))) <= shares removed'; - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB: AVAILABILITY // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB: AVAILABILITY // + /////////////////////////////////////////////////////////////////////////////////////////////// - string constant INV_HUB_AVAILABILITY_A = "INV_HUB_AVAILABILITY_A: getAddedAssets must not revert"; + string constant INV_HUB_AVAILABILITY_A = 'INV_HUB_AVAILABILITY_A: getAddedAssets must not revert'; - string constant INV_HUB_AVAILABILITY_B = "INV_HUB_AVAILABILITY_B: getAssetOwed must not revert"; + string constant INV_HUB_AVAILABILITY_B = 'INV_HUB_AVAILABILITY_B: getAssetOwed must not revert'; - string constant INV_HUB_AVAILABILITY_C = "INV_HUB_AVAILABILITY_C: getAssetTotalOwed must not revert"; + string constant INV_HUB_AVAILABILITY_C = + 'INV_HUB_AVAILABILITY_C: getAssetTotalOwed must not revert'; - string constant INV_HUB_AVAILABILITY_D = "INV_HUB_AVAILABILITY_D: getAssetPremiumRay must not revert"; + string constant INV_HUB_AVAILABILITY_D = + 'INV_HUB_AVAILABILITY_D: getAssetPremiumRay must not revert'; - string constant INV_HUB_AVAILABILITY_E = "INV_HUB_AVAILABILITY_E: getAssetAccruedFees must not revert"; + string constant INV_HUB_AVAILABILITY_E = + 'INV_HUB_AVAILABILITY_E: getAssetAccruedFees must not revert'; - string constant INV_HUB_AVAILABILITY_F = "INV_HUB_AVAILABILITY_F: getSpokeAddedAssets must not revert"; + string constant INV_HUB_AVAILABILITY_F = + 'INV_HUB_AVAILABILITY_F: getSpokeAddedAssets must not revert'; - string constant INV_HUB_AVAILABILITY_G = "INV_HUB_AVAILABILITY_G: getSpokeOwed must not revert"; + string constant INV_HUB_AVAILABILITY_G = 'INV_HUB_AVAILABILITY_G: getSpokeOwed must not revert'; - string constant INV_HUB_AVAILABILITY_H = "INV_HUB_AVAILABILITY_H: getSpokeTotalOwed must not revert"; + string constant INV_HUB_AVAILABILITY_H = + 'INV_HUB_AVAILABILITY_H: getSpokeTotalOwed must not revert'; - string constant INV_HUB_AVAILABILITY_I = "INV_HUB_AVAILABILITY_I: getSpokePremiumRay must not revert"; + string constant INV_HUB_AVAILABILITY_I = + 'INV_HUB_AVAILABILITY_I: getSpokePremiumRay must not revert'; } diff --git a/tests/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol b/tests/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol index 1f5bb7fab..bd11d05c2 100644 --- a/tests/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol +++ b/tests/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {PostconditionsSpec} from "../../protocol-suite/specs/PostconditionsSpec.t.sol"; +import {PostconditionsSpec} from '../../protocol-suite/specs/PostconditionsSpec.t.sol'; /// @title PostconditionsSpec /// @notice Postcoditions specification for the protocol /// @dev Contains pseudo code and description for the postcondition properties in the protocol abstract contract HubPostconditionsSpec is PostconditionsSpec { - /*///////////////////////////////////////////////////////////////////////////////////////////// + /*///////////////////////////////////////////////////////////////////////////////////////////// // PROPERTY TYPES // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -23,5 +23,4 @@ abstract contract HubPostconditionsSpec is PostconditionsSpec { /// - Implemented within each handler function, under the HANDLER-SPECIFIC POSTCONDITIONS section. /////////////////////////////////////////////////////////////////////////////////////////////*/ - - } +} diff --git a/tests/invariants/hub-suite/utils/PropertiesConstants.sol b/tests/invariants/hub-suite/utils/PropertiesConstants.sol index ff4154b5f..d7a07f005 100644 --- a/tests/invariants/hub-suite/utils/PropertiesConstants.sol +++ b/tests/invariants/hub-suite/utils/PropertiesConstants.sol @@ -1,29 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {Vm} from "forge-std/Base.sol"; +import {Vm} from 'forge-std/Base.sol'; /// @notice Testing constants and properties for hub-focused invariant suite abstract contract PropertiesConstants { - /// @dev Cheat code address - address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - - /// @dev Virtual machine instance - Vm internal constant vm = Vm(VM_ADDRESS); + /// @dev Cheat code address + address internal constant VM_ADDRESS = address(uint160(uint256(keccak256('hevm cheat code')))); - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB PROPERTY STRINGS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /// @dev Virtual machine instance + Vm internal constant vm = Vm(VM_ADDRESS); - // Hub Invariants - string internal constant HUB_INV_LIQUIDITY = "HUB_INV: Liquidity accounting mismatch"; - string internal constant HUB_INV_SHARES = "HUB_INV: Share calculation error"; - string internal constant HUB_INV_INTEREST = "HUB_INV: Interest accrual mismatch"; - string internal constant HUB_INV_DEFICIT = "HUB_INV: Deficit tracking error"; - - // Hub Postconditions - string internal constant HUB_POST_SUPPLY = "HUB_POST: Supply state invalid"; - string internal constant HUB_POST_DRAW = "HUB_POST: Draw state invalid"; - string internal constant HUB_POST_REPAY = "HUB_POST: Repay state invalid"; - string internal constant HUB_POST_CAP = "HUB_POST: Cap constraint violated"; + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB PROPERTY STRINGS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Hub Invariants + string internal constant HUB_INV_LIQUIDITY = 'HUB_INV: Liquidity accounting mismatch'; + string internal constant HUB_INV_SHARES = 'HUB_INV: Share calculation error'; + string internal constant HUB_INV_INTEREST = 'HUB_INV: Interest accrual mismatch'; + string internal constant HUB_INV_DEFICIT = 'HUB_INV: Deficit tracking error'; + + // Hub Postconditions + string internal constant HUB_POST_SUPPLY = 'HUB_POST: Supply state invalid'; + string internal constant HUB_POST_DRAW = 'HUB_POST: Draw state invalid'; + string internal constant HUB_POST_REPAY = 'HUB_POST: Repay state invalid'; + string internal constant HUB_POST_CAP = 'HUB_POST: Cap constraint violated'; } diff --git a/tests/invariants/hub-suite/utils/StdAsserts.sol b/tests/invariants/hub-suite/utils/StdAsserts.sol index a6d548432..a9720f170 100644 --- a/tests/invariants/hub-suite/utils/StdAsserts.sol +++ b/tests/invariants/hub-suite/utils/StdAsserts.sol @@ -3,47 +3,47 @@ pragma solidity ^0.8.19; /// @notice Standard assertions for the test suite abstract contract StdAsserts { - /////////////////////////////////////////////////////////////////////////////////////////////// - // ASSERTION HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Assert that a value is true - function assertTrue(bool condition, string memory errorMessage) internal pure { - require(condition, errorMessage); - } - - /// @notice Assert that a value is false - function assertFalse(bool condition, string memory errorMessage) internal pure { - require(!condition, errorMessage); - } - - /// @notice Assert two values are equal - function assertEqual(uint256 a, uint256 b, string memory errorMessage) internal pure { - require(a == b, errorMessage); - } - - /// @notice Assert a is less than or equal to b - function assertLe(uint256 a, uint256 b, string memory errorMessage) internal pure { - require(a <= b, errorMessage); - } - - /// @notice Assert a is greater than or equal to b - function assertGe(uint256 a, uint256 b, string memory errorMessage) internal pure { - require(a >= b, errorMessage); - } - - /// @notice Assert a is less than b - function assertLt(uint256 a, uint256 b, string memory errorMessage) internal pure { - require(a < b, errorMessage); - } - - /// @notice Assert a is greater than b - function assertGt(uint256 a, uint256 b, string memory errorMessage) internal pure { - require(a > b, errorMessage); - } - - /// @notice Assert two addresses are equal - function assertEq(address a, address b, string memory errorMessage) internal pure { - require(a == b, errorMessage); - } + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSERTION HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Assert that a value is true + function assertTrue(bool condition, string memory errorMessage) internal pure { + require(condition, errorMessage); + } + + /// @notice Assert that a value is false + function assertFalse(bool condition, string memory errorMessage) internal pure { + require(!condition, errorMessage); + } + + /// @notice Assert two values are equal + function assertEqual(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a == b, errorMessage); + } + + /// @notice Assert a is less than or equal to b + function assertLe(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a <= b, errorMessage); + } + + /// @notice Assert a is greater than or equal to b + function assertGe(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a >= b, errorMessage); + } + + /// @notice Assert a is less than b + function assertLt(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a < b, errorMessage); + } + + /// @notice Assert a is greater than b + function assertGt(uint256 a, uint256 b, string memory errorMessage) internal pure { + require(a > b, errorMessage); + } + + /// @notice Assert two addresses are equal + function assertEq(address a, address b, string memory errorMessage) internal pure { + require(a == b, errorMessage); + } } diff --git a/tests/invariants/protocol-suite/HandlerAggregator.t.sol b/tests/invariants/protocol-suite/HandlerAggregator.t.sol index 654270a9c..563e9172f 100644 --- a/tests/invariants/protocol-suite/HandlerAggregator.t.sol +++ b/tests/invariants/protocol-suite/HandlerAggregator.t.sol @@ -2,24 +2,24 @@ pragma solidity ^0.8.19; // Handler contracts -import {SpokeHandler} from "./handlers/spoke/SpokeHandler.t.sol"; -import {TreasurySpokeHandler} from "./handlers/spoke/TreasurySpokeHandler.t.sol"; -import {HubConfiguratorHandler} from "./handlers/hub/HubConfiguratorHandler.t.sol"; -import {SpokeConfiguratorHandler} from "./handlers/spoke/SpokeConfiguratorHandler.t.sol"; +import {SpokeHandler} from './handlers/spoke/SpokeHandler.t.sol'; +import {TreasurySpokeHandler} from './handlers/spoke/TreasurySpokeHandler.t.sol'; +import {HubConfiguratorHandler} from './handlers/hub/HubConfiguratorHandler.t.sol'; +import {SpokeConfiguratorHandler} from './handlers/spoke/SpokeConfiguratorHandler.t.sol'; // Simulator contracts -import {PriceFeedSimulatorHandler} from "./handlers/simulators/PriceFeedSimulatorHandler.t.sol"; -import {DonationAttackHandler} from "./handlers/simulators/DonationAttackHandler.t.sol"; +import {PriceFeedSimulatorHandler} from './handlers/simulators/PriceFeedSimulatorHandler.t.sol'; +import {DonationAttackHandler} from './handlers/simulators/DonationAttackHandler.t.sol'; /// @notice Helper contract to aggregate all handler contracts, inherited in BaseInvariants abstract contract HandlerAggregator is - SpokeHandler, // Main handlers - TreasurySpokeHandler, - HubConfiguratorHandler, // Configurators - SpokeConfiguratorHandler, - PriceFeedSimulatorHandler, // Simulators - DonationAttackHandler + SpokeHandler, // Main handlers + TreasurySpokeHandler, + HubConfiguratorHandler, // Configurators + SpokeConfiguratorHandler, + PriceFeedSimulatorHandler, // Simulators + DonationAttackHandler { - /// @notice Helper function in case any handler requires additional setup - function _setUpHandlers() internal {} + /// @notice Helper function in case any handler requires additional setup + function _setUpHandlers() internal {} } diff --git a/tests/invariants/protocol-suite/Invariants.t.sol b/tests/invariants/protocol-suite/Invariants.t.sol index bf53fe126..e93273899 100644 --- a/tests/invariants/protocol-suite/Invariants.t.sol +++ b/tests/invariants/protocol-suite/Invariants.t.sol @@ -2,70 +2,70 @@ pragma solidity ^0.8.19; // Interfaces -import {IHub} from "src/hub/interfaces/IHub.sol"; +import {IHub} from 'src/hub/interfaces/IHub.sol'; // Invariant Contracts -import {HubInvariants} from "./invariants/HubInvariants.t.sol"; -import {SpokeInvariants} from "./invariants/SpokeInvariants.t.sol"; +import {HubInvariants} from './invariants/HubInvariants.t.sol'; +import {SpokeInvariants} from './invariants/SpokeInvariants.t.sol'; /// @title Invariants /// @notice Wrappers for the protocol invariants implemented in each invariants contract /// @dev recognised by Echidna when property mode is activated /// @dev Inherits HubInvariants, SpokeInvariants abstract contract Invariants is HubInvariants, SpokeInvariants { - /////////////////////////////////////////////////////////////////////////////////////////////// - // BASE INVARIANTS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // BASE INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// - function invariant_INV_HUB() public returns (bool) { - // Applied per hub - for (uint256 i; i < hubAddresses.length; i++) { - address hubAddress = hubAddresses[i]; + function invariant_INV_HUB() public returns (bool) { + // Applied per hub + for (uint256 i; i < hubAddresses.length; i++) { + address hubAddress = hubAddresses[i]; - // Applied per asset of the hub - uint256 assetCount = IHub(hubAddress).getAssetCount(); - for (uint256 j; j < assetCount; j++) { - assert_INV_HUB_A(hubAddress, j); - assert_INV_HUB_B(hubAddress, j); - assert_INV_HUB_C(hubAddress, j); - assert_INV_HUB_EF(hubAddress, j); - assert_INV_HUB_GH(hubAddress, j); - assert_INV_HUB_I(hubAddress, j); - assert_INV_HUB_K(hubAddress, j); - assert_INV_HUB_L(hubAddress, j); - assert_INV_HUB_O(hubAddress, j); - assert_INV_HUB_P(hubAddress, j); - assert_INV_HUB_N(hubAddress, j); - } - } - - return true; + // Applied per asset of the hub + uint256 assetCount = IHub(hubAddress).getAssetCount(); + for (uint256 j; j < assetCount; j++) { + assert_INV_HUB_A(hubAddress, j); + assert_INV_HUB_B(hubAddress, j); + assert_INV_HUB_C(hubAddress, j); + assert_INV_HUB_EF(hubAddress, j); + assert_INV_HUB_GH(hubAddress, j); + assert_INV_HUB_I(hubAddress, j); + assert_INV_HUB_K(hubAddress, j); + assert_INV_HUB_L(hubAddress, j); + assert_INV_HUB_O(hubAddress, j); + assert_INV_HUB_P(hubAddress, j); + assert_INV_HUB_N(hubAddress, j); + } } - function invariant_INV_SP() public returns (bool) { - // Applied per spoke - for (uint256 i; i < spokesAddresses.length; i++) { - address spoke = spokesAddresses[i]; + return true; + } + + function invariant_INV_SP() public returns (bool) { + // Applied per spoke + for (uint256 i; i < spokesAddresses.length; i++) { + address spoke = spokesAddresses[i]; - // Applied per actor on the spoke - for (uint256 j; j < actorAddresses.length; j++) { - assert_INV_SP_D(spoke, actorAddresses[j]); - } + // Applied per actor on the spoke + for (uint256 j; j < actorAddresses.length; j++) { + assert_INV_SP_D(spoke, actorAddresses[j]); + } - // Applied per reserve of the spoke - for (uint256 j; j < spokeReserveIds[spoke].length; j++) { - uint256 reserveId = spokeReserveIds[spoke][j]; - assert_INV_SP_A(spoke, reserveId); - assert_INV_SP_C(spoke, reserveId); - assert_INV_SP_E(spoke, reserveId); - assert_INV_SP_F(spoke, reserveId); + // Applied per reserve of the spoke + for (uint256 j; j < spokeReserveIds[spoke].length; j++) { + uint256 reserveId = spokeReserveIds[spoke][j]; + assert_INV_SP_A(spoke, reserveId); + assert_INV_SP_C(spoke, reserveId); + assert_INV_SP_E(spoke, reserveId); + assert_INV_SP_F(spoke, reserveId); - // Applied per actor per reserve of the spoke - for (uint256 k; k < actorAddresses.length; k++) { - assert_INV_SP_B(spoke, reserveId, actorAddresses[k]); - } - } + // Applied per actor per reserve of the spoke + for (uint256 k; k < actorAddresses.length; k++) { + assert_INV_SP_B(spoke, reserveId, actorAddresses[k]); } - return true; + } } + return true; + } } diff --git a/tests/invariants/protocol-suite/README.md b/tests/invariants/protocol-suite/README.md index 8b9465bac..c98c91aa9 100644 --- a/tests/invariants/protocol-suite/README.md +++ b/tests/invariants/protocol-suite/README.md @@ -5,6 +5,7 @@ A comprehensive handler-based invariant testing suite for the Aave v4 protocol. ## Overview The suite tests a complex multi-hub, multi-spoke deployment with: + - **2 Hubs** with distinct interest rate strategies and asset configurations - **2 Spokes** with varying risk parameters (conservative vs. aggressive) - **Cross-hub liquidity flows** simulating bridge mechanics with different capacity caps @@ -15,6 +16,7 @@ All protocol actions are monitored by hooks that snapshot state and verify postc ## Tooling Compatible with industry-standard fuzzing tools: + - **Echidna** - battle tested haskell based property-based fuzzer - **Medusa** - parallelized, coverage-guided, smart contract fuzzing, powered by go-ethereum - **Foundry** - native invariant testing framework @@ -24,11 +26,13 @@ Compatible with industry-standard fuzzing tools: ### Core Components **Setup Layer** (`Setup.t.sol`, `base/`) + - Deploys 2-hub, 2-spoke architecture with deterministic addresses (CREATE3) - Configures distinct collateral factors, liquidation parameters, and interest rate curves - Initializes multiple actors with protocol permissions **Handler Layer** (`handlers/`) + - `SpokeHandler` – user operations (supply, borrow, repay, withdraw, liquidations) - `HubHandler` – liquidity management and treasury operations - `TreasurySpoke` – fee collection and distribution @@ -36,12 +40,14 @@ Compatible with industry-standard fuzzing tools: - `Simulator Handlers` – price feeds, donation attacks **Verification Layer** (`hooks/`, `invariants/`) + - Before/after hooks with state snapshots - Global and handler-specific postcondition assertions - Hub invariants (liquidity accounting, share calculations, interest accrual) - Spoke invariants (position tracking, collateralization, debt limits) **Replay Layer** (`replays/`) + - Minimal reproduction tests for discovered violations - Facilitates debugging and regression prevention @@ -72,6 +78,7 @@ make runes-medusa ## Advanced Usage **Echidna Modes:** + ```bash make echidna # Property mode (boolean invariants) make echidna-assert # Assertion mode (require/assert violations) @@ -79,11 +86,13 @@ make echidna-explore # Exploration mode (maximize coverage) ``` **Foundry:** + ```bash make foundry-invariants # Native Foundry invariant runner ``` **Replay Specific Failure:** + ```bash forge test --mc ReplayTest_1 -vvv ``` @@ -98,4 +107,3 @@ forge test --mc ReplayTest_1 -vvv --- **Note:** This suite complements unit tests by exploring unbounded state spaces and adversarial scenarios that are difficult to anticipate manually. - diff --git a/tests/invariants/protocol-suite/Setup.t.sol b/tests/invariants/protocol-suite/Setup.t.sol index ab419a568..8b697964c 100644 --- a/tests/invariants/protocol-suite/Setup.t.sol +++ b/tests/invariants/protocol-suite/Setup.t.sol @@ -2,568 +2,617 @@ pragma solidity ^0.8.19; // Libraries -import {CREATE3} from "../shared/utils/CREATE3.sol"; -import {ActorsUtils} from "../shared/utils/ActorsUtils.sol"; -import {Constants} from "tests/Constants.sol"; -import {Roles} from "src/libraries/types/Roles.sol"; -import "forge-std/console.sol"; +import {CREATE3} from '../shared/utils/CREATE3.sol'; +import {ActorsUtils} from '../shared/utils/ActorsUtils.sol'; +import {Constants} from 'tests/Constants.sol'; +import {Roles} from 'src/libraries/types/Roles.sol'; +import 'forge-std/console.sol'; // Interfaces -import {IAaveOracle} from "src/spoke/interfaces/IAaveOracle.sol"; -import {ISpoke} from "src/spoke/Spoke.sol"; -import {ITreasurySpoke} from "src/spoke/TreasurySpoke.sol"; -import {IHub} from "src/hub/Hub.sol"; +import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; +import {ISpoke} from 'src/spoke/Spoke.sol'; +import {ITreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; +import {IHub} from 'src/hub/Hub.sol'; // Test Contracts -import {Actor} from "../shared/utils/Actor.sol"; -import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; -import {MockPriceFeedSimulator} from "../shared/mocks/MockPriceFeedSimulator.sol"; +import {Actor} from '../shared/utils/Actor.sol'; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; +import {MockPriceFeedSimulator} from '../shared/mocks/MockPriceFeedSimulator.sol'; // Contracts -import {BaseTest} from "./base/BaseTest.t.sol"; -import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; -import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; -import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; -import {Hub} from "src/hub/Hub.sol"; -import {TreasurySpoke} from "src/spoke/TreasurySpoke.sol"; -import {SpokeInstance} from "src/spoke/instances/SpokeInstance.sol"; -import {Spoke} from "src/spoke/Spoke.sol"; -import {TransparentUpgradeableProxy} from "src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol"; -import {AaveOracle} from "src/spoke/AaveOracle.sol"; -import {HubConfigurator} from "src/hub/HubConfigurator.sol"; -import {SpokeConfigurator} from "src/spoke/SpokeConfigurator.sol"; +import {BaseTest} from './base/BaseTest.t.sol'; +import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; +import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; +import {Hub} from 'src/hub/Hub.sol'; +import {TreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; +import {SpokeInstance} from 'src/spoke/instances/SpokeInstance.sol'; +import {Spoke} from 'src/spoke/Spoke.sol'; +import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import {AaveOracle} from 'src/spoke/AaveOracle.sol'; +import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; +import {SpokeConfigurator} from 'src/spoke/SpokeConfigurator.sol'; /// @notice Setup contract for the invariant test Suite, inherited by Tester contract Setup is BaseTest { - /// @notice Number of actors to deploy - function _setUp() internal { - // Deploy the suite assets - _deployAssets(); - - // Deploy protocol contracts and protocol actors - _deployProtocolCore(); - - // Configure the token list on the protocol - _configureTokenList(); - - // Deploy actors - _setUpActors(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // ASSETS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Deploy the suite assets - function _deployAssets() internal { - usdc = new TestnetERC20("USDC", "USDC", 6); - weth = new TestnetERC20("WETH", "WETH", 18); - - baseAssets.push(AssetInfo({underlying: address(usdc), decimals: 6})); - baseAssets.push(AssetInfo({underlying: address(weth), decimals: 18})); - - vm.label(address(usdc), "usdc"); - vm.label(address(weth), "weth"); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CORE // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Deploy protocol core contracts - function _deployProtocolCore() internal { - // Access manager - accessManager = new AccessManager(admin); - - // Hub 1 - hub1 = new Hub(address(accessManager)); - irStrategy1 = new AssetInterestRateStrategy(address(hub1)); - hubInfo[address(hub1)] = HubInfo({treasurySpoke: address(treasurySpoke1), irStrategy: address(irStrategy1)}); - hubAddresses.push(address(hub1)); - - // Hub 2 - hub2 = new Hub(address(accessManager)); - irStrategy2 = new AssetInterestRateStrategy(address(hub2)); - hubInfo[address(hub2)] = HubInfo({treasurySpoke: address(treasurySpoke2), irStrategy: address(irStrategy2)}); - hubAddresses.push(address(hub2)); - - // Spokes - (spoke1, oracle1) = _deploySpokeWithOracle(admin, address(accessManager), "Spoke 1 (USD)"); - (spoke2, oracle2) = _deploySpokeWithOracle(admin, address(accessManager), "Spoke 2 (USD)"); - treasurySpoke1 = ITreasurySpoke(new TreasurySpoke(admin, address(hub1))); - treasurySpoke2 = ITreasurySpoke(new TreasurySpoke(admin, address(hub2))); - allSpokes.push(address(treasurySpoke1)); - allSpokes.push(address(treasurySpoke2)); - - // Configurators - hubConfigurator = new HubConfigurator(admin); - spokeConfigurator = new SpokeConfigurator(admin); - _setUpConfiguratorRoles(); - - vm.label(address(accessManager), "accessManager"); - vm.label(address(hub1), "hub1"); - vm.label(address(hub2), "hub2"); - vm.label(address(hubConfigurator), "hubConfigurator"); - vm.label(address(spokeConfigurator), "spokeConfigurator"); - vm.label(address(irStrategy1), "irStrategy1"); - vm.label(address(irStrategy2), "irStrategy2"); - vm.label(address(spoke1), "spoke1"); - vm.label(address(spoke2), "spoke2"); - vm.label(address(treasurySpoke1), "treasurySpoke1"); - vm.label(address(treasurySpoke2), "treasurySpoke2"); - vm.label(address(oracle1), "oracle1"); - vm.label(address(oracle2), "oracle2"); - } - - /// @notice Deploy a spoke with an oracle using CREATE3 - function _deploySpokeWithOracle(address proxyAdminOwner, address _accessManager, string memory _oracleDesc) - internal - returns (ISpoke, IAaveOracle) + /// @notice Number of actors to deploy + function _setUp() internal { + // Deploy the suite assets + _deployAssets(); + + // Deploy protocol contracts and protocol actors + _deployProtocolCore(); + + // Configure the token list on the protocol + _configureTokenList(); + + // Deploy actors + _setUpActors(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSETS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy the suite assets + function _deployAssets() internal { + usdc = new TestnetERC20('USDC', 'USDC', 6); + weth = new TestnetERC20('WETH', 'WETH', 18); + + baseAssets.push(AssetInfo({underlying: address(usdc), decimals: 6})); + baseAssets.push(AssetInfo({underlying: address(weth), decimals: 18})); + + vm.label(address(usdc), 'usdc'); + vm.label(address(weth), 'weth'); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CORE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy protocol core contracts + function _deployProtocolCore() internal { + // Access manager + accessManager = new AccessManager(admin); + + // Hub 1 + hub1 = new Hub(address(accessManager)); + irStrategy1 = new AssetInterestRateStrategy(address(hub1)); + hubInfo[address(hub1)] = HubInfo({ + treasurySpoke: address(treasurySpoke1), + irStrategy: address(irStrategy1) + }); + hubAddresses.push(address(hub1)); + + // Hub 2 + hub2 = new Hub(address(accessManager)); + irStrategy2 = new AssetInterestRateStrategy(address(hub2)); + hubInfo[address(hub2)] = HubInfo({ + treasurySpoke: address(treasurySpoke2), + irStrategy: address(irStrategy2) + }); + hubAddresses.push(address(hub2)); + + // Spokes + (spoke1, oracle1) = _deploySpokeWithOracle(admin, address(accessManager), 'Spoke 1 (USD)'); + (spoke2, oracle2) = _deploySpokeWithOracle(admin, address(accessManager), 'Spoke 2 (USD)'); + treasurySpoke1 = ITreasurySpoke(new TreasurySpoke(admin, address(hub1))); + treasurySpoke2 = ITreasurySpoke(new TreasurySpoke(admin, address(hub2))); + allSpokes.push(address(treasurySpoke1)); + allSpokes.push(address(treasurySpoke2)); + + // Configurators + hubConfigurator = new HubConfigurator(admin); + spokeConfigurator = new SpokeConfigurator(admin); + _setUpConfiguratorRoles(); + + vm.label(address(accessManager), 'accessManager'); + vm.label(address(hub1), 'hub1'); + vm.label(address(hub2), 'hub2'); + vm.label(address(hubConfigurator), 'hubConfigurator'); + vm.label(address(spokeConfigurator), 'spokeConfigurator'); + vm.label(address(irStrategy1), 'irStrategy1'); + vm.label(address(irStrategy2), 'irStrategy2'); + vm.label(address(spoke1), 'spoke1'); + vm.label(address(spoke2), 'spoke2'); + vm.label(address(treasurySpoke1), 'treasurySpoke1'); + vm.label(address(treasurySpoke2), 'treasurySpoke2'); + vm.label(address(oracle1), 'oracle1'); + vm.label(address(oracle2), 'oracle2'); + } + + /// @notice Deploy a spoke with an oracle using CREATE3 + function _deploySpokeWithOracle( + address proxyAdminOwner, + address _accessManager, + string memory _oracleDesc + ) internal returns (ISpoke, IAaveOracle) { + bytes32 salt = keccak256(abi.encodePacked(_oracleDesc)); + address predictedSpoke = CREATE3.predictDeterministicAddress(salt, admin); + + // Deploy oracle with predicted spoke address + IAaveOracle oracle = new AaveOracle(predictedSpoke, uint8(8), _oracleDesc); + + // Deploy spoke implementation with oracle address + address spokeImpl = address(new SpokeInstance(address(oracle))); + + // Deploy spoke proxy using CREATE3 + bytes memory proxyCreationCode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(spokeImpl, proxyAdminOwner, abi.encodeCall(Spoke.initialize, (_accessManager))) + ); + address spokeProxy = CREATE3.deployDeterministic(proxyCreationCode, salt); + ISpoke spoke = ISpoke(spokeProxy); + + assertEq(address(spoke), predictedSpoke, 'predictedSpoke mismatch'); + assertEq(spoke.ORACLE(), address(oracle), 'spoke.ORACLE() mismatch'); + assertEq(oracle.SPOKE(), address(spoke), 'oracle.SPOKE() mismatch'); + + spokesAddresses.push(address(spoke)); + allSpokes.push(address(spoke)); + + return (spoke, oracle); + } + + /// @notice Proxify an implementation contract using TransparentUpgradeableProxy + function _proxify( + address impl, + address proxyAdminOwner, + bytes memory initData + ) internal returns (address) { + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + impl, + proxyAdminOwner, + initData + ); + return address(proxy); + } + + function _deployMockPriceFeed(ISpoke spoke, uint256 price) internal returns (address) { + AaveOracle oracle = AaveOracle(spoke.ORACLE()); + return address(new MockPriceFeedSimulator(oracle.DECIMALS(), oracle.DESCRIPTION(), price)); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CONFIGS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _configureTokenList() internal { + // Configure hubs + _configureHubs(); + + // Configure spokes + _configureSpokes(); + } + + /// @notice Configure the hubs + function _configureHubs() internal { + // HUB 1 + bytes memory encodedIrData1 = abi.encode( + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: OPTIMAL_USAGE_RATIO_IR1, + baseVariableBorrowRate: BASE_VARIABLE_BORROW_RATE_IR1, + variableRateSlope1: VARIABLE_RATE_SLOPE_1_IR1, + variableRateSlope2: VARIABLE_RATE_SLOPE_2_IR1 + }) + ); + + // Add USDC + hub1UsdcAssetId = hub1.addAsset( + address(usdc), + usdc.decimals(), + address(treasurySpoke1), + address(irStrategy1), + encodedIrData1 + ); + hub1.updateAssetConfig( + hub1UsdcAssetId, + IHub.AssetConfig({ + liquidityFee: 5_00, + feeReceiver: address(treasurySpoke1), + irStrategy: address(irStrategy1), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds[address(hub1)].push(hub1UsdcAssetId); + + // Add WETH + hub1WethAssetId = hub1.addAsset( + address(weth), + weth.decimals(), + address(treasurySpoke1), + address(irStrategy1), + encodedIrData1 + ); + hub1.updateAssetConfig( + hub1WethAssetId, + IHub.AssetConfig({ + liquidityFee: 10_00, + feeReceiver: address(treasurySpoke1), + irStrategy: address(irStrategy1), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds[address(hub1)].push(hub1WethAssetId); + + // HUB 2 + bytes memory encodedIrData2 = abi.encode( + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: OPTIMAL_USAGE_RATIO_IR2, + baseVariableBorrowRate: BASE_VARIABLE_BORROW_RATE_IR2, + variableRateSlope1: VARIABLE_RATE_SLOPE_1_IR2, + variableRateSlope2: VARIABLE_RATE_SLOPE_2_IR2 + }) + ); + + // Add WETH + hub2WethAssetId = hub2.addAsset( + address(weth), + weth.decimals(), + address(treasurySpoke2), + address(irStrategy2), + encodedIrData2 + ); + hub2.updateAssetConfig( + hub2WethAssetId, + IHub.AssetConfig({ + liquidityFee: 10_00, + feeReceiver: address(treasurySpoke2), + irStrategy: address(irStrategy2), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds[address(hub2)].push(hub2WethAssetId); + + // Add USDC + hub2UsdcAssetId = hub2.addAsset( + address(usdc), + usdc.decimals(), + address(treasurySpoke2), + address(irStrategy2), + encodedIrData2 + ); + hub2.updateAssetConfig( + hub2UsdcAssetId, + IHub.AssetConfig({ + liquidityFee: 5_00, + feeReceiver: address(treasurySpoke2), + irStrategy: address(irStrategy2), + reinvestmentController: address(0) + }), + new bytes(0) + ); + hubAssetIds[address(hub2)].push(hub2UsdcAssetId); + } + + function _configureSpokes() internal { + // Configure spoke liquidation configs + spokeConfigurator.updateLiquidationTargetHealthFactor( + address(spoke1), + TARGET_HEALTH_FACTOR_SPOKE1 + ); + spokeConfigurator.updateLiquidationTargetHealthFactor( + address(spoke2), + TARGET_HEALTH_FACTOR_SPOKE2 + ); + + // Spoke 1 reserve configs + spokeInfo[spoke1].usdc.reserveConfig = ISpoke.ReserveConfig({ + paused: false, + frozen: false, + borrowable: true, + liquidatable: true, + receiveSharesEnabled: true, + collateralRisk: 30_00 + }); + spokeInfo[spoke1].usdc.dynReserveConfig = ISpoke.DynamicReserveConfig({ + collateralFactor: 90_00, + maxLiquidationBonus: 100_00, + liquidationFee: 0 + }); + + spokeInfo[spoke1].weth.reserveConfig = ISpoke.ReserveConfig({ + paused: false, + frozen: false, + borrowable: true, + liquidatable: true, + receiveSharesEnabled: true, + collateralRisk: 20_00 + }); + spokeInfo[spoke1].weth.dynReserveConfig = ISpoke.DynamicReserveConfig({ + collateralFactor: 80_00, + maxLiquidationBonus: 105_00, + liquidationFee: 0 + }); + + // Spoke 2 reserve configs + spokeInfo[spoke2].weth.reserveConfig = ISpoke.ReserveConfig({ + paused: false, + frozen: false, + borrowable: true, + liquidatable: true, + receiveSharesEnabled: true, + collateralRisk: 10_00 + }); + spokeInfo[spoke2].weth.dynReserveConfig = ISpoke.DynamicReserveConfig({ + collateralFactor: 70_00, + maxLiquidationBonus: 105_00, + liquidationFee: 0 + }); + + spokeInfo[spoke2].usdc.reserveConfig = ISpoke.ReserveConfig({ + paused: false, + frozen: false, + borrowable: true, + liquidatable: true, + receiveSharesEnabled: true, + collateralRisk: 15_00 + }); + spokeInfo[spoke2].usdc.dynReserveConfig = ISpoke.DynamicReserveConfig({ + collateralFactor: 80_00, + maxLiquidationBonus: 100_00, + liquidationFee: 0 + }); + + // Deploy price feeds + priceFeeds.push(_deployMockPriceFeed(spoke1, 1e8)); + priceFeeds.push(_deployMockPriceFeed(spoke1, 2000e8)); + + // Add reserves to spoke 1 + spokeInfo[spoke1].usdc.reserveId = spoke1.addReserve( + address(hub1), + hub1UsdcAssetId, + priceFeeds[0], + spokeInfo[spoke1].usdc.reserveConfig, + spokeInfo[spoke1].usdc.dynReserveConfig + ); + spokeInfo[spoke1].usdc2.reserveId = spoke1.addReserve( + address(hub2), + hub2UsdcAssetId, + priceFeeds[0], + spokeInfo[spoke1].usdc.reserveConfig, + spokeInfo[spoke1].usdc.dynReserveConfig + ); + spokeInfo[spoke1].weth.reserveId = spoke1.addReserve( + address(hub1), + hub1WethAssetId, + priceFeeds[1], + spokeInfo[spoke1].weth.reserveConfig, + spokeInfo[spoke1].weth.dynReserveConfig + ); + spokeInfo[spoke1].weth2.reserveId = spoke1.addReserve( + address(hub2), + hub2WethAssetId, + priceFeeds[1], + spokeInfo[spoke1].weth.reserveConfig, + spokeInfo[spoke1].weth.dynReserveConfig + ); + + // Add reserves to spoke 2 + spokeInfo[spoke2].weth.reserveId = spoke2.addReserve( + address(hub1), + hub1WethAssetId, + priceFeeds[1], + spokeInfo[spoke2].weth.reserveConfig, + spokeInfo[spoke2].weth.dynReserveConfig + ); + spokeInfo[spoke2].weth2.reserveId = spoke2.addReserve( + address(hub2), + hub2WethAssetId, + priceFeeds[1], + spokeInfo[spoke2].weth.reserveConfig, + spokeInfo[spoke2].weth.dynReserveConfig + ); + spokeInfo[spoke2].usdc.reserveId = spoke2.addReserve( + address(hub1), + hub1UsdcAssetId, + priceFeeds[0], + spokeInfo[spoke2].usdc.reserveConfig, + spokeInfo[spoke2].usdc.dynReserveConfig + ); + spokeInfo[spoke2].usdc2.reserveId = spoke2.addReserve( + address(hub2), + hub2UsdcAssetId, + priceFeeds[0], + spokeInfo[spoke2].usdc.reserveConfig, + spokeInfo[spoke2].usdc.dynReserveConfig + ); + + // Map ids for spoke 1 + assetIdToReserveId[address(spoke1)][hub1UsdcAssetId] = spokeInfo[spoke1].usdc.reserveId; + assetIdToReserveId[address(spoke1)][hub2UsdcAssetId] = spokeInfo[spoke1].usdc2.reserveId; + assetIdToReserveId[address(spoke1)][hub1WethAssetId] = spokeInfo[spoke1].weth.reserveId; + assetIdToReserveId[address(spoke1)][hub2WethAssetId] = spokeInfo[spoke1].weth2.reserveId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].usdc.reserveId] = hub1UsdcAssetId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].usdc2.reserveId] = hub2UsdcAssetId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].weth.reserveId] = hub1WethAssetId; + reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].weth2.reserveId] = hub2WethAssetId; + reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].usdc.reserveId] = address(hub1); + reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].usdc2.reserveId] = address(hub2); + reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].weth.reserveId] = address(hub1); + reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].weth2.reserveId] = address(hub2); + + // Map ids for spoke 2 + assetIdToReserveId[address(spoke2)][hub1UsdcAssetId] = spokeInfo[spoke2].usdc.reserveId; + assetIdToReserveId[address(spoke2)][hub2UsdcAssetId] = spokeInfo[spoke2].usdc2.reserveId; + assetIdToReserveId[address(spoke2)][hub1WethAssetId] = spokeInfo[spoke2].weth.reserveId; + assetIdToReserveId[address(spoke2)][hub2WethAssetId] = spokeInfo[spoke2].weth2.reserveId; + reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].usdc.reserveId] = hub1UsdcAssetId; + reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].usdc2.reserveId] = hub2UsdcAssetId; + reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].weth.reserveId] = hub1WethAssetId; + reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].weth2.reserveId] = hub2WethAssetId; + reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].usdc.reserveId] = address(hub1); + reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].usdc2.reserveId] = address(hub2); + reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].weth.reserveId] = address(hub1); + reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].weth2.reserveId] = address(hub2); + + // Store spoke reserve ids on array + spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].usdc.reserveId); + spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].usdc2.reserveId); + spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].weth.reserveId); + spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].weth2.reserveId); + spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].usdc.reserveId); + spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].usdc2.reserveId); + spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].weth.reserveId); + spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].weth2.reserveId); + + // TreasurySpoke uses assetIds directly as reserveIds + spokeReserveIds[address(treasurySpoke1)].push(hub1UsdcAssetId); + spokeReserveIds[address(treasurySpoke1)].push(hub1WethAssetId); + spokeReserveIds[address(treasurySpoke2)].push(hub2UsdcAssetId); + spokeReserveIds[address(treasurySpoke2)].push(hub2WethAssetId); + + // Add SPOKE 1 assets to hubs + hub1.addSpoke( + hub1UsdcAssetId, + address(spoke1), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub2.addSpoke( + hub2UsdcAssetId, + address(spoke1), + IHub.SpokeConfig({ + addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, + drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub1.addSpoke( + hub1WethAssetId, + address(spoke1), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub2.addSpoke( + hub2WethAssetId, + address(spoke1), + IHub.SpokeConfig({ + addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, + drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + + // Add SPOKE 2 assets to hubs + hub2.addSpoke( + hub2WethAssetId, + address(spoke2), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub1.addSpoke( + hub1WethAssetId, + address(spoke2), + IHub.SpokeConfig({ + addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, + drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub2.addSpoke( + hub2UsdcAssetId, + address(spoke2), + IHub.SpokeConfig({ + addCap: Constants.MAX_ALLOWED_SPOKE_CAP, + drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + hub1.addSpoke( + hub1UsdcAssetId, + address(spoke2), + IHub.SpokeConfig({ + addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, + drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, + riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, + active: true, + paused: false + }) + ); + } + + /// @notice Set up roles for the configurators + function _setUpConfiguratorRoles() internal virtual { + // Grant roles to configurators + accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); + accessManager.grantRole(Roles.SPOKE_ADMIN_ROLE, address(spokeConfigurator), 0); + + // Grant responsibilities to spokes { - bytes32 salt = keccak256(abi.encodePacked(_oracleDesc)); - address predictedSpoke = CREATE3.predictDeterministicAddress(salt, admin); - - // Deploy oracle with predicted spoke address - IAaveOracle oracle = new AaveOracle(predictedSpoke, uint8(8), _oracleDesc); - - // Deploy spoke implementation with oracle address - address spokeImpl = address(new SpokeInstance(address(oracle))); - - // Deploy spoke proxy using CREATE3 - bytes memory proxyCreationCode = abi.encodePacked( - type(TransparentUpgradeableProxy).creationCode, - abi.encode(spokeImpl, proxyAdminOwner, abi.encodeCall(Spoke.initialize, (_accessManager))) - ); - address spokeProxy = CREATE3.deployDeterministic(proxyCreationCode, salt); - ISpoke spoke = ISpoke(spokeProxy); - - assertEq(address(spoke), predictedSpoke, "predictedSpoke mismatch"); - assertEq(spoke.ORACLE(), address(oracle), "spoke.ORACLE() mismatch"); - assertEq(oracle.SPOKE(), address(spoke), "oracle.SPOKE() mismatch"); - - spokesAddresses.push(address(spoke)); - allSpokes.push(address(spoke)); - - return (spoke, oracle); - } - - /// @notice Proxify an implementation contract using TransparentUpgradeableProxy - function _proxify(address impl, address proxyAdminOwner, bytes memory initData) internal returns (address) { - TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(impl, proxyAdminOwner, initData); - return address(proxy); - } - - function _deployMockPriceFeed(ISpoke spoke, uint256 price) internal returns (address) { - AaveOracle oracle = AaveOracle(spoke.ORACLE()); - return address(new MockPriceFeedSimulator(oracle.DECIMALS(), oracle.DESCRIPTION(), price)); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CONFIGS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _configureTokenList() internal { - // Configure hubs - _configureHubs(); - - // Configure spokes - _configureSpokes(); - } - - /// @notice Configure the hubs - function _configureHubs() internal { - // HUB 1 - bytes memory encodedIrData1 = abi.encode( - IAssetInterestRateStrategy.InterestRateData({ - optimalUsageRatio: OPTIMAL_USAGE_RATIO_IR1, - baseVariableBorrowRate: BASE_VARIABLE_BORROW_RATE_IR1, - variableRateSlope1: VARIABLE_RATE_SLOPE_1_IR1, - variableRateSlope2: VARIABLE_RATE_SLOPE_2_IR1 - }) - ); - - // Add USDC - hub1UsdcAssetId = hub1.addAsset( - address(usdc), usdc.decimals(), address(treasurySpoke1), address(irStrategy1), encodedIrData1 - ); - hub1.updateAssetConfig( - hub1UsdcAssetId, - IHub.AssetConfig({ - liquidityFee: 5_00, - feeReceiver: address(treasurySpoke1), - irStrategy: address(irStrategy1), - reinvestmentController: address(0) - }), - new bytes(0) - ); - hubAssetIds[address(hub1)].push(hub1UsdcAssetId); - - // Add WETH - hub1WethAssetId = hub1.addAsset( - address(weth), weth.decimals(), address(treasurySpoke1), address(irStrategy1), encodedIrData1 - ); - hub1.updateAssetConfig( - hub1WethAssetId, - IHub.AssetConfig({ - liquidityFee: 10_00, - feeReceiver: address(treasurySpoke1), - irStrategy: address(irStrategy1), - reinvestmentController: address(0) - }), - new bytes(0) - ); - hubAssetIds[address(hub1)].push(hub1WethAssetId); - - // HUB 2 - bytes memory encodedIrData2 = abi.encode( - IAssetInterestRateStrategy.InterestRateData({ - optimalUsageRatio: OPTIMAL_USAGE_RATIO_IR2, - baseVariableBorrowRate: BASE_VARIABLE_BORROW_RATE_IR2, - variableRateSlope1: VARIABLE_RATE_SLOPE_1_IR2, - variableRateSlope2: VARIABLE_RATE_SLOPE_2_IR2 - }) - ); - - // Add WETH - hub2WethAssetId = hub2.addAsset( - address(weth), weth.decimals(), address(treasurySpoke2), address(irStrategy2), encodedIrData2 - ); - hub2.updateAssetConfig( - hub2WethAssetId, - IHub.AssetConfig({ - liquidityFee: 10_00, - feeReceiver: address(treasurySpoke2), - irStrategy: address(irStrategy2), - reinvestmentController: address(0) - }), - new bytes(0) - ); - hubAssetIds[address(hub2)].push(hub2WethAssetId); - - // Add USDC - hub2UsdcAssetId = hub2.addAsset( - address(usdc), usdc.decimals(), address(treasurySpoke2), address(irStrategy2), encodedIrData2 - ); - hub2.updateAssetConfig( - hub2UsdcAssetId, - IHub.AssetConfig({ - liquidityFee: 5_00, - feeReceiver: address(treasurySpoke2), - irStrategy: address(irStrategy2), - reinvestmentController: address(0) - }), - new bytes(0) - ); - hubAssetIds[address(hub2)].push(hub2UsdcAssetId); - } - - function _configureSpokes() internal { - // Configure spoke liquidation configs - spokeConfigurator.updateLiquidationTargetHealthFactor(address(spoke1), TARGET_HEALTH_FACTOR_SPOKE1); - spokeConfigurator.updateLiquidationTargetHealthFactor(address(spoke2), TARGET_HEALTH_FACTOR_SPOKE2); - - // Spoke 1 reserve configs - spokeInfo[spoke1].usdc.reserveConfig = ISpoke.ReserveConfig({ - paused: false, - frozen: false, - borrowable: true, - liquidatable: true, - receiveSharesEnabled: true, - collateralRisk: 30_00 - }); - spokeInfo[spoke1].usdc.dynReserveConfig = - ISpoke.DynamicReserveConfig({collateralFactor: 90_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); - - spokeInfo[spoke1].weth.reserveConfig = ISpoke.ReserveConfig({ - paused: false, - frozen: false, - borrowable: true, - liquidatable: true, - receiveSharesEnabled: true, - collateralRisk: 20_00 - }); - spokeInfo[spoke1].weth.dynReserveConfig = - ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 105_00, liquidationFee: 0}); - - // Spoke 2 reserve configs - spokeInfo[spoke2].weth.reserveConfig = ISpoke.ReserveConfig({ - paused: false, - frozen: false, - borrowable: true, - liquidatable: true, - receiveSharesEnabled: true, - collateralRisk: 10_00 - }); - spokeInfo[spoke2].weth.dynReserveConfig = - ISpoke.DynamicReserveConfig({collateralFactor: 70_00, maxLiquidationBonus: 105_00, liquidationFee: 0}); - - spokeInfo[spoke2].usdc.reserveConfig = ISpoke.ReserveConfig({ - paused: false, - frozen: false, - borrowable: true, - liquidatable: true, - receiveSharesEnabled: true, - collateralRisk: 15_00 - }); - spokeInfo[spoke2].usdc.dynReserveConfig = - ISpoke.DynamicReserveConfig({collateralFactor: 80_00, maxLiquidationBonus: 100_00, liquidationFee: 0}); - - // Deploy price feeds - priceFeeds.push(_deployMockPriceFeed(spoke1, 1e8)); - priceFeeds.push(_deployMockPriceFeed(spoke1, 2000e8)); - - // Add reserves to spoke 1 - spokeInfo[spoke1].usdc.reserveId = spoke1.addReserve( - address(hub1), - hub1UsdcAssetId, - priceFeeds[0], - spokeInfo[spoke1].usdc.reserveConfig, - spokeInfo[spoke1].usdc.dynReserveConfig - ); - spokeInfo[spoke1].usdc2.reserveId = spoke1.addReserve( - address(hub2), - hub2UsdcAssetId, - priceFeeds[0], - spokeInfo[spoke1].usdc.reserveConfig, - spokeInfo[spoke1].usdc.dynReserveConfig - ); - spokeInfo[spoke1].weth.reserveId = spoke1.addReserve( - address(hub1), - hub1WethAssetId, - priceFeeds[1], - spokeInfo[spoke1].weth.reserveConfig, - spokeInfo[spoke1].weth.dynReserveConfig - ); - spokeInfo[spoke1].weth2.reserveId = spoke1.addReserve( - address(hub2), - hub2WethAssetId, - priceFeeds[1], - spokeInfo[spoke1].weth.reserveConfig, - spokeInfo[spoke1].weth.dynReserveConfig - ); - - // Add reserves to spoke 2 - spokeInfo[spoke2].weth.reserveId = spoke2.addReserve( - address(hub1), - hub1WethAssetId, - priceFeeds[1], - spokeInfo[spoke2].weth.reserveConfig, - spokeInfo[spoke2].weth.dynReserveConfig - ); - spokeInfo[spoke2].weth2.reserveId = spoke2.addReserve( - address(hub2), - hub2WethAssetId, - priceFeeds[1], - spokeInfo[spoke2].weth.reserveConfig, - spokeInfo[spoke2].weth.dynReserveConfig - ); - spokeInfo[spoke2].usdc.reserveId = spoke2.addReserve( - address(hub1), - hub1UsdcAssetId, - priceFeeds[0], - spokeInfo[spoke2].usdc.reserveConfig, - spokeInfo[spoke2].usdc.dynReserveConfig - ); - spokeInfo[spoke2].usdc2.reserveId = spoke2.addReserve( - address(hub2), - hub2UsdcAssetId, - priceFeeds[0], - spokeInfo[spoke2].usdc.reserveConfig, - spokeInfo[spoke2].usdc.dynReserveConfig - ); - - // Map ids for spoke 1 - assetIdToReserveId[address(spoke1)][hub1UsdcAssetId] = spokeInfo[spoke1].usdc.reserveId; - assetIdToReserveId[address(spoke1)][hub2UsdcAssetId] = spokeInfo[spoke1].usdc2.reserveId; - assetIdToReserveId[address(spoke1)][hub1WethAssetId] = spokeInfo[spoke1].weth.reserveId; - assetIdToReserveId[address(spoke1)][hub2WethAssetId] = spokeInfo[spoke1].weth2.reserveId; - reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].usdc.reserveId] = hub1UsdcAssetId; - reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].usdc2.reserveId] = hub2UsdcAssetId; - reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].weth.reserveId] = hub1WethAssetId; - reserveIdToAssetId[address(spoke1)][spokeInfo[spoke1].weth2.reserveId] = hub2WethAssetId; - reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].usdc.reserveId] = address(hub1); - reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].usdc2.reserveId] = address(hub2); - reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].weth.reserveId] = address(hub1); - reserveIdToHubAddress[address(spoke1)][spokeInfo[spoke1].weth2.reserveId] = address(hub2); - - // Map ids for spoke 2 - assetIdToReserveId[address(spoke2)][hub1UsdcAssetId] = spokeInfo[spoke2].usdc.reserveId; - assetIdToReserveId[address(spoke2)][hub2UsdcAssetId] = spokeInfo[spoke2].usdc2.reserveId; - assetIdToReserveId[address(spoke2)][hub1WethAssetId] = spokeInfo[spoke2].weth.reserveId; - assetIdToReserveId[address(spoke2)][hub2WethAssetId] = spokeInfo[spoke2].weth2.reserveId; - reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].usdc.reserveId] = hub1UsdcAssetId; - reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].usdc2.reserveId] = hub2UsdcAssetId; - reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].weth.reserveId] = hub1WethAssetId; - reserveIdToAssetId[address(spoke2)][spokeInfo[spoke2].weth2.reserveId] = hub2WethAssetId; - reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].usdc.reserveId] = address(hub1); - reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].usdc2.reserveId] = address(hub2); - reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].weth.reserveId] = address(hub1); - reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].weth2.reserveId] = address(hub2); - - // Store spoke reserve ids on array - spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].usdc.reserveId); - spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].usdc2.reserveId); - spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].weth.reserveId); - spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].weth2.reserveId); - spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].usdc.reserveId); - spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].usdc2.reserveId); - spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].weth.reserveId); - spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].weth2.reserveId); - - // TreasurySpoke uses assetIds directly as reserveIds - spokeReserveIds[address(treasurySpoke1)].push(hub1UsdcAssetId); - spokeReserveIds[address(treasurySpoke1)].push(hub1WethAssetId); - spokeReserveIds[address(treasurySpoke2)].push(hub2UsdcAssetId); - spokeReserveIds[address(treasurySpoke2)].push(hub2WethAssetId); - - // Add SPOKE 1 assets to hubs - hub1.addSpoke( - hub1UsdcAssetId, - address(spoke1), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub2.addSpoke( - hub2UsdcAssetId, - address(spoke1), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub1.addSpoke( - hub1WethAssetId, - address(spoke1), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub2.addSpoke( - hub2WethAssetId, - address(spoke1), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - - // Add SPOKE 2 assets to hubs - hub2.addSpoke( - hub2WethAssetId, - address(spoke2), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub1.addSpoke( - hub1WethAssetId, - address(spoke2), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 2, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub2.addSpoke( - hub2UsdcAssetId, - address(spoke2), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - hub1.addSpoke( - hub1UsdcAssetId, - address(spoke2), - IHub.SpokeConfig({ - addCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - drawCap: Constants.MAX_ALLOWED_SPOKE_CAP / 10 * 3, - riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, - active: true, - paused: false - }) - ); - } - - /// @notice Set up roles for the configurators - function _setUpConfiguratorRoles() internal virtual { - // Grant roles to configurators - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); - accessManager.grantRole(Roles.SPOKE_ADMIN_ROLE, address(spokeConfigurator), 0); - - // Grant responsibilities to spokes - { - bytes4[] memory selectors = new bytes4[](6); - selectors[0] = ISpoke.updateLiquidationConfig.selector; - selectors[1] = ISpoke.updateReserveConfig.selector; - selectors[2] = ISpoke.updateDynamicReserveConfig.selector; - selectors[3] = ISpoke.addDynamicReserveConfig.selector; - selectors[4] = ISpoke.updatePositionManager.selector; - selectors[5] = ISpoke.updateReservePriceSource.selector; - accessManager.setTargetFunctionRole(address(spoke1), selectors, Roles.SPOKE_ADMIN_ROLE); - accessManager.setTargetFunctionRole(address(spoke2), selectors, Roles.SPOKE_ADMIN_ROLE); - } - - // Grant responsibilities to hubs - { - bytes4[] memory selectors = new bytes4[](2); - selectors[0] = IHub.updateSpokeConfig.selector; - selectors[1] = IHub.setInterestRateData.selector; - accessManager.setTargetFunctionRole(address(hub1), selectors, Roles.HUB_ADMIN_ROLE); - accessManager.setTargetFunctionRole(address(hub2), selectors, Roles.HUB_ADMIN_ROLE); - } + bytes4[] memory selectors = new bytes4[](6); + selectors[0] = ISpoke.updateLiquidationConfig.selector; + selectors[1] = ISpoke.updateReserveConfig.selector; + selectors[2] = ISpoke.updateDynamicReserveConfig.selector; + selectors[3] = ISpoke.addDynamicReserveConfig.selector; + selectors[4] = ISpoke.updatePositionManager.selector; + selectors[5] = ISpoke.updateReservePriceSource.selector; + accessManager.setTargetFunctionRole(address(spoke1), selectors, Roles.SPOKE_ADMIN_ROLE); + accessManager.setTargetFunctionRole(address(spoke2), selectors, Roles.SPOKE_ADMIN_ROLE); } - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTORS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Deploy protocol actors and initialize their balances - function _setUpActors() internal { - // Initialize the three actors of the fuzzers - address[] memory addresses = new address[](3); - addresses[0] = USER1; - addresses[1] = USER2; - addresses[2] = USER3; - - // Initialize the tokens array - address[] memory tokens = new address[](2); - tokens[0] = address(usdc); - tokens[1] = address(weth); - - address[] memory contracts = new address[](4); - contracts[0] = address(hub1); - contracts[1] = address(hub2); - contracts[2] = address(spoke1); - contracts[3] = address(spoke2); - - actorAddresses = ActorsUtils.setUpActors(addresses, tokens, contracts); - actors[USER1] = Actor(payable(actorAddresses[0])); - actors[USER2] = Actor(payable(actorAddresses[1])); - actors[USER3] = Actor(payable(actorAddresses[2])); + // Grant responsibilities to hubs + { + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = IHub.updateSpokeConfig.selector; + selectors[1] = IHub.setInterestRateData.selector; + accessManager.setTargetFunctionRole(address(hub1), selectors, Roles.HUB_ADMIN_ROLE); + accessManager.setTargetFunctionRole(address(hub2), selectors, Roles.HUB_ADMIN_ROLE); } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Deploy protocol actors and initialize their balances + function _setUpActors() internal { + // Initialize the three actors of the fuzzers + address[] memory addresses = new address[](3); + addresses[0] = USER1; + addresses[1] = USER2; + addresses[2] = USER3; + + // Initialize the tokens array + address[] memory tokens = new address[](2); + tokens[0] = address(usdc); + tokens[1] = address(weth); + + address[] memory contracts = new address[](4); + contracts[0] = address(hub1); + contracts[1] = address(hub2); + contracts[2] = address(spoke1); + contracts[3] = address(spoke2); + + actorAddresses = ActorsUtils.setUpActors(addresses, tokens, contracts); + actors[USER1] = Actor(payable(actorAddresses[0])); + actors[USER2] = Actor(payable(actorAddresses[1])); + actors[USER3] = Actor(payable(actorAddresses[2])); + } } diff --git a/tests/invariants/protocol-suite/SpecAggregator.t.sol b/tests/invariants/protocol-suite/SpecAggregator.t.sol index ebbf14686..14259ce14 100644 --- a/tests/invariants/protocol-suite/SpecAggregator.t.sol +++ b/tests/invariants/protocol-suite/SpecAggregator.t.sol @@ -2,41 +2,31 @@ pragma solidity ^0.8.19; // Test Contracts -import {InvariantsSpec} from "./specs/InvariantsSpec.t.sol"; -import {PostconditionsSpec} from "./specs/PostconditionsSpec.t.sol"; +import {InvariantsSpec} from './specs/InvariantsSpec.t.sol'; +import {PostconditionsSpec} from './specs/PostconditionsSpec.t.sol'; /// @title SpecAggregator /// @notice Helper contract to aggregate all spec contracts, inherited in BaseHooks /// @dev inherits InvariantsSpec, PostconditionsSpec -abstract contract SpecAggregator is - InvariantsSpec, - PostconditionsSpec -{ -/////////////////////////////////////////////////////////////////////////////////////////////// -// PROPERTY TYPES // -/////////////////////////////////////////////////////////////////////////////////////////////// - -/// In this invariant testing framework, there are two types of properties: - -/// - INVARIANTS (INV): -/// - Properties that should always hold true in the system. -/// - Implemented in the /invariants folder. - -/// - POSTCONDITIONS: -/// - Properties that should hold true after an action is executed. -/// - Implemented in the /hooks and /handlers folders. - -/// - There are two types of POSTCONDITIONS: - -/// - GLOBAL POSTCONDITIONS (GPOST): -/// - Properties that should always hold true after any action is executed. -/// - Checked in the `_checkPostConditions` function within the HookAggregator contract. - -/// - HANDLER-SPECIFIC POSTCONDITIONS (HSPOST): -/// - Properties that should hold true after a specific action is executed in a specific context. -/// - Implemented within each handler function, under the HANDLER-SPECIFIC POSTCONDITIONS section. - -/// - ERC4626 PROPERTIES: -/// - Properties that should always hold true in the system, which check compliance with the ERC4626 standard. -/// - Implemented across the testing suite as invariants, postconditions and specific custom handlers. +abstract contract SpecAggregator is InvariantsSpec, PostconditionsSpec { + /////////////////////////////////////////////////////////////////////////////////////////////// + // PROPERTY TYPES // + /////////////////////////////////////////////////////////////////////////////////////////////// + /// In this invariant testing framework, there are two types of properties: + /// - INVARIANTS (INV): + /// - Properties that should always hold true in the system. + /// - Implemented in the /invariants folder. + /// - POSTCONDITIONS: + /// - Properties that should hold true after an action is executed. + /// - Implemented in the /hooks and /handlers folders. + /// - There are two types of POSTCONDITIONS: + /// - GLOBAL POSTCONDITIONS (GPOST): + /// - Properties that should always hold true after any action is executed. + /// - Checked in the `_checkPostConditions` function within the HookAggregator contract. + /// - HANDLER-SPECIFIC POSTCONDITIONS (HSPOST): + /// - Properties that should hold true after a specific action is executed in a specific context. + /// - Implemented within each handler function, under the HANDLER-SPECIFIC POSTCONDITIONS section. + /// - ERC4626 PROPERTIES: + /// - Properties that should always hold true in the system, which check compliance with the ERC4626 standard. + /// - Implemented across the testing suite as invariants, postconditions and specific custom handlers. } diff --git a/tests/invariants/protocol-suite/Tester.t.sol b/tests/invariants/protocol-suite/Tester.t.sol index 53a7493ec..f8edf57bf 100644 --- a/tests/invariants/protocol-suite/Tester.t.sol +++ b/tests/invariants/protocol-suite/Tester.t.sol @@ -1,21 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {Invariants} from "./Invariants.t.sol"; -import {Setup} from "./Setup.t.sol"; +import {Invariants} from './Invariants.t.sol'; +import {Setup} from './Setup.t.sol'; /// @title Tester /// @notice Entry point for invariant testing, inherits all contracts, invariants & handler /// @dev Mono contract that contains all the testing logic contract Tester is Invariants, Setup { - constructor() payable { - // Deploy protocol contracts and protocol actors - setUp(); - } + constructor() payable { + // Deploy protocol contracts and protocol actors + setUp(); + } - /// @dev Foundry compatibility faster setup debugging - function setUp() internal { - // Deploy protocol contracts and protocol actors - _setUp(); - } + /// @dev Foundry compatibility faster setup debugging + function setUp() internal { + // Deploy protocol contracts and protocol actors + _setUp(); + } } diff --git a/tests/invariants/protocol-suite/TesterFoundry.t.sol b/tests/invariants/protocol-suite/TesterFoundry.t.sol index 722147488..485caa48b 100644 --- a/tests/invariants/protocol-suite/TesterFoundry.t.sol +++ b/tests/invariants/protocol-suite/TesterFoundry.t.sol @@ -2,43 +2,43 @@ pragma solidity ^0.8.19; // Contracts -import {StdInvariant} from "forge-std/StdInvariant.sol"; -import {Invariants} from "./Invariants.t.sol"; -import {Setup} from "./Setup.t.sol"; +import {StdInvariant} from 'forge-std/StdInvariant.sol'; +import {Invariants} from './Invariants.t.sol'; +import {Setup} from './Setup.t.sol'; /// @title TesterFoundry /// @notice Entry point for invariant testing, inherits all contracts, invariants & handler /// @dev Mono contract that contains all the testing logic contract TesterFoundry is Invariants, Setup, StdInvariant { - /// forge-config: default.invariant.fail-on-revert = false - /// forge-config: default.invariant.runs = 1000 - /// forge-config: default.invariant.depth = 100 - /// forge-config: default.invariant.corpus-dir = "tests/invariants/_corpus/foundry" - /// forge-config: default.invariant.show-solidity = true - /// forge-config: default.invariant.show-metrics = true - /// forge-config: default.fuzz.seed = '0x1' - /// forge-config: default.fuzz.include-storage = true - /// forge-config: default.fuzz.include-push-bytes = true - /// forge-config: default.fuzz.call-override = false - /// forge-config: default.fuzz.dictionary-weight = 80 - /// forge-config: default.fuzz.shrink-sequence = true - /// @dev Foundry compatibility faster setup debugging - function setUp() public { - // Deploy protocol contracts and protocol actors - _setUp(); + /// forge-config: default.invariant.fail-on-revert = false + /// forge-config: default.invariant.runs = 1000 + /// forge-config: default.invariant.depth = 100 + /// forge-config: default.invariant.corpus-dir = "tests/invariants/_corpus/foundry" + /// forge-config: default.invariant.show-solidity = true + /// forge-config: default.invariant.show-metrics = true + /// forge-config: default.fuzz.seed = '0x1' + /// forge-config: default.fuzz.include-storage = true + /// forge-config: default.fuzz.include-push-bytes = true + /// forge-config: default.fuzz.call-override = false + /// forge-config: default.fuzz.dictionary-weight = 80 + /// forge-config: default.fuzz.shrink-sequence = true + /// @dev Foundry compatibility faster setup debugging + function setUp() public { + // Deploy protocol contracts and protocol actors + _setUp(); - // Set the target contract - targetContract(address(this)); + // Set the target contract + targetContract(address(this)); - // Exclude target selectors - bytes4[] memory selectors = new bytes4[](1); - selectors[0] = bytes4(keccak256("checkPostConditions()")); + // Exclude target selectors + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = bytes4(keccak256('checkPostConditions()')); - excludeSelector(FuzzSelector({addr: address(this), selectors: selectors})); + excludeSelector(FuzzSelector({addr: address(this), selectors: selectors})); - // Set the target senders - targetSender(USER1); - targetSender(USER2); - targetSender(USER3); - } + // Set the target senders + targetSender(USER1); + targetSender(USER2); + targetSender(USER3); + } } diff --git a/tests/invariants/protocol-suite/_config/echidna_config.yaml b/tests/invariants/protocol-suite/_config/echidna_config.yaml index b846f8dde..9fb7e5754 100644 --- a/tests/invariants/protocol-suite/_config/echidna_config.yaml +++ b/tests/invariants/protocol-suite/_config/echidna_config.yaml @@ -26,7 +26,7 @@ propMaxGas: 1000000000 testMaxGas: 1000000000 # list of methods to filter -filterFunctions: [ "Tester.checkPostConditions()" ] +filterFunctions: ["Tester.checkPostConditions()"] # by default, blacklist methods in filterFunctions #filterBlacklist: false @@ -39,7 +39,7 @@ stopOnFail: false coverage: true # list of file formats to save coverage reports in; default is all possible formats -coverageFormats: [ "html" ] +coverageFormats: ["html"] #directory to save the corpus; by default is disabled corpusDir: "tests/invariants/protocol-suite/_corpus/echidna/default/_data/corpus" @@ -47,9 +47,9 @@ corpusDir: "tests/invariants/protocol-suite/_corpus/echidna/default/_data/corpus #mutConsts: [100, 1, 1] #remappings -cryticArgs: [ "--compile-libraries=(LiquidationLogic,0xf01)" ] +cryticArgs: ["--compile-libraries=(LiquidationLogic,0xf01)"] -deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] +deployContracts: [["0xf01", "LiquidationLogic"]] # maximum value to send to payable functions maxValue: 1e+30 # 100000000000 eth diff --git a/tests/invariants/protocol-suite/_config/echidna_config_ci.yaml b/tests/invariants/protocol-suite/_config/echidna_config_ci.yaml index 056c9bc11..11027a560 100644 --- a/tests/invariants/protocol-suite/_config/echidna_config_ci.yaml +++ b/tests/invariants/protocol-suite/_config/echidna_config_ci.yaml @@ -26,7 +26,7 @@ propMaxGas: 1000000000 testMaxGas: 1000000000 # list of methods to filter -filterFunctions: [ "Tester.checkPostConditions()" ] +filterFunctions: ["Tester.checkPostConditions()"] # by default, blacklist methods in filterFunctions #filterBlacklist: false @@ -39,7 +39,7 @@ stopOnFail: false coverage: true # list of file formats to save coverage reports in; default is all possible formats -coverageFormats: [ "html" ] +coverageFormats: ["html"] #directory to save the corpus; by default is disabled corpusDir: "corpus" @@ -47,9 +47,9 @@ corpusDir: "corpus" #mutConsts: [100, 1, 1] #remappings -cryticArgs: [ "--ignore-compile", "--compile-libraries=(LiquidationLogic,0xf01)" ] +cryticArgs: ["--ignore-compile", "--compile-libraries=(LiquidationLogic,0xf01)"] -deployContracts: [ [ "0xf01", "LiquidationLogic" ] ] +deployContracts: [["0xf01", "LiquidationLogic"]] # maximum value to send to payable functions maxValue: 1e+30 # 100000000000 eth diff --git a/tests/invariants/protocol-suite/base/BaseHandler.t.sol b/tests/invariants/protocol-suite/base/BaseHandler.t.sol index 42fe39868..a5e422751 100644 --- a/tests/invariants/protocol-suite/base/BaseHandler.t.sol +++ b/tests/invariants/protocol-suite/base/BaseHandler.t.sol @@ -2,80 +2,86 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; // Libraries -import {MockERC20} from "tests/mocks/MockERC20.sol"; +import {MockERC20} from 'tests/mocks/MockERC20.sol'; // Contracts -import {Actor} from "../../shared/utils/Actor.sol"; -import {HookAggregator} from "../hooks/HookAggregator.t.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; +import {HookAggregator} from '../hooks/HookAggregator.t.sol'; /// @title BaseHandler /// @notice Contains common logic for all handlers /// @dev inherits all suite assertions since per action assertions are implmenteds in the handlers contract BaseHandler is HookAggregator { - /////////////////////////////////////////////////////////////////////////////////////////////// - // MODIFIERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // MODIFIERS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // SHARED VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // SHARED VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice Helper function to randomize a uint256 seed with a string salt - function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { - return uint256(keccak256(abi.encodePacked(seed, salt))); - } + /// @notice Helper function to randomize a uint256 seed with a string salt + function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(seed, salt))); + } - /// @notice Helper function to get a random value - function _getRandomValue(uint256 modulus) internal view returns (uint256) { - uint256 randomNumber = uint256(keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender))); - return randomNumber % modulus; // Adjust the modulus to the desired range - } + /// @notice Helper function to get a random value + function _getRandomValue(uint256 modulus) internal view returns (uint256) { + uint256 randomNumber = uint256( + keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender)) + ); + return randomNumber % modulus; // Adjust the modulus to the desired range + } - /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor - function _approve(address token, Actor actor_, address spender, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor_.proxy(token, abi.encodeWithSelector(0x095ea7b3, spender, amount)); - require(success, string(returnData)); - } + /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor + function _approve(address token, Actor actor_, address spender, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor_.proxy( + token, + abi.encodeWithSelector(0x095ea7b3, spender, amount) + ); + require(success, string(returnData)); + } - /// @notice Helper function to safely approve an amount of tokens to a spender + /// @notice Helper function to safely approve an amount of tokens to a spender - function _approve(address token, address owner, address spender, uint256 amount) internal { - vm.prank(owner); - _safeApprove(token, spender, 0); - vm.prank(owner); - _safeApprove(token, spender, amount); - } + function _approve(address token, address owner, address spender, uint256 amount) internal { + vm.prank(owner); + _safeApprove(token, spender, 0); + vm.prank(owner); + _safeApprove(token, spender, amount); + } - /// @notice Helper function to safely approve an amount of tokens to a spender - /// @dev This function is used to revert on failed approvals - function _safeApprove(address token, address spender, uint256 amount) internal { - (bool success, bytes memory retdata) = - token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); - assert(success); - if (retdata.length > 0) assert(abi.decode(retdata, (bool))); - } + /// @notice Helper function to safely approve an amount of tokens to a spender + /// @dev This function is used to revert on failed approvals + function _safeApprove(address token, address spender, uint256 amount) internal { + (bool success, bytes memory retdata) = token.call( + abi.encodeWithSelector(IERC20.approve.selector, spender, amount) + ); + assert(success); + if (retdata.length > 0) assert(abi.decode(retdata, (bool))); + } - /// @notice Helper function to mint an amount of tokens to an address - function _mint(address token, address receiver, uint256 amount) internal { - MockERC20(token).mint(receiver, amount); - } + /// @notice Helper function to mint an amount of tokens to an address + function _mint(address token, address receiver, uint256 amount) internal { + MockERC20(token).mint(receiver, amount); + } - /// @notice Helper function to mint an amount of tokens to an address and approve them to a spender - /// @param token Address of the token to mint - /// @param owner Address of the new owner of the tokens - /// @param spender Address of the spender to approve the tokens to - /// @param amount Amount of tokens to mint and approve - function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { - _mint(token, owner, amount); - _approve(token, owner, spender, amount); - } + /// @notice Helper function to mint an amount of tokens to an address and approve them to a spender + /// @param token Address of the token to mint + /// @param owner Address of the new owner of the tokens + /// @param spender Address of the spender to approve the tokens to + /// @param amount Amount of tokens to mint and approve + function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { + _mint(token, owner, amount); + _approve(token, owner, spender, amount); + } } diff --git a/tests/invariants/protocol-suite/base/BaseHooks.t.sol b/tests/invariants/protocol-suite/base/BaseHooks.t.sol index 68e4ef887..e5015ea22 100644 --- a/tests/invariants/protocol-suite/base/BaseHooks.t.sol +++ b/tests/invariants/protocol-suite/base/BaseHooks.t.sol @@ -2,17 +2,17 @@ pragma solidity ^0.8.19; // Contracts -import {ProtocolAssertions} from "./ProtocolAssertions.t.sol"; +import {ProtocolAssertions} from './ProtocolAssertions.t.sol'; // Test Contracts -import {SpecAggregator} from "../SpecAggregator.t.sol"; +import {SpecAggregator} from '../SpecAggregator.t.sol'; /// @title BaseHooks /// @notice Contains common logic for all hooks /// @dev inherits all suite assertions since per-action assertions are implemented in the handlers /// @dev inherits SpecAggregator contract BaseHooks is ProtocolAssertions, SpecAggregator { -/////////////////////////////////////////////////////////////////////////////////////////////// -// HELPERS // -/////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/invariants/protocol-suite/base/BaseStorage.t.sol b/tests/invariants/protocol-suite/base/BaseStorage.t.sol index bece75eb1..eeeca44b7 100644 --- a/tests/invariants/protocol-suite/base/BaseStorage.t.sol +++ b/tests/invariants/protocol-suite/base/BaseStorage.t.sol @@ -2,159 +2,159 @@ pragma solidity ^0.8.19; // Contracts -import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; -import {IHub} from "src/hub/Hub.sol"; -import {ITreasurySpoke} from "src/spoke/TreasurySpoke.sol"; -import {ISpoke} from "src/spoke/Spoke.sol"; -import {IAaveOracle} from "src/spoke/interfaces/IAaveOracle.sol"; -import {AssetInterestRateStrategy} from "src/hub/AssetInterestRateStrategy.sol"; -import {AccessManager} from "src/dependencies/openzeppelin/AccessManager.sol"; -import {HubConfigurator} from "src/hub/HubConfigurator.sol"; -import {SpokeConfigurator} from "src/spoke/SpokeConfigurator.sol"; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; +import {IHub} from 'src/hub/Hub.sol'; +import {ITreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; +import {ISpoke} from 'src/spoke/Spoke.sol'; +import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; +import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; +import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; +import {SpokeConfigurator} from 'src/spoke/SpokeConfigurator.sol'; // Mock Contracts // Test Contracts // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; /// @notice BaseStorage contract for all test contracts, works in tandem with BaseTest abstract contract BaseStorage { - /////////////////////////////////////////////////////////////////////////////////////////////// - // CONSTANTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - uint256 constant MAX_TOKEN_AMOUNT = 1e29; - - uint256 constant ONE_DAY = 1 days; - uint256 constant ONE_MONTH = ONE_YEAR / 12; - uint256 constant ONE_YEAR = 365 days; - - uint256 internal constant NUMBER_OF_ACTORS = 3; - uint256 internal constant INITIAL_COLL_BALANCE = 1e21; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTORS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice The address of the admin, the tester itself - address internal admin = address(this); - - /// @notice Stores the actor during a handler call - Actor internal actor; - - /// @notice Mapping of fuzzer user addresses to actors - mapping(address => Actor) internal actors; - - /// @notice Array of all actor addresses - address[] internal actorAddresses; - - /// @notice The address that is targeted when executing an action (OPTIONAL) - address internal targetActor; - - /// @notice The signature of the action that is being executed - bytes4 internal currentActionSignature; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // ASSETS STORAGE // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice The USDC token - TestnetERC20 internal usdc; - /// @notice The WETH token - TestnetERC20 internal weth; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // SUITE STORAGE // - /////////////////////////////////////////////////////////////////////////////////////////////// - - // HUB CONTRACTS - IHub internal hub1; - IHub internal hub2; - AssetInterestRateStrategy internal irStrategy1; - AssetInterestRateStrategy internal irStrategy2; - HubConfigurator internal hubConfigurator; - - // SPOKE CONTRACTS - ITreasurySpoke internal treasurySpoke1; - ITreasurySpoke internal treasurySpoke2; - ISpoke internal spoke1; - ISpoke internal spoke2; - SpokeConfigurator internal spokeConfigurator; - - // ORACLES - IAaveOracle internal oracle1; - IAaveOracle internal oracle2; - - // CONFIGURATION - AccessManager internal accessManager; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // EXTRA VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// - - // ASSETS - /// @notice Array of base assets for the suite - AssetInfo[] internal baseAssets; - - // HUB - uint256 internal hub1WethAssetId; - uint256 internal hub1UsdcAssetId; - uint256 internal hub2WethAssetId; - uint256 internal hub2UsdcAssetId; - - /// @notice Array of hub addresses for the suite - address[] internal hubAddresses; - /// @notice Spoke configurations - mapping(address => HubInfo) internal hubInfo; - /// @notice Hub assetIds - mapping(address => uint256[]) internal hubAssetIds; - - // SPOKES - /// @notice Array of spokes addresses for the suite - address[] internal spokesAddresses; - /// @notice spokesAddresses + treasurySpoke address - address[] internal allSpokes; - /// @notice Spoke configurations - mapping(ISpoke => SpokeInfo) internal spokeInfo; - /// @notice Spoke reserveIds - mapping(address => uint256[]) internal spokeReserveIds; - /// @notice Spoke reserveIds to global assetIds - mapping(address => mapping(uint256 => uint256)) internal reserveIdToAssetId; - /// @notice Spoke assetIds to reserveIds info - mapping(address => mapping(uint256 => uint256)) internal assetIdToReserveId; - /// @notice Spoke reserveIds to hub addresses - mapping(address => mapping(uint256 => address)) internal reserveIdToHubAddress; - - // PRICE FEEDS - address[] internal priceFeeds; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // STRUCTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - struct SpokeInfo { - ReserveInfo weth; - ReserveInfo usdc; - ReserveInfo weth2; - ReserveInfo usdc2; - uint256 MAX_ALLOWED_ASSET_ID; - } - - struct HubInfo { - address treasurySpoke; - address irStrategy; - } - - struct ReserveInfo { - uint256 reserveId; - ISpoke.ReserveConfig reserveConfig; - ISpoke.DynamicReserveConfig dynReserveConfig; - } - - struct AssetInfo { - address underlying; - uint8 decimals; - } + /////////////////////////////////////////////////////////////////////////////////////////////// + // CONSTANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + uint256 constant MAX_TOKEN_AMOUNT = 1e29; + + uint256 constant ONE_DAY = 1 days; + uint256 constant ONE_MONTH = ONE_YEAR / 12; + uint256 constant ONE_YEAR = 365 days; + + uint256 internal constant NUMBER_OF_ACTORS = 3; + uint256 internal constant INITIAL_COLL_BALANCE = 1e21; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice The address of the admin, the tester itself + address internal admin = address(this); + + /// @notice Stores the actor during a handler call + Actor internal actor; + + /// @notice Mapping of fuzzer user addresses to actors + mapping(address => Actor) internal actors; + + /// @notice Array of all actor addresses + address[] internal actorAddresses; + + /// @notice The address that is targeted when executing an action (OPTIONAL) + address internal targetActor; + + /// @notice The signature of the action that is being executed + bytes4 internal currentActionSignature; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSETS STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice The USDC token + TestnetERC20 internal usdc; + /// @notice The WETH token + TestnetERC20 internal weth; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SUITE STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // HUB CONTRACTS + IHub internal hub1; + IHub internal hub2; + AssetInterestRateStrategy internal irStrategy1; + AssetInterestRateStrategy internal irStrategy2; + HubConfigurator internal hubConfigurator; + + // SPOKE CONTRACTS + ITreasurySpoke internal treasurySpoke1; + ITreasurySpoke internal treasurySpoke2; + ISpoke internal spoke1; + ISpoke internal spoke2; + SpokeConfigurator internal spokeConfigurator; + + // ORACLES + IAaveOracle internal oracle1; + IAaveOracle internal oracle2; + + // CONFIGURATION + AccessManager internal accessManager; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // EXTRA VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // ASSETS + /// @notice Array of base assets for the suite + AssetInfo[] internal baseAssets; + + // HUB + uint256 internal hub1WethAssetId; + uint256 internal hub1UsdcAssetId; + uint256 internal hub2WethAssetId; + uint256 internal hub2UsdcAssetId; + + /// @notice Array of hub addresses for the suite + address[] internal hubAddresses; + /// @notice Spoke configurations + mapping(address => HubInfo) internal hubInfo; + /// @notice Hub assetIds + mapping(address => uint256[]) internal hubAssetIds; + + // SPOKES + /// @notice Array of spokes addresses for the suite + address[] internal spokesAddresses; + /// @notice spokesAddresses + treasurySpoke address + address[] internal allSpokes; + /// @notice Spoke configurations + mapping(ISpoke => SpokeInfo) internal spokeInfo; + /// @notice Spoke reserveIds + mapping(address => uint256[]) internal spokeReserveIds; + /// @notice Spoke reserveIds to global assetIds + mapping(address => mapping(uint256 => uint256)) internal reserveIdToAssetId; + /// @notice Spoke assetIds to reserveIds info + mapping(address => mapping(uint256 => uint256)) internal assetIdToReserveId; + /// @notice Spoke reserveIds to hub addresses + mapping(address => mapping(uint256 => address)) internal reserveIdToHubAddress; + + // PRICE FEEDS + address[] internal priceFeeds; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct SpokeInfo { + ReserveInfo weth; + ReserveInfo usdc; + ReserveInfo weth2; + ReserveInfo usdc2; + uint256 MAX_ALLOWED_ASSET_ID; + } + + struct HubInfo { + address treasurySpoke; + address irStrategy; + } + + struct ReserveInfo { + uint256 reserveId; + ISpoke.ReserveConfig reserveConfig; + ISpoke.DynamicReserveConfig dynReserveConfig; + } + + struct AssetInfo { + address underlying; + uint8 decimals; + } } diff --git a/tests/invariants/protocol-suite/base/BaseTest.t.sol b/tests/invariants/protocol-suite/base/BaseTest.t.sol index b3386549b..46112c78a 100644 --- a/tests/invariants/protocol-suite/base/BaseTest.t.sol +++ b/tests/invariants/protocol-suite/base/BaseTest.t.sol @@ -2,158 +2,165 @@ pragma solidity ^0.8.19; // Libraries -import {Vm} from "forge-std/Base.sol"; -import {StdUtils} from "forge-std/StdUtils.sol"; -import "forge-std/console.sol"; -import {Constants} from "tests/Constants.sol"; +import {Vm} from 'forge-std/Base.sol'; +import {StdUtils} from 'forge-std/StdUtils.sol'; +import 'forge-std/console.sol'; +import {Constants} from 'tests/Constants.sol'; // Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; -import {PropertiesConstants} from "../../shared/utils/PropertiesConstants.sol"; -import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; +import {PropertiesConstants} from '../../shared/utils/PropertiesConstants.sol'; +import {StdAsserts} from '../../shared/utils/StdAsserts.sol'; // Base -import {BaseStorage} from "./BaseStorage.t.sol"; +import {BaseStorage} from './BaseStorage.t.sol'; /// @notice Base contract for all test contracts extends BaseStorage /// @dev Provides setup modifier and cheat code setup /// @dev inherits Storage, Testing constants assertions and utils needed for testing abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdUtils { - bool internal IS_TEST = true; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTOR PROXY MECHANISM // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @dev Actor proxy mechanism - modifier setup() virtual { - actor = actors[msg.sender]; - _; - delete actor; - } - - /// @dev Solves medusa backward time warp issue - modifier monotonicTimestamp() virtual { - /// @dev: Implement monotonic timestamp if needed - _; - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CALLBACKS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - receive() external payable {} - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CHEAT CODE SETUP // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @dev Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. - address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - - /// @dev Virtual machine instance - Vm internal constant vm = Vm(VM_ADDRESS); - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS: RANDOM GETTERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Get a random actor proxy address - function _getRandomActor(uint256 _i) internal view returns (address) { - uint256 _actorIndex = _i % NUMBER_OF_ACTORS; - return actorAddresses[_actorIndex]; - } - - /// @notice Helper function to get a random base asset - function _getRandomBaseAsset(uint256 i) internal view returns (address) { - uint256 _assetIndex = i % baseAssets.length; - return baseAssets[_assetIndex].underlying; - } - - /// @notice Helper function to get random base asset full info - function _getRandomBaseAssetFullInfo(uint256 i) internal view returns (AssetInfo memory) { - uint256 _assetIndex = i % baseAssets.length; - return baseAssets[_assetIndex]; - } - - /// @notice Helper function to get a random hub asset id - function _getRandomHubAssetId(address hub, uint256 i) internal view returns (uint256) { - uint256 _assetIndex = i % hubAssetIds[hub].length; - return hubAssetIds[hub][_assetIndex]; - } - - /// @notice Helper function to get a random spoke address - function _getRandomSpoke(uint256 i) internal view returns (address) { - uint256 _spokeIndex = i % spokesAddresses.length; - return spokesAddresses[_spokeIndex]; - } - - /// @notice Helper function to get a random reserve id from a spoke - function _getRandomReserveId(address spoke, uint256 i) internal view returns (uint256) { - uint256 _reserveIndex = i % spokeReserveIds[spoke].length; - return spokeReserveIds[spoke][_reserveIndex]; - } - - /// @notice Helper function to get a random price feed address - function _getRandomPriceFeed(uint256 i) internal view returns (address) { - uint256 _priceFeedIndex = i % priceFeeds.length; - return priceFeeds[_priceFeedIndex]; - } - - /// @notice Helper function to get a random hub address - function _getRandomHub(uint256 i) internal view returns (address) { - uint256 _hubIndex = i % hubAddresses.length; - return hubAddresses[_hubIndex]; - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS: GETTERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _getAssetId(address spoke, uint256 reserveId) internal view returns (uint256) { - return reserveIdToAssetId[spoke][reserveId]; - } - - function _getReserveId(address spoke, uint256 assetId) internal view returns (uint256) { - return assetIdToReserveId[spoke][assetId]; - } - - function _getHubAddress(address spoke, uint256 reserveId) internal view returns (address) { - return reserveIdToHubAddress[spoke][reserveId]; - } - - function _isHealthy(address spoke, address user) internal view returns (bool) { - return - ISpoke(spoke).getUserAccountData(user).healthFactor >= Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD; - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Get a random address - function _makeAddr(string memory name) internal pure returns (address addr) { - uint256 privateKey = uint256(keccak256(abi.encodePacked(name))); - addr = vm.addr(privateKey); - } - - /// @notice Helper function to transfer tokens by actor - function _transferByActor(address token, address to, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor.proxy(token, abi.encodeWithSelector(IERC20.transfer.selector, to, amount)); - require(success, string(returnData)); - } - - /// @notice Helper function to approve tokens by actor - function _approveByActor(address token, address spender, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor.proxy(token, abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); - require(success, string(returnData)); - } + bool internal IS_TEST = true; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTOR PROXY MECHANISM // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Actor proxy mechanism + modifier setup() virtual { + actor = actors[msg.sender]; + _; + delete actor; + } + + /// @dev Solves medusa backward time warp issue + modifier monotonicTimestamp() virtual { + /// @dev: Implement monotonic timestamp if needed + _; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CALLBACKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + receive() external payable {} + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CHEAT CODE SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. + address internal constant VM_ADDRESS = address(uint160(uint256(keccak256('hevm cheat code')))); + + /// @dev Virtual machine instance + Vm internal constant vm = Vm(VM_ADDRESS); + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS: RANDOM GETTERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Get a random actor proxy address + function _getRandomActor(uint256 _i) internal view returns (address) { + uint256 _actorIndex = _i % NUMBER_OF_ACTORS; + return actorAddresses[_actorIndex]; + } + + /// @notice Helper function to get a random base asset + function _getRandomBaseAsset(uint256 i) internal view returns (address) { + uint256 _assetIndex = i % baseAssets.length; + return baseAssets[_assetIndex].underlying; + } + + /// @notice Helper function to get random base asset full info + function _getRandomBaseAssetFullInfo(uint256 i) internal view returns (AssetInfo memory) { + uint256 _assetIndex = i % baseAssets.length; + return baseAssets[_assetIndex]; + } + + /// @notice Helper function to get a random hub asset id + function _getRandomHubAssetId(address hub, uint256 i) internal view returns (uint256) { + uint256 _assetIndex = i % hubAssetIds[hub].length; + return hubAssetIds[hub][_assetIndex]; + } + + /// @notice Helper function to get a random spoke address + function _getRandomSpoke(uint256 i) internal view returns (address) { + uint256 _spokeIndex = i % spokesAddresses.length; + return spokesAddresses[_spokeIndex]; + } + + /// @notice Helper function to get a random reserve id from a spoke + function _getRandomReserveId(address spoke, uint256 i) internal view returns (uint256) { + uint256 _reserveIndex = i % spokeReserveIds[spoke].length; + return spokeReserveIds[spoke][_reserveIndex]; + } + + /// @notice Helper function to get a random price feed address + function _getRandomPriceFeed(uint256 i) internal view returns (address) { + uint256 _priceFeedIndex = i % priceFeeds.length; + return priceFeeds[_priceFeedIndex]; + } + + /// @notice Helper function to get a random hub address + function _getRandomHub(uint256 i) internal view returns (address) { + uint256 _hubIndex = i % hubAddresses.length; + return hubAddresses[_hubIndex]; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS: GETTERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _getAssetId(address spoke, uint256 reserveId) internal view returns (uint256) { + return reserveIdToAssetId[spoke][reserveId]; + } + + function _getReserveId(address spoke, uint256 assetId) internal view returns (uint256) { + return assetIdToReserveId[spoke][assetId]; + } + + function _getHubAddress(address spoke, uint256 reserveId) internal view returns (address) { + return reserveIdToHubAddress[spoke][reserveId]; + } + + function _isHealthy(address spoke, address user) internal view returns (bool) { + return + ISpoke(spoke).getUserAccountData(user).healthFactor >= + Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Get a random address + function _makeAddr(string memory name) internal pure returns (address addr) { + uint256 privateKey = uint256(keccak256(abi.encodePacked(name))); + addr = vm.addr(privateKey); + } + + /// @notice Helper function to transfer tokens by actor + function _transferByActor(address token, address to, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor.proxy( + token, + abi.encodeWithSelector(IERC20.transfer.selector, to, amount) + ); + require(success, string(returnData)); + } + + /// @notice Helper function to approve tokens by actor + function _approveByActor(address token, address spender, uint256 amount) internal { + bool success; + bytes memory returnData; + (success, returnData) = actor.proxy( + token, + abi.encodeWithSelector(IERC20.approve.selector, spender, amount) + ); + require(success, string(returnData)); + } } diff --git a/tests/invariants/protocol-suite/base/ProtocolAssertions.t.sol b/tests/invariants/protocol-suite/base/ProtocolAssertions.t.sol index 18434085f..30b41eb54 100644 --- a/tests/invariants/protocol-suite/base/ProtocolAssertions.t.sol +++ b/tests/invariants/protocol-suite/base/ProtocolAssertions.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.19; // Base -import {BaseTest} from "./BaseTest.t.sol"; -import {StdAsserts} from "../../shared/utils/StdAsserts.sol"; +import {BaseTest} from './BaseTest.t.sol'; +import {StdAsserts} from '../../shared/utils/StdAsserts.sol'; /// @title ProtocolAssertions /// @notice Helper contract for protocol specific assertions diff --git a/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol b/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol index dcabc6549..37b356e76 100644 --- a/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol @@ -2,58 +2,63 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {IHubConfiguratorHandler} from "../interfaces/IHubConfiguratorHandler.sol"; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IHubConfiguratorHandler} from '../interfaces/IHubConfiguratorHandler.sol'; // Libraries -import "forge-std/console.sol"; +import 'forge-std/console.sol'; // Test Contracts -import {Actor} from "../../../shared/utils/Actor.sol"; -import {BaseHandler} from "../../base/BaseHandler.t.sol"; +import {Actor} from '../../../shared/utils/Actor.sol'; +import {BaseHandler} from '../../base/BaseHandler.t.sol'; /// @title HubConfiguratorHandler /// @notice Handler test contract for a set of actions contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STATE VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j, uint8 k) external setup { - address hub = _getRandomHub(i); - uint256 assetId = _getRandomHubAssetId(hub, j); - address spoke = _getRandomSpoke(k); - hubConfigurator.updateSpokeSupplyCap(hub, assetId, spoke, addCap); - } - - function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j, uint8 k) external setup { - address hub = _getRandomHub(i); - uint256 assetId = _getRandomHubAssetId(hub, j); - address spoke = _getRandomSpoke(k); - hubConfigurator.updateSpokeDrawCap(hub, assetId, spoke, drawCap); - } - - function updateSpokeRiskPremiumThreshold(uint256 riskPremiumThreshold, uint8 i, uint8 j, uint8 k) external setup { - address hub = _getRandomHub(i); - uint256 assetId = _getRandomHubAssetId(hub, j); - address spoke = _getRandomSpoke(k); - hubConfigurator.updateSpokeRiskPremiumThreshold(hub, assetId, spoke, riskPremiumThreshold); - } - - function updateSpokePaused(bool paused, uint8 i, uint8 j, uint8 k) external setup { - address hub = _getRandomHub(i); - uint256 assetId = _getRandomHubAssetId(hub, j); - address spoke = _getRandomSpoke(k); - hubConfigurator.updateSpokePaused(hub, assetId, spoke, paused); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j, uint8 k) external setup { + address hub = _getRandomHub(i); + uint256 assetId = _getRandomHubAssetId(hub, j); + address spoke = _getRandomSpoke(k); + hubConfigurator.updateSpokeSupplyCap(hub, assetId, spoke, addCap); + } + + function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j, uint8 k) external setup { + address hub = _getRandomHub(i); + uint256 assetId = _getRandomHubAssetId(hub, j); + address spoke = _getRandomSpoke(k); + hubConfigurator.updateSpokeDrawCap(hub, assetId, spoke, drawCap); + } + + function updateSpokeRiskPremiumThreshold( + uint256 riskPremiumThreshold, + uint8 i, + uint8 j, + uint8 k + ) external setup { + address hub = _getRandomHub(i); + uint256 assetId = _getRandomHubAssetId(hub, j); + address spoke = _getRandomSpoke(k); + hubConfigurator.updateSpokeRiskPremiumThreshold(hub, assetId, spoke, riskPremiumThreshold); + } + + function updateSpokePaused(bool paused, uint8 i, uint8 j, uint8 k) external setup { + address hub = _getRandomHub(i); + uint256 assetId = _getRandomHubAssetId(hub, j); + address spoke = _getRandomSpoke(k); + hubConfigurator.updateSpokePaused(hub, assetId, spoke, paused); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol b/tests/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol index e76505679..839a07d0f 100644 --- a/tests/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol +++ b/tests/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol @@ -4,12 +4,19 @@ pragma solidity ^0.8.19; /// @title ISpokeHandler /// @notice Interface for the SpokeHandler interface ISpokeHandler { - function supply(uint256 amount, uint8 i, uint8 j, uint8 k) external; - function withdraw(uint256 amount, uint8 i, uint8 j, uint8 k) external; - function borrow(uint256 amount, uint8 i, uint8 j, uint8 k) external; - function repay(uint256 amount, uint8 i, uint8 j, uint8 k) external; - function liquidationCall(uint256 debtToCover, bool receiveShares, uint8 i, uint8 j, uint8 k, uint8 l) external; - function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external; - function updateUserRiskPremium(uint8 i) external; - function updateUserDynamicConfig(uint8 i) external; + function supply(uint256 amount, uint8 i, uint8 j, uint8 k) external; + function withdraw(uint256 amount, uint8 i, uint8 j, uint8 k) external; + function borrow(uint256 amount, uint8 i, uint8 j, uint8 k) external; + function repay(uint256 amount, uint8 i, uint8 j, uint8 k) external; + function liquidationCall( + uint256 debtToCover, + bool receiveShares, + uint8 i, + uint8 j, + uint8 k, + uint8 l + ) external; + function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external; + function updateUserRiskPremium(uint8 i) external; + function updateUserDynamicConfig(uint8 i) external; } diff --git a/tests/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol b/tests/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol index 2b87b0255..063084a73 100644 --- a/tests/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol @@ -2,43 +2,43 @@ pragma solidity ^0.8.19; // Test Contracts -import {BaseHandler} from "../../base/BaseHandler.t.sol"; -import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; +import {BaseHandler} from '../../base/BaseHandler.t.sol'; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; /// @title DonationAttackHandler /// @notice Handler test contract for a set of actions contract DonationAttackHandler is BaseHandler { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STATE VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function donateUnderlyingToHub(uint256 amount, uint8 i, uint8 j) external { - // Get one of the hub addresses randomly - address hubAddress = _getRandomHub(j); - // Get one of the assets IDs randomly - address underlying = _getRandomBaseAsset(i); - - TestnetERC20(underlying).mint(hubAddress, amount); - } - - function donateUnderlyingToSpoke(uint256 amount, uint8 i, uint8 j) external { - // Get one of the spoke addresses randomly - address spoke = _getRandomSpoke(j); - // Get one of the assets IDs randomly - address underlying = _getRandomBaseAsset(i); - - TestnetERC20(underlying).mint(spoke, amount); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function donateUnderlyingToHub(uint256 amount, uint8 i, uint8 j) external { + // Get one of the hub addresses randomly + address hubAddress = _getRandomHub(j); + // Get one of the assets IDs randomly + address underlying = _getRandomBaseAsset(i); + + TestnetERC20(underlying).mint(hubAddress, amount); + } + + function donateUnderlyingToSpoke(uint256 amount, uint8 i, uint8 j) external { + // Get one of the spoke addresses randomly + address spoke = _getRandomSpoke(j); + // Get one of the assets IDs randomly + address underlying = _getRandomBaseAsset(i); + + TestnetERC20(underlying).mint(spoke, amount); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol b/tests/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol index 6fa378e95..bd932bd1f 100644 --- a/tests/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol @@ -2,35 +2,35 @@ pragma solidity ^0.8.19; // Test Contracts -import {BaseHandler} from "../../base/BaseHandler.t.sol"; -import {MockPriceFeedSimulator} from "../../../shared/mocks/MockPriceFeedSimulator.sol"; +import {BaseHandler} from '../../base/BaseHandler.t.sol'; +import {MockPriceFeedSimulator} from '../../../shared/mocks/MockPriceFeedSimulator.sol'; -import "forge-std/console.sol"; +import 'forge-std/console.sol'; /// @title PriceFeedSimulatorHandler /// @notice Handler test contract for a set of actions contract PriceFeedSimulatorHandler is BaseHandler { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STATE VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// - function setPrice(int256 price, uint8 i) public { - address priceFeed = _getRandomPriceFeed(i); + function setPrice(int256 price, uint8 i) public { + address priceFeed = _getRandomPriceFeed(i); - price = clampBetween(price, PRICE_MIN, PRICE_MAX); + price = clampBetween(price, PRICE_MIN, PRICE_MAX); - MockPriceFeedSimulator(priceFeed).setPrice(price); - } + MockPriceFeedSimulator(priceFeed).setPrice(price); + } - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol b/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol index 5a1215d79..9f1df1d98 100644 --- a/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol @@ -2,73 +2,73 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {ISpokeConfiguratorHandler} from "../interfaces/ISpokeConfiguratorHandler.sol"; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {ISpokeConfiguratorHandler} from '../interfaces/ISpokeConfiguratorHandler.sol'; // Libraries -import "forge-std/console.sol"; +import 'forge-std/console.sol'; // Test Contracts -import {Actor} from "../../../shared/utils/Actor.sol"; -import {BaseHandler} from "../../base/BaseHandler.t.sol"; +import {Actor} from '../../../shared/utils/Actor.sol'; +import {BaseHandler} from '../../base/BaseHandler.t.sol'; /// @title SpokeConfiguratorHandler /// @notice Handler test contract for a set of actions contract SpokeConfiguratorHandler is BaseHandler, ISpokeConfiguratorHandler { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STATE VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// - function updateLiquidationTargetHealthFactor(uint256 targetHealthFactor, uint8 i) external setup { - address spoke = _getRandomSpoke(i); - spokeConfigurator.updateLiquidationTargetHealthFactor(spoke, targetHealthFactor); - } + function updateLiquidationTargetHealthFactor(uint256 targetHealthFactor, uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.updateLiquidationTargetHealthFactor(spoke, targetHealthFactor); + } - function updateHealthFactorForMaxBonus(uint256 healthFactorForMaxBonus, uint8 i) external setup { - address spoke = _getRandomSpoke(i); - spokeConfigurator.updateHealthFactorForMaxBonus(spoke, healthFactorForMaxBonus); - } + function updateHealthFactorForMaxBonus(uint256 healthFactorForMaxBonus, uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.updateHealthFactorForMaxBonus(spoke, healthFactorForMaxBonus); + } - function updateLiquidationBonusFactor(uint256 liquidationBonusFactor, uint8 i) external setup { - address spoke = _getRandomSpoke(i); - spokeConfigurator.updateLiquidationBonusFactor(spoke, liquidationBonusFactor); - } + function updateLiquidationBonusFactor(uint256 liquidationBonusFactor, uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.updateLiquidationBonusFactor(spoke, liquidationBonusFactor); + } - function updatePaused(bool paused, uint8 i, uint8 j) external setup { - address spoke = _getRandomSpoke(i); - uint256 reserveId = _getRandomReserveId(spoke, j); - spokeConfigurator.updatePaused(spoke, reserveId, paused); - } + function updatePaused(bool paused, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updatePaused(spoke, reserveId, paused); + } - function updateFrozen(bool frozen, uint8 i, uint8 j) external setup { - address spoke = _getRandomSpoke(i); - uint256 reserveId = _getRandomReserveId(spoke, j); - spokeConfigurator.updateFrozen(spoke, reserveId, frozen); - } + function updateFrozen(bool frozen, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updateFrozen(spoke, reserveId, frozen); + } - function updateBorrowable(bool borrowable, uint8 i, uint8 j) external setup { - address spoke = _getRandomSpoke(i); - uint256 reserveId = _getRandomReserveId(spoke, j); - spokeConfigurator.updateBorrowable(spoke, reserveId, borrowable); - } + function updateBorrowable(bool borrowable, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updateBorrowable(spoke, reserveId, borrowable); + } - function pauseAllReserves(uint8 i) external setup { - address spoke = _getRandomSpoke(i); - spokeConfigurator.pauseAllReserves(spoke); - } + function pauseAllReserves(uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.pauseAllReserves(spoke); + } - function freezeAllReserves(uint8 i) external setup { - address spoke = _getRandomSpoke(i); - spokeConfigurator.freezeAllReserves(spoke); - } + function freezeAllReserves(uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.freezeAllReserves(spoke); + } - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index 41440073e..0a2e911c1 100644 --- a/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -2,396 +2,461 @@ pragma solidity ^0.8.19; // Interfaces -import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; -import {ISpokeHandler} from "../interfaces/ISpokeHandler.sol"; -import {IERC20} from "src/dependencies/openzeppelin/IERC20.sol"; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {ISpokeHandler} from '../interfaces/ISpokeHandler.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; // Libraries -import {Constants} from "tests/Constants.sol"; -import "forge-std/console.sol"; +import {Constants} from 'tests/Constants.sol'; +import 'forge-std/console.sol'; // Test Contracts -import {Actor} from "../../../shared/utils/Actor.sol"; -import {BaseHandler} from "../../base/BaseHandler.t.sol"; +import {Actor} from '../../../shared/utils/Actor.sol'; +import {BaseHandler} from '../../base/BaseHandler.t.sol'; // Contracts -import {Spoke} from "src/spoke/Spoke.sol"; -import {IHub} from "src/hub/interfaces/IHub.sol"; +import {Spoke} from 'src/spoke/Spoke.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; /// @title SpokeHandler /// @notice Handler test contract for a set of actions contract SpokeHandler is BaseHandler, ISpokeHandler { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STATE VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// - - struct LiquidationVars { - // Spoke - address violator; - address liquidator; - address spoke; - address underlying; - // Debt reserve - uint256 debtReserveId; - uint256 collateralReserveId; - uint256 reserveDebtBefore; - uint256 reserveDebtAfter; - // Liquidation - uint256 debtToCover; - uint256 debtLiquidated; - uint256 totalDebtValueBefore; - // Liquidator - uint256 liquidatorCollateralBalanceBefore; - uint256 liquidatorCollateralBalanceAfter; + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct LiquidationVars { + // Spoke + address violator; + address liquidator; + address spoke; + address underlying; + // Debt reserve + uint256 debtReserveId; + uint256 collateralReserveId; + uint256 reserveDebtBefore; + uint256 reserveDebtAfter; + // Liquidation + uint256 debtToCover; + uint256 debtLiquidated; + uint256 totalDebtValueBefore; + // Liquidator + uint256 liquidatorCollateralBalanceBefore; + uint256 liquidatorCollateralBalanceAfter; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function supply(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { + bool success; + bytes memory returnData; + + // Get one of the three actors randomly + address onBehalfOf = _getRandomActor(i); + + address spoke = _getRandomSpoke(j); + + // Get one of the reserves IDs randomly + uint256 reserveId = _getRandomReserveId(spoke, k); + + // Register user to check postconditions + _registerUserToCheck(spoke, reserveId, onBehalfOf); + + _before(); + (success, returnData) = actor.proxy( + spoke, + abi.encodeCall(Spoke.supply, (reserveId, amount, onBehalfOf)) + ); + + if (success) { + _after(); + } else { + revert('SpokeHandler: supply failed'); } + } - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// + function withdraw(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { + bool success; + bytes memory returnData; - function supply(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - bool success; - bytes memory returnData; + // Get one of the three actors randomly + address onBehalfOf = _getRandomActor(i); - // Get one of the three actors randomly - address onBehalfOf = _getRandomActor(i); + address spoke = _getRandomSpoke(j); - address spoke = _getRandomSpoke(j); + // Get one of the reserves IDs randomly + uint256 reserveId = _getRandomReserveId(spoke, k); - // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(spoke, k); + uint256 userAmount = IHub(_getHubAddress(spoke, reserveId)).previewRemoveByShares( + reserveId, + ISpoke(spoke).getUserSuppliedShares(reserveId, onBehalfOf) + ); - // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, onBehalfOf); + // Register user to check postconditions + _registerUserToCheck(spoke, reserveId, onBehalfOf); - _before(); - (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.supply, (reserveId, amount, onBehalfOf))); + _before(); + (success, returnData) = actor.proxy( + spoke, + abi.encodeCall(Spoke.withdraw, (reserveId, amount, onBehalfOf)) + ); - if (success) { - _after(); - } else { - revert("SpokeHandler: supply failed"); - } + // Implemented outside the success check to assert success + if ( + defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt == 0 && + (amount > 0 && userAmount != 0) + ) { + assertTrue(success, GPOST_SP_H); } - function withdraw(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - bool success; - bytes memory returnData; - - // Get one of the three actors randomly - address onBehalfOf = _getRandomActor(i); - - address spoke = _getRandomSpoke(j); - - // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(spoke, k); - - uint256 userAmount = IHub(_getHubAddress(spoke, reserveId)) - .previewRemoveByShares(reserveId, ISpoke(spoke).getUserSuppliedShares(reserveId, onBehalfOf)); - - // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, onBehalfOf); - - _before(); - (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.withdraw, (reserveId, amount, onBehalfOf))); - - // Implemented outside the success check to assert success - if (defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt == 0 && (amount > 0 && userAmount != 0)) - { - assertTrue(success, GPOST_SP_H); - } - - if (success) { - _after(); - } else { - revert("SpokeHandler: withdraw failed"); - } + if (success) { + _after(); + } else { + revert('SpokeHandler: withdraw failed'); } + } - function borrow(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - bool success; - bytes memory returnData; + function borrow(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { + bool success; + bytes memory returnData; - // Get one of the three actors randomly - address onBehalfOf = _getRandomActor(i); + // Get one of the three actors randomly + address onBehalfOf = _getRandomActor(i); - address spoke = _getRandomSpoke(j); + address spoke = _getRandomSpoke(j); - // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(spoke, k); + // Get one of the reserves IDs randomly + uint256 reserveId = _getRandomReserveId(spoke, k); - // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, onBehalfOf); + // Register user to check postconditions + _registerUserToCheck(spoke, reserveId, onBehalfOf); - // Check if user is healthy - bool isHealthy = _isHealthy(spoke, onBehalfOf); + // Check if user is healthy + bool isHealthy = _isHealthy(spoke, onBehalfOf); - _before(); - (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.borrow, (reserveId, amount, onBehalfOf))); + _before(); + (success, returnData) = actor.proxy( + spoke, + abi.encodeCall(Spoke.borrow, (reserveId, amount, onBehalfOf)) + ); - if (success) { - _after(); + if (success) { + _after(); - ///// HSPOST ///// + ///// HSPOST ///// - assertTrue(isHealthy, HSPOST_SP_D); - } else { - revert("SpokeHandler: borrow failed"); - } + assertTrue(isHealthy, HSPOST_SP_D); + } else { + revert('SpokeHandler: borrow failed'); } + } - function repay(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - bool success; - bytes memory returnData; + function repay(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { + bool success; + bytes memory returnData; - // Get one of the three actors randomly - address onBehalfOf = _getRandomActor(i); + // Get one of the three actors randomly + address onBehalfOf = _getRandomActor(i); - address spoke = _getRandomSpoke(j); + address spoke = _getRandomSpoke(j); - // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(spoke, k); + // Get one of the reserves IDs randomly + uint256 reserveId = _getRandomReserveId(spoke, k); - // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, onBehalfOf); + // Register user to check postconditions + _registerUserToCheck(spoke, reserveId, onBehalfOf); - _before(); - (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.repay, (reserveId, amount, onBehalfOf))); + _before(); + (success, returnData) = actor.proxy( + spoke, + abi.encodeCall(Spoke.repay, (reserveId, amount, onBehalfOf)) + ); - if (success) { - _after(); + if (success) { + _after(); - ///// HSPOST ///// + ///// HSPOST ///// - assertLe( - defaultVarsAfter.userVars[spoke][reserveId][onBehalfOf].totalDebt, - defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt, - HSPOST_SP_C - ); - } else { - revert("SpokeHandler: repay failed"); - } + assertLe( + defaultVarsAfter.userVars[spoke][reserveId][onBehalfOf].totalDebt, + defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt, + HSPOST_SP_C + ); + } else { + revert('SpokeHandler: repay failed'); + } + } + + function liquidationCall( + uint256 debtToCover, + bool receiveShares, + uint8 i, + uint8 j, + uint8 k, + uint8 l + ) external setup { + bool success; + bytes memory returnData; + + LiquidationVars memory liquidationVars; + + // Get one of the three actors randomly + liquidationVars.spoke = _getRandomSpoke(j); + liquidationVars.debtToCover = debtToCover; + + liquidationVars.violator = _getRandomActor(i); + liquidationVars.liquidator = address(actor); + + // Get both reserves IDs randomly and the collateral underlying asset + liquidationVars.collateralReserveId = _getRandomReserveId(liquidationVars.spoke, k); + liquidationVars.debtReserveId = _getRandomReserveId(liquidationVars.spoke, l); + liquidationVars.underlying = ISpoke(liquidationVars.spoke) + .getReserve(liquidationVars.collateralReserveId) + .underlying; + + uint256 violatorCollateralBalanceBefore = ISpoke(liquidationVars.spoke).getUserSuppliedAssets( + liquidationVars.collateralReserveId, + liquidationVars.violator + ); + + liquidationVars.totalDebtValueBefore = ISpoke(liquidationVars.spoke) + .getUserAccountData(_getRandomActor(i)) + .totalDebtValue; + liquidationVars.reserveDebtBefore = ISpoke(liquidationVars.spoke).getReserveTotalDebt( + liquidationVars.debtReserveId + ); + + if (receiveShares) { + liquidationVars.liquidatorCollateralBalanceBefore = ISpoke(liquidationVars.spoke) + .getUserSuppliedAssets(liquidationVars.collateralReserveId, address(actor)); + } else { + liquidationVars.liquidatorCollateralBalanceBefore = IERC20(liquidationVars.underlying) + .balanceOf(address(actor)); } - function liquidationCall(uint256 debtToCover, bool receiveShares, uint8 i, uint8 j, uint8 k, uint8 l) - external - setup - { - bool success; - bytes memory returnData; - - LiquidationVars memory liquidationVars; - - // Get one of the three actors randomly - liquidationVars.spoke = _getRandomSpoke(j); - liquidationVars.debtToCover = debtToCover; - - liquidationVars.violator = _getRandomActor(i); - liquidationVars.liquidator = address(actor); - - // Get both reserves IDs randomly and the collateral underlying asset - liquidationVars.collateralReserveId = _getRandomReserveId(liquidationVars.spoke, k); - liquidationVars.debtReserveId = _getRandomReserveId(liquidationVars.spoke, l); - liquidationVars.underlying = - ISpoke(liquidationVars.spoke).getReserve(liquidationVars.collateralReserveId).underlying; - - uint256 violatorCollateralBalanceBefore = ISpoke(liquidationVars.spoke) - .getUserSuppliedAssets(liquidationVars.collateralReserveId, liquidationVars.violator); - - liquidationVars.totalDebtValueBefore = - ISpoke(liquidationVars.spoke).getUserAccountData(_getRandomActor(i)).totalDebtValue; - liquidationVars.reserveDebtBefore = - ISpoke(liquidationVars.spoke).getReserveTotalDebt(liquidationVars.debtReserveId); - - if (receiveShares) { - liquidationVars.liquidatorCollateralBalanceBefore = ISpoke(liquidationVars.spoke) - .getUserSuppliedAssets(liquidationVars.collateralReserveId, address(actor)); - } else { - liquidationVars.liquidatorCollateralBalanceBefore = - IERC20(liquidationVars.underlying).balanceOf(address(actor)); - } - - // Register users to check postconditions: liquidated user and liquidator for both reserves - _registerUserToCheck(liquidationVars.spoke, liquidationVars.debtReserveId, liquidationVars.violator); - _registerUserToCheck(liquidationVars.spoke, liquidationVars.collateralReserveId, liquidationVars.violator); - _registerUserToCheck(liquidationVars.spoke, liquidationVars.debtReserveId, liquidationVars.liquidator); - _registerUserToCheck(liquidationVars.spoke, liquidationVars.collateralReserveId, liquidationVars.liquidator); - - _before(); - (success, returnData) = actor.proxy( - liquidationVars.spoke, - abi.encodeCall( - Spoke.liquidationCall, - ( - liquidationVars.collateralReserveId, - liquidationVars.debtReserveId, - liquidationVars.violator, - liquidationVars.debtToCover, - receiveShares - ) - ) + // Register users to check postconditions: liquidated user and liquidator for both reserves + _registerUserToCheck( + liquidationVars.spoke, + liquidationVars.debtReserveId, + liquidationVars.violator + ); + _registerUserToCheck( + liquidationVars.spoke, + liquidationVars.collateralReserveId, + liquidationVars.violator + ); + _registerUserToCheck( + liquidationVars.spoke, + liquidationVars.debtReserveId, + liquidationVars.liquidator + ); + _registerUserToCheck( + liquidationVars.spoke, + liquidationVars.collateralReserveId, + liquidationVars.liquidator + ); + + _before(); + (success, returnData) = actor.proxy( + liquidationVars.spoke, + abi.encodeCall( + Spoke.liquidationCall, + ( + liquidationVars.collateralReserveId, + liquidationVars.debtReserveId, + liquidationVars.violator, + liquidationVars.debtToCover, + receiveShares + ) + ) + ); + + if (success) { + _after(); + + // Calculate the debt liquidated + liquidationVars.reserveDebtAfter = ISpoke(liquidationVars.spoke).getReserveTotalDebt( + liquidationVars.debtReserveId + ); + liquidationVars.debtLiquidated = (liquidationVars.reserveDebtBefore > + liquidationVars.reserveDebtAfter) + ? liquidationVars.reserveDebtBefore - liquidationVars.reserveDebtAfter + : 0; + + if (receiveShares) { + liquidationVars.liquidatorCollateralBalanceAfter = ISpoke(liquidationVars.spoke) + .getUserSuppliedAssets(liquidationVars.collateralReserveId, address(actor)); + } else { + liquidationVars.liquidatorCollateralBalanceAfter = IERC20(liquidationVars.underlying) + .balanceOf(address(actor)); + } + + ///// HSPOST ///// + assertLe( + liquidationVars.debtLiquidated, + defaultVarsBefore + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] + .totalDebt, + HSPOST_SP_LIQ_A + ); + + if ( + liquidationVars.liquidatorCollateralBalanceAfter > + liquidationVars.liquidatorCollateralBalanceBefore + ) { + assertLe( + liquidationVars.liquidatorCollateralBalanceAfter - + liquidationVars.liquidatorCollateralBalanceBefore, + violatorCollateralBalanceBefore, + HSPOST_SP_LIQ_B ); - - if (success) { - _after(); - - // Calculate the debt liquidated - liquidationVars.reserveDebtAfter = - ISpoke(liquidationVars.spoke).getReserveTotalDebt(liquidationVars.debtReserveId); - liquidationVars.debtLiquidated = (liquidationVars.reserveDebtBefore > liquidationVars.reserveDebtAfter) - ? liquidationVars.reserveDebtBefore - liquidationVars.reserveDebtAfter - : 0; - - if (receiveShares) { - liquidationVars.liquidatorCollateralBalanceAfter = ISpoke(liquidationVars.spoke) - .getUserSuppliedAssets(liquidationVars.collateralReserveId, address(actor)); - } else { - liquidationVars.liquidatorCollateralBalanceAfter = - IERC20(liquidationVars.underlying).balanceOf(address(actor)); - } - - ///// HSPOST ///// - assertLe( - liquidationVars.debtLiquidated, - defaultVarsBefore.userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator].totalDebt, - HSPOST_SP_LIQ_A - ); - - if (liquidationVars.liquidatorCollateralBalanceAfter > liquidationVars.liquidatorCollateralBalanceBefore) { - assertLe( - liquidationVars.liquidatorCollateralBalanceAfter - - liquidationVars.liquidatorCollateralBalanceBefore, - violatorCollateralBalanceBefore, - HSPOST_SP_LIQ_B - ); - } - - if (liquidationVars.totalDebtValueBefore < Constants.DUST_LIQUIDATION_THRESHOLD) { - assertEq( - defaultVarsAfter.userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor( - i - )].totalDebt, - 0, - HSPOST_SP_LIQ_C - ); - } - - assertGe(liquidationVars.debtToCover, liquidationVars.debtLiquidated, HSPOST_SP_LIQ_D); - - assertLt( - defaultVarsBefore.userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, - Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, - HSPOST_SP_LIQ_E - ); - - if ( - defaultVarsAfter.userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor( - i - )].totalDebt > 0 - ) { - assertGt( - defaultVarsAfter.userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, - defaultVarsBefore.userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, - HSPOST_SP_LIQ_G - ); - } - } else { - revert("SpokeHandler: liquidationCall failed"); - } + } + + if (liquidationVars.totalDebtValueBefore < Constants.DUST_LIQUIDATION_THRESHOLD) { + assertEq( + defaultVarsAfter + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] + .totalDebt, + 0, + HSPOST_SP_LIQ_C + ); + } + + assertGe(liquidationVars.debtToCover, liquidationVars.debtLiquidated, HSPOST_SP_LIQ_D); + + assertLt( + defaultVarsBefore + .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] + .healthFactor, + Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + HSPOST_SP_LIQ_E + ); + + if ( + defaultVarsAfter + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] + .totalDebt > 0 + ) { + assertGt( + defaultVarsAfter + .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] + .healthFactor, + defaultVarsBefore + .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] + .healthFactor, + HSPOST_SP_LIQ_G + ); + } + } else { + revert('SpokeHandler: liquidationCall failed'); } + } - function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external setup { - bool success; - bytes memory returnData; + function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external setup { + bool success; + bytes memory returnData; - address onBehalfOf = address(actor); + address onBehalfOf = address(actor); - address spoke = _getRandomSpoke(i); + address spoke = _getRandomSpoke(i); - uint256 reserveId = _getRandomReserveId(spoke, j); + uint256 reserveId = _getRandomReserveId(spoke, j); - (bool isUsingAsCollateral,) = ISpoke(spoke).getUserReserveStatus(reserveId, onBehalfOf); + (bool isUsingAsCollateral, ) = ISpoke(spoke).getUserReserveStatus(reserveId, onBehalfOf); - require(usingAsCollateral != isUsingAsCollateral, "SpokeHandler: usingAsCollateral already set"); + require( + usingAsCollateral != isUsingAsCollateral, + 'SpokeHandler: usingAsCollateral already set' + ); - // Register user to check postconditions - /// @dev setUsingAsCollateral(reserveId, FALSE) all reserves in user position should be refreshed, - /// so we check all reserves in user position - /// setUsingAsCollateral(reserveId, TRUE) only reserveId in user position should be refreshed, - /// so we check only the reserveId in user position - _registerUserToCheck(spoke, (usingAsCollateral ? reserveId : CHECK_ALL_RESERVES), onBehalfOf); + // Register user to check postconditions + /// @dev setUsingAsCollateral(reserveId, FALSE) all reserves in user position should be refreshed, + /// so we check all reserves in user position + /// setUsingAsCollateral(reserveId, TRUE) only reserveId in user position should be refreshed, + /// so we check only the reserveId in user position + _registerUserToCheck(spoke, (usingAsCollateral ? reserveId : CHECK_ALL_RESERVES), onBehalfOf); - _before(); - (success, returnData) = - actor.proxy(spoke, abi.encodeCall(Spoke.setUsingAsCollateral, (reserveId, usingAsCollateral, onBehalfOf))); + _before(); + (success, returnData) = actor.proxy( + spoke, + abi.encodeCall(Spoke.setUsingAsCollateral, (reserveId, usingAsCollateral, onBehalfOf)) + ); - if (success) { - _after(); - } else { - revert("SpokeHandler: setUsingAsCollateral failed"); - } + if (success) { + _after(); + } else { + revert('SpokeHandler: setUsingAsCollateral failed'); } + } - function updateUserRiskPremium(uint8 i) external setup { - bool success; - bytes memory returnData; + function updateUserRiskPremium(uint8 i) external setup { + bool success; + bytes memory returnData; - address onBehalfOf = address(actor); + address onBehalfOf = address(actor); - address spoke = _getRandomSpoke(i); + address spoke = _getRandomSpoke(i); - uint256 totalDebt = _getTotalDebt(spoke, onBehalfOf); + uint256 totalDebt = _getTotalDebt(spoke, onBehalfOf); - // Register user to check postconditions - _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); + // Register user to check postconditions + _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); - _before(); - (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.updateUserRiskPremium, (onBehalfOf))); + _before(); + (success, returnData) = actor.proxy( + spoke, + abi.encodeCall(Spoke.updateUserRiskPremium, (onBehalfOf)) + ); - if (success) { - _after(); + if (success) { + _after(); - ///// HSPOST ///// - assertEq(_getTotalDebt(spoke, onBehalfOf), totalDebt, HSPOST_SP_F); - } else { - revert("SpokeHandler: updateUserRiskPremium failed"); - } + ///// HSPOST ///// + assertEq(_getTotalDebt(spoke, onBehalfOf), totalDebt, HSPOST_SP_F); + } else { + revert('SpokeHandler: updateUserRiskPremium failed'); } + } - function updateUserDynamicConfig(uint8 i) external setup { - bool success; - bytes memory returnData; + function updateUserDynamicConfig(uint8 i) external setup { + bool success; + bytes memory returnData; - address onBehalfOf = address(actor); + address onBehalfOf = address(actor); - address spoke = _getRandomSpoke(i); + address spoke = _getRandomSpoke(i); - _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); + _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); - _before(); - (success, returnData) = actor.proxy(spoke, abi.encodeCall(Spoke.updateUserDynamicConfig, (onBehalfOf))); + _before(); + (success, returnData) = actor.proxy( + spoke, + abi.encodeCall(Spoke.updateUserDynamicConfig, (onBehalfOf)) + ); - if (success) { - _after(); - } else { - revert("SpokeHandler: updateUserDynamicConfig failed"); - } + if (success) { + _after(); + } else { + revert('SpokeHandler: updateUserDynamicConfig failed'); } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _getTotalDebt(address spoke, address user) internal view returns (uint256) { - uint256 totalDebt; - uint256 reserveCount = spokeReserveIds[spoke].length; - for (uint256 i; i < reserveCount; i++) { - totalDebt += ISpoke(spoke).getUserTotalDebt(spokeReserveIds[spoke][i], user); - } - return totalDebt; + function _getTotalDebt(address spoke, address user) internal view returns (uint256) { + uint256 totalDebt; + uint256 reserveCount = spokeReserveIds[spoke].length; + for (uint256 i; i < reserveCount; i++) { + totalDebt += ISpoke(spoke).getUserTotalDebt(spokeReserveIds[spoke][i], user); } + return totalDebt; + } } diff --git a/tests/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol b/tests/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol index 7f48d9e20..c10f58948 100644 --- a/tests/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol @@ -2,89 +2,89 @@ pragma solidity ^0.8.19; // Interfaces -import {ITreasurySpokeHandler} from "../interfaces/ITreasurySpokeHandler.sol"; -import {ITreasurySpoke} from "src/spoke/interfaces/ITreasurySpoke.sol"; +import {ITreasurySpokeHandler} from '../interfaces/ITreasurySpokeHandler.sol'; +import {ITreasurySpoke} from 'src/spoke/interfaces/ITreasurySpoke.sol'; // Libraries -import "forge-std/console.sol"; +import 'forge-std/console.sol'; // Test Contracts -import {BaseHandler} from "../../base/BaseHandler.t.sol"; +import {BaseHandler} from '../../base/BaseHandler.t.sol'; /// @title TreasurySpokeHandler /// @notice Handler test contract for a set of actions contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STATE VARIABLES // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATE VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// - /* + /* E.g. num of active pools uint256 public activePools; */ - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // OWNER ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// - function supply(uint256 amount, uint8 i, uint8 j) external { - // TODO fix coverage issues + function supply(uint256 amount, uint8 i, uint8 j) external { + // TODO fix coverage issues // Get one of the hub addresses randomly - address hubAddress = _getRandomHub(i); - address treasurySpoke = hubInfo[hubAddress].treasurySpoke; - - // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(treasurySpoke, j); - - _before(); - try ITreasurySpoke(treasurySpoke).supply(reserveId, amount, msg.sender) { - _after(); - } catch { - revert("TreasurySpokeHandler: supply failed"); - } + address hubAddress = _getRandomHub(i); + address treasurySpoke = hubInfo[hubAddress].treasurySpoke; + + // Get one of the reserves IDs randomly + uint256 reserveId = _getRandomReserveId(treasurySpoke, j); + + _before(); + try ITreasurySpoke(treasurySpoke).supply(reserveId, amount, msg.sender) { + _after(); + } catch { + revert('TreasurySpokeHandler: supply failed'); } + } - function withdraw(uint256 amount, uint8 i, uint8 j) external { - // Get one of the hub addresses randomly - address hubAddress = _getRandomHub(i); - address treasurySpoke = hubInfo[hubAddress].treasurySpoke; + function withdraw(uint256 amount, uint8 i, uint8 j) external { + // Get one of the hub addresses randomly + address hubAddress = _getRandomHub(i); + address treasurySpoke = hubInfo[hubAddress].treasurySpoke; - // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(treasurySpoke, j); + // Get one of the reserves IDs randomly + uint256 reserveId = _getRandomReserveId(treasurySpoke, j); - _before(); - try ITreasurySpoke(treasurySpoke).withdraw(reserveId, amount, msg.sender) { - _after(); - } catch { - revert("TreasurySpokeHandler: withdraw failed"); - } + _before(); + try ITreasurySpoke(treasurySpoke).withdraw(reserveId, amount, msg.sender) { + _after(); + } catch { + revert('TreasurySpokeHandler: withdraw failed'); } + } - function transfer(uint256 amount, uint8 i, uint8 j, uint8 k) external { - // Get one of the hub addresses randomly - address hubAddress = _getRandomHub(i); + function transfer(uint256 amount, uint8 i, uint8 j, uint8 k) external { + // Get one of the hub addresses randomly + address hubAddress = _getRandomHub(i); - // Get one of the assets IDs randomly - address asset = _getRandomBaseAsset(j); + // Get one of the assets IDs randomly + address asset = _getRandomBaseAsset(j); - // Get one of the actors randomly - address to = _getRandomActor(k); + // Get one of the actors randomly + address to = _getRandomActor(k); - _before(); - try ITreasurySpoke(hubInfo[hubAddress].treasurySpoke).transfer(asset, to, amount) { - _after(); - } catch { - revert("TreasurySpokeHandler: transfer failed"); - } + _before(); + try ITreasurySpoke(hubInfo[hubAddress].treasurySpoke).transfer(asset, to, amount) { + _after(); + } catch { + revert('TreasurySpokeHandler: transfer failed'); } + } - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// } diff --git a/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index a24d2f3fd..9989287e8 100644 --- a/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -2,338 +2,359 @@ pragma solidity ^0.8.19; // Libraries -import {MathUtils} from "src/libraries/math/MathUtils.sol"; -import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; -import "forge-std/console.sol"; +import {MathUtils} from 'src/libraries/math/MathUtils.sol'; +import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; +import 'forge-std/console.sol'; // Utils -import {Constants} from "tests/Constants.sol"; +import {Constants} from 'tests/Constants.sol'; // Interfaces -import {ISpokeHandler} from "../handlers/interfaces/ISpokeHandler.sol"; -import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; -import {IHub} from "src/hub/interfaces/IHub.sol"; -import {IAssetInterestRateStrategy} from "src/hub/interfaces/IAssetInterestRateStrategy.sol"; +import {ISpokeHandler} from '../handlers/interfaces/ISpokeHandler.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; // Contracts -import {BaseHooks} from "../base/BaseHooks.t.sol"; +import {BaseHooks} from '../base/BaseHooks.t.sol'; /// @title DefaultBeforeAfterHooks /// @notice Helper contract for before and after hooks, state variable caching and postconditions /// @dev This contract is inherited by handlers abstract contract DefaultBeforeAfterHooks is BaseHooks { - /////////////////////////////////////////////////////////////////////////////////////////////// - // STRUCTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - struct AssetVars { - uint256 drawnIndex; - uint256 totalAssets; - uint256 totalShares; - uint256 drawn; - uint256 premium; - uint256 lastUpdateTimestamp; + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct AssetVars { + uint256 drawnIndex; + uint256 totalAssets; + uint256 totalShares; + uint256 drawn; + uint256 premium; + uint256 lastUpdateTimestamp; + } + + struct UserVars { + uint256 drawnDebt; + uint256 premiumDebt; + uint256 totalDebt; + } + + struct UserAccountDataVars { + uint256 healthFactor; + } + + struct DefaultVars { + mapping(address hub => mapping(uint256 assetId => AssetVars)) assetVars; + mapping(address spoke => mapping(uint256 reserveId => mapping(address user => UserVars))) userVars; + mapping(address spoke => mapping(address user => UserAccountDataVars)) userAccountDataVars; + } + + struct UserInfo { + address spoke; + uint256 reserveId; + address user; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Default variables before and after + DefaultVars defaultVarsBefore; + DefaultVars defaultVarsAfter; + + // Temp array of users to check postconditions for, reset after each handler on _resetState + UserInfo[] usersToCheck; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Default hooks setup + function _setUpDefaultHooks() internal {} + + /// @notice Helper to initialize storage arrays of default vars + function _setUpDefaultVars(DefaultVars storage _defaultVars) internal {} + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _defaultHooksBefore() internal { + // Asset values + _setAssetValues(defaultVarsBefore); + // User values + _setUserValues(defaultVarsBefore); + } + + function _defaultHooksAfter() internal { + // Asset values + _setAssetValues(defaultVarsAfter); + // User values + _setUserValues(defaultVarsAfter); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _setAssetValues(DefaultVars storage _defaultVars) internal { + for (uint256 i; i < hubAddresses.length; i++) { + address hubAddress = hubAddresses[i]; + uint256 assetCount = IHub(hubAddress).getAssetCount(); + for (uint256 j; j < assetCount; j++) { + _defaultVars.assetVars[hubAddress][j].drawnIndex = IHub(hubAddress).getAssetDrawnIndex(j); + _defaultVars.assetVars[hubAddress][j].totalAssets = IHub(hubAddress).getAddedAssets(j); + _defaultVars.assetVars[hubAddress][j].totalShares = IHub(hubAddress).getAddedShares(j); + ( + _defaultVars.assetVars[hubAddress][j].drawn, + _defaultVars.assetVars[hubAddress][j].premium + ) = IHub(hubAddress).getAssetOwed(j); + _defaultVars.assetVars[hubAddress][j].lastUpdateTimestamp = IHub(hubAddress) + .getAsset(j) + .lastUpdateTimestamp; + } } - - struct UserVars { - uint256 drawnDebt; - uint256 premiumDebt; - uint256 totalDebt; - } - - struct UserAccountDataVars { - uint256 healthFactor; - } - - struct DefaultVars { - mapping(address hub => mapping(uint256 assetId => AssetVars)) assetVars; - mapping(address spoke => mapping(uint256 reserveId => mapping(address user => UserVars))) userVars; - mapping(address spoke => mapping(address user => UserAccountDataVars)) userAccountDataVars; - } - - struct UserInfo { - address spoke; - uint256 reserveId; - address user; - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HOOKS STORAGE // - /////////////////////////////////////////////////////////////////////////////////////////////// - - // Default variables before and after - DefaultVars defaultVarsBefore; - DefaultVars defaultVarsAfter; - - // Temp array of users to check postconditions for, reset after each handler on _resetState - UserInfo[] usersToCheck; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // SETUP // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Default hooks setup - function _setUpDefaultHooks() internal {} - - /// @notice Helper to initialize storage arrays of default vars - function _setUpDefaultVars(DefaultVars storage _defaultVars) internal {} - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HOOKS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _defaultHooksBefore() internal { - // Asset values - _setAssetValues(defaultVarsBefore); - // User values - _setUserValues(defaultVarsBefore); - } - - function _defaultHooksAfter() internal { - // Asset values - _setAssetValues(defaultVarsAfter); - // User values - _setUserValues(defaultVarsAfter); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function _setAssetValues(DefaultVars storage _defaultVars) internal { - for (uint256 i; i < hubAddresses.length; i++) { - address hubAddress = hubAddresses[i]; - uint256 assetCount = IHub(hubAddress).getAssetCount(); - for (uint256 j; j < assetCount; j++) { - _defaultVars.assetVars[hubAddress][j].drawnIndex = IHub(hubAddress).getAssetDrawnIndex(j); - _defaultVars.assetVars[hubAddress][j].totalAssets = IHub(hubAddress).getAddedAssets(j); - _defaultVars.assetVars[hubAddress][j].totalShares = IHub(hubAddress).getAddedShares(j); - (_defaultVars.assetVars[hubAddress][j].drawn, _defaultVars.assetVars[hubAddress][j].premium) = - IHub(hubAddress).getAssetOwed(j); - _defaultVars.assetVars[hubAddress][j].lastUpdateTimestamp = - IHub(hubAddress).getAsset(j).lastUpdateTimestamp; - } - } - } - - function _setUserValues(DefaultVars storage _defaultVars) internal { - // Iterate through all users to check - for (uint256 i; i < usersToCheck.length; i++) { - UserInfo memory userInfo = usersToCheck[i]; - - // Cache values for the user's account data - ISpoke.UserAccountData memory userAccountData = ISpoke(userInfo.spoke).getUserAccountData(userInfo.user); - _defaultVars.userAccountDataVars[userInfo.spoke][userInfo.user].healthFactor = userAccountData.healthFactor; - - // Cache values for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig - if (userInfo.reserveId == CHECK_ALL_RESERVES) { - // Iterate through all reserves of the spoke - for (uint256 j; j < spokeReserveIds[userInfo.spoke].length; j++) { - ( - _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].drawnDebt, - _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] - .premiumDebt - ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); - _defaultVars.userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].totalDebt = - ISpoke(userInfo.spoke).getUserTotalDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); - } - } else { - // Cache values for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral - ( - _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].drawnDebt, - _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].premiumDebt - ) = ISpoke(userInfo.spoke).getUserDebt(userInfo.reserveId, userInfo.user); - _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].totalDebt = - ISpoke(userInfo.spoke).getUserTotalDebt(userInfo.reserveId, userInfo.user); - } + } + + function _setUserValues(DefaultVars storage _defaultVars) internal { + // Iterate through all users to check + for (uint256 i; i < usersToCheck.length; i++) { + UserInfo memory userInfo = usersToCheck[i]; + + // Cache values for the user's account data + ISpoke.UserAccountData memory userAccountData = ISpoke(userInfo.spoke).getUserAccountData( + userInfo.user + ); + _defaultVars.userAccountDataVars[userInfo.spoke][userInfo.user].healthFactor = userAccountData + .healthFactor; + + // Cache values for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig + if (userInfo.reserveId == CHECK_ALL_RESERVES) { + // Iterate through all reserves of the spoke + for (uint256 j; j < spokeReserveIds[userInfo.spoke].length; j++) { + ( + _defaultVars + .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + .drawnDebt, + _defaultVars + .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + .premiumDebt + ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); + _defaultVars + .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + .totalDebt = ISpoke(userInfo.spoke).getUserTotalDebt( + spokeReserveIds[userInfo.spoke][j], + userInfo.user + ); } + } else { + // Cache values for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral + ( + _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].drawnDebt, + _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].premiumDebt + ) = ISpoke(userInfo.spoke).getUserDebt(userInfo.reserveId, userInfo.user); + _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].totalDebt = ISpoke( + userInfo.spoke + ).getUserTotalDebt(userInfo.reserveId, userInfo.user); + } } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // POST CONDITIONS: HUB // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function assert_GPOST_HUB_A(address hubAddress, uint256 assetId) internal { - assertGe( - defaultVarsAfter.assetVars[hubAddress][assetId].drawnIndex, - defaultVarsBefore.assetVars[hubAddress][assetId].drawnIndex, - GPOST_HUB_A - ); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POST CONDITIONS: HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_GPOST_HUB_A(address hubAddress, uint256 assetId) internal { + assertGe( + defaultVarsAfter.assetVars[hubAddress][assetId].drawnIndex, + defaultVarsBefore.assetVars[hubAddress][assetId].drawnIndex, + GPOST_HUB_A + ); + } + + function assert_GPOST_HUB_B(address hubAddress, uint256 assetId) internal { + assertFullMulGe( + defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets + 1e6, + defaultVarsBefore.assetVars[hubAddress][assetId].totalShares + 1e6, + defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets + 1e6, + defaultVarsAfter.assetVars[hubAddress][assetId].totalShares + 1e6, + GPOST_HUB_B + ); + } + + function assert_GPOST_HUB_C(address hubAddress, uint256 assetId) internal { + // Read the cached signature of the current action + bytes4 signature = currentActionSignature; + if ( + signature == ISpokeHandler.supply.selector || + signature == ISpokeHandler.withdraw.selector || + signature == ISpokeHandler.borrow.selector || + signature == ISpokeHandler.repay.selector || + signature == ISpokeHandler.updateUserRiskPremium.selector || + signature == ISpokeHandler.liquidationCall.selector + ) { + assertEq( + IHub(hubAddress).getAssetDrawnRate(assetId), + IAssetInterestRateStrategy(hubInfo[hubAddress].irStrategy).calculateInterestRate( + assetId, + IHub(hubAddress).getAssetLiquidity(assetId), + defaultVarsAfter.assetVars[hubAddress][assetId].drawn, + 0, // Unused in the interest rate calculation + IHub(hubAddress).getAssetSwept(assetId) + ), + GPOST_HUB_C + ); } - - function assert_GPOST_HUB_B(address hubAddress, uint256 assetId) internal { - assertFullMulGe( - defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets + 1e6, - defaultVarsBefore.assetVars[hubAddress][assetId].totalShares + 1e6, - defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets + 1e6, - defaultVarsAfter.assetVars[hubAddress][assetId].totalShares + 1e6, - GPOST_HUB_B + } + + function assert_GPOST_HUB_D(address hubAddress, uint256 assetId) internal { + assertLe( + defaultVarsAfter.assetVars[hubAddress][assetId].lastUpdateTimestamp, + block.timestamp, + GPOST_HUB_D + ); + } + + function assert_GPOST_HUB_EF(address hubAddress, uint256 assetId, address spoke) internal { + // Get the spoke config + IHub.SpokeConfig memory spokeConfig = IHub(hubAddress).getSpokeConfig(assetId, spoke); + (, uint8 decimals) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); + + // GPOST_HUB_E + if ( + defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets > + defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets + ) { + if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, + spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_E ); + } } - function assert_GPOST_HUB_C(address hubAddress, uint256 assetId) internal { - // Read the cached signature of the current action - bytes4 signature = currentActionSignature; - if ( - signature == ISpokeHandler.supply.selector || signature == ISpokeHandler.withdraw.selector - || signature == ISpokeHandler.borrow.selector || signature == ISpokeHandler.repay.selector - || signature == ISpokeHandler.updateUserRiskPremium.selector - || signature == ISpokeHandler.liquidationCall.selector - ) { - assertEq( - IHub(hubAddress).getAssetDrawnRate(assetId), - IAssetInterestRateStrategy(hubInfo[hubAddress].irStrategy) - .calculateInterestRate( - assetId, - IHub(hubAddress).getAssetLiquidity(assetId), - defaultVarsAfter.assetVars[hubAddress][assetId].drawn, - 0, // Unused in the interest rate calculation - IHub(hubAddress).getAssetSwept(assetId) - ), - GPOST_HUB_C - ); - } - } - - function assert_GPOST_HUB_D(address hubAddress, uint256 assetId) internal { - assertLe(defaultVarsAfter.assetVars[hubAddress][assetId].lastUpdateTimestamp, block.timestamp, GPOST_HUB_D); - } - - function assert_GPOST_HUB_EF(address hubAddress, uint256 assetId, address spoke) internal { - // Get the spoke config - IHub.SpokeConfig memory spokeConfig = IHub(hubAddress).getSpokeConfig(assetId, spoke); - (, uint8 decimals) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); - - // GPOST_HUB_E - if ( - defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets - > defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets - ) { - if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { - assertLe( - defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, - spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), - GPOST_HUB_E - ); - } - } - - // GPOST_HUB_F - if ( - defaultVarsAfter.assetVars[hubAddress][assetId].drawn - > defaultVarsBefore.assetVars[hubAddress][assetId].drawn - ) { - if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { - assertLe( - defaultVarsAfter.assetVars[hubAddress][assetId].drawn, - spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), - GPOST_HUB_F - ); - } - } - } - - function assert_GPOST_HUB_G(address hubAddress, uint256 assetId) internal { - assertGe( - defaultVarsAfter.assetVars[hubAddress][assetId].lastUpdateTimestamp, - defaultVarsBefore.assetVars[hubAddress][assetId].lastUpdateTimestamp, - GPOST_HUB_G + // GPOST_HUB_F + if ( + defaultVarsAfter.assetVars[hubAddress][assetId].drawn > + defaultVarsBefore.assetVars[hubAddress][assetId].drawn + ) { + if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { + assertLe( + defaultVarsAfter.assetVars[hubAddress][assetId].drawn, + spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), + GPOST_HUB_F ); + } } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // POST CONDITIONS: SPOKE // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function assert_GPOST_SP_A(address spoke, uint256 reserveId, address user) internal { - ISpoke.UserPosition memory userPosition = ISpoke(spoke).getUserPosition(reserveId, user); - uint256 userRiskPremium = ISpoke(spoke).getUserAccountData(user).riskPremium; - - uint256 expected = PercentageMath.percentMulUp(userPosition.drawnShares, userRiskPremium); - assertEq(userPosition.premiumShares, expected, GPOST_SP_A); + } + + function assert_GPOST_HUB_G(address hubAddress, uint256 assetId) internal { + assertGe( + defaultVarsAfter.assetVars[hubAddress][assetId].lastUpdateTimestamp, + defaultVarsBefore.assetVars[hubAddress][assetId].lastUpdateTimestamp, + GPOST_HUB_G + ); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POST CONDITIONS: SPOKE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_GPOST_SP_A(address spoke, uint256 reserveId, address user) internal { + ISpoke.UserPosition memory userPosition = ISpoke(spoke).getUserPosition(reserveId, user); + uint256 userRiskPremium = ISpoke(spoke).getUserAccountData(user).riskPremium; + + uint256 expected = PercentageMath.percentMulUp(userPosition.drawnShares, userRiskPremium); + assertEq(userPosition.premiumShares, expected, GPOST_SP_A); + } + + function assert_GPOST_SP_B(address spoke, uint256 reserveId, address user) internal { + if ( + defaultVarsAfter.userVars[spoke][reserveId][user].premiumDebt < + defaultVarsBefore.userVars[spoke][reserveId][user].premiumDebt + ) { + assertTrue( + currentActionSignature == ISpokeHandler.repay.selector || + currentActionSignature == ISpokeHandler.liquidationCall.selector, + GPOST_SP_B + ); } - function assert_GPOST_SP_B(address spoke, uint256 reserveId, address user) internal { - if ( - defaultVarsAfter.userVars[spoke][reserveId][user].premiumDebt - < defaultVarsBefore.userVars[spoke][reserveId][user].premiumDebt - ) { - assertTrue( - currentActionSignature == ISpokeHandler.repay.selector - || currentActionSignature == ISpokeHandler.liquidationCall.selector, - GPOST_SP_B - ); - } - - if ( - defaultVarsAfter.userVars[spoke][reserveId][user].drawnDebt - < defaultVarsBefore.userVars[spoke][reserveId][user].drawnDebt - ) { - assertTrue( - currentActionSignature == ISpokeHandler.repay.selector - || currentActionSignature == ISpokeHandler.liquidationCall.selector, - GPOST_SP_B2 - ); - assertEq(defaultVarsAfter.userVars[spoke][reserveId][user].premiumDebt, 0, GPOST_SP_B2); - } + if ( + defaultVarsAfter.userVars[spoke][reserveId][user].drawnDebt < + defaultVarsBefore.userVars[spoke][reserveId][user].drawnDebt + ) { + assertTrue( + currentActionSignature == ISpokeHandler.repay.selector || + currentActionSignature == ISpokeHandler.liquidationCall.selector, + GPOST_SP_B2 + ); + assertEq(defaultVarsAfter.userVars[spoke][reserveId][user].premiumDebt, 0, GPOST_SP_B2); } - - function assert_GPOST_SP_E(address spoke, uint256 reserveId, address user) internal { - // latest reserve key - uint24 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; - // user-stored key - uint24 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; - - // Read the cached signature of the current action - bytes4 signature = currentActionSignature; - - if ( - signature == ISpokeHandler.borrow.selector || signature == ISpokeHandler.withdraw.selector - || signature == ISpokeHandler.setUsingAsCollateral.selector - || signature == ISpokeHandler.updateUserDynamicConfig.selector - ) { - assertEq(latestKey, userKey, GPOST_SP_E); - } + } + + function assert_GPOST_SP_E(address spoke, uint256 reserveId, address user) internal { + // latest reserve key + uint24 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; + // user-stored key + uint24 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; + + // Read the cached signature of the current action + bytes4 signature = currentActionSignature; + + if ( + signature == ISpokeHandler.borrow.selector || + signature == ISpokeHandler.withdraw.selector || + signature == ISpokeHandler.setUsingAsCollateral.selector || + signature == ISpokeHandler.updateUserDynamicConfig.selector + ) { + assertEq(latestKey, userKey, GPOST_SP_E); } + } - function assert_GPOST_LIQ_G(address spoke, address user) internal { - // Read the cached values of the user's health factor - uint256 healthFactorBefore = defaultVarsBefore.userAccountDataVars[spoke][user].healthFactor; - uint256 healthFactorAfter = defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor; + function assert_GPOST_LIQ_G(address spoke, address user) internal { + // Read the cached values of the user's health factor + uint256 healthFactorBefore = defaultVarsBefore.userAccountDataVars[spoke][user].healthFactor; + uint256 healthFactorAfter = defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor; - // Read the cached signature of the current action - bytes4 signature = currentActionSignature; + // Read the cached signature of the current action + bytes4 signature = currentActionSignature; - if ( - healthFactorBefore < Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD && healthFactorAfter < healthFactorBefore - ) { - assertTrue(signature == ISpokeHandler.liquidationCall.selector, GPOST_SP_LIQ_G); - } + if ( + healthFactorBefore < Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD && + healthFactorAfter < healthFactorBefore + ) { + assertTrue(signature == ISpokeHandler.liquidationCall.selector, GPOST_SP_LIQ_G); } - - function assert_GPOST_SP_LIQ_H(address spoke, address user) internal { - if ( - defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor - < Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD - ) { - assertTrue( - currentActionSignature == ISpokeHandler.supply.selector - || currentActionSignature == ISpokeHandler.repay.selector - || currentActionSignature == ISpokeHandler.liquidationCall.selector, - GPOST_SP_LIQ_H - ); - } + } + + function assert_GPOST_SP_LIQ_H(address spoke, address user) internal { + if ( + defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor < + Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD + ) { + assertTrue( + currentActionSignature == ISpokeHandler.supply.selector || + currentActionSignature == ISpokeHandler.repay.selector || + currentActionSignature == ISpokeHandler.liquidationCall.selector, + GPOST_SP_LIQ_H + ); } + } - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// - function _registerUserToCheck(address spoke, uint256 reserveId, address user) internal { - usersToCheck.push(UserInfo(spoke, reserveId, user)); - } + function _registerUserToCheck(address spoke, uint256 reserveId, address user) internal { + usersToCheck.push(UserInfo(spoke, reserveId, user)); + } - function _cacheCurrentActionSignature() internal { - currentActionSignature = bytes4(msg.sig); - } + function _cacheCurrentActionSignature() internal { + currentActionSignature = bytes4(msg.sig); + } } diff --git a/tests/invariants/protocol-suite/hooks/HookAggregator.t.sol b/tests/invariants/protocol-suite/hooks/HookAggregator.t.sol index c07c28c6c..66f778ff9 100644 --- a/tests/invariants/protocol-suite/hooks/HookAggregator.t.sol +++ b/tests/invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -2,120 +2,119 @@ pragma solidity ^0.8.19; // Hook Contracts -import {DefaultBeforeAfterHooks} from "./DefaultBeforeAfterHooks.t.sol"; +import {DefaultBeforeAfterHooks} from './DefaultBeforeAfterHooks.t.sol'; // Utils -import {ErrorHandlers} from "../../shared/utils/ErrorHandlers.sol"; -import "forge-std/console.sol"; +import {ErrorHandlers} from '../../shared/utils/ErrorHandlers.sol'; +import 'forge-std/console.sol'; /// @title HookAggregator /// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler abstract contract HookAggregator is DefaultBeforeAfterHooks { - /////////////////////////////////////////////////////////////////////////////////////////////// - // SETUP // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Initializer for the hooks - function _setUpHooks() internal { - _setUpDefaultHooks(); + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Initializer for the hooks + function _setUpHooks() internal { + _setUpDefaultHooks(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HOOKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Before hook for the handlers + function _before() internal { + _defaultHooksBefore(); + } + + /// @notice After hook for the handlers + function _after() internal { + _defaultHooksAfter(); + + // POST-CONDITIONS + _checkPostConditions(); + + // Reset the state + _resetState(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // POSTCONDITION CHECKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Postconditions for the handlers + function _checkPostConditions() internal { + // Store the message signature to avoid losing it inside the checkPostConditions call context + _cacheCurrentActionSignature(); + + try this.checkPostConditions() {} catch (bytes memory returnData) { + ErrorHandlers.handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HOOKS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Before hook for the handlers - function _before() internal { - _defaultHooksBefore(); - } - - /// @notice After hook for the handlers - function _after() internal { - _defaultHooksAfter(); - - // POST-CONDITIONS - _checkPostConditions(); - - // Reset the state - _resetState(); + } + + /// @dev postconditions checks entrypoint, should be self-called + function checkPostConditions() external { + // Hub postconditions + _hubPostConditions(); + // Spoke postconditions + _spokePostConditions(); + } + + function _hubPostConditions() internal { + // Iterate through all users to check + for (uint256 i; i < usersToCheck.length; i++) { + // Avoid checking postconditions for CHECK_ALL_RESERVES actions + if (usersToCheck[i].reserveId != CHECK_ALL_RESERVES) { + uint256 assetId = _getAssetId(usersToCheck[i].spoke, usersToCheck[i].reserveId); + address hubAddress = _getHubAddress(usersToCheck[i].spoke, usersToCheck[i].reserveId); + + assert_GPOST_HUB_A(hubAddress, assetId); + assert_GPOST_HUB_B(hubAddress, assetId); + assert_GPOST_HUB_C(hubAddress, assetId); + assert_GPOST_HUB_D(hubAddress, assetId); + assert_GPOST_HUB_EF(hubAddress, assetId, usersToCheck[i].spoke); + assert_GPOST_HUB_G(hubAddress, assetId); + } } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // POSTCONDITION CHECKS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Postconditions for the handlers - function _checkPostConditions() internal { - // Store the message signature to avoid losing it inside the checkPostConditions call context - _cacheCurrentActionSignature(); - - try this.checkPostConditions() {} - catch (bytes memory returnData) { - ErrorHandlers.handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); + } + + function _spokePostConditions() internal { + // Iterate through all users to check + for (uint256 i; i < usersToCheck.length; i++) { + uint256 reserveId = usersToCheck[i].reserveId; + address spoke = usersToCheck[i].spoke; + address user = usersToCheck[i].user; + + // Check properties for the spoke + assert_GPOST_LIQ_G(spoke, user); + assert_GPOST_SP_LIQ_H(spoke, user); + + // Check properties for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig + if (reserveId == CHECK_ALL_RESERVES) { + // Iterate through all reserves of the spoke + for (uint256 j; j < spokeReserveIds[spoke].length; j++) { + assert_GPOST_SP_A(spoke, spokeReserveIds[spoke][j], user); + assert_GPOST_SP_B(spoke, spokeReserveIds[spoke][j], user); + assert_GPOST_SP_E(spoke, spokeReserveIds[spoke][j], user); } + } else { + // Check properties for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral + assert_GPOST_SP_A(spoke, reserveId, user); + assert_GPOST_SP_B(spoke, reserveId, user); + assert_GPOST_SP_E(spoke, reserveId, user); + } } + } - /// @dev postconditions checks entrypoint, should be self-called - function checkPostConditions() external { - // Hub postconditions - _hubPostConditions(); - // Spoke postconditions - _spokePostConditions(); - } + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// - function _hubPostConditions() internal { - // Iterate through all users to check - for (uint256 i; i < usersToCheck.length; i++) { - // Avoid checking postconditions for CHECK_ALL_RESERVES actions - if (usersToCheck[i].reserveId != CHECK_ALL_RESERVES) { - uint256 assetId = _getAssetId(usersToCheck[i].spoke, usersToCheck[i].reserveId); - address hubAddress = _getHubAddress(usersToCheck[i].spoke, usersToCheck[i].reserveId); - - assert_GPOST_HUB_A(hubAddress, assetId); - assert_GPOST_HUB_B(hubAddress, assetId); - assert_GPOST_HUB_C(hubAddress, assetId); - assert_GPOST_HUB_D(hubAddress, assetId); - assert_GPOST_HUB_EF(hubAddress, assetId, usersToCheck[i].spoke); - assert_GPOST_HUB_G(hubAddress, assetId); - } - } - } - - function _spokePostConditions() internal { - // Iterate through all users to check - for (uint256 i; i < usersToCheck.length; i++) { - uint256 reserveId = usersToCheck[i].reserveId; - address spoke = usersToCheck[i].spoke; - address user = usersToCheck[i].user; - - // Check properties for the spoke - assert_GPOST_LIQ_G(spoke, user); - assert_GPOST_SP_LIQ_H(spoke, user); - - // Check properties for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig - if (reserveId == CHECK_ALL_RESERVES) { - // Iterate through all reserves of the spoke - for (uint256 j; j < spokeReserveIds[spoke].length; j++) { - assert_GPOST_SP_A(spoke, spokeReserveIds[spoke][j], user); - assert_GPOST_SP_B(spoke, spokeReserveIds[spoke][j], user); - assert_GPOST_SP_E(spoke, spokeReserveIds[spoke][j], user); - } - } else { - // Check properties for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral - assert_GPOST_SP_A(spoke, reserveId, user); - assert_GPOST_SP_B(spoke, reserveId, user); - assert_GPOST_SP_E(spoke, reserveId, user); - } - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Resets the state of the handlers - function _resetState() internal { - delete usersToCheck; - delete currentActionSignature; - } + /// @notice Resets the state of the handlers + function _resetState() internal { + delete usersToCheck; + delete currentActionSignature; + } } diff --git a/tests/invariants/protocol-suite/invariants/HubInvariants.t.sol b/tests/invariants/protocol-suite/invariants/HubInvariants.t.sol index 9748eba71..70bccf59e 100644 --- a/tests/invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/tests/invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -2,189 +2,203 @@ pragma solidity ^0.8.19; // Libraries -import {WadRayMath} from "src/libraries/math/WadRayMath.sol"; -import {PercentageMath} from "src/libraries/math/PercentageMath.sol"; -import "forge-std/console.sol"; +import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; +import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; +import 'forge-std/console.sol'; // Interfaces -import {IHub} from "src/hub/interfaces/IHub.sol"; -import {IERC20} from "src/dependencies/openzeppelin/IERC20.sol"; +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; // Contracts -import {HandlerAggregator} from "../HandlerAggregator.t.sol"; +import {HandlerAggregator} from '../HandlerAggregator.t.sol'; /// @title HubInvariants /// @notice Implements Hub Invariants for the protocol /// @dev Inherits HandlerAggregator to check actions in assertion testing mode abstract contract HubInvariants is HandlerAggregator { - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// - function assert_INV_HUB_A(address hubAddress, uint256 assetId) internal { - uint256 assets = IHub(hubAddress).getAddedAssets(assetId); + function assert_INV_HUB_A(address hubAddress, uint256 assetId) internal { + uint256 assets = IHub(hubAddress).getAddedAssets(assetId); - if (assets == 0) { - assertEq(IHub(hubAddress).getAddedShares(assetId), 0, INV_HUB_A2); - } + if (assets == 0) { + assertEq(IHub(hubAddress).getAddedShares(assetId), 0, INV_HUB_A2); } + } - function assert_INV_HUB_B(address hubAddress, uint256 assetId) internal { - // Sum per-spoke values - uint256 spokeCount = spokesAddresses.length; - uint256 sumDebt; + function assert_INV_HUB_B(address hubAddress, uint256 assetId) internal { + // Sum per-spoke values + uint256 spokeCount = spokesAddresses.length; + uint256 sumDebt; - for (uint256 i; i < spokeCount; i++) { - (uint256 d, uint256 p) = IHub(hubAddress).getSpokeOwed(assetId, spokesAddresses[i]); - sumDebt += d + p; - } - - uint256 assetTotal = IHub(hubAddress).getAssetTotalOwed(assetId); // drawn + premium - assertGe(sumDebt, assetTotal, INV_HUB_B); - } - - function assert_INV_HUB_C(address hubAddress, uint256 assetId) internal { - // Sum per-spoke values - uint256 spokeCount = spokesAddresses.length; - - uint256 sumDrawnShares; - uint256 sumPremDrawnShares; - int256 sumPremOffsetRay; - - for (uint256 i; i < spokeCount; i++) { - address spoke = spokesAddresses[i]; - sumDrawnShares += IHub(hubAddress).getSpokeDrawnShares(assetId, spoke); - (uint256 premiumDrawnShares, int256 premiumOffsetRay) = IHub(hubAddress).getSpokePremiumData(assetId, spoke); - sumPremDrawnShares += premiumDrawnShares; - sumPremOffsetRay += premiumOffsetRay; - } - - // Asset totals - IHub.Asset memory a = IHub(hubAddress).getAsset(assetId); - - // Checks - assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); - assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); - assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); - } - - function assert_INV_HUB_EF(address hubAddress, uint256 assetId) internal { - // Total amounts - uint256 totalSuppliedAssets = IHub(hubAddress).getAddedAssets(assetId); - - IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); - uint256 totalDebt = IHub(hubAddress).getAssetTotalOwed(assetId); - uint256 accruedFees = IHub(hubAddress).getAssetAccruedFees(assetId); - - // Checks //TODO check todo file INV_HUB_E - assertApproxEqAbs( - totalSuppliedAssets, - IHub(hubAddress).previewRemoveByShares(assetId, IHub(hubAddress).getAddedShares(assetId)), - IHub(hubAddress).previewRemoveByShares(assetId, 1), // tolerance of 1 share for rounding - INV_HUB_E - ); - - // totalAddedAssets + fees = liquidity + totalDebt + deficit + swept - // Note: uses approx equality due to rounding differences between totalOwed (rounds twice) - // and aggregatedOwedRay.fromRayUp() (rounds once) - assertApproxEqAbs( - (totalSuppliedAssets + accruedFees) * WadRayMath.RAY, - asset.liquidity * WadRayMath.RAY + totalDebt * WadRayMath.RAY + asset.deficitRay + asset.swept - * WadRayMath.RAY, - 2 * WadRayMath.RAY, // tolerance of 2 units for rounding - INV_HUB_F - ); - } - - function assert_INV_HUB_GH(address hubAddress, uint256 assetId) internal { - uint256 spokeCount = allSpokes.length; - - // Sum per-spoke values - uint256 totalAddedAssets; - uint256 totalAddedShares; - for (uint256 i; i < spokeCount; i++) { - totalAddedAssets += IHub(hubAddress).getSpokeAddedAssets(assetId, allSpokes[i]); - totalAddedShares += IHub(hubAddress).getSpokeAddedShares(assetId, allSpokes[i]); - } - - // Checks - assertApproxEqAbs(totalAddedAssets, IHub(hubAddress).getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); - assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); - } - - function assert_INV_HUB_I(address hubAddress, uint256 assetId) internal { - // Get underlying from assetId - (address underlying,) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); - - // Query values - uint256 liquidity = IHub(hubAddress).getAssetLiquidity(assetId); - uint256 swept = IHub(hubAddress).getAssetSwept(assetId); - uint256 underlyingBalance = IERC20(underlying).balanceOf(address(IHub(hubAddress))); - - // Checks - assertGe(underlyingBalance + swept, liquidity, INV_HUB_I); + for (uint256 i; i < spokeCount; i++) { + (uint256 d, uint256 p) = IHub(hubAddress).getSpokeOwed(assetId, spokesAddresses[i]); + sumDebt += d + p; } - function assert_INV_HUB_K(address hubAddress, uint256 assetId) internal { - /// @dev TODO for this check to be meaningful, strategy configuration operations have to be integrated - IHub.AssetConfig memory assetConfig = IHub(hubAddress).getAssetConfig(assetId); - - // Checks - assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); - } - - function assert_INV_HUB_L(address hubAddress, uint256 assetId) internal { - (uint256 premiumShares, int256 premiumOffsetRay) = IHub(hubAddress).getAssetPremiumData(assetId); - - assertGe( - int256(IHub(hubAddress).previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), - premiumOffsetRay, - INV_HUB_L - ); + uint256 assetTotal = IHub(hubAddress).getAssetTotalOwed(assetId); // drawn + premium + assertGe(sumDebt, assetTotal, INV_HUB_B); + } + + function assert_INV_HUB_C(address hubAddress, uint256 assetId) internal { + // Sum per-spoke values + uint256 spokeCount = spokesAddresses.length; + + uint256 sumDrawnShares; + uint256 sumPremDrawnShares; + int256 sumPremOffsetRay; + + for (uint256 i; i < spokeCount; i++) { + address spoke = spokesAddresses[i]; + sumDrawnShares += IHub(hubAddress).getSpokeDrawnShares(assetId, spoke); + (uint256 premiumDrawnShares, int256 premiumOffsetRay) = IHub(hubAddress).getSpokePremiumData( + assetId, + spoke + ); + sumPremDrawnShares += premiumDrawnShares; + sumPremOffsetRay += premiumOffsetRay; } - function assert_INV_HUB_O(address hubAddress, uint256 assetId) internal { - uint256 spokeCount = allSpokes.length; - uint256 totalDeficitRay; - for (uint256 i; i < spokeCount; i++) { - totalDeficitRay += IHub(hubAddress).getSpokeDeficitRay(assetId, allSpokes[i]); - } - assertEq(totalDeficitRay, IHub(hubAddress).getAssetDeficitRay(assetId), INV_HUB_O); + // Asset totals + IHub.Asset memory a = IHub(hubAddress).getAsset(assetId); + + // Checks + assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); + assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); + assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); + } + + function assert_INV_HUB_EF(address hubAddress, uint256 assetId) internal { + // Total amounts + uint256 totalSuppliedAssets = IHub(hubAddress).getAddedAssets(assetId); + + IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); + uint256 totalDebt = IHub(hubAddress).getAssetTotalOwed(assetId); + uint256 accruedFees = IHub(hubAddress).getAssetAccruedFees(assetId); + + // Checks //TODO check todo file INV_HUB_E + assertApproxEqAbs( + totalSuppliedAssets, + IHub(hubAddress).previewRemoveByShares(assetId, IHub(hubAddress).getAddedShares(assetId)), + IHub(hubAddress).previewRemoveByShares(assetId, 1), // tolerance of 1 share for rounding + INV_HUB_E + ); + + // totalAddedAssets + fees = liquidity + totalDebt + deficit + swept + // Note: uses approx equality due to rounding differences between totalOwed (rounds twice) + // and aggregatedOwedRay.fromRayUp() (rounds once) + assertApproxEqAbs( + (totalSuppliedAssets + accruedFees) * WadRayMath.RAY, + asset.liquidity * WadRayMath.RAY + + totalDebt * WadRayMath.RAY + + asset.deficitRay + + asset.swept * WadRayMath.RAY, + 2 * WadRayMath.RAY, // tolerance of 2 units for rounding + INV_HUB_F + ); + } + + function assert_INV_HUB_GH(address hubAddress, uint256 assetId) internal { + uint256 spokeCount = allSpokes.length; + + // Sum per-spoke values + uint256 totalAddedAssets; + uint256 totalAddedShares; + for (uint256 i; i < spokeCount; i++) { + totalAddedAssets += IHub(hubAddress).getSpokeAddedAssets(assetId, allSpokes[i]); + totalAddedShares += IHub(hubAddress).getSpokeAddedShares(assetId, allSpokes[i]); } - function assert_INV_HUB_P(address hubAddress, uint256 assetId) internal { - (uint256 premiumShares, int256 premiumOffsetRay) = IHub(hubAddress).getAssetPremiumData(assetId); - uint256 drawnIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); - assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); - } - - function assert_INV_HUB_N(address hubAddress, uint256 assetId) internal { - IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); - - // Skip if no debt (no interest can accrue) - if (asset.drawnShares == 0 && asset.premiumShares == 0) return; - - // Get current index (includes unrealized interest) vs stored index - uint256 currentIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); - uint256 storedIndex = asset.drawnIndex; - - // Skip if no index growth (no interest accrued) - if (currentIndex == storedIndex) return; - - // Calculate accrued interest from index growth - uint256 indexGrowth = currentIndex - storedIndex; - uint256 totalDebtShares = uint256(asset.drawnShares) + uint256(asset.premiumShares); - uint256 accruedInterestRay = (totalDebtShares * indexGrowth); - - // Get unrealized fees - uint256 unrealizedFees = IHub(hubAddress).getAssetAccruedFees(assetId) - asset.realizedFees; - - // Invariant: fees × PERCENTAGE_FACTOR × RAY ≈ accruedInterestRay × liquidityFee - uint256 lhs = unrealizedFees * PercentageMath.PERCENTAGE_FACTOR * WadRayMath.RAY; - uint256 rhs = accruedInterestRay * asset.liquidityFee; - - // Tolerance: 1 wei rounding scaled up - uint256 tolerance = WadRayMath.RAY * PercentageMath.PERCENTAGE_FACTOR; - assertApproxEqAbs(lhs, rhs, tolerance, INV_HUB_N); + // Checks + assertApproxEqAbs( + totalAddedAssets, + IHub(hubAddress).getAddedAssets(assetId), + SPOKE_COUNT, + INV_HUB_G + ); + assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); + } + + function assert_INV_HUB_I(address hubAddress, uint256 assetId) internal { + // Get underlying from assetId + (address underlying, ) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); + + // Query values + uint256 liquidity = IHub(hubAddress).getAssetLiquidity(assetId); + uint256 swept = IHub(hubAddress).getAssetSwept(assetId); + uint256 underlyingBalance = IERC20(underlying).balanceOf(address(IHub(hubAddress))); + + // Checks + assertGe(underlyingBalance + swept, liquidity, INV_HUB_I); + } + + function assert_INV_HUB_K(address hubAddress, uint256 assetId) internal { + /// @dev TODO for this check to be meaningful, strategy configuration operations have to be integrated + IHub.AssetConfig memory assetConfig = IHub(hubAddress).getAssetConfig(assetId); + + // Checks + assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); + } + + function assert_INV_HUB_L(address hubAddress, uint256 assetId) internal { + (uint256 premiumShares, int256 premiumOffsetRay) = IHub(hubAddress).getAssetPremiumData( + assetId + ); + + assertGe( + int256(IHub(hubAddress).previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), + premiumOffsetRay, + INV_HUB_L + ); + } + + function assert_INV_HUB_O(address hubAddress, uint256 assetId) internal { + uint256 spokeCount = allSpokes.length; + uint256 totalDeficitRay; + for (uint256 i; i < spokeCount; i++) { + totalDeficitRay += IHub(hubAddress).getSpokeDeficitRay(assetId, allSpokes[i]); } + assertEq(totalDeficitRay, IHub(hubAddress).getAssetDeficitRay(assetId), INV_HUB_O); + } + + function assert_INV_HUB_P(address hubAddress, uint256 assetId) internal { + (uint256 premiumShares, int256 premiumOffsetRay) = IHub(hubAddress).getAssetPremiumData( + assetId + ); + uint256 drawnIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); + assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); + } + + function assert_INV_HUB_N(address hubAddress, uint256 assetId) internal { + IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); + + // Skip if no debt (no interest can accrue) + if (asset.drawnShares == 0 && asset.premiumShares == 0) return; + + // Get current index (includes unrealized interest) vs stored index + uint256 currentIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); + uint256 storedIndex = asset.drawnIndex; + + // Skip if no index growth (no interest accrued) + if (currentIndex == storedIndex) return; + + // Calculate accrued interest from index growth + uint256 indexGrowth = currentIndex - storedIndex; + uint256 totalDebtShares = uint256(asset.drawnShares) + uint256(asset.premiumShares); + uint256 accruedInterestRay = (totalDebtShares * indexGrowth); + + // Get unrealized fees + uint256 unrealizedFees = IHub(hubAddress).getAssetAccruedFees(assetId) - asset.realizedFees; + + // Invariant: fees × PERCENTAGE_FACTOR × RAY ≈ accruedInterestRay × liquidityFee + uint256 lhs = unrealizedFees * PercentageMath.PERCENTAGE_FACTOR * WadRayMath.RAY; + uint256 rhs = accruedInterestRay * asset.liquidityFee; + + // Tolerance: 1 wei rounding scaled up + uint256 tolerance = WadRayMath.RAY * PercentageMath.PERCENTAGE_FACTOR; + assertApproxEqAbs(lhs, rhs, tolerance, INV_HUB_N); + } } diff --git a/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol index 04855d780..eaa20c934 100644 --- a/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol +++ b/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol @@ -2,96 +2,103 @@ pragma solidity ^0.8.19; // Interfaces -import {ISpokeBase} from "src/spoke/interfaces/ISpokeBase.sol"; -import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; -import {IHub} from "src/hub/interfaces/IHub.sol"; +import {ISpokeBase} from 'src/spoke/interfaces/ISpokeBase.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; // Contracts -import {HandlerAggregator} from "../HandlerAggregator.t.sol"; +import {HandlerAggregator} from '../HandlerAggregator.t.sol'; /// @title SpokeInvariants /// @notice Implements Spoke Invariants for the protocol /// @dev Inherits HandlerAggregator to check actions in assertion testing mode abstract contract SpokeInvariants is HandlerAggregator { - /////////////////////////////////////////////////////////////////////////////////////////////// - // SPOKE // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE // + /////////////////////////////////////////////////////////////////////////////////////////////// - function assert_INV_SP_A(address spoke, uint256 reserveId) internal { - // Get the assetId related to the reserveId of the spoke - uint256 assetId = _getAssetId(spoke, reserveId); + function assert_INV_SP_A(address spoke, uint256 reserveId) internal { + // Get the assetId related to the reserveId of the spoke + uint256 assetId = _getAssetId(spoke, reserveId); - IHub hub = IHub(_getHubAddress(spoke, reserveId)); + IHub hub = IHub(_getHubAddress(spoke, reserveId)); - // supply - assertEq( - ISpokeBase(spoke).getReserveSuppliedShares(reserveId), hub.getSpokeAddedShares(assetId, spoke), INV_SP_A - ); - assertEq( - ISpokeBase(spoke).getReserveSuppliedAssets(reserveId), hub.getSpokeAddedAssets(assetId, spoke), INV_SP_A - ); + // supply + assertEq( + ISpokeBase(spoke).getReserveSuppliedShares(reserveId), + hub.getSpokeAddedShares(assetId, spoke), + INV_SP_A + ); + assertEq( + ISpokeBase(spoke).getReserveSuppliedAssets(reserveId), + hub.getSpokeAddedAssets(assetId, spoke), + INV_SP_A + ); - // debt - (uint256 d1, uint256 p1) = hub.getSpokeOwed(assetId, spoke); - (uint256 d2, uint256 p2) = ISpokeBase(spoke).getReserveDebt(reserveId); - assertEq(d2, d1, INV_SP_A); - assertEq(p2, p1, INV_SP_A); - } + // debt + (uint256 d1, uint256 p1) = hub.getSpokeOwed(assetId, spoke); + (uint256 d2, uint256 p2) = ISpokeBase(spoke).getReserveDebt(reserveId); + assertEq(d2, d1, INV_SP_A); + assertEq(p2, p1, INV_SP_A); + } - function assert_INV_SP_B(address spoke, uint256 reserveId, address user) internal { - // reserve supply - if (ISpokeBase(spoke).getReserveSuppliedAssets(reserveId) > 0) { - assertGt(ISpokeBase(spoke).getReserveSuppliedShares(reserveId), 0, INV_SP_B); - } - // reserve debt - if (ISpokeBase(spoke).getReserveTotalDebt(reserveId) > 0) { - assertGt( - IHub(_getHubAddress(spoke, reserveId)).getSpokeDrawnShares(_getAssetId(spoke, reserveId), spoke), - 0, - INV_SP_B - ); - } - // user supply - if (ISpokeBase(spoke).getUserSuppliedAssets(reserveId, user) > 0) { - assertGt(ISpokeBase(spoke).getUserSuppliedShares(reserveId, user), 0, INV_SP_B); - } - // user debt - if (ISpokeBase(spoke).getUserTotalDebt(reserveId, user) > 0) { - ISpoke.UserPosition memory up = ISpoke(spoke).getUserPosition(reserveId, user); - assertTrue(up.drawnShares > 0 || up.premiumShares > 0, INV_SP_B); - } + function assert_INV_SP_B(address spoke, uint256 reserveId, address user) internal { + // reserve supply + if (ISpokeBase(spoke).getReserveSuppliedAssets(reserveId) > 0) { + assertGt(ISpokeBase(spoke).getReserveSuppliedShares(reserveId), 0, INV_SP_B); + } + // reserve debt + if (ISpokeBase(spoke).getReserveTotalDebt(reserveId) > 0) { + assertGt( + IHub(_getHubAddress(spoke, reserveId)).getSpokeDrawnShares( + _getAssetId(spoke, reserveId), + spoke + ), + 0, + INV_SP_B + ); + } + // user supply + if (ISpokeBase(spoke).getUserSuppliedAssets(reserveId, user) > 0) { + assertGt(ISpokeBase(spoke).getUserSuppliedShares(reserveId, user), 0, INV_SP_B); + } + // user debt + if (ISpokeBase(spoke).getUserTotalDebt(reserveId, user) > 0) { + ISpoke.UserPosition memory up = ISpoke(spoke).getUserPosition(reserveId, user); + assertTrue(up.drawnShares > 0 || up.premiumShares > 0, INV_SP_B); } + } - function assert_INV_SP_C(address spoke, uint256 reserveId) internal { - uint256 sumSpokeDebts; - for (uint256 i; i < actorAddresses.length; i++) { - sumSpokeDebts += ISpokeBase(spoke).getUserTotalDebt(reserveId, actorAddresses[i]); - } - assertGe(sumSpokeDebts, ISpokeBase(spoke).getReserveTotalDebt(reserveId), INV_SP_C); + function assert_INV_SP_C(address spoke, uint256 reserveId) internal { + uint256 sumSpokeDebts; + for (uint256 i; i < actorAddresses.length; i++) { + sumSpokeDebts += ISpokeBase(spoke).getUserTotalDebt(reserveId, actorAddresses[i]); } + assertGe(sumSpokeDebts, ISpokeBase(spoke).getReserveTotalDebt(reserveId), INV_SP_C); + } - function assert_INV_SP_D(address spoke, address user) internal { - ISpoke.UserAccountData memory d = ISpoke(spoke).getUserAccountData(user); - if (d.totalCollateralValue == 0) { - assertEq(d.totalDebtValue, 0, INV_SP_D); - } + function assert_INV_SP_D(address spoke, address user) internal { + ISpoke.UserAccountData memory d = ISpoke(spoke).getUserAccountData(user); + if (d.totalCollateralValue == 0) { + assertEq(d.totalDebtValue, 0, INV_SP_D); } + } - function assert_INV_SP_E(address spoke, uint256 reserveId) internal { - uint256 sumUserShares; - for (uint256 i; i < actorAddresses.length; i++) { - sumUserShares += ISpokeBase(spoke).getUserSuppliedShares(reserveId, actorAddresses[i]); - } - assertEq(sumUserShares, ISpokeBase(spoke).getReserveSuppliedShares(reserveId), INV_SP_E); + function assert_INV_SP_E(address spoke, uint256 reserveId) internal { + uint256 sumUserShares; + for (uint256 i; i < actorAddresses.length; i++) { + sumUserShares += ISpokeBase(spoke).getUserSuppliedShares(reserveId, actorAddresses[i]); } + assertEq(sumUserShares, ISpokeBase(spoke).getReserveSuppliedShares(reserveId), INV_SP_E); + } - function assert_INV_SP_F(address spoke, uint256 reserveId) internal { - uint256 sumUserAssets; - for (uint256 i; i < actorAddresses.length; i++) { - sumUserAssets += ISpokeBase(spoke).getUserSuppliedAssets(reserveId, actorAddresses[i]); - } - uint256 reserveSuppliedAssets = ISpokeBase(spoke).getReserveSuppliedAssets(reserveId); - assertLe(sumUserAssets, reserveSuppliedAssets, INV_SP_F); - assertApproxEqAbs(sumUserAssets, reserveSuppliedAssets, NUMBER_OF_ACTORS, INV_SP_F); + function assert_INV_SP_F(address spoke, uint256 reserveId) internal { + uint256 sumUserAssets; + for (uint256 i; i < actorAddresses.length; i++) { + sumUserAssets += ISpokeBase(spoke).getUserSuppliedAssets(reserveId, actorAddresses[i]); } + uint256 reserveSuppliedAssets = ISpokeBase(spoke).getReserveSuppliedAssets(reserveId); + assertLe(sumUserAssets, reserveSuppliedAssets, INV_SP_F); + assertApproxEqAbs(sumUserAssets, reserveSuppliedAssets, NUMBER_OF_ACTORS, INV_SP_F); + } } diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_1.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_1.t.sol index 3ef1d025a..d92ab34f6 100644 --- a/tests/invariants/protocol-suite/replays/ReplayTest_1.t.sol +++ b/tests/invariants/protocol-suite/replays/ReplayTest_1.t.sol @@ -2,95 +2,95 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest1 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest1 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_1_supply() public { - _setUpActor(USER3); - _delay(140400); - Tester.supply(7, 242, 154, 0); - _delay(543845); - Tester.setUsingAsCollateral(true, 74, 212); - _delay(527372); - Tester.borrow(2, 218, 0, 0); - _delay(116349); - Tester.supply(8, 128, 2, 0); - } - - function test_replay_1_repay() public { - _setUpActor(USER3); - _delay(140400); - Tester.supply(10388, 95, 174, 0); - _delay(543845); - Tester.setUsingAsCollateral(true, 0, 52); - _delay(527372); - Tester.borrow(8603, 248, 142, 0); - _delay(243334); - Tester.repay(1, 116, 254, 252); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest1 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_1_supply() public { + _setUpActor(USER3); + _delay(140400); + Tester.supply(7, 242, 154, 0); + _delay(543845); + Tester.setUsingAsCollateral(true, 74, 212); + _delay(527372); + Tester.borrow(2, 218, 0, 0); + _delay(116349); + Tester.supply(8, 128, 2, 0); + } + + function test_replay_1_repay() public { + _setUpActor(USER3); + _delay(140400); + Tester.supply(10388, 95, 174, 0); + _delay(543845); + Tester.setUsingAsCollateral(true, 0, 52); + _delay(527372); + Tester.borrow(8603, 248, 142, 0); + _delay(243334); + Tester.repay(1, 116, 254, 252); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_2.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_2.t.sol index e4347bc11..4b4def943 100644 --- a/tests/invariants/protocol-suite/replays/ReplayTest_2.t.sol +++ b/tests/invariants/protocol-suite/replays/ReplayTest_2.t.sol @@ -2,86 +2,86 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest2 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest2 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_2_INV_HUB_B() public { - _setUpActor(USER3); - _delay(140400); - Tester.supply(16, 176, 48, 0); - _delay(543845); - Tester.setUsingAsCollateral(true, 196, 188); - _delay(527372); - Tester.borrow(1, 80, 98, 60); - _delay(284444); - Tester.updateUserRiskPremium(98); - _delay(52383); - Tester.updateUserRiskPremium(102); - invariant_INV_HUB(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest2 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_2_INV_HUB_B() public { + _setUpActor(USER3); + _delay(140400); + Tester.supply(16, 176, 48, 0); + _delay(543845); + Tester.setUsingAsCollateral(true, 196, 188); + _delay(527372); + Tester.borrow(1, 80, 98, 60); + _delay(284444); + Tester.updateUserRiskPremium(98); + _delay(52383); + Tester.updateUserRiskPremium(102); + invariant_INV_HUB(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_3.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_3.t.sol index dd81dd5fe..4718fff90 100644 --- a/tests/invariants/protocol-suite/replays/ReplayTest_3.t.sol +++ b/tests/invariants/protocol-suite/replays/ReplayTest_3.t.sol @@ -2,82 +2,82 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest3 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest3 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_3_setUsingAsCollateral() public { - _setUpActor(USER3); - Tester.supply(790, 128, 71, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 203, 9); - _setUpActor(USER1); - _delay(689004); - Tester.setUsingAsCollateral(false, 15, 13); - invariant_INV_HUB(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest3 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_3_setUsingAsCollateral() public { + _setUpActor(USER3); + Tester.supply(790, 128, 71, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 203, 9); + _setUpActor(USER1); + _delay(689004); + Tester.setUsingAsCollateral(false, 15, 13); + invariant_INV_HUB(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_4.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_4.t.sol index f8cce7643..792cfd6df 100644 --- a/tests/invariants/protocol-suite/replays/ReplayTest_4.t.sol +++ b/tests/invariants/protocol-suite/replays/ReplayTest_4.t.sol @@ -2,81 +2,81 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest4 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest4 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_4_supply() public { - _setUpActor(USER3); - Tester.supply(790, 128, 71, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 95, 9); - _setUpActor(USER1); - _delay(434894); - Tester.supply(423, 66, 149, 45); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest4 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_4_supply() public { + _setUpActor(USER3); + Tester.supply(790, 128, 71, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 95, 9); + _setUpActor(USER1); + _delay(434894); + Tester.supply(423, 66, 149, 45); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_5.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_5.t.sol index 3d742caca..94fb3e87b 100644 --- a/tests/invariants/protocol-suite/replays/ReplayTest_5.t.sol +++ b/tests/invariants/protocol-suite/replays/ReplayTest_5.t.sol @@ -2,71 +2,71 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest5 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest5 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest5 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_6.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_6.t.sol index 123264c2f..1be745481 100644 --- a/tests/invariants/protocol-suite/replays/ReplayTest_6.t.sol +++ b/tests/invariants/protocol-suite/replays/ReplayTest_6.t.sol @@ -2,93 +2,93 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest6 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest6 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_6_freezeAllReserves() public { - _setUpActor(USER3); - Tester.supply(673, 128, 1, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 203, 9); - _setUpActor(USER1); - _delay(338347); - Tester.freezeAllReserves(32); - invariant_INV_HUB(); - } - - function test_replay_6_supply() public { - _setUpActor(USER3); - Tester.supply(790, 197, 87, 201); - Tester.setUsingAsCollateral(true, 197, 253); - Tester.borrow(527, 68, 65, 9); - _setUpActor(USER1); - _delay(467); - Tester.supply(1327428228, 3, 151, 97); - invariant_INV_SP(); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest6 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_6_freezeAllReserves() public { + _setUpActor(USER3); + Tester.supply(673, 128, 1, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 203, 9); + _setUpActor(USER1); + _delay(338347); + Tester.freezeAllReserves(32); + invariant_INV_HUB(); + } + + function test_replay_6_supply() public { + _setUpActor(USER3); + Tester.supply(790, 197, 87, 201); + Tester.setUsingAsCollateral(true, 197, 253); + Tester.borrow(527, 68, 65, 9); + _setUpActor(USER1); + _delay(467); + Tester.supply(1327428228, 3, 151, 97); + invariant_INV_SP(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol b/tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol index 8993c0ca3..35acda80c 100644 --- a/tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol +++ b/tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol @@ -2,138 +2,146 @@ pragma solidity ^0.8.0; // Libraries -import "forge-std/Test.sol"; -import "forge-std/console.sol"; +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; // Contracts -import {Invariants} from "../Invariants.t.sol"; -import {Setup} from "../Setup.t.sol"; +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; // Utils -import {Actor} from "../../shared/utils/Actor.sol"; +import {Actor} from '../../shared/utils/Actor.sol'; contract ReplayTest7 is Invariants, Setup { - // Generated from Echidna reproducers - - // Target contract instance (you may need to adjust this) - ReplayTest7 Tester = this; - - modifier setup() override { - _; - } - - function setUp() public { - // Deploy protocol contracts - _setUp(); - - /// @dev fixes the actor to the first user - actor = actors[USER1]; - - vm.warp(101007); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_7_updateUserRiskPremium() public { - _setUpActor(USER3); - Tester.supply(790, 128, 71, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 203, 9); - _setUpActor(USER1); - _delay(1812425); - _delay(320876); - Tester.updateFrozen(false, 59, 85); - _setUpActor(USER2); - _delay(343393); - Tester.updateHealthFactorForMaxBonus(105, 146); - _setUpActor(USER1); - _delay(2272303); - _setUpActor(USER2); - _delay(62189); - Tester.updateUserRiskPremium(101); - _setUpActor(USER3); - _delay(590687); - Tester.setUsingAsCollateral(true, 111, 253); - _delay(323286); - Tester.donateUnderlyingToHub(194, 231, 231); - _delay(505810); - Tester.borrow(56, 77, 155, 21); - _setUpActor(USER2); - _delay(112744); - Tester.updateBorrowable(false, 223, 157); - _setUpActor(USER1); - _delay(1632525); - _setUpActor(USER2); - _delay(128066); - Tester.updateFrozen(false, 52, 208); - _delay(180364); - Tester.setPrice(44015031536544474472307730349212877645270025151054012889336165188860863984788, 87); - _setUpActor(USER3); - _delay(460111); - Tester.updateBorrowable(true, 160, 11); - _setUpActor(USER1); - _delay(898801); - _setUpActor(USER3); - _delay(271962); - Tester.pauseAllReserves(251); - _setUpActor(USER2); - _delay(928317); - _setUpActor(USER1); - _delay(23908); - Tester.updateBorrowable(true, 217, 53); - _setUpActor(USER3); - _delay(582766); - Tester.updateUserRiskPremium(25); - } - - function test_replay_7_repay() public { - _setUpActor(USER3); - _delay(321376); - Tester.supply(790, 197, 87, 201); - Tester.supply(100000000000000000000000002, 50, 228, 214); - Tester.setUsingAsCollateral(true, 197, 253); - _delay(20833); - Tester.borrow(2973933138, 65, 107, 14); - _setUpActor(USER1); - Tester.updateSpokeSupplyCap(172, 180, 253, 253); - _setUpActor(USER3); - _delay(997); - Tester.repay(1209722426464509529070304541882533570806727645123886685504166337177518312, 62, 253, 254); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HELPERS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Fast forward the time and set up an actor, - /// @dev Use for ECHIDNA call-traces - function _delay(uint256 _seconds) internal { - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up an actor - function _setUpActor(address _origin) internal { - actor = actors[_origin]; - } - - /// @notice Set up an actor and fast forward the time - /// @dev Use for ECHIDNA call-traces - function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; - vm.warp(block.timestamp + _seconds); - } - - /// @notice Set up a specific block and actor - function _setUpBlockAndActor(uint256 _block, address _user) internal { - vm.roll(_block); - actor = actors[_user]; - } - - /// @notice Set up a specific timestamp and actor - function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { - vm.warp(_timestamp); - actor = actors[_user]; - } + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest7 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_7_updateUserRiskPremium() public { + _setUpActor(USER3); + Tester.supply(790, 128, 71, 201); + Tester.setUsingAsCollateral(true, 111, 253); + Tester.borrow(527, 68, 203, 9); + _setUpActor(USER1); + _delay(1812425); + _delay(320876); + Tester.updateFrozen(false, 59, 85); + _setUpActor(USER2); + _delay(343393); + Tester.updateHealthFactorForMaxBonus(105, 146); + _setUpActor(USER1); + _delay(2272303); + _setUpActor(USER2); + _delay(62189); + Tester.updateUserRiskPremium(101); + _setUpActor(USER3); + _delay(590687); + Tester.setUsingAsCollateral(true, 111, 253); + _delay(323286); + Tester.donateUnderlyingToHub(194, 231, 231); + _delay(505810); + Tester.borrow(56, 77, 155, 21); + _setUpActor(USER2); + _delay(112744); + Tester.updateBorrowable(false, 223, 157); + _setUpActor(USER1); + _delay(1632525); + _setUpActor(USER2); + _delay(128066); + Tester.updateFrozen(false, 52, 208); + _delay(180364); + Tester.setPrice( + 44015031536544474472307730349212877645270025151054012889336165188860863984788, + 87 + ); + _setUpActor(USER3); + _delay(460111); + Tester.updateBorrowable(true, 160, 11); + _setUpActor(USER1); + _delay(898801); + _setUpActor(USER3); + _delay(271962); + Tester.pauseAllReserves(251); + _setUpActor(USER2); + _delay(928317); + _setUpActor(USER1); + _delay(23908); + Tester.updateBorrowable(true, 217, 53); + _setUpActor(USER3); + _delay(582766); + Tester.updateUserRiskPremium(25); + } + + function test_replay_7_repay() public { + _setUpActor(USER3); + _delay(321376); + Tester.supply(790, 197, 87, 201); + Tester.supply(100000000000000000000000002, 50, 228, 214); + Tester.setUsingAsCollateral(true, 197, 253); + _delay(20833); + Tester.borrow(2973933138, 65, 107, 14); + _setUpActor(USER1); + Tester.updateSpokeSupplyCap(172, 180, 253, 253); + _setUpActor(USER3); + _delay(997); + Tester.repay( + 1209722426464509529070304541882533570806727645123886685504166337177518312, + 62, + 253, + 254 + ); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } } diff --git a/tests/invariants/protocol-suite/specs/InvariantsSpec.t.sol b/tests/invariants/protocol-suite/specs/InvariantsSpec.t.sol index 51922ac89..e6147de6c 100644 --- a/tests/invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/tests/invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.19; /// @notice Invariants specification for the protocol /// @dev Contains pseudo code and description for the invariant properties in the protocol abstract contract InvariantsSpec { - // TODO: check invariant overlap - /*///////////////////////////////////////////////////////////////////////////////////////////// + // TODO: check invariant overlap + /*///////////////////////////////////////////////////////////////////////////////////////////// // PROPERTY TYPES // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -16,63 +16,66 @@ abstract contract InvariantsSpec { /////////////////////////////////////////////////////////////////////////////////////////////*/ - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// - string constant INV_HUB_A2 = "INV_HUB_A2: If hub assets = 0 => shares 0"; + string constant INV_HUB_A2 = 'INV_HUB_A2: If hub assets = 0 => shares 0'; - string constant INV_HUB_B = - "INV_HUB_B: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset"; + string constant INV_HUB_B = + 'INV_HUB_B: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset'; - string constant INV_HUB_C = - "INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub"; + string constant INV_HUB_C = + 'INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub'; - string constant INV_HUB_E = - "INV_HUB_E: hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss"; + string constant INV_HUB_E = + 'INV_HUB_E: hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss'; - string constant INV_HUB_F = - "INV_HUB_F: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + totalDebt + deficit + swept"; + string constant INV_HUB_F = + 'INV_HUB_F: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + totalDebt + deficit + swept'; - string constant INV_HUB_G = - "INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT";// TODO check if tolerance is correct + string constant INV_HUB_G = + 'INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT'; // TODO check if tolerance is correct - string constant INV_HUB_H = "INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes"; + string constant INV_HUB_H = + 'INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes'; - string constant INV_HUB_I = "INV_HUB_I: asset.underlying.balanceOf(hub) + asset.swept >= asset.liquidity"; + string constant INV_HUB_I = + 'INV_HUB_I: asset.underlying.balanceOf(hub) + asset.swept >= asset.liquidity'; - string constant INV_HUB_K = - "INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset"; + string constant INV_HUB_K = + 'INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset'; - string constant INV_HUB_L = - "INV_HUB_L: Asset.premiumShares.toDrawnAssetsUp() * WadRayMath.RAY >= Asset.premiumOffsetRay * WadRayMath.RAY"; + string constant INV_HUB_L = + 'INV_HUB_L: Asset.premiumShares.toDrawnAssetsUp() * WadRayMath.RAY >= Asset.premiumOffsetRay * WadRayMath.RAY'; - string constant INV_HUB_N = - "INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares"; + string constant INV_HUB_N = + 'INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares'; - string constant INV_HUB_O = - "INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay"; + string constant INV_HUB_O = + 'INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay'; - string constant INV_HUB_P = "INV_HUB_P: Premium offset should not exceed premium shares * drawnIndex"; + string constant INV_HUB_P = + 'INV_HUB_P: Premium offset should not exceed premium shares * drawnIndex'; - /////////////////////////////////////////////////////////////////////////////////////////////// - // SPOKE // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE // + /////////////////////////////////////////////////////////////////////////////////////////////// - string constant INV_SP_A = - "INV_SP_A: Spoke reserve accounting should always match hub's spoke accounting on the corresponding registered asset."; + string constant INV_SP_A = + "INV_SP_A: Spoke reserve accounting should always match hub's spoke accounting on the corresponding registered asset."; - string constant INV_SP_B = - "INV_SP_B: User/Reserve cannot have non-zero assets and zero shares in supply or debt sides."; + string constant INV_SP_B = + 'INV_SP_B: User/Reserve cannot have non-zero assets and zero shares in supply or debt sides.'; - string constant INV_SP_C = - "INV_SP_C: Sum of spoke debts on a single asset must be greater or equal than the total debt of the reserve"; + string constant INV_SP_C = + 'INV_SP_C: Sum of spoke debts on a single asset must be greater or equal than the total debt of the reserve'; - string constant INV_SP_D = "INV_SP_D: Users without collateral also have no debt."; + string constant INV_SP_D = 'INV_SP_D: Users without collateral also have no debt.'; - string constant INV_SP_E = - "INV_SP_E: Sum of user supplied shares on a spoke for a given asset == spoke supplied shares (hub spoke added shares)"; + string constant INV_SP_E = + 'INV_SP_E: Sum of user supplied shares on a spoke for a given asset == spoke supplied shares (hub spoke added shares)'; - string constant INV_SP_F = - "INV_SP_F: Sum of user supplied assets on a spoke for a given asset == spoke supplied assets (hub spoke added assets)"; + string constant INV_SP_F = + 'INV_SP_F: Sum of user supplied assets on a spoke for a given asset == spoke supplied assets (hub spoke added assets)'; } diff --git a/tests/invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/tests/invariants/protocol-suite/specs/PostconditionsSpec.t.sol index 377f3bb3e..3e0613d84 100644 --- a/tests/invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/tests/invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.19; /// @notice Postcoditions specification for the protocol /// @dev Contains pseudo code and description for the postcondition properties in the protocol abstract contract PostconditionsSpec { - /*///////////////////////////////////////////////////////////////////////////////////////////// + /*///////////////////////////////////////////////////////////////////////////////////////////// // PROPERTY TYPES // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -22,87 +22,88 @@ abstract contract PostconditionsSpec { /////////////////////////////////////////////////////////////////////////////////////////////*/ - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // - ///////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + ///////////////////////////////////////////////////////////////////////////////////////////////// - string constant GPOST_HUB_A = - "GPOST_HUB_A: Drawn index cannot decrease (remains constant or increases). If no time passes, it stays constant. only increases due to interest accumulation"; + string constant GPOST_HUB_A = + 'GPOST_HUB_A: Drawn index cannot decrease (remains constant or increases). If no time passes, it stays constant. only increases due to interest accumulation'; - string constant GPOST_HUB_B = - "GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding)."; + string constant GPOST_HUB_B = + "GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding)."; - string constant GPOST_HUB_C = - "GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block."; + string constant GPOST_HUB_C = + 'GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block.'; - string constant GPOST_HUB_D = - "GPOST_HUB_D: lastUpdateTimestamp must be <= block.timestamp after any action (timestamps cannot be in the future)."; + string constant GPOST_HUB_D = + 'GPOST_HUB_D: lastUpdateTimestamp must be <= block.timestamp after any action (timestamps cannot be in the future).'; - string constant GPOST_HUB_E = - "GPOST_HUB_E: if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)"; + string constant GPOST_HUB_E = + 'GPOST_HUB_E: if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)'; - string constant GPOST_HUB_F = - "GPOST_HUB_F: if owed for a spoke & assetId increase, owed <= drawCap * precision (when cap != MAX)"; // TODO-ok take into account the deficit, use Owed instead of drawn -> review fix + string constant GPOST_HUB_F = + 'GPOST_HUB_F: if owed for a spoke & assetId increase, owed <= drawCap * precision (when cap != MAX)'; // TODO-ok take into account the deficit, use Owed instead of drawn -> review fix - string constant GPOST_HUB_G = - "GPOST_HUB_G: lastUpdateTimestamp is monotonic non-decreasing across actions (time does not go backwards)"; + string constant GPOST_HUB_G = + 'GPOST_HUB_G: lastUpdateTimestamp is monotonic non-decreasing across actions (time does not go backwards)'; - string constant HSPOST_HUB_M = - "HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution)"; + string constant HSPOST_HUB_M = + 'HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution)'; - /////////////////////////////////////////////////////////////////////////////////////////////// - // SPOKE // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE // + /////////////////////////////////////////////////////////////////////////////////////////////// - string constant GPOST_SP_A = - "GPOST_SP_A: Stored (user.premiumDrawnShares/user.baseDrawnShares) & calculated user risk premium (calculation based on user's position, via spoke.calculateUserAccountData) need to be the same right after an operation"; + string constant GPOST_SP_A = + "GPOST_SP_A: Stored (user.premiumDrawnShares/user.baseDrawnShares) & calculated user risk premium (calculation based on user's position, via spoke.calculateUserAccountData) need to be the same right after an operation"; - string constant GPOST_SP_B = - "GPOST_SP_B: Premium debt of an individual user can only decrease by calling repay or liquidationCall when premium debt is not zero"; + string constant GPOST_SP_B = + 'GPOST_SP_B: Premium debt of an individual user can only decrease by calling repay or liquidationCall when premium debt is not zero'; - string constant GPOST_SP_B2 = - "GPOST_SP_B2: Drawn debt of an individual user can only decrease by calling repay or liquidationCall and if premium debt is zero after the action"; + string constant GPOST_SP_B2 = + 'GPOST_SP_B2: Drawn debt of an individual user can only decrease by calling repay or liquidationCall and if premium debt is zero after the action'; - string constant HSPOST_SP_C = "HSPOST_SP_C: User liability should decrease after repayment"; + string constant HSPOST_SP_C = 'HSPOST_SP_C: User liability should decrease after repayment'; - string constant HSPOST_SP_D = "HSPOST_SP_D: Unhealthy users cannot borrow more"; + string constant HSPOST_SP_D = 'HSPOST_SP_D: Unhealthy users cannot borrow more'; - string constant GPOST_SP_E = - "GPOST_SP_E: DynamicRiskConfiguration for a user position is updated to latest reserve state whenever an action can potentially make their position less healthy"; - // - Updates on: borrow, withdraw, disableAsCollateral. - // - Unchanged on: supply, repay, liquidate, updateUserRiskPremium, setUserPositionManager. - // - Enabling collateral updates only the relevant reserve's dynamic config. - // - Exception: updateUserDynamicConfig explicitly refreshes the user's dynamic config. + string constant GPOST_SP_E = + 'GPOST_SP_E: DynamicRiskConfiguration for a user position is updated to latest reserve state whenever an action can potentially make their position less healthy'; + // - Updates on: borrow, withdraw, disableAsCollateral. + // - Unchanged on: supply, repay, liquidate, updateUserRiskPremium, setUserPositionManager. + // - Enabling collateral updates only the relevant reserve's dynamic config. + // - Exception: updateUserDynamicConfig explicitly refreshes the user's dynamic config. - string constant HSPOST_SP_F = "HSPOST_SP_F: Total debt of a user should not change after updateUserRiskPremium"; + string constant HSPOST_SP_F = + 'HSPOST_SP_F: Total debt of a user should not change after updateUserRiskPremium'; - string constant GPOST_SP_H = - "GPOST_SP_H: if user totalDebt == 0 and withdraw is called, user can withdraw all supplied"; + string constant GPOST_SP_H = + 'GPOST_SP_H: if user totalDebt == 0 and withdraw is called, user can withdraw all supplied'; - /////////////////////////////////////////////////////////////////////////////////////////////// - // SPOKE: LIQUIDATION // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE: LIQUIDATION // + /////////////////////////////////////////////////////////////////////////////////////////////// - string constant HSPOST_SP_LIQ_A = - "HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position"; + string constant HSPOST_SP_LIQ_A = + "HSPOST_SP_LIQ_A: Liquidation cannot result in an amount of liquidated debt > user's total debt position"; - string constant HSPOST_SP_LIQ_B = - "HSPOST_SP_LIQ_B: Liquidation cannot result in an amount of seized collateral (sold collateral + liquidation bonus) > user's collateral position"; + string constant HSPOST_SP_LIQ_B = + "HSPOST_SP_LIQ_B: Liquidation cannot result in an amount of seized collateral (sold collateral + liquidation bonus) > user's collateral position"; - string constant HSPOST_SP_LIQ_C = - "HSPOST_SP_LIQ_C: Liquidator is always forced to repay all the debt of a user if debt value is below DUST_DEBT_LIQUIDATION_THRESHOLD"; + string constant HSPOST_SP_LIQ_C = + 'HSPOST_SP_LIQ_C: Liquidator is always forced to repay all the debt of a user if debt value is below DUST_DEBT_LIQUIDATION_THRESHOLD'; - string constant HSPOST_SP_LIQ_D = - "HSPOST_SP_LIQ_D: Liquidation cannot result in an amount of liquidated debt > debtToCover"; + string constant HSPOST_SP_LIQ_D = + 'HSPOST_SP_LIQ_D: Liquidation cannot result in an amount of liquidated debt > debtToCover'; - string constant HSPOST_SP_LIQ_E = "HSPOST_SP_LIQ_E: Only unhealthy users can be liquidated"; + string constant HSPOST_SP_LIQ_E = 'HSPOST_SP_LIQ_E: Only unhealthy users can be liquidated'; - string constant HSPOST_SP_LIQ_G = - "HSPOST_SP_LIQ_G: After liquidation, if debt remains, health factor should improve toward target"; + string constant HSPOST_SP_LIQ_G = + 'HSPOST_SP_LIQ_G: After liquidation, if debt remains, health factor should improve toward target'; - string constant GPOST_SP_LIQ_G = - "GPOST_SP_LIQ_G: Only liquidations can deteriorate the health factor of an already unhealthy account"; + string constant GPOST_SP_LIQ_G = + 'GPOST_SP_LIQ_G: Only liquidations can deteriorate the health factor of an already unhealthy account'; - string constant GPOST_SP_LIQ_H = - "GPOST_SP_LIQ_H: Only a supply, repay & liquidationCall can leave an account in an unhealthy state"; + string constant GPOST_SP_LIQ_H = + 'GPOST_SP_LIQ_H: Only a supply, repay & liquidationCall can leave an account in an unhealthy state'; } diff --git a/tests/invariants/shared/mocks/MockPriceFeedSimulator.sol b/tests/invariants/shared/mocks/MockPriceFeedSimulator.sol index be1352577..10dda6073 100644 --- a/tests/invariants/shared/mocks/MockPriceFeedSimulator.sol +++ b/tests/invariants/shared/mocks/MockPriceFeedSimulator.sol @@ -2,45 +2,53 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import {AggregatorV3Interface} from "src/dependencies/chainlink/AggregatorV3Interface.sol"; +import {AggregatorV3Interface} from 'src/dependencies/chainlink/AggregatorV3Interface.sol'; contract MockPriceFeedSimulator is AggregatorV3Interface { - uint8 public immutable override decimals; - string public override description; - - int256 private _price; - - error OperationNotSupported(); - - constructor(uint8 decimals_, string memory description_, uint256 price_) { - decimals = decimals_; - description = description_; - _price = int256(price_); - } - - function version() external pure override returns (uint256) { - return 1; - } - - function getRoundData(uint80) external pure override returns (uint80, int256, uint256, uint256, uint80) { - revert OperationNotSupported(); - } - - function latestRoundData() - external - view - virtual - override - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - roundId = uint80(block.timestamp); - answer = _price; - startedAt = block.timestamp; - updatedAt = block.timestamp; - answeredInRound = roundId; - } - - function setPrice(int256 price) external { - _price = price; - } + uint8 public immutable override decimals; + string public override description; + + int256 private _price; + + error OperationNotSupported(); + + constructor(uint8 decimals_, string memory description_, uint256 price_) { + decimals = decimals_; + description = description_; + _price = int256(price_); + } + + function version() external pure override returns (uint256) { + return 1; + } + + function getRoundData( + uint80 + ) external pure override returns (uint80, int256, uint256, uint256, uint80) { + revert OperationNotSupported(); + } + + function latestRoundData() + external + view + virtual + override + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + roundId = uint80(block.timestamp); + answer = _price; + startedAt = block.timestamp; + updatedAt = block.timestamp; + answeredInRound = roundId; + } + + function setPrice(int256 price) external { + _price = price; + } } diff --git a/tests/invariants/shared/utils/Actor.sol b/tests/invariants/shared/utils/Actor.sol index 5b24cb8bf..4b5bdbc47 100644 --- a/tests/invariants/shared/utils/Actor.sol +++ b/tests/invariants/shared/utils/Actor.sol @@ -2,59 +2,63 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; /// @notice Proxy contract for invariant suite actors to avoid aTester calling contracts contract Actor { - /// @notice list of tokens to approve - address[] internal tokens; - /// @notice list of contracts to approve tokens to - address[] internal contracts; - - constructor(address[] memory _tokens, address[] memory _contracts) payable { - tokens = _tokens; - contracts = _contracts; - for (uint256 i = 0; i < tokens.length; i++) { - for (uint256 j = 0; j < contracts.length; j++) { - IERC20(tokens[i]).approve(contracts[j], type(uint256).max); - } - } + /// @notice list of tokens to approve + address[] internal tokens; + /// @notice list of contracts to approve tokens to + address[] internal contracts; + + constructor(address[] memory _tokens, address[] memory _contracts) payable { + tokens = _tokens; + contracts = _contracts; + for (uint256 i = 0; i < tokens.length; i++) { + for (uint256 j = 0; j < contracts.length; j++) { + IERC20(tokens[i]).approve(contracts[j], type(uint256).max); + } } + } - /// @notice Helper function to proxy a call to a target contract, used to avoid Tester calling contracts - function proxy(address _target, bytes memory _calldata) public returns (bool success, bytes memory returnData) { - (success, returnData) = address(_target).call(_calldata); + /// @notice Helper function to proxy a call to a target contract, used to avoid Tester calling contracts + function proxy( + address _target, + bytes memory _calldata + ) public returns (bool success, bytes memory returnData) { + (success, returnData) = address(_target).call(_calldata); - handleAssertionError(success, returnData); - } + handleAssertionError(success, returnData); + } - /// @notice Helper function to proxy a call and value to a target contract, used to avoid Tester calling contracts - function proxy(address _target, bytes memory _calldata, uint256 value) - public - returns (bool success, bytes memory returnData) - { - (success, returnData) = address(_target).call{value: value}(_calldata); + /// @notice Helper function to proxy a call and value to a target contract, used to avoid Tester calling contracts + function proxy( + address _target, + bytes memory _calldata, + uint256 value + ) public returns (bool success, bytes memory returnData) { + (success, returnData) = address(_target).call{value: value}(_calldata); - handleAssertionError(success, returnData); - } + handleAssertionError(success, returnData); + } + + /// @notice Checks if a call failed due to an assertion error and propagates the error if found. + /// @param success Indicates whether the call was successful. + /// @param returnData The data returned from the call. + function handleAssertionError(bool success, bytes memory returnData) internal pure { + if (!success && returnData.length == 36) { + bytes4 selector; + uint256 code; + assembly { + selector := mload(add(returnData, 0x20)) + code := mload(add(returnData, 0x24)) + } - /// @notice Checks if a call failed due to an assertion error and propagates the error if found. - /// @param success Indicates whether the call was successful. - /// @param returnData The data returned from the call. - function handleAssertionError(bool success, bytes memory returnData) internal pure { - if (!success && returnData.length == 36) { - bytes4 selector; - uint256 code; - assembly { - selector := mload(add(returnData, 0x20)) - code := mload(add(returnData, 0x24)) - } - - if (selector == bytes4(0x4e487b71) && code == 1) { - assert(false); - } - } + if (selector == bytes4(0x4e487b71) && code == 1) { + assert(false); + } } + } - receive() external payable {} + receive() external payable {} } diff --git a/tests/invariants/shared/utils/ActorsUtils.sol b/tests/invariants/shared/utils/ActorsUtils.sol index 357b9e040..342cdf51f 100644 --- a/tests/invariants/shared/utils/ActorsUtils.sol +++ b/tests/invariants/shared/utils/ActorsUtils.sol @@ -2,48 +2,52 @@ pragma solidity ^0.8.19; // Test Contracts -import {Actor} from "./Actor.sol"; -import {TestnetERC20} from "tests/mocks/TestnetERC20.sol"; +import {Actor} from './Actor.sol'; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; library ActorsUtils { - uint256 internal constant INITIAL_ETH_BALANCE = 1e26; - uint256 constant INITIAL_BALANCE = 1e12; + uint256 internal constant INITIAL_ETH_BALANCE = 1e26; + uint256 constant INITIAL_BALANCE = 1e12; - /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTORS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice Deploy protocol actors and initialize their balances - function setUpActors(address[] memory addresses, address[] memory tokens, address[] memory contracts) - internal - returns (address[] memory actorAddresses) - { - actorAddresses = new address[](addresses.length); + /// @notice Deploy protocol actors and initialize their balances + function setUpActors( + address[] memory addresses, + address[] memory tokens, + address[] memory contracts + ) internal returns (address[] memory actorAddresses) { + actorAddresses = new address[](addresses.length); - // Initialize the three actors of the fuzzers - for (uint256 i; i < addresses.length; i++) { - // Deploy actor proxies and approve system contracts - address _actor = setUpActor(tokens, contracts); + // Initialize the three actors of the fuzzers + for (uint256 i; i < addresses.length; i++) { + // Deploy actor proxies and approve system contracts + address _actor = setUpActor(tokens, contracts); - // Mint initial balances to actors - for (uint256 j = 0; j < tokens.length; j++) { - TestnetERC20 _token = TestnetERC20(tokens[j]); - uint256 decimals = _token.decimals(); - _token.mint(_actor, INITIAL_BALANCE * 10 ** decimals); - } - actorAddresses[i] = _actor; - } + // Mint initial balances to actors + for (uint256 j = 0; j < tokens.length; j++) { + TestnetERC20 _token = TestnetERC20(tokens[j]); + uint256 decimals = _token.decimals(); + _token.mint(_actor, INITIAL_BALANCE * 10 ** decimals); + } + actorAddresses[i] = _actor; } + } - /// @notice Deploy an actor proxy contract - /// @param tokens Array of token addresses - /// @param contracts Array of contract addresses to aprove tokens to - /// @return actorAddress Address of the deployed actor - function setUpActor(address[] memory tokens, address[] memory contracts) internal returns (address actorAddress) { - bool success; - Actor _actor = new Actor(tokens, contracts); - (success,) = address(_actor).call{value: INITIAL_ETH_BALANCE}(""); - assert(success); - actorAddress = address(_actor); - } + /// @notice Deploy an actor proxy contract + /// @param tokens Array of token addresses + /// @param contracts Array of contract addresses to aprove tokens to + /// @return actorAddress Address of the deployed actor + function setUpActor( + address[] memory tokens, + address[] memory contracts + ) internal returns (address actorAddress) { + bool success; + Actor _actor = new Actor(tokens, contracts); + (success, ) = address(_actor).call{value: INITIAL_ETH_BALANCE}(''); + assert(success); + actorAddress = address(_actor); + } } diff --git a/tests/invariants/shared/utils/CREATE3.sol b/tests/invariants/shared/utils/CREATE3.sol index 5b3beab01..281a36c45 100644 --- a/tests/invariants/shared/utils/CREATE3.sol +++ b/tests/invariants/shared/utils/CREATE3.sol @@ -6,123 +6,123 @@ pragma solidity ^0.8.4; /// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/CREATE3.sol) /// @author Modified from 0xSequence (https://github.com/0xSequence/create3/blob/master/contracts/Create3.sol) library CREATE3 { - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* CUSTOM ERRORS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @dev Unable to deploy the contract. - error DeploymentFailed(); + /// @dev Unable to deploy the contract. + error DeploymentFailed(); - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* BYTECODE CONSTANTS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* BYTECODE CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /** - * -------------------------------------------------------------------+ - * Opcode | Mnemonic | Stack | Memory | - * -------------------------------------------------------------------| - * 36 | CALLDATASIZE | cds | | - * 3d | RETURNDATASIZE | 0 cds | | - * 3d | RETURNDATASIZE | 0 0 cds | | - * 37 | CALLDATACOPY | | [0..cds): calldata | - * 36 | CALLDATASIZE | cds | [0..cds): calldata | - * 3d | RETURNDATASIZE | 0 cds | [0..cds): calldata | - * 34 | CALLVALUE | value 0 cds | [0..cds): calldata | - * f0 | CREATE | newContract | [0..cds): calldata | - * -------------------------------------------------------------------| - * Opcode | Mnemonic | Stack | Memory | - * -------------------------------------------------------------------| - * 67 bytecode | PUSH8 bytecode | bytecode | | - * 3d | RETURNDATASIZE | 0 bytecode | | - * 52 | MSTORE | | [0..8): bytecode | - * 60 0x08 | PUSH1 0x08 | 0x08 | [0..8): bytecode | - * 60 0x18 | PUSH1 0x18 | 0x18 0x08 | [0..8): bytecode | - * f3 | RETURN | | [0..8): bytecode | - * -------------------------------------------------------------------+ - */ + /** + * -------------------------------------------------------------------+ + * Opcode | Mnemonic | Stack | Memory | + * -------------------------------------------------------------------| + * 36 | CALLDATASIZE | cds | | + * 3d | RETURNDATASIZE | 0 cds | | + * 3d | RETURNDATASIZE | 0 0 cds | | + * 37 | CALLDATACOPY | | [0..cds): calldata | + * 36 | CALLDATASIZE | cds | [0..cds): calldata | + * 3d | RETURNDATASIZE | 0 cds | [0..cds): calldata | + * 34 | CALLVALUE | value 0 cds | [0..cds): calldata | + * f0 | CREATE | newContract | [0..cds): calldata | + * -------------------------------------------------------------------| + * Opcode | Mnemonic | Stack | Memory | + * -------------------------------------------------------------------| + * 67 bytecode | PUSH8 bytecode | bytecode | | + * 3d | RETURNDATASIZE | 0 bytecode | | + * 52 | MSTORE | | [0..8): bytecode | + * 60 0x08 | PUSH1 0x08 | 0x08 | [0..8): bytecode | + * 60 0x18 | PUSH1 0x18 | 0x18 0x08 | [0..8): bytecode | + * f3 | RETURN | | [0..8): bytecode | + * -------------------------------------------------------------------+ + */ - /// @dev The proxy initialization code. - uint256 private constant _PROXY_INITCODE = 0x67363d3d37363d34f03d5260086018f3; + /// @dev The proxy initialization code. + uint256 private constant _PROXY_INITCODE = 0x67363d3d37363d34f03d5260086018f3; - /// @dev Hash of the `_PROXY_INITCODE`. - /// Equivalent to `keccak256(abi.encodePacked(hex"67363d3d37363d34f03d5260086018f3"))`. - bytes32 internal constant PROXY_INITCODE_HASH = - 0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f; + /// @dev Hash of the `_PROXY_INITCODE`. + /// Equivalent to `keccak256(abi.encodePacked(hex"67363d3d37363d34f03d5260086018f3"))`. + bytes32 internal constant PROXY_INITCODE_HASH = + 0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f; - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* CREATE3 OPERATIONS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CREATE3 OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @dev Deploys `initCode` deterministically with a `salt`. - /// Returns the deterministic address of the deployed contract, - /// which solely depends on `salt`. - function deployDeterministic(bytes memory initCode, bytes32 salt) - internal - returns (address deployed) - { - deployed = deployDeterministic(0, initCode, salt); - } + /// @dev Deploys `initCode` deterministically with a `salt`. + /// Returns the deterministic address of the deployed contract, + /// which solely depends on `salt`. + function deployDeterministic( + bytes memory initCode, + bytes32 salt + ) internal returns (address deployed) { + deployed = deployDeterministic(0, initCode, salt); + } - /// @dev Deploys `initCode` deterministically with a `salt`. - /// The deployed contract is funded with `value` (in wei) ETH. - /// Returns the deterministic address of the deployed contract, - /// which solely depends on `salt`. - function deployDeterministic(uint256 value, bytes memory initCode, bytes32 salt) - internal - returns (address deployed) - { - /// @solidity memory-safe-assembly - assembly { - mstore(0x00, _PROXY_INITCODE) // Store the `_PROXY_INITCODE`. - let proxy := create2(0, 0x10, 0x10, salt) - if iszero(proxy) { - mstore(0x00, 0x30116425) // `DeploymentFailed()`. - revert(0x1c, 0x04) - } - mstore(0x14, proxy) // Store the proxy's address. - // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01). - // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex). - mstore(0x00, 0xd694) - mstore8(0x34, 0x01) // Nonce of the proxy contract (1). - deployed := keccak256(0x1e, 0x17) - if iszero( - mul( // The arguments of `mul` are evaluated last to first. - extcodesize(deployed), - call(gas(), proxy, value, add(initCode, 0x20), mload(initCode), 0x00, 0x00) - ) - ) { - mstore(0x00, 0x30116425) // `DeploymentFailed()`. - revert(0x1c, 0x04) - } - } + /// @dev Deploys `initCode` deterministically with a `salt`. + /// The deployed contract is funded with `value` (in wei) ETH. + /// Returns the deterministic address of the deployed contract, + /// which solely depends on `salt`. + function deployDeterministic( + uint256 value, + bytes memory initCode, + bytes32 salt + ) internal returns (address deployed) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, _PROXY_INITCODE) // Store the `_PROXY_INITCODE`. + let proxy := create2(0, 0x10, 0x10, salt) + if iszero(proxy) { + mstore(0x00, 0x30116425) // `DeploymentFailed()`. + revert(0x1c, 0x04) + } + mstore(0x14, proxy) // Store the proxy's address. + // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01). + // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex). + mstore(0x00, 0xd694) + mstore8(0x34, 0x01) // Nonce of the proxy contract (1). + deployed := keccak256(0x1e, 0x17) + if iszero( + mul( // The arguments of `mul` are evaluated last to first. + extcodesize(deployed), + call(gas(), proxy, value, add(initCode, 0x20), mload(initCode), 0x00, 0x00) + ) + ) { + mstore(0x00, 0x30116425) // `DeploymentFailed()`. + revert(0x1c, 0x04) + } } + } - /// @dev Returns the deterministic address for `salt`. - function predictDeterministicAddress(bytes32 salt) internal view returns (address deployed) { - deployed = predictDeterministicAddress(salt, address(this)); - } + /// @dev Returns the deterministic address for `salt`. + function predictDeterministicAddress(bytes32 salt) internal view returns (address deployed) { + deployed = predictDeterministicAddress(salt, address(this)); + } - /// @dev Returns the deterministic address for `salt` with `deployer`. - function predictDeterministicAddress(bytes32 salt, address deployer) - internal - pure - returns (address deployed) - { - /// @solidity memory-safe-assembly - assembly { - let m := mload(0x40) // Cache the free memory pointer. - mstore(0x00, deployer) // Store `deployer`. - mstore8(0x0b, 0xff) // Store the prefix. - mstore(0x20, salt) // Store the salt. - mstore(0x40, PROXY_INITCODE_HASH) // Store the bytecode hash. + /// @dev Returns the deterministic address for `salt` with `deployer`. + function predictDeterministicAddress( + bytes32 salt, + address deployer + ) internal pure returns (address deployed) { + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Cache the free memory pointer. + mstore(0x00, deployer) // Store `deployer`. + mstore8(0x0b, 0xff) // Store the prefix. + mstore(0x20, salt) // Store the salt. + mstore(0x40, PROXY_INITCODE_HASH) // Store the bytecode hash. - mstore(0x14, keccak256(0x0b, 0x55)) // Store the proxy's address. - mstore(0x40, m) // Restore the free memory pointer. - // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01). - // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex). - mstore(0x00, 0xd694) - mstore8(0x34, 0x01) // Nonce of the proxy contract (1). - deployed := keccak256(0x1e, 0x17) - } + mstore(0x14, keccak256(0x0b, 0x55)) // Store the proxy's address. + mstore(0x40, m) // Restore the free memory pointer. + // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01). + // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex). + mstore(0x00, 0xd694) + mstore8(0x34, 0x01) // Nonce of the proxy contract (1). + deployed := keccak256(0x1e, 0x17) } -} \ No newline at end of file + } +} diff --git a/tests/invariants/shared/utils/DeployPermit2.sol b/tests/invariants/shared/utils/DeployPermit2.sol index 3fe83dcd3..c1445f1b3 100644 --- a/tests/invariants/shared/utils/DeployPermit2.sol +++ b/tests/invariants/shared/utils/DeployPermit2.sol @@ -5,18 +5,18 @@ pragma solidity ^0.8.17; /// @dev useful if testing externally against permit2 and want to avoid /// recompiling entirely and requiring viaIR compilation library DeployPermit2 { - /// @notice deploy permit2 - function deployPermit2() internal returns (address) { - bytes memory bytecode = - hex"60c0346100bb574660a052602081017f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a60408301524660608301523060808301526080825260a082019180831060018060401b038411176100a557826040525190206080526123c090816100c1823960805181611b47015260a05181611b210152f35b634e487b7160e01b600052604160045260246000fd5b600080fdfe6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000003611b69577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a"; + /// @notice deploy permit2 + function deployPermit2() internal returns (address) { + bytes + memory bytecode = hex'60c0346100bb574660a052602081017f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a60408301524660608301523060808301526080825260a082019180831060018060401b038411176100a557826040525190206080526123c090816100c1823960805181611b47015260a05181611b210152f35b634e487b7160e01b600052604160045260246000fd5b600080fdfe6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000003611b69577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a'; - return deployFromBytecode(bytecode); - } + return deployFromBytecode(bytecode); + } - /// @notice helper function to deploy bytecode - function deployFromBytecode(bytes memory bytecode) internal returns (address child) { - assembly { - child := create(0, add(bytecode, 0x20), mload(bytecode)) - } + /// @notice helper function to deploy bytecode + function deployFromBytecode(bytes memory bytecode) internal returns (address child) { + assembly { + child := create(0, add(bytecode, 0x20), mload(bytecode)) } + } } diff --git a/tests/invariants/shared/utils/ErrorHandlers.sol b/tests/invariants/shared/utils/ErrorHandlers.sol index fd73a54f6..862fba2e4 100644 --- a/tests/invariants/shared/utils/ErrorHandlers.sol +++ b/tests/invariants/shared/utils/ErrorHandlers.sol @@ -4,54 +4,54 @@ pragma solidity ^0.8.19; /// @title ErrorHandlers /// @notice Library for handling errors in the test suite library ErrorHandlers { - /// @dev Selector for Panic(uint256) as defined by Solidity - bytes4 internal constant _PANIC_SELECTOR = 0x4e487b71; - /// @dev Panic code for assertion failed (0x01) - uint256 internal constant _PANIC_ASSERTION_FAILED = 0x01; + /// @dev Selector for Panic(uint256) as defined by Solidity + bytes4 internal constant _PANIC_SELECTOR = 0x4e487b71; + /// @dev Panic code for assertion failed (0x01) + uint256 internal constant _PANIC_ASSERTION_FAILED = 0x01; - event AssertFail(string); + event AssertFail(string); - /// @notice Checks if a call failed due to an assertion error and propagates the error if found. - /////////////////////////////////////////////////////////////////////////////////////////////// - // ASSERTION ERRORS // - /////////////////////////////////////////////////////////////////////////////////////////////// + /// @notice Checks if a call failed due to an assertion error and propagates the error if found. + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSERTION ERRORS // + /////////////////////////////////////////////////////////////////////////////////////////////// - /// @param success Indicates whether the call was successful. - /// @param returnData The data returned from the call. - function handleAssertionError( - bool success, - bytes memory returnData, - bool detectNonAssertionErrors, - string memory errorMessage - ) internal { - // Case 1: do nothing if success is true - if (success) return; + /// @param success Indicates whether the call was successful. + /// @param returnData The data returned from the call. + function handleAssertionError( + bool success, + bytes memory returnData, + bool detectNonAssertionErrors, + string memory errorMessage + ) internal { + // Case 1: do nothing if success is true + if (success) return; - // Case 2: detect Panic(0x01) "Assertion" errors - // Decode potential Panic(uint256) (selector + uint256 = 36 bytes) - if (returnData.length == 36) { - bytes4 selector; - uint256 code; - assembly { - selector := mload(add(returnData, 0x20)) - code := mload(add(returnData, 0x24)) - } - // Case 3: if Panic(0x01) "Assertion" -> assert(false), this propagates the assertion error to the Tester context - if (selector == _PANIC_SELECTOR && code == _PANIC_ASSERTION_FAILED) { - assert(false); - } - } + // Case 2: detect Panic(0x01) "Assertion" errors + // Decode potential Panic(uint256) (selector + uint256 = 36 bytes) + if (returnData.length == 36) { + bytes4 selector; + uint256 code; + assembly { + selector := mload(add(returnData, 0x20)) + code := mload(add(returnData, 0x24)) + } + // Case 3: if Panic(0x01) "Assertion" -> assert(false), this propagates the assertion error to the Tester context + if (selector == _PANIC_SELECTOR && code == _PANIC_ASSERTION_FAILED) { + assert(false); + } + } - // Case 3: detect non-assertion errors and assert with the error message - if (detectNonAssertionErrors) { - assertWithMsg(false, errorMessage); - } + // Case 3: detect non-assertion errors and assert with the error message + if (detectNonAssertionErrors) { + assertWithMsg(false, errorMessage); } + } - function assertWithMsg(bool b, string memory reason) internal { - if (!b) { - emit AssertFail(reason); - assert(false); - } + function assertWithMsg(bool b, string memory reason) internal { + if (!b) { + emit AssertFail(reason); + assert(false); } + } } diff --git a/tests/invariants/shared/utils/PropertiesAsserts.sol b/tests/invariants/shared/utils/PropertiesAsserts.sol index b04dfad9d..003971c76 100644 --- a/tests/invariants/shared/utils/PropertiesAsserts.sol +++ b/tests/invariants/shared/utils/PropertiesAsserts.sol @@ -4,359 +4,475 @@ pragma solidity ^0.8.0; /// @notice PropertiesAsserts is a library that provides assertions for properties of Solidity contracts. /// @dev Added more assertions to the original PropertiesAsserts library. abstract contract PropertiesAsserts { - event LogUint256(string, uint256); - event LogAddress(string, address); - event LogString(string); - - event AssertFail(string); - event AssertEqFail(string); - event AssertNeqFail(string); - event AssertGeFail(string); - event AssertGtFail(string); - event AssertLeFail(string); - event AssertLtFail(string); - - function assertWithMsg(bool b, string memory reason) internal { - if (!b) { - emit AssertFail(reason); - assert(false); - } - } - - /// @notice asserts that a is equal to b. - function assertEq(uint256 a, uint256 b) internal pure { - if (a != b) { - assert(false); - } - } - - function assertEq(int256 a, int256 b) internal pure { - if (a != b) { - assert(false); - } - } - - //write the function below for address - function assertEq(address a, address b) internal pure { - if (a != b) { - assert(false); - } - } - - // now with a reason parameter - function assertEq(address a, address b, string memory reason) internal { - if (a != b) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "!=", bStr, ", reason: ", reason); - emit AssertEqFail(string(assertMsg)); - assert(false); - } - } - - /// @notice asserts that a is equal to b. Violations are logged using reason. - function assertEq(uint256 a, uint256 b, string memory reason) internal { - if (a != b) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "!=", bStr, ", reason: ", reason); - emit AssertEqFail(string(assertMsg)); - assert(false); - } - } - - /// @notice int256 version of assertEq - function assertEq(int256 a, int256 b, string memory reason) internal { - if (a != b) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "!=", bStr, ", reason: ", reason); - emit AssertEqFail(string(assertMsg)); - assert(false); - } - } - - /// @notice asserts that a is not equal to b. Violations are logged using reason. - function assertNeq(uint256 a, uint256 b, string memory reason) internal { - if (a == b) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "==", bStr, ", reason: ", reason); - emit AssertNeqFail(string(assertMsg)); - assert(false); - } - } - - /// @notice int256 version of assertNeq - function assertNeq(int256 a, int256 b, string memory reason) internal { - if (a == b) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "==", bStr, ", reason: ", reason); - emit AssertNeqFail(string(assertMsg)); - assert(false); - } - } - - /// @notice asserts that a is greater than or equal to b. Violations are logged using reason. - function assertGe(uint256 a, uint256 b, string memory reason) internal { - if (!(a >= b)) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "<", bStr, " failed, reason: ", reason); - emit AssertGeFail(string(assertMsg)); - assert(false); - } - } - - /// @notice int256 version of assertGe - function assertGe(int256 a, int256 b, string memory reason) internal { - if (!(a >= b)) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "<", bStr, " failed, reason: ", reason); - emit AssertGeFail(string(assertMsg)); - assert(false); - } - } - - /// @dev Asserts that `a * b >= x * y` with full 512-bit precision. - function assertFullMulGe(uint256 a, uint256 b, uint256 x, uint256 y, string memory reason) internal { - if (!fullMulGte(a, b, x, y)) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - string memory xStr = PropertiesLibString.toString(x); - string memory yStr = PropertiesLibString.toString(y); - - bytes memory assertMsg = - abi.encodePacked("Invalid: ", aStr, "*", bStr, " < ", xStr, "*", yStr, " failed, reason: ", reason); - - emit AssertGeFail(string(assertMsg)); - assert(false); - } - } - - /// @notice asserts that a is greater than b. Violations are logged using reason. - function assertGt(uint256 a, uint256 b, string memory reason) internal { - if (!(a > b)) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "<=", bStr, " failed, reason: ", reason); - emit AssertGtFail(string(assertMsg)); - assert(false); - } - } - - /// @notice int256 version of assertGt - function assertGt(int256 a, int256 b, string memory reason) internal { - if (!(a > b)) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, "<=", bStr, " failed, reason: ", reason); - emit AssertGtFail(string(assertMsg)); - assert(false); - } - } - - /// @notice asserts that a is less than or equal to b. Violations are logged using reason. - function assertLe(uint256 a, uint256 b, string memory reason) internal { - if (!(a <= b)) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, ">", bStr, " failed, reason: ", reason); - emit AssertLeFail(string(assertMsg)); - assert(false); - } - } - - /// @notice int256 version of assertLe - function assertLe(int256 a, int256 b, string memory reason) internal { - if (!(a <= b)) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, ">", bStr, " failed, reason: ", reason); - emit AssertLeFail(string(assertMsg)); - assert(false); - } - } - - /// @notice asserts that a is less than b. Violations are logged using reason. - function assertLt(uint256 a, uint256 b, string memory reason) internal { - if (!(a < b)) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, ">=", bStr, " failed, reason: ", reason); - emit AssertLtFail(string(assertMsg)); - assert(false); - } - } - - /// @notice int256 version of assertLt - function assertLt(int256 a, int256 b, string memory reason) internal { - if (!(a < b)) { - string memory aStr = PropertiesLibString.toString(a); - string memory bStr = PropertiesLibString.toString(b); - bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr, ">=", bStr, " failed, reason: ", reason); - emit AssertLtFail(string(assertMsg)); - assert(false); - } - } - - /// @notice Clamps value to be between low and high, both inclusive - function clampBetween(uint256 value, uint256 low, uint256 high) internal returns (uint256) { - if (value < low || value > high) { - uint256 ans = low + (value % (high - low + 1)); - string memory valueStr = PropertiesLibString.toString(value); - string memory ansStr = PropertiesLibString.toString(ans); - bytes memory message = abi.encodePacked("Clamping value ", valueStr, " to ", ansStr); - emit LogString(string(message)); - return ans; - } - return value; - } - - /// @notice int256 version of clampBetween - function clampBetween(int256 value, int256 low, int256 high) internal returns (int256) { - if (value < low || value > high) { - int256 range = high - low + 1; - int256 clamped = (value - low) % (range); - if (clamped < 0) clamped += range; - int256 ans = low + clamped; - string memory valueStr = PropertiesLibString.toString(value); - string memory ansStr = PropertiesLibString.toString(ans); - bytes memory message = abi.encodePacked("Clamping value ", valueStr, " to ", ansStr); - emit LogString(string(message)); - return ans; - } - return value; - } - - /// @notice clamps a to be less than b - function clampLt(uint256 a, uint256 b) internal returns (uint256) { - if (!(a < b)) { - assertNeq(b, 0, "clampLt cannot clamp value a to be less than zero. Check your inputs/assumptions."); - uint256 value = a % b; - string memory aStr = PropertiesLibString.toString(a); - string memory valueStr = PropertiesLibString.toString(value); - bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); - emit LogString(string(message)); - return value; - } - return a; - } - - /// @notice int256 version of clampLt - function clampLt(int256 a, int256 b) internal returns (int256) { - if (!(a < b)) { - int256 value = b - 1; - string memory aStr = PropertiesLibString.toString(a); - string memory valueStr = PropertiesLibString.toString(value); - bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); - emit LogString(string(message)); - return value; - } - return a; - } - - /// @notice clamps a to be less than or equal to b - function clampLe(uint256 a, uint256 b) internal returns (uint256) { - if (!(a <= b)) { - uint256 value = a % (b + 1); - string memory aStr = PropertiesLibString.toString(a); - string memory valueStr = PropertiesLibString.toString(value); - bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); - emit LogString(string(message)); - return value; - } - return a; - } - - /// @notice int256 version of clampLe - function clampLe(int256 a, int256 b) internal returns (int256) { - if (!(a <= b)) { - int256 value = b; - string memory aStr = PropertiesLibString.toString(a); - string memory valueStr = PropertiesLibString.toString(value); - bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); - emit LogString(string(message)); - return value; - } - return a; - } - - /// @notice clamps a to be greater than b - function clampGt(uint256 a, uint256 b) internal returns (uint256) { - if (!(a > b)) { - assertNeq( - b, - type(uint256).max, - "clampGt cannot clamp value a to be larger than uint256.max. Check your inputs/assumptions." - ); - uint256 value = b + 1; - string memory aStr = PropertiesLibString.toString(a); - string memory valueStr = PropertiesLibString.toString(value); - bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); - emit LogString(string(message)); - return value; - } else { - return a; - } - } - - /// @notice int256 version of clampGt - function clampGt(int256 a, int256 b) internal returns (int256) { - if (!(a > b)) { - int256 value = b + 1; - string memory aStr = PropertiesLibString.toString(a); - string memory valueStr = PropertiesLibString.toString(value); - bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); - emit LogString(string(message)); - return value; - } else { - return a; - } - } - - /// @notice clamps a to be greater than or equal to b - function clampGe(uint256 a, uint256 b) internal returns (uint256) { - if (!(a > b)) { - uint256 value = b; - string memory aStr = PropertiesLibString.toString(a); - string memory valueStr = PropertiesLibString.toString(value); - bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); - emit LogString(string(message)); - return value; - } - return a; - } - - /// @notice int256 version of clampGe - function clampGe(int256 a, int256 b) internal returns (int256) { - if (!(a > b)) { - int256 value = b; - string memory aStr = PropertiesLibString.toString(a); - string memory valueStr = PropertiesLibString.toString(value); - bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr); - emit LogString(string(message)); - return value; - } - return a; - } - - /// @dev Returns a * b >= x * y, with full precision. - function fullMulGte(uint256 a, uint256 b, uint256 x, uint256 y) internal pure returns (bool result) { - /// @solidity memory-safe-assembly - assembly { - let m := not(0) - let mm1 := mulmod(a, b, m) - let lo1 := mul(a, b) - let hi1 := sub(sub(mm1, lo1), lt(mm1, lo1)) - let mm2 := mulmod(x, y, m) - let lo2 := mul(x, y) - let hi2 := sub(sub(mm2, lo2), lt(mm2, lo2)) - result := or(gt(hi1, hi2), and(eq(hi1, hi2), iszero(lt(lo1, lo2)))) - } - } + event LogUint256(string, uint256); + event LogAddress(string, address); + event LogString(string); + + event AssertFail(string); + event AssertEqFail(string); + event AssertNeqFail(string); + event AssertGeFail(string); + event AssertGtFail(string); + event AssertLeFail(string); + event AssertLtFail(string); + + function assertWithMsg(bool b, string memory reason) internal { + if (!b) { + emit AssertFail(reason); + assert(false); + } + } + + /// @notice asserts that a is equal to b. + function assertEq(uint256 a, uint256 b) internal pure { + if (a != b) { + assert(false); + } + } + + function assertEq(int256 a, int256 b) internal pure { + if (a != b) { + assert(false); + } + } + + //write the function below for address + function assertEq(address a, address b) internal pure { + if (a != b) { + assert(false); + } + } + + // now with a reason parameter + function assertEq(address a, address b, string memory reason) internal { + if (a != b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '!=', + bStr, + ', reason: ', + reason + ); + emit AssertEqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is equal to b. Violations are logged using reason. + function assertEq(uint256 a, uint256 b, string memory reason) internal { + if (a != b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '!=', + bStr, + ', reason: ', + reason + ); + emit AssertEqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertEq + function assertEq(int256 a, int256 b, string memory reason) internal { + if (a != b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '!=', + bStr, + ', reason: ', + reason + ); + emit AssertEqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is not equal to b. Violations are logged using reason. + function assertNeq(uint256 a, uint256 b, string memory reason) internal { + if (a == b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '==', + bStr, + ', reason: ', + reason + ); + emit AssertNeqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertNeq + function assertNeq(int256 a, int256 b, string memory reason) internal { + if (a == b) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '==', + bStr, + ', reason: ', + reason + ); + emit AssertNeqFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is greater than or equal to b. Violations are logged using reason. + function assertGe(uint256 a, uint256 b, string memory reason) internal { + if (!(a >= b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '<', + bStr, + ' failed, reason: ', + reason + ); + emit AssertGeFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertGe + function assertGe(int256 a, int256 b, string memory reason) internal { + if (!(a >= b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '<', + bStr, + ' failed, reason: ', + reason + ); + emit AssertGeFail(string(assertMsg)); + assert(false); + } + } + + /// @dev Asserts that `a * b >= x * y` with full 512-bit precision. + function assertFullMulGe( + uint256 a, + uint256 b, + uint256 x, + uint256 y, + string memory reason + ) internal { + if (!fullMulGte(a, b, x, y)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + string memory xStr = PropertiesLibString.toString(x); + string memory yStr = PropertiesLibString.toString(y); + + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '*', + bStr, + ' < ', + xStr, + '*', + yStr, + ' failed, reason: ', + reason + ); + + emit AssertGeFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is greater than b. Violations are logged using reason. + function assertGt(uint256 a, uint256 b, string memory reason) internal { + if (!(a > b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '<=', + bStr, + ' failed, reason: ', + reason + ); + emit AssertGtFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertGt + function assertGt(int256 a, int256 b, string memory reason) internal { + if (!(a > b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '<=', + bStr, + ' failed, reason: ', + reason + ); + emit AssertGtFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is less than or equal to b. Violations are logged using reason. + function assertLe(uint256 a, uint256 b, string memory reason) internal { + if (!(a <= b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '>', + bStr, + ' failed, reason: ', + reason + ); + emit AssertLeFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertLe + function assertLe(int256 a, int256 b, string memory reason) internal { + if (!(a <= b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '>', + bStr, + ' failed, reason: ', + reason + ); + emit AssertLeFail(string(assertMsg)); + assert(false); + } + } + + /// @notice asserts that a is less than b. Violations are logged using reason. + function assertLt(uint256 a, uint256 b, string memory reason) internal { + if (!(a < b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '>=', + bStr, + ' failed, reason: ', + reason + ); + emit AssertLtFail(string(assertMsg)); + assert(false); + } + } + + /// @notice int256 version of assertLt + function assertLt(int256 a, int256 b, string memory reason) internal { + if (!(a < b)) { + string memory aStr = PropertiesLibString.toString(a); + string memory bStr = PropertiesLibString.toString(b); + bytes memory assertMsg = abi.encodePacked( + 'Invalid: ', + aStr, + '>=', + bStr, + ' failed, reason: ', + reason + ); + emit AssertLtFail(string(assertMsg)); + assert(false); + } + } + + /// @notice Clamps value to be between low and high, both inclusive + function clampBetween(uint256 value, uint256 low, uint256 high) internal returns (uint256) { + if (value < low || value > high) { + uint256 ans = low + (value % (high - low + 1)); + string memory valueStr = PropertiesLibString.toString(value); + string memory ansStr = PropertiesLibString.toString(ans); + bytes memory message = abi.encodePacked('Clamping value ', valueStr, ' to ', ansStr); + emit LogString(string(message)); + return ans; + } + return value; + } + + /// @notice int256 version of clampBetween + function clampBetween(int256 value, int256 low, int256 high) internal returns (int256) { + if (value < low || value > high) { + int256 range = high - low + 1; + int256 clamped = (value - low) % (range); + if (clamped < 0) clamped += range; + int256 ans = low + clamped; + string memory valueStr = PropertiesLibString.toString(value); + string memory ansStr = PropertiesLibString.toString(ans); + bytes memory message = abi.encodePacked('Clamping value ', valueStr, ' to ', ansStr); + emit LogString(string(message)); + return ans; + } + return value; + } + + /// @notice clamps a to be less than b + function clampLt(uint256 a, uint256 b) internal returns (uint256) { + if (!(a < b)) { + assertNeq( + b, + 0, + 'clampLt cannot clamp value a to be less than zero. Check your inputs/assumptions.' + ); + uint256 value = a % b; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked('Clamping value ', aStr, ' to ', valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice int256 version of clampLt + function clampLt(int256 a, int256 b) internal returns (int256) { + if (!(a < b)) { + int256 value = b - 1; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked('Clamping value ', aStr, ' to ', valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice clamps a to be less than or equal to b + function clampLe(uint256 a, uint256 b) internal returns (uint256) { + if (!(a <= b)) { + uint256 value = a % (b + 1); + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked('Clamping value ', aStr, ' to ', valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice int256 version of clampLe + function clampLe(int256 a, int256 b) internal returns (int256) { + if (!(a <= b)) { + int256 value = b; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked('Clamping value ', aStr, ' to ', valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice clamps a to be greater than b + function clampGt(uint256 a, uint256 b) internal returns (uint256) { + if (!(a > b)) { + assertNeq( + b, + type(uint256).max, + 'clampGt cannot clamp value a to be larger than uint256.max. Check your inputs/assumptions.' + ); + uint256 value = b + 1; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked('Clamping value ', aStr, ' to ', valueStr); + emit LogString(string(message)); + return value; + } else { + return a; + } + } + + /// @notice int256 version of clampGt + function clampGt(int256 a, int256 b) internal returns (int256) { + if (!(a > b)) { + int256 value = b + 1; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked('Clamping value ', aStr, ' to ', valueStr); + emit LogString(string(message)); + return value; + } else { + return a; + } + } + + /// @notice clamps a to be greater than or equal to b + function clampGe(uint256 a, uint256 b) internal returns (uint256) { + if (!(a > b)) { + uint256 value = b; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked('Clamping value ', aStr, ' to ', valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @notice int256 version of clampGe + function clampGe(int256 a, int256 b) internal returns (int256) { + if (!(a > b)) { + int256 value = b; + string memory aStr = PropertiesLibString.toString(a); + string memory valueStr = PropertiesLibString.toString(value); + bytes memory message = abi.encodePacked('Clamping value ', aStr, ' to ', valueStr); + emit LogString(string(message)); + return value; + } + return a; + } + + /// @dev Returns a * b >= x * y, with full precision. + function fullMulGte( + uint256 a, + uint256 b, + uint256 x, + uint256 y + ) internal pure returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + let m := not(0) + let mm1 := mulmod(a, b, m) + let lo1 := mul(a, b) + let hi1 := sub(sub(mm1, lo1), lt(mm1, lo1)) + let mm2 := mulmod(x, y, m) + let lo2 := mul(x, y) + let hi2 := sub(sub(mm2, lo2), lt(mm2, lo2)) + result := or(gt(hi1, hi2), and(eq(hi1, hi2), iszero(lt(lo1, lo2)))) + } + } } /// @notice Efficient library for creating string representations of integers. @@ -364,39 +480,38 @@ abstract contract PropertiesAsserts { /// @author Modified from Solady (https://github.com/Vectorized/solady/blob/main/src/utils/LibString.sol) /// @dev Name of the library is modified to prevent collisions with contract-under-test uses of LibString library PropertiesLibString { - function toString(int256 value) internal pure returns (string memory str) { - uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); - str = toString(absValue); - - if (value < 0) { - str = string(abi.encodePacked("-", str)); - } - } - - function toString(uint256 value) internal pure returns (string memory str) { - /// @solidity memory-safe-assembly - assembly { - // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes - // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the - // trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. - let newFreeMemoryPointer := add(mload(0x40), 160) - - // Update the free memory pointer to avoid overriding our string. - mstore(0x40, newFreeMemoryPointer) - - // Assign str to the end of the zone of newly allocated memory. - str := sub(newFreeMemoryPointer, 32) - - // Clean the last word of memory it may not be overwritten. - mstore(str, 0) - - // Cache the end of the memory to calculate the length later. - let end := str - - // We write the string from rightmost digit to leftmost digit. - // The following is essentially a do-while loop that also handles the zero case. - // prettier-ignore - for { let temp := value } 1 {} { + function toString(int256 value) internal pure returns (string memory str) { + uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); + str = toString(absValue); + + if (value < 0) { + str = string(abi.encodePacked('-', str)); + } + } + + function toString(uint256 value) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes + // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the + // trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. + let newFreeMemoryPointer := add(mload(0x40), 160) + + // Update the free memory pointer to avoid overriding our string. + mstore(0x40, newFreeMemoryPointer) + + // Assign str to the end of the zone of newly allocated memory. + str := sub(newFreeMemoryPointer, 32) + + // Clean the last word of memory it may not be overwritten. + mstore(str, 0) + + // Cache the end of the memory to calculate the length later. + let end := str + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + // prettier-ignore + for { let temp := value } 1 {} { // Move the pointer 1 byte to the left. str := sub(str, 1) @@ -411,31 +526,31 @@ library PropertiesLibString { if iszero(temp) { break } } - // Compute and cache the final total length of the string. - let length := sub(end, str) + // Compute and cache the final total length of the string. + let length := sub(end, str) - // Move the pointer 32 bytes leftwards to make room for the length. - str := sub(str, 32) + // Move the pointer 32 bytes leftwards to make room for the length. + str := sub(str, 32) - // Store the string's length at the start of memory allocated for our string. - mstore(str, length) - } + // Store the string's length at the start of memory allocated for our string. + mstore(str, length) } + } - function toString(address value) internal pure returns (string memory str) { - bytes memory s = new bytes(40); - for (uint256 i = 0; i < 20; i++) { - bytes1 b = bytes1(uint8(uint256(uint160(value)) / (2 ** (8 * (19 - i))))); - bytes1 hi = bytes1(uint8(b) / 16); - bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); - s[2 * i] = char(hi); - s[2 * i + 1] = char(lo); - } - return string(s); + function toString(address value) internal pure returns (string memory str) { + bytes memory s = new bytes(40); + for (uint256 i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint256(uint160(value)) / (2 ** (8 * (19 - i))))); + bytes1 hi = bytes1(uint8(b) / 16); + bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); + s[2 * i] = char(hi); + s[2 * i + 1] = char(lo); } + return string(s); + } - function char(bytes1 b) internal pure returns (bytes1 c) { - if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); - else return bytes1(uint8(b) + 0x57); - } + function char(bytes1 b) internal pure returns (bytes1 c) { + if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); + else return bytes1(uint8(b) + 0x57); + } } diff --git a/tests/invariants/shared/utils/PropertiesConstants.sol b/tests/invariants/shared/utils/PropertiesConstants.sol index 839bb7d03..c21a5d016 100644 --- a/tests/invariants/shared/utils/PropertiesConstants.sol +++ b/tests/invariants/shared/utils/PropertiesConstants.sol @@ -2,36 +2,36 @@ pragma solidity ^0.8.19; abstract contract PropertiesConstants { - // Echidna constants - address constant USER1 = address(0x10000); - address constant USER2 = address(0x20000); - address constant USER3 = address(0x30000); + // Echidna constants + address constant USER1 = address(0x10000); + address constant USER2 = address(0x20000); + address constant USER3 = address(0x30000); - // Suite constants - uint256 constant CHECK_ALL_RESERVES = type(uint256).max; - string constant GPOST_CHECK_FAILED = "GPOST_CHECK_FAILED: checkPostConditions reverted"; - int256 constant PRICE_MIN = 0.0001e8; - int256 constant PRICE_MAX = 1e14; + // Suite constants + uint256 constant CHECK_ALL_RESERVES = type(uint256).max; + string constant GPOST_CHECK_FAILED = 'GPOST_CHECK_FAILED: checkPostConditions reverted'; + int256 constant PRICE_MIN = 0.0001e8; + int256 constant PRICE_MAX = 1e14; - // Protocol constants - uint256 constant SPOKE_COUNT = 3; - uint40 constant MAX_ALLOWED_SPOKE_CAP = type(uint40).max; + // Protocol constants + uint256 constant SPOKE_COUNT = 3; + uint40 constant MAX_ALLOWED_SPOKE_CAP = type(uint40).max; - // Interest rate 1 data - uint16 constant OPTIMAL_USAGE_RATIO_IR1 = 85_00; // 85.00% - uint16 constant BASE_VARIABLE_BORROW_RATE_IR1 = 1_00; // 1.00% - uint16 constant VARIABLE_RATE_SLOPE_1_IR1 = 4_00; // 4.00% - uint16 constant VARIABLE_RATE_SLOPE_2_IR1 = 55_00; // 55.00% + // Interest rate 1 data + uint16 constant OPTIMAL_USAGE_RATIO_IR1 = 85_00; // 85.00% + uint16 constant BASE_VARIABLE_BORROW_RATE_IR1 = 1_00; // 1.00% + uint16 constant VARIABLE_RATE_SLOPE_1_IR1 = 4_00; // 4.00% + uint16 constant VARIABLE_RATE_SLOPE_2_IR1 = 55_00; // 55.00% - // Interest rate 2 data - uint16 constant OPTIMAL_USAGE_RATIO_IR2 = 65_00; // 65.00% - uint16 constant BASE_VARIABLE_BORROW_RATE_IR2 = 2_00; // 2.00% - uint16 constant VARIABLE_RATE_SLOPE_1_IR2 = 7_00; // 7.00% - uint16 constant VARIABLE_RATE_SLOPE_2_IR2 = 75_00; // 75.00% + // Interest rate 2 data + uint16 constant OPTIMAL_USAGE_RATIO_IR2 = 65_00; // 65.00% + uint16 constant BASE_VARIABLE_BORROW_RATE_IR2 = 2_00; // 2.00% + uint16 constant VARIABLE_RATE_SLOPE_1_IR2 = 7_00; // 7.00% + uint16 constant VARIABLE_RATE_SLOPE_2_IR2 = 75_00; // 75.00% - // Spoke 1 liquidation config - uint128 constant TARGET_HEALTH_FACTOR_SPOKE1 = 1.05e18; + // Spoke 1 liquidation config + uint128 constant TARGET_HEALTH_FACTOR_SPOKE1 = 1.05e18; - // Spoke 2 liquidation config - uint128 constant TARGET_HEALTH_FACTOR_SPOKE2 = 1.02e18; + // Spoke 2 liquidation config + uint128 constant TARGET_HEALTH_FACTOR_SPOKE2 = 1.02e18; } diff --git a/tests/invariants/shared/utils/StdAsserts.sol b/tests/invariants/shared/utils/StdAsserts.sol index e43d10e86..ab9912c01 100644 --- a/tests/invariants/shared/utils/StdAsserts.sol +++ b/tests/invariants/shared/utils/StdAsserts.sol @@ -1,443 +1,490 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.6.2 <0.9.0; -import {PropertiesAsserts} from "./PropertiesAsserts.sol"; -import {stdMath} from "forge-std/StdMath.sol"; +import {PropertiesAsserts} from './PropertiesAsserts.sol'; +import {stdMath} from 'forge-std/StdMath.sol'; /// @notice Standardized assertions for use in Invariant tests, inherits PropertiesAsserts /// @dev Adapted from forge to work with echidna & medusa abstract contract StdAsserts is PropertiesAsserts { - event log(string); - event logs(bytes); - - event log_address(address); - event log_bytes32(bytes32); - event log_int(int256); - event log_uint(uint256); - event log_bytes(bytes); - event log_string(string); - - event log_named_address(string key, address val); - event log_named_bytes32(string key, bytes32 val); - event log_named_decimal_int(string key, int256 val, uint256 decimals); - event log_named_decimal_uint(string key, uint256 val, uint256 decimals); - event log_named_int(string key, int256 val); - event log_named_uint(string key, uint256 val); - event log_named_bytes(string key, bytes val); - event log_named_string(string key, string val); - event log_array(uint256[] val); - event log_array(int256[] val); - event log_array(address[] val); - event log_named_array(string key, uint256[] val); - event log_named_array(string key, int256[] val); - event log_named_array(string key, address[] val); - - function fail(string memory err) internal virtual { - emit log_named_string("Error", err); - fail(); - } - - function fail() internal virtual { - assert(false); - } - - function assertTrue(bool condition) internal { - if (!condition) { - emit log("Error: Assertion Failed"); - fail(); - } - } - - function assertTrue(bool condition, string memory err) internal { - if (!condition) { - emit log_named_string("Error", err); - assertTrue(condition); - } - } - - function assertFalse(bool data) internal virtual { - assertTrue(!data); - } - - function assertFalse(bool data, string memory err) internal virtual { - assertTrue(!data, err); - } - - function checkEq0(bytes memory a, bytes memory b) internal pure returns (bool ok) { - ok = true; - if (a.length == b.length) { - for (uint256 i = 0; i < a.length; i++) { - if (a[i] != b[i]) { - ok = false; - } - } - } else { - ok = false; - } - } - - function assertEq0(bytes memory a, bytes memory b) internal { - if (!checkEq0(a, b)) { - emit log("Error: a == b not satisfied [bytes]"); - emit log_named_bytes(" Expected", a); - emit log_named_bytes(" Actual", b); - fail(); - } - } - - function assertEq0(bytes memory a, bytes memory b, string memory err) internal { - if (!checkEq0(a, b)) { - emit log_named_string("Error", err); - assertEq0(a, b); - } - } - - function assertEq(bool a, bool b) internal virtual { - if (a != b) { - emit log("Error: a == b not satisfied [bool]"); - emit log_named_string(" Left", a ? "true" : "false"); - emit log_named_string(" Right", b ? "true" : "false"); - fail(); - } - } - - function assertEq(bool a, bool b, string memory err) internal virtual { - if (a != b) { - emit log_named_string("Error", err); - assertEq(a, b); - } - } - - function assertEq(bytes memory a, bytes memory b) internal virtual { - assertEq0(a, b); - } - - function assertEq(bytes memory a, bytes memory b, string memory err) internal virtual { - assertEq0(a, b, err); - } - - function assertEq(uint256[] memory a, uint256[] memory b) internal virtual { - if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { - emit log("Error: a == b not satisfied [uint[]]"); - emit log_named_array(" Left", a); - emit log_named_array(" Right", b); - fail(); - } - } - - function assertEq(int256[] memory a, int256[] memory b) internal virtual { - if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { - emit log("Error: a == b not satisfied [int[]]"); - emit log_named_array(" Left", a); - emit log_named_array(" Right", b); - fail(); - } - } - - function assertEq(address[] memory a, address[] memory b) internal virtual { - if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { - emit log("Error: a == b not satisfied [address[]]"); - emit log_named_array(" Left", a); - emit log_named_array(" Right", b); - fail(); - } - } - - function assertEq(uint256[] memory a, uint256[] memory b, string memory err) internal virtual { - if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { - emit log_named_string("Error", err); - assertEq(a, b); - } - } - - function assertEq(int256[] memory a, int256[] memory b, string memory err) internal virtual { - if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { - emit log_named_string("Error", err); - assertEq(a, b); - } - } - - function assertEq(address[] memory a, address[] memory b, string memory err) internal virtual { - if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { - emit log_named_string("Error", err); - assertEq(a, b); - } - } - - // Legacy helper - function assertEqUint(uint256 a, uint256 b) internal virtual { - assertEq(uint256(a), uint256(b)); - } - - function assertApproxEqAbs(uint256 a, uint256 b, uint256 maxDelta) internal virtual { - uint256 delta = stdMath.delta(a, b); - - if (delta > maxDelta) { - emit log("Error: a ~= b not satisfied [uint]"); - emit log_named_uint(" Left", a); - emit log_named_uint(" Right", b); - emit log_named_uint(" Max Delta", maxDelta); - emit log_named_uint(" Delta", delta); - fail(); - } - } - - function assertApproxEqAbs(uint256 a, uint256 b, uint256 maxDelta, string memory err) internal virtual { - uint256 delta = stdMath.delta(a, b); - - if (delta > maxDelta) { - emit log_named_string("Error", err); - assertApproxEqAbs(a, b, maxDelta); - } - } - - function assertApproxEqAbsDecimal(uint256 a, uint256 b, uint256 maxDelta, uint256 decimals) internal virtual { - uint256 delta = stdMath.delta(a, b); - - if (delta > maxDelta) { - emit log("Error: a ~= b not satisfied [uint]"); - emit log_named_decimal_uint(" Left", a, decimals); - emit log_named_decimal_uint(" Right", b, decimals); - emit log_named_decimal_uint(" Max Delta", maxDelta, decimals); - emit log_named_decimal_uint(" Delta", delta, decimals); - fail(); - } - } - - function assertApproxEqAbsDecimal(uint256 a, uint256 b, uint256 maxDelta, uint256 decimals, string memory err) - internal - virtual - { - uint256 delta = stdMath.delta(a, b); - - if (delta > maxDelta) { - emit log_named_string("Error", err); - assertApproxEqAbsDecimal(a, b, maxDelta, decimals); - } - } - - function assertApproxEqAbs(int256 a, int256 b, uint256 maxDelta) internal virtual { - uint256 delta = stdMath.delta(a, b); - - if (delta > maxDelta) { - emit log("Error: a ~= b not satisfied [int]"); - emit log_named_int(" Left", a); - emit log_named_int(" Right", b); - emit log_named_uint(" Max Delta", maxDelta); - emit log_named_uint(" Delta", delta); - fail(); - } - } - - function assertApproxEqAbs(int256 a, int256 b, uint256 maxDelta, string memory err) internal virtual { - uint256 delta = stdMath.delta(a, b); - - if (delta > maxDelta) { - emit log_named_string("Error", err); - assertApproxEqAbs(a, b, maxDelta); - } - } - - function assertApproxEqAbsDecimal(int256 a, int256 b, uint256 maxDelta, uint256 decimals) internal virtual { - uint256 delta = stdMath.delta(a, b); - - if (delta > maxDelta) { - emit log("Error: a ~= b not satisfied [int]"); - emit log_named_decimal_int(" Left", a, decimals); - emit log_named_decimal_int(" Right", b, decimals); - emit log_named_decimal_uint(" Max Delta", maxDelta, decimals); - emit log_named_decimal_uint(" Delta", delta, decimals); - fail(); - } - } - - function assertApproxEqAbsDecimal(int256 a, int256 b, uint256 maxDelta, uint256 decimals, string memory err) - internal - virtual - { - uint256 delta = stdMath.delta(a, b); - - if (delta > maxDelta) { - emit log_named_string("Error", err); - assertApproxEqAbsDecimal(a, b, maxDelta, decimals); - } - } - - function assertApproxEqRel( - uint256 a, - uint256 b, - uint256 maxPercentDelta // An 18 decimal fixed point number, where 1e18 == 100% - ) internal virtual { - if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. - - uint256 percentDelta = stdMath.percentDelta(a, b); - - if (percentDelta > maxPercentDelta) { - emit log("Error: a ~= b not satisfied [uint]"); - emit log_named_uint(" Left", a); - emit log_named_uint(" Right", b); - emit log_named_decimal_uint(" Max % Delta", maxPercentDelta * 100, 18); - emit log_named_decimal_uint(" % Delta", percentDelta * 100, 18); - fail(); - } - } - - function assertApproxEqRel( - uint256 a, - uint256 b, - uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% - string memory err - ) internal virtual { - if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. - - uint256 percentDelta = stdMath.percentDelta(a, b); - - if (percentDelta > maxPercentDelta) { - emit log_named_string("Error", err); - assertApproxEqRel(a, b, maxPercentDelta); - } - } - - function assertApproxEqRelDecimal( - uint256 a, - uint256 b, - uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% - uint256 decimals - ) internal virtual { - if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. - - uint256 percentDelta = stdMath.percentDelta(a, b); - - if (percentDelta > maxPercentDelta) { - emit log("Error: a ~= b not satisfied [uint]"); - emit log_named_decimal_uint(" Left", a, decimals); - emit log_named_decimal_uint(" Right", b, decimals); - emit log_named_decimal_uint(" Max % Delta", maxPercentDelta * 100, 18); - emit log_named_decimal_uint(" % Delta", percentDelta * 100, 18); - fail(); - } - } - - function assertApproxEqRelDecimal( - uint256 a, - uint256 b, - uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% - uint256 decimals, - string memory err - ) internal virtual { - if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. - - uint256 percentDelta = stdMath.percentDelta(a, b); - - if (percentDelta > maxPercentDelta) { - emit log_named_string("Error", err); - assertApproxEqRelDecimal(a, b, maxPercentDelta, decimals); - } - } - - function assertApproxEqRel(int256 a, int256 b, uint256 maxPercentDelta) internal virtual { - if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. - - uint256 percentDelta = stdMath.percentDelta(a, b); - - if (percentDelta > maxPercentDelta) { - emit log("Error: a ~= b not satisfied [int]"); - emit log_named_int(" Left", a); - emit log_named_int(" Right", b); - emit log_named_decimal_uint(" Max % Delta", maxPercentDelta * 100, 18); - emit log_named_decimal_uint(" % Delta", percentDelta * 100, 18); - fail(); - } - } - - function assertApproxEqRel(int256 a, int256 b, uint256 maxPercentDelta, string memory err) internal virtual { - if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. - - uint256 percentDelta = stdMath.percentDelta(a, b); - - if (percentDelta > maxPercentDelta) { - emit log_named_string("Error", err); - assertApproxEqRel(a, b, maxPercentDelta); - } - } - - function assertApproxEqRelDecimal(int256 a, int256 b, uint256 maxPercentDelta, uint256 decimals) internal virtual { - if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. - - uint256 percentDelta = stdMath.percentDelta(a, b); - - if (percentDelta > maxPercentDelta) { - emit log("Error: a ~= b not satisfied [int]"); - emit log_named_decimal_int(" Left", a, decimals); - emit log_named_decimal_int(" Right", b, decimals); - emit log_named_decimal_uint(" Max % Delta", maxPercentDelta * 100, 18); - emit log_named_decimal_uint(" % Delta", percentDelta * 100, 18); - fail(); - } - } - - function assertApproxEqRelDecimal(int256 a, int256 b, uint256 maxPercentDelta, uint256 decimals, string memory err) - internal - virtual - { - if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. - - uint256 percentDelta = stdMath.percentDelta(a, b); - - if (percentDelta > maxPercentDelta) { - emit log_named_string("Error", err); - assertApproxEqRelDecimal(a, b, maxPercentDelta, decimals); - } - } - - function assertEqCall(address target, bytes memory callDataA, bytes memory callDataB) internal virtual { - assertEqCall(target, callDataA, target, callDataB, true); - } - - function assertEqCall(address targetA, bytes memory callDataA, address targetB, bytes memory callDataB) - internal - virtual - { - assertEqCall(targetA, callDataA, targetB, callDataB, true); - } - - function assertEqCall(address target, bytes memory callDataA, bytes memory callDataB, bool strictRevertData) - internal - virtual - { - assertEqCall(target, callDataA, target, callDataB, strictRevertData); - } - - function assertEqCall( - address targetA, - bytes memory callDataA, - address targetB, - bytes memory callDataB, - bool strictRevertData - ) internal virtual { - (bool successA, bytes memory returnDataA) = address(targetA).call(callDataA); - (bool successB, bytes memory returnDataB) = address(targetB).call(callDataB); - - if (successA && successB) { - assertEq(returnDataA, returnDataB, "Call return data does not match"); - } - - if (!successA && !successB && strictRevertData) { - assertEq(returnDataA, returnDataB, "Call revert data does not match"); - } - - if (!successA && successB) { - emit log("Error: Calls were not equal"); - emit log_named_bytes(" Left call revert data", returnDataA); - emit log_named_bytes(" Right call return data", returnDataB); - fail(); - } - - if (successA && !successB) { - emit log("Error: Calls were not equal"); - emit log_named_bytes(" Left call return data", returnDataA); - emit log_named_bytes(" Right call revert data", returnDataB); - fail(); - } - } + event log(string); + event logs(bytes); + + event log_address(address); + event log_bytes32(bytes32); + event log_int(int256); + event log_uint(uint256); + event log_bytes(bytes); + event log_string(string); + + event log_named_address(string key, address val); + event log_named_bytes32(string key, bytes32 val); + event log_named_decimal_int(string key, int256 val, uint256 decimals); + event log_named_decimal_uint(string key, uint256 val, uint256 decimals); + event log_named_int(string key, int256 val); + event log_named_uint(string key, uint256 val); + event log_named_bytes(string key, bytes val); + event log_named_string(string key, string val); + event log_array(uint256[] val); + event log_array(int256[] val); + event log_array(address[] val); + event log_named_array(string key, uint256[] val); + event log_named_array(string key, int256[] val); + event log_named_array(string key, address[] val); + + function fail(string memory err) internal virtual { + emit log_named_string('Error', err); + fail(); + } + + function fail() internal virtual { + assert(false); + } + + function assertTrue(bool condition) internal { + if (!condition) { + emit log('Error: Assertion Failed'); + fail(); + } + } + + function assertTrue(bool condition, string memory err) internal { + if (!condition) { + emit log_named_string('Error', err); + assertTrue(condition); + } + } + + function assertFalse(bool data) internal virtual { + assertTrue(!data); + } + + function assertFalse(bool data, string memory err) internal virtual { + assertTrue(!data, err); + } + + function checkEq0(bytes memory a, bytes memory b) internal pure returns (bool ok) { + ok = true; + if (a.length == b.length) { + for (uint256 i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + ok = false; + } + } + } else { + ok = false; + } + } + + function assertEq0(bytes memory a, bytes memory b) internal { + if (!checkEq0(a, b)) { + emit log('Error: a == b not satisfied [bytes]'); + emit log_named_bytes(' Expected', a); + emit log_named_bytes(' Actual', b); + fail(); + } + } + + function assertEq0(bytes memory a, bytes memory b, string memory err) internal { + if (!checkEq0(a, b)) { + emit log_named_string('Error', err); + assertEq0(a, b); + } + } + + function assertEq(bool a, bool b) internal virtual { + if (a != b) { + emit log('Error: a == b not satisfied [bool]'); + emit log_named_string(' Left', a ? 'true' : 'false'); + emit log_named_string(' Right', b ? 'true' : 'false'); + fail(); + } + } + + function assertEq(bool a, bool b, string memory err) internal virtual { + if (a != b) { + emit log_named_string('Error', err); + assertEq(a, b); + } + } + + function assertEq(bytes memory a, bytes memory b) internal virtual { + assertEq0(a, b); + } + + function assertEq(bytes memory a, bytes memory b, string memory err) internal virtual { + assertEq0(a, b, err); + } + + function assertEq(uint256[] memory a, uint256[] memory b) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log('Error: a == b not satisfied [uint[]]'); + emit log_named_array(' Left', a); + emit log_named_array(' Right', b); + fail(); + } + } + + function assertEq(int256[] memory a, int256[] memory b) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log('Error: a == b not satisfied [int[]]'); + emit log_named_array(' Left', a); + emit log_named_array(' Right', b); + fail(); + } + } + + function assertEq(address[] memory a, address[] memory b) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log('Error: a == b not satisfied [address[]]'); + emit log_named_array(' Left', a); + emit log_named_array(' Right', b); + fail(); + } + } + + function assertEq(uint256[] memory a, uint256[] memory b, string memory err) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log_named_string('Error', err); + assertEq(a, b); + } + } + + function assertEq(int256[] memory a, int256[] memory b, string memory err) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log_named_string('Error', err); + assertEq(a, b); + } + } + + function assertEq(address[] memory a, address[] memory b, string memory err) internal virtual { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log_named_string('Error', err); + assertEq(a, b); + } + } + + // Legacy helper + function assertEqUint(uint256 a, uint256 b) internal virtual { + assertEq(uint256(a), uint256(b)); + } + + function assertApproxEqAbs(uint256 a, uint256 b, uint256 maxDelta) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log('Error: a ~= b not satisfied [uint]'); + emit log_named_uint(' Left', a); + emit log_named_uint(' Right', b); + emit log_named_uint(' Max Delta', maxDelta); + emit log_named_uint(' Delta', delta); + fail(); + } + } + + function assertApproxEqAbs( + uint256 a, + uint256 b, + uint256 maxDelta, + string memory err + ) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log_named_string('Error', err); + assertApproxEqAbs(a, b, maxDelta); + } + } + + function assertApproxEqAbsDecimal( + uint256 a, + uint256 b, + uint256 maxDelta, + uint256 decimals + ) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log('Error: a ~= b not satisfied [uint]'); + emit log_named_decimal_uint(' Left', a, decimals); + emit log_named_decimal_uint(' Right', b, decimals); + emit log_named_decimal_uint(' Max Delta', maxDelta, decimals); + emit log_named_decimal_uint(' Delta', delta, decimals); + fail(); + } + } + + function assertApproxEqAbsDecimal( + uint256 a, + uint256 b, + uint256 maxDelta, + uint256 decimals, + string memory err + ) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log_named_string('Error', err); + assertApproxEqAbsDecimal(a, b, maxDelta, decimals); + } + } + + function assertApproxEqAbs(int256 a, int256 b, uint256 maxDelta) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log('Error: a ~= b not satisfied [int]'); + emit log_named_int(' Left', a); + emit log_named_int(' Right', b); + emit log_named_uint(' Max Delta', maxDelta); + emit log_named_uint(' Delta', delta); + fail(); + } + } + + function assertApproxEqAbs( + int256 a, + int256 b, + uint256 maxDelta, + string memory err + ) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log_named_string('Error', err); + assertApproxEqAbs(a, b, maxDelta); + } + } + + function assertApproxEqAbsDecimal( + int256 a, + int256 b, + uint256 maxDelta, + uint256 decimals + ) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log('Error: a ~= b not satisfied [int]'); + emit log_named_decimal_int(' Left', a, decimals); + emit log_named_decimal_int(' Right', b, decimals); + emit log_named_decimal_uint(' Max Delta', maxDelta, decimals); + emit log_named_decimal_uint(' Delta', delta, decimals); + fail(); + } + } + + function assertApproxEqAbsDecimal( + int256 a, + int256 b, + uint256 maxDelta, + uint256 decimals, + string memory err + ) internal virtual { + uint256 delta = stdMath.delta(a, b); + + if (delta > maxDelta) { + emit log_named_string('Error', err); + assertApproxEqAbsDecimal(a, b, maxDelta, decimals); + } + } + + function assertApproxEqRel( + uint256 a, + uint256 b, + uint256 maxPercentDelta // An 18 decimal fixed point number, where 1e18 == 100% + ) internal virtual { + if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log('Error: a ~= b not satisfied [uint]'); + emit log_named_uint(' Left', a); + emit log_named_uint(' Right', b); + emit log_named_decimal_uint(' Max % Delta', maxPercentDelta * 100, 18); + emit log_named_decimal_uint(' % Delta', percentDelta * 100, 18); + fail(); + } + } + + function assertApproxEqRel( + uint256 a, + uint256 b, + uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% + string memory err + ) internal virtual { + if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log_named_string('Error', err); + assertApproxEqRel(a, b, maxPercentDelta); + } + } + + function assertApproxEqRelDecimal( + uint256 a, + uint256 b, + uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% + uint256 decimals + ) internal virtual { + if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log('Error: a ~= b not satisfied [uint]'); + emit log_named_decimal_uint(' Left', a, decimals); + emit log_named_decimal_uint(' Right', b, decimals); + emit log_named_decimal_uint(' Max % Delta', maxPercentDelta * 100, 18); + emit log_named_decimal_uint(' % Delta', percentDelta * 100, 18); + fail(); + } + } + + function assertApproxEqRelDecimal( + uint256 a, + uint256 b, + uint256 maxPercentDelta, // An 18 decimal fixed point number, where 1e18 == 100% + uint256 decimals, + string memory err + ) internal virtual { + if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log_named_string('Error', err); + assertApproxEqRelDecimal(a, b, maxPercentDelta, decimals); + } + } + + function assertApproxEqRel(int256 a, int256 b, uint256 maxPercentDelta) internal virtual { + if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log('Error: a ~= b not satisfied [int]'); + emit log_named_int(' Left', a); + emit log_named_int(' Right', b); + emit log_named_decimal_uint(' Max % Delta', maxPercentDelta * 100, 18); + emit log_named_decimal_uint(' % Delta', percentDelta * 100, 18); + fail(); + } + } + + function assertApproxEqRel( + int256 a, + int256 b, + uint256 maxPercentDelta, + string memory err + ) internal virtual { + if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log_named_string('Error', err); + assertApproxEqRel(a, b, maxPercentDelta); + } + } + + function assertApproxEqRelDecimal( + int256 a, + int256 b, + uint256 maxPercentDelta, + uint256 decimals + ) internal virtual { + if (b == 0) return assertEq(a, b); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log('Error: a ~= b not satisfied [int]'); + emit log_named_decimal_int(' Left', a, decimals); + emit log_named_decimal_int(' Right', b, decimals); + emit log_named_decimal_uint(' Max % Delta', maxPercentDelta * 100, 18); + emit log_named_decimal_uint(' % Delta', percentDelta * 100, 18); + fail(); + } + } + + function assertApproxEqRelDecimal( + int256 a, + int256 b, + uint256 maxPercentDelta, + uint256 decimals, + string memory err + ) internal virtual { + if (b == 0) return assertEq(a, b, err); // If the left is 0, right must be too. + + uint256 percentDelta = stdMath.percentDelta(a, b); + + if (percentDelta > maxPercentDelta) { + emit log_named_string('Error', err); + assertApproxEqRelDecimal(a, b, maxPercentDelta, decimals); + } + } + + function assertEqCall( + address target, + bytes memory callDataA, + bytes memory callDataB + ) internal virtual { + assertEqCall(target, callDataA, target, callDataB, true); + } + + function assertEqCall( + address targetA, + bytes memory callDataA, + address targetB, + bytes memory callDataB + ) internal virtual { + assertEqCall(targetA, callDataA, targetB, callDataB, true); + } + + function assertEqCall( + address target, + bytes memory callDataA, + bytes memory callDataB, + bool strictRevertData + ) internal virtual { + assertEqCall(target, callDataA, target, callDataB, strictRevertData); + } + + function assertEqCall( + address targetA, + bytes memory callDataA, + address targetB, + bytes memory callDataB, + bool strictRevertData + ) internal virtual { + (bool successA, bytes memory returnDataA) = address(targetA).call(callDataA); + (bool successB, bytes memory returnDataB) = address(targetB).call(callDataB); + + if (successA && successB) { + assertEq(returnDataA, returnDataB, 'Call return data does not match'); + } + + if (!successA && !successB && strictRevertData) { + assertEq(returnDataA, returnDataB, 'Call revert data does not match'); + } + + if (!successA && successB) { + emit log('Error: Calls were not equal'); + emit log_named_bytes(' Left call revert data', returnDataA); + emit log_named_bytes(' Right call return data', returnDataB); + fail(); + } + + if (successA && !successB) { + emit log('Error: Calls were not equal'); + emit log_named_bytes(' Left call return data', returnDataA); + emit log_named_bytes(' Right call revert data', returnDataB); + fail(); + } + } } From a26d6a7b18fd628cf771fc2ebe32d683c11bc0c6 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:51:23 +0530 Subject: [PATCH 034/106] chore: checkpoint --- medusa.protocol.json | 31 +++++-- tests/invariants/hub-suite/Setup.t.sol | 23 +++-- .../hub-suite/base/BaseStorage.t.sol | 2 +- .../handlers/HubConfiguratorHandler.t.sol | 4 +- .../hub-suite/handlers/HubHandler.t.sol | 2 +- .../hub-suite/replays/ReplayTest_1.t.sol | 2 +- tests/invariants/protocol-suite/Setup.t.sol | 84 +++++++------------ .../protocol-suite/base/BaseStorage.t.sol | 4 +- .../handlers/hub/HubConfiguratorHandler.t.sol | 4 +- .../spoke/SpokeConfiguratorHandler.t.sol | 4 +- .../handlers/spoke/SpokeHandler.t.sol | 44 +++++----- .../hooks/DefaultBeforeAfterHooks.t.sol | 12 ++- .../invariants/SpokeInvariants.t.sol | 2 +- 13 files changed, 102 insertions(+), 116 deletions(-) diff --git a/medusa.protocol.json b/medusa.protocol.json index 3e075f680..c1e0cb662 100644 --- a/medusa.protocol.json +++ b/medusa.protocol.json @@ -5,10 +5,14 @@ "timeout": 0, "testLimit": 0, "callSequenceLength": 100, - "corpusDirectory": "tests/enigma-dark-invariants/protocol-suite/_corpus/medusa", + "corpusDirectory": "tests/invariants/protocol-suite/_corpus/medusa", "coverageEnabled": true, - "deploymentOrder": ["Tester"], - "targetContracts": ["Tester"], + "deploymentOrder": [ + "Tester" + ], + "targetContracts": [ + "Tester" + ], "targetContractsBalances": [ "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" ], @@ -17,7 +21,11 @@ }, "constructorArgs": {}, "deployerAddress": "0x30000", - "senderAddresses": ["0x10000", "0x20000", "0x30000"], + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], "blockNumberDelayMax": 60480, "blockTimestampDelayMax": 604800, "blockGasLimit": 12500000000, @@ -46,13 +54,20 @@ }, "propertyTesting": { "enabled": true, - "testPrefixes": ["fuzz_", "invariant_"] + "testPrefixes": [ + "fuzz_", + "invariant_" + ] }, "optimizationTesting": { "enabled": false, - "testPrefixes": ["optimize_"] + "testPrefixes": [ + "optimize_" + ] }, - "excludeFunctionSignatures": ["Tester.checkPostConditions()"] + "excludeFunctionSignatures": [ + "Tester.checkPostConditions()" + ] }, "chainConfig": { "codeSizeCheckDisabled": true, @@ -65,7 +80,7 @@ "compilation": { "platform": "crytic-compile", "platformConfig": { - "target": "tests/enigma-dark-invariants/protocol-suite/Tester.t.sol", + "target": "tests/invariants/protocol-suite/Tester.t.sol", "solcVersion": "", "exportDirectory": "", "args": [ diff --git a/tests/invariants/hub-suite/Setup.t.sol b/tests/invariants/hub-suite/Setup.t.sol index 955e48733..7a4eccd64 100644 --- a/tests/invariants/hub-suite/Setup.t.sol +++ b/tests/invariants/hub-suite/Setup.t.sol @@ -9,18 +9,17 @@ import {Actor} from '../shared/utils/Actor.sol'; import 'forge-std/console.sol'; // Interfaces -import {IHub} from 'src/hub/Hub.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; // Test Contracts import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; // Contracts import {BaseTest} from './base/BaseTest.t.sol'; +import {DeployUtils} from 'tests/DeployUtils.sol'; import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; -import {Hub} from 'src/hub/Hub.sol'; -import {Spoke} from 'src/spoke/Spoke.sol'; import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; /// @notice Setup contract for the invariant test Suite, inherited by Tester @@ -69,7 +68,7 @@ contract Setup is BaseTest { accessManager = new AccessManager(admin); // Hub 1 - hub = new Hub(address(accessManager)); + hub = DeployUtils.deployHub(address(accessManager)); irStrategy = new AssetInterestRateStrategy(address(hub)); // Configurators @@ -187,7 +186,7 @@ contract Setup is BaseTest { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub.addSpoke( @@ -198,7 +197,7 @@ contract Setup is BaseTest { drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub.addSpoke( @@ -209,7 +208,7 @@ contract Setup is BaseTest { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); @@ -222,7 +221,7 @@ contract Setup is BaseTest { drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub.addSpoke( @@ -233,7 +232,7 @@ contract Setup is BaseTest { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); @@ -246,7 +245,7 @@ contract Setup is BaseTest { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub.addSpoke( @@ -257,7 +256,7 @@ contract Setup is BaseTest { drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub.addSpoke( @@ -268,7 +267,7 @@ contract Setup is BaseTest { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); diff --git a/tests/invariants/hub-suite/base/BaseStorage.t.sol b/tests/invariants/hub-suite/base/BaseStorage.t.sol index 87892350f..604076cf0 100644 --- a/tests/invariants/hub-suite/base/BaseStorage.t.sol +++ b/tests/invariants/hub-suite/base/BaseStorage.t.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.19; // Contracts import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; -import {IHub} from 'src/hub/Hub.sol'; import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; // Utils import {Actor} from '../../shared/utils/Actor.sol'; diff --git a/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol index 3b3afc936..fd6ce3a49 100644 --- a/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol +++ b/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol @@ -52,10 +52,10 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { ); } - function updateSpokePaused(bool paused, uint8 i, uint8 j) external setup { + function updateSpokeHalted(bool halted, uint8 i, uint8 j) external setup { uint256 assetId = _getRandomBaseAssetId(i); address spoke = _getRandomActor(j); - hubConfigurator.updateSpokePaused(address(hub), assetId, spoke, paused); + hubConfigurator.updateSpokeHalted(address(hub), assetId, spoke, halted); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/invariants/hub-suite/handlers/HubHandler.t.sol b/tests/invariants/hub-suite/handlers/HubHandler.t.sol index 377e766af..4c2aad51d 100644 --- a/tests/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/tests/invariants/hub-suite/handlers/HubHandler.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; // Interfaces import {IERC20} from 'forge-std/interfaces/IERC20.sol'; -import {IHub, IHubBase} from 'src/hub/Hub.sol'; +import {IHub, IHubBase} from 'src/hub/interfaces/IHub.sol'; import {IHubHandler} from './interfaces/IHubHandler.sol'; // Libraries diff --git a/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol b/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol index e6a652c4a..2ab57c299 100644 --- a/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol +++ b/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol @@ -40,7 +40,7 @@ contract ReplayTest1 is Invariants, Setup { function test_replay_1_add() public { _setUpActor(USER3); _delay(67960); - Tester.updateSpokePaused(true, 26, 105); + Tester.updateSpokeHalted(true, 26, 105); _delay(289607); Tester.add(1524785992, 171); } diff --git a/tests/invariants/protocol-suite/Setup.t.sol b/tests/invariants/protocol-suite/Setup.t.sol index 8b697964c..2d70747cc 100644 --- a/tests/invariants/protocol-suite/Setup.t.sol +++ b/tests/invariants/protocol-suite/Setup.t.sol @@ -10,9 +10,10 @@ import 'forge-std/console.sol'; // Interfaces import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; -import {ISpoke} from 'src/spoke/Spoke.sol'; -import {ITreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; -import {IHub} from 'src/hub/Hub.sol'; +import {ISpokeInstance} from 'tests/mocks/ISpokeInstance.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {ITreasurySpoke} from 'src/spoke/interfaces/ITreasurySpoke.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; // Test Contracts import {Actor} from '../shared/utils/Actor.sol'; @@ -21,14 +22,10 @@ import {MockPriceFeedSimulator} from '../shared/mocks/MockPriceFeedSimulator.sol // Contracts import {BaseTest} from './base/BaseTest.t.sol'; -import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; -import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; +import {DeployUtils} from 'tests/DeployUtils.sol'; +import {AssetInterestRateStrategy, IAssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; -import {Hub} from 'src/hub/Hub.sol'; import {TreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; -import {SpokeInstance} from 'src/spoke/instances/SpokeInstance.sol'; -import {Spoke} from 'src/spoke/Spoke.sol'; -import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; import {AaveOracle} from 'src/spoke/AaveOracle.sol'; import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; import {SpokeConfigurator} from 'src/spoke/SpokeConfigurator.sol'; @@ -76,7 +73,7 @@ contract Setup is BaseTest { accessManager = new AccessManager(admin); // Hub 1 - hub1 = new Hub(address(accessManager)); + hub1 = DeployUtils.deployHub(address(accessManager)); irStrategy1 = new AssetInterestRateStrategy(address(hub1)); hubInfo[address(hub1)] = HubInfo({ treasurySpoke: address(treasurySpoke1), @@ -85,7 +82,7 @@ contract Setup is BaseTest { hubAddresses.push(address(hub1)); // Hub 2 - hub2 = new Hub(address(accessManager)); + hub2 = DeployUtils.deployHub(address(accessManager)); irStrategy2 = new AssetInterestRateStrategy(address(hub2)); hubInfo[address(hub2)] = HubInfo({ treasurySpoke: address(treasurySpoke2), @@ -121,32 +118,29 @@ contract Setup is BaseTest { vm.label(address(oracle2), 'oracle2'); } - /// @notice Deploy a spoke with an oracle using CREATE3 + /// @notice Deploy a spoke with an oracle function _deploySpokeWithOracle( address proxyAdminOwner, address _accessManager, string memory _oracleDesc ) internal returns (ISpoke, IAaveOracle) { - bytes32 salt = keccak256(abi.encodePacked(_oracleDesc)); - address predictedSpoke = CREATE3.predictDeterministicAddress(salt, admin); + address deployer = _makeAddr('deployer'); - // Deploy oracle with predicted spoke address - IAaveOracle oracle = new AaveOracle(predictedSpoke, uint8(8), _oracleDesc); + vm.startPrank(deployer); + IAaveOracle oracle = new AaveOracle(8, _oracleDesc); - // Deploy spoke implementation with oracle address - address spokeImpl = address(new SpokeInstance(address(oracle))); - - // Deploy spoke proxy using CREATE3 - bytes memory proxyCreationCode = abi.encodePacked( - type(TransparentUpgradeableProxy).creationCode, - abi.encode(spokeImpl, proxyAdminOwner, abi.encodeCall(Spoke.initialize, (_accessManager))) + ISpoke spoke = DeployUtils.deploySpoke( + address(oracle), + Constants.MAX_ALLOWED_USER_RESERVES_LIMIT, + proxyAdminOwner, + abi.encodeCall(ISpokeInstance.initialize, (_accessManager)) ); - address spokeProxy = CREATE3.deployDeterministic(proxyCreationCode, salt); - ISpoke spoke = ISpoke(spokeProxy); - assertEq(address(spoke), predictedSpoke, 'predictedSpoke mismatch'); - assertEq(spoke.ORACLE(), address(oracle), 'spoke.ORACLE() mismatch'); - assertEq(oracle.SPOKE(), address(spoke), 'oracle.SPOKE() mismatch'); + oracle.setSpoke(address(spoke)); + vm.stopPrank(); + + assertEq(spoke.ORACLE(), address(oracle)); + assertEq(oracle.SPOKE(), address(spoke)); spokesAddresses.push(address(spoke)); allSpokes.push(address(spoke)); @@ -154,20 +148,6 @@ contract Setup is BaseTest { return (spoke, oracle); } - /// @notice Proxify an implementation contract using TransparentUpgradeableProxy - function _proxify( - address impl, - address proxyAdminOwner, - bytes memory initData - ) internal returns (address) { - TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( - impl, - proxyAdminOwner, - initData - ); - return address(proxy); - } - function _deployMockPriceFeed(ISpoke spoke, uint256 price) internal returns (address) { AaveOracle oracle = AaveOracle(spoke.ORACLE()); return address(new MockPriceFeedSimulator(oracle.DECIMALS(), oracle.DESCRIPTION(), price)); @@ -304,7 +284,6 @@ contract Setup is BaseTest { paused: false, frozen: false, borrowable: true, - liquidatable: true, receiveSharesEnabled: true, collateralRisk: 30_00 }); @@ -318,7 +297,6 @@ contract Setup is BaseTest { paused: false, frozen: false, borrowable: true, - liquidatable: true, receiveSharesEnabled: true, collateralRisk: 20_00 }); @@ -333,7 +311,6 @@ contract Setup is BaseTest { paused: false, frozen: false, borrowable: true, - liquidatable: true, receiveSharesEnabled: true, collateralRisk: 10_00 }); @@ -347,7 +324,6 @@ contract Setup is BaseTest { paused: false, frozen: false, borrowable: true, - liquidatable: true, receiveSharesEnabled: true, collateralRisk: 15_00 }); @@ -474,7 +450,7 @@ contract Setup is BaseTest { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub2.addSpoke( @@ -485,7 +461,7 @@ contract Setup is BaseTest { drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub1.addSpoke( @@ -496,7 +472,7 @@ contract Setup is BaseTest { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub2.addSpoke( @@ -507,7 +483,7 @@ contract Setup is BaseTest { drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); @@ -520,7 +496,7 @@ contract Setup is BaseTest { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub1.addSpoke( @@ -531,7 +507,7 @@ contract Setup is BaseTest { drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub2.addSpoke( @@ -542,7 +518,7 @@ contract Setup is BaseTest { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); hub1.addSpoke( @@ -553,7 +529,7 @@ contract Setup is BaseTest { drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, riskPremiumThreshold: Constants.MAX_RISK_PREMIUM_THRESHOLD, active: true, - paused: false + halted: false }) ); } diff --git a/tests/invariants/protocol-suite/base/BaseStorage.t.sol b/tests/invariants/protocol-suite/base/BaseStorage.t.sol index eeeca44b7..4f6938428 100644 --- a/tests/invariants/protocol-suite/base/BaseStorage.t.sol +++ b/tests/invariants/protocol-suite/base/BaseStorage.t.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.19; // Contracts import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; -import {IHub} from 'src/hub/Hub.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; import {ITreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; -import {ISpoke} from 'src/spoke/Spoke.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; diff --git a/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol b/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol index 37b356e76..98a8b5f29 100644 --- a/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol @@ -51,11 +51,11 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { hubConfigurator.updateSpokeRiskPremiumThreshold(hub, assetId, spoke, riskPremiumThreshold); } - function updateSpokePaused(bool paused, uint8 i, uint8 j, uint8 k) external setup { + function updateSpokeHalted(bool halted, uint8 i, uint8 j, uint8 k) external setup { address hub = _getRandomHub(i); uint256 assetId = _getRandomHubAssetId(hub, j); address spoke = _getRandomSpoke(k); - hubConfigurator.updateSpokePaused(hub, assetId, spoke, paused); + hubConfigurator.updateSpokeHalted(hub, assetId, spoke, halted); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol b/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol index 9f1df1d98..2ff1f2c21 100644 --- a/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol @@ -40,10 +40,10 @@ contract SpokeConfiguratorHandler is BaseHandler, ISpokeConfiguratorHandler { spokeConfigurator.updateLiquidationBonusFactor(spoke, liquidationBonusFactor); } - function updatePaused(bool paused, uint8 i, uint8 j) external setup { + function updatePaused(bool halted, uint8 i, uint8 j) external setup { address spoke = _getRandomSpoke(i); uint256 reserveId = _getRandomReserveId(spoke, j); - spokeConfigurator.updatePaused(spoke, reserveId, paused); + spokeConfigurator.updatePaused(spoke, reserveId, halted); } function updateFrozen(bool frozen, uint8 i, uint8 j) external setup { diff --git a/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index 0a2e911c1..b648ca258 100644 --- a/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -2,11 +2,13 @@ pragma solidity ^0.8.19; // Interfaces -import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {ISpoke, ISpokeBase} from 'src/spoke/interfaces/ISpoke.sol'; import {ISpokeHandler} from '../interfaces/ISpokeHandler.sol'; import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; // Libraries +import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {Constants} from 'tests/Constants.sol'; import 'forge-std/console.sol'; @@ -14,13 +16,11 @@ import 'forge-std/console.sol'; import {Actor} from '../../../shared/utils/Actor.sol'; import {BaseHandler} from '../../base/BaseHandler.t.sol'; -// Contracts -import {Spoke} from 'src/spoke/Spoke.sol'; -import {IHub} from 'src/hub/interfaces/IHub.sol'; - /// @title SpokeHandler /// @notice Handler test contract for a set of actions contract SpokeHandler is BaseHandler, ISpokeHandler { + using WadRayMath for uint256; + /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -67,7 +67,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy( spoke, - abi.encodeCall(Spoke.supply, (reserveId, amount, onBehalfOf)) + abi.encodeCall(ISpokeBase.supply, (reserveId, amount, onBehalfOf)) ); if (success) { @@ -100,7 +100,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy( spoke, - abi.encodeCall(Spoke.withdraw, (reserveId, amount, onBehalfOf)) + abi.encodeCall(ISpokeBase.withdraw, (reserveId, amount, onBehalfOf)) ); // Implemented outside the success check to assert success @@ -139,7 +139,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy( spoke, - abi.encodeCall(Spoke.borrow, (reserveId, amount, onBehalfOf)) + abi.encodeCall(ISpokeBase.borrow, (reserveId, amount, onBehalfOf)) ); if (success) { @@ -171,7 +171,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy( spoke, - abi.encodeCall(Spoke.repay, (reserveId, amount, onBehalfOf)) + abi.encodeCall(ISpokeBase.repay, (reserveId, amount, onBehalfOf)) ); if (success) { @@ -223,7 +223,8 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { liquidationVars.totalDebtValueBefore = ISpoke(liquidationVars.spoke) .getUserAccountData(_getRandomActor(i)) - .totalDebtValue; + .totalDebtValueRay + .fromRayUp(); // todo fix liquidationVars.reserveDebtBefore = ISpoke(liquidationVars.spoke).getReserveTotalDebt( liquidationVars.debtReserveId ); @@ -262,7 +263,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { (success, returnData) = actor.proxy( liquidationVars.spoke, abi.encodeCall( - Spoke.liquidationCall, + ISpokeBase.liquidationCall, ( liquidationVars.collateralReserveId, liquidationVars.debtReserveId, @@ -297,7 +298,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertLe( liquidationVars.debtLiquidated, defaultVarsBefore - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] .totalDebt, HSPOST_SP_LIQ_A ); @@ -317,7 +318,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (liquidationVars.totalDebtValueBefore < Constants.DUST_LIQUIDATION_THRESHOLD) { assertEq( defaultVarsAfter - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] .totalDebt, 0, HSPOST_SP_LIQ_C @@ -328,24 +329,21 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertLt( defaultVarsBefore - .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] - .healthFactor, + .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, HSPOST_SP_LIQ_E ); if ( defaultVarsAfter - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] .totalDebt > 0 ) { assertGt( defaultVarsAfter - .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] - .healthFactor, + .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, defaultVarsBefore - .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] - .healthFactor, + .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, HSPOST_SP_LIQ_G ); } @@ -381,7 +379,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy( spoke, - abi.encodeCall(Spoke.setUsingAsCollateral, (reserveId, usingAsCollateral, onBehalfOf)) + abi.encodeCall(ISpoke.setUsingAsCollateral, (reserveId, usingAsCollateral, onBehalfOf)) ); if (success) { @@ -407,7 +405,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy( spoke, - abi.encodeCall(Spoke.updateUserRiskPremium, (onBehalfOf)) + abi.encodeCall(ISpoke.updateUserRiskPremium, (onBehalfOf)) ); if (success) { @@ -433,7 +431,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _before(); (success, returnData) = actor.proxy( spoke, - abi.encodeCall(Spoke.updateUserDynamicConfig, (onBehalfOf)) + abi.encodeCall(ISpoke.updateUserDynamicConfig, (onBehalfOf)) ); if (success) { diff --git a/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index 9989287e8..4dd1bdd87 100644 --- a/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -137,14 +137,12 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { for (uint256 j; j < spokeReserveIds[userInfo.spoke].length; j++) { ( _defaultVars - .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] - .drawnDebt, + .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].drawnDebt, _defaultVars - .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] - .premiumDebt + .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].premiumDebt ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); _defaultVars - .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] .totalDebt = ISpoke(userInfo.spoke).getUserTotalDebt( spokeReserveIds[userInfo.spoke][j], userInfo.user @@ -299,9 +297,9 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function assert_GPOST_SP_E(address spoke, uint256 reserveId, address user) internal { // latest reserve key - uint24 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; + uint32 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; // user-stored key - uint24 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; + uint32 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; // Read the cached signature of the current action bytes4 signature = currentActionSignature; diff --git a/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol index eaa20c934..751a84179 100644 --- a/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol +++ b/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol @@ -80,7 +80,7 @@ abstract contract SpokeInvariants is HandlerAggregator { function assert_INV_SP_D(address spoke, address user) internal { ISpoke.UserAccountData memory d = ISpoke(spoke).getUserAccountData(user); if (d.totalCollateralValue == 0) { - assertEq(d.totalDebtValue, 0, INV_SP_D); + assertEq(d.totalDebtValueRay, 0, INV_SP_D); // todo fix } } From 1a02aa78c78a33fa265ac0c87a4e4c1e214ad666 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:53:04 +0530 Subject: [PATCH 035/106] chore: debug --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 4fdd4cf5b..8faf2b272 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,7 +11,7 @@ optimizer_runs = 444444444444 bytecode_hash = "none" gas_snapshot_check = false gas_limit = 1099511627776 -dynamic_test_linking = true +dynamic_test_linking = false # https://github.com/crytic/crytic-compile/issues/651#issuecomment-3813020679 additional_compiler_profiles = [ { name = "hub", optimizer = true, via_ir = true, optimizer_runs = 22_300 }, From f909853a7c1fdfd13bff32a141c381c49c32b79e Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:33:33 +0530 Subject: [PATCH 036/106] feat: foundry-invariants working --- tests/DeployUtils.sol | 17 ++++ tests/invariants/protocol-suite/Setup.t.sol | 87 +++++++++++++++++-- .../protocol-suite/base/BaseTest.t.sol | 2 +- 3 files changed, 97 insertions(+), 9 deletions(-) diff --git a/tests/DeployUtils.sol b/tests/DeployUtils.sol index e928effd6..84a367b27 100644 --- a/tests/DeployUtils.sol +++ b/tests/DeployUtils.sol @@ -47,6 +47,23 @@ library DeployUtils { ); } + function deploySpoke( + address oracle, + bytes32 implSalt, + uint16 maxUserReservesLimit, + address proxyAdminOwner, + bytes memory initData + ) internal returns (ISpoke) { + return + ISpoke( + proxify( + address(deploySpokeImplementation(oracle, maxUserReservesLimit, implSalt)), + proxyAdminOwner, + initData + ) + ); + } + function getDeterministicSpokeInstanceAddress( address oracle, uint16 maxUserReservesLimit diff --git a/tests/invariants/protocol-suite/Setup.t.sol b/tests/invariants/protocol-suite/Setup.t.sol index 2d70747cc..22fd0fb16 100644 --- a/tests/invariants/protocol-suite/Setup.t.sol +++ b/tests/invariants/protocol-suite/Setup.t.sol @@ -23,12 +23,15 @@ import {MockPriceFeedSimulator} from '../shared/mocks/MockPriceFeedSimulator.sol // Contracts import {BaseTest} from './base/BaseTest.t.sol'; import {DeployUtils} from 'tests/DeployUtils.sol'; -import {AssetInterestRateStrategy, IAssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import { + AssetInterestRateStrategy, + IAssetInterestRateStrategy +} from 'src/hub/AssetInterestRateStrategy.sol'; import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; import {TreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; import {AaveOracle} from 'src/spoke/AaveOracle.sol'; -import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; -import {SpokeConfigurator} from 'src/spoke/SpokeConfigurator.sol'; +import {HubConfigurator, IHubConfigurator} from 'src/hub/HubConfigurator.sol'; +import {SpokeConfigurator, ISpokeConfigurator} from 'src/spoke/SpokeConfigurator.sol'; /// @notice Setup contract for the invariant test Suite, inherited by Tester contract Setup is BaseTest { @@ -73,7 +76,7 @@ contract Setup is BaseTest { accessManager = new AccessManager(admin); // Hub 1 - hub1 = DeployUtils.deployHub(address(accessManager)); + hub1 = DeployUtils.deployHub(address(accessManager), 'hub1'); irStrategy1 = new AssetInterestRateStrategy(address(hub1)); hubInfo[address(hub1)] = HubInfo({ treasurySpoke: address(treasurySpoke1), @@ -82,7 +85,7 @@ contract Setup is BaseTest { hubAddresses.push(address(hub1)); // Hub 2 - hub2 = DeployUtils.deployHub(address(accessManager)); + hub2 = DeployUtils.deployHub(address(accessManager), 'hub2'); irStrategy2 = new AssetInterestRateStrategy(address(hub2)); hubInfo[address(hub2)] = HubInfo({ treasurySpoke: address(treasurySpoke2), @@ -99,8 +102,8 @@ contract Setup is BaseTest { allSpokes.push(address(treasurySpoke2)); // Configurators - hubConfigurator = new HubConfigurator(admin); - spokeConfigurator = new SpokeConfigurator(admin); + hubConfigurator = new HubConfigurator(address(accessManager)); + spokeConfigurator = new SpokeConfigurator(address(accessManager)); _setUpConfiguratorRoles(); vm.label(address(accessManager), 'accessManager'); @@ -131,6 +134,7 @@ contract Setup is BaseTest { ISpoke spoke = DeployUtils.deploySpoke( address(oracle), + 'spoke1', Constants.MAX_ALLOWED_USER_RESERVES_LIMIT, proxyAdminOwner, abi.encodeCall(ISpokeInstance.initialize, (_accessManager)) @@ -542,7 +546,7 @@ contract Setup is BaseTest { // Grant responsibilities to spokes { - bytes4[] memory selectors = new bytes4[](6); + bytes4[] memory selectors = new bytes4[](7); selectors[0] = ISpoke.updateLiquidationConfig.selector; selectors[1] = ISpoke.updateReserveConfig.selector; selectors[2] = ISpoke.updateDynamicReserveConfig.selector; @@ -561,6 +565,73 @@ contract Setup is BaseTest { accessManager.setTargetFunctionRole(address(hub1), selectors, Roles.HUB_ADMIN_ROLE); accessManager.setTargetFunctionRole(address(hub2), selectors, Roles.HUB_ADMIN_ROLE); } + + { + bytes4[] memory selectors = new bytes4[](22); + selectors[0] = IHubConfigurator.updateLiquidityFee.selector; + selectors[1] = IHubConfigurator.updateFeeReceiver.selector; + selectors[2] = IHubConfigurator.updateFeeConfig.selector; + selectors[3] = IHubConfigurator.updateInterestRateStrategy.selector; + selectors[4] = IHubConfigurator.updateReinvestmentController.selector; + selectors[5] = IHubConfigurator.resetAssetCaps.selector; + selectors[6] = IHubConfigurator.deactivateAsset.selector; + selectors[7] = IHubConfigurator.haltAsset.selector; + selectors[8] = IHubConfigurator.addSpoke.selector; + selectors[9] = IHubConfigurator.addSpokeToAssets.selector; + selectors[10] = IHubConfigurator.updateSpokeActive.selector; + selectors[11] = IHubConfigurator.updateSpokeHalted.selector; + selectors[12] = IHubConfigurator.updateSpokeSupplyCap.selector; + selectors[13] = IHubConfigurator.updateSpokeDrawCap.selector; + selectors[14] = IHubConfigurator.updateSpokeRiskPremiumThreshold.selector; + selectors[15] = IHubConfigurator.updateSpokeCaps.selector; + selectors[16] = IHubConfigurator.deactivateSpoke.selector; + selectors[17] = IHubConfigurator.haltSpoke.selector; + selectors[18] = IHubConfigurator.resetSpokeCaps.selector; + selectors[19] = IHubConfigurator.updateInterestRateData.selector; + selectors[20] = IHubConfigurator.addAsset.selector; + selectors[21] = IHubConfigurator.addAssetWithDecimals.selector; + + accessManager.setTargetFunctionRole( + address(hubConfigurator), + selectors, + accessManager.PUBLIC_ROLE() + ); + } + + { + bytes4[] memory selectors = new bytes4[](25); + selectors[0] = ISpokeConfigurator.updateReservePriceSource.selector; + selectors[1] = ISpokeConfigurator.updateLiquidationTargetHealthFactor.selector; + selectors[2] = ISpokeConfigurator.updateHealthFactorForMaxBonus.selector; + selectors[3] = ISpokeConfigurator.updateLiquidationBonusFactor.selector; + selectors[4] = ISpokeConfigurator.updateLiquidationConfig.selector; + selectors[5] = ISpokeConfigurator.updateMaxReserves.selector; + selectors[6] = ISpokeConfigurator.addReserve.selector; + selectors[7] = ISpokeConfigurator.updatePaused.selector; + selectors[8] = ISpokeConfigurator.updateFrozen.selector; + selectors[9] = ISpokeConfigurator.updateBorrowable.selector; + selectors[10] = ISpokeConfigurator.updateReceiveSharesEnabled.selector; + selectors[11] = ISpokeConfigurator.updateCollateralRisk.selector; + selectors[12] = ISpokeConfigurator.addCollateralFactor.selector; + selectors[13] = ISpokeConfigurator.updateCollateralFactor.selector; + selectors[14] = ISpokeConfigurator.addMaxLiquidationBonus.selector; + selectors[15] = ISpokeConfigurator.updateMaxLiquidationBonus.selector; + selectors[16] = ISpokeConfigurator.addLiquidationFee.selector; + selectors[17] = ISpokeConfigurator.updateLiquidationFee.selector; + selectors[18] = ISpokeConfigurator.addDynamicReserveConfig.selector; + selectors[19] = ISpokeConfigurator.updateDynamicReserveConfig.selector; + selectors[20] = ISpokeConfigurator.pauseAllReserves.selector; + selectors[21] = ISpokeConfigurator.freezeAllReserves.selector; + selectors[22] = ISpokeConfigurator.pauseReserve.selector; + selectors[23] = ISpokeConfigurator.freezeReserve.selector; + selectors[24] = ISpokeConfigurator.updatePositionManager.selector; + + accessManager.setTargetFunctionRole( + address(spokeConfigurator), + selectors, + accessManager.PUBLIC_ROLE() + ); + } } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tests/invariants/protocol-suite/base/BaseTest.t.sol b/tests/invariants/protocol-suite/base/BaseTest.t.sol index 46112c78a..ac83d8e56 100644 --- a/tests/invariants/protocol-suite/base/BaseTest.t.sol +++ b/tests/invariants/protocol-suite/base/BaseTest.t.sol @@ -38,7 +38,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @dev Solves medusa backward time warp issue modifier monotonicTimestamp() virtual { - /// @dev: Implement monotonic timestamp if needed + /// @dev Implement monotonic timestamp if needed _; } From 00e460f462f776ea5c78e8547a7b651db6e65635 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:33:59 +0530 Subject: [PATCH 037/106] chore: lint --- .../handlers/spoke/SpokeHandler.t.sol | 15 +++++++++------ .../hooks/DefaultBeforeAfterHooks.t.sol | 8 +++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index b648ca258..c43c6a14c 100644 --- a/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -298,7 +298,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertLe( liquidationVars.debtLiquidated, defaultVarsBefore - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] .totalDebt, HSPOST_SP_LIQ_A ); @@ -318,7 +318,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (liquidationVars.totalDebtValueBefore < Constants.DUST_LIQUIDATION_THRESHOLD) { assertEq( defaultVarsAfter - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] .totalDebt, 0, HSPOST_SP_LIQ_C @@ -329,21 +329,24 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertLt( defaultVarsBefore - .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, + .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] + .healthFactor, Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, HSPOST_SP_LIQ_E ); if ( defaultVarsAfter - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] .totalDebt > 0 ) { assertGt( defaultVarsAfter - .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, + .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] + .healthFactor, defaultVarsBefore - .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)].healthFactor, + .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] + .healthFactor, HSPOST_SP_LIQ_G ); } diff --git a/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index 4dd1bdd87..eb685733b 100644 --- a/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -137,12 +137,14 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { for (uint256 j; j < spokeReserveIds[userInfo.spoke].length; j++) { ( _defaultVars - .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].drawnDebt, + .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + .drawnDebt, _defaultVars - .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user].premiumDebt + .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + .premiumDebt ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); _defaultVars - .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] + .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] .totalDebt = ISpoke(userInfo.spoke).getUserTotalDebt( spokeReserveIds[userInfo.spoke][j], userInfo.user From 5e612c786bff2576b6f4ada661cc1c2d1de269b5 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:52:56 +0530 Subject: [PATCH 038/106] chore: move invariants to separate dir, hopefully tmp --- .github/workflows/enigma-dark-invariants.yml | 10 +++--- Makefile | 26 ++++++++-------- foundry.toml | 13 ++++++-- .../hub-suite/HandlerAggregator.t.sol | 0 .../hub-suite/Invariants.t.sol | 0 .../hub-suite/README.md | 0 .../hub-suite/Setup.t.sol | 0 .../hub-suite/SpecAggregator.t.sol | 0 .../hub-suite/Tester.t.sol | 0 .../hub-suite/_config/echidna_config.yaml | 2 +- .../hub-suite/_config/echidna_config_ci.yaml | 0 .../hub-suite/base/BaseHandler.t.sol | 0 .../hub-suite/base/BaseHooks.t.sol | 0 .../hub-suite/base/BaseStorage.t.sol | 0 .../hub-suite/base/BaseTest.t.sol | 0 .../hub-suite/base/ProtocolAssertions.t.sol | 0 .../handlers/HubConfiguratorHandler.t.sol | 0 .../hub-suite/handlers/HubHandler.t.sol | 0 .../interfaces/IHubConfiguratorHandler.sol | 0 .../handlers/interfaces/IHubHandler.sol | 0 .../simulators/DonationAttackHandler.t.sol | 0 .../hooks/DefaultBeforeAfterHooks.t.sol | 0 .../hub-suite/hooks/HookAggregator.t.sol | 0 .../hub-suite/invariants/HubInvariants.t.sol | 0 .../hub-suite/replays/ReplayTest_1.t.sol | 0 .../hub-suite/replays/ReplayTest_2.t.sol | 0 .../hub-suite/replays/ReplayTest_3.t.sol | 0 .../hub-suite/replays/ReplayTest_4.t.sol | 0 .../hub-suite/replays/ReplayTest_5.t.sol | 0 .../hub-suite/replays/ReplayTest_6.t.sol | 0 .../hub-suite/replays/ReplayTest_7.t.sol | 0 .../hub-suite/replays/ReplayTest_8.t.sol | 0 .../hub-suite/replays/ReplayTest_9.t.sol | 0 .../hub-suite/specs/HubInvariantsSpec.t.sol | 0 .../specs/HubPostconditionsSpec.t.sol | 0 .../hub-suite/utils/PropertiesConstants.sol | 0 .../hub-suite/utils/StdAsserts.sol | 0 .../protocol-suite/HandlerAggregator.t.sol | 0 .../protocol-suite/Invariants.t.sol | 0 .../protocol-suite/README.md | 0 .../protocol-suite/Setup.t.sol | 24 +++++++------- .../protocol-suite/SpecAggregator.t.sol | 0 .../protocol-suite/TODO | 0 .../protocol-suite/Tester.t.sol | 0 .../protocol-suite/TesterFoundry.t.sol | 2 +- .../_config/echidna_config.yaml | 2 +- .../_config/echidna_config_ci.yaml | 0 .../protocol-suite/base/BaseHandler.t.sol | 0 .../protocol-suite/base/BaseHooks.t.sol | 0 .../protocol-suite/base/BaseStorage.t.sol | 0 .../protocol-suite/base/BaseTest.t.sol | 0 .../base/ProtocolAssertions.t.sol | 0 .../handlers/hub/HubConfiguratorHandler.t.sol | 0 .../interfaces/IHubConfiguratorHandler.sol | 0 .../handlers/interfaces/IHubHandler.sol | 0 .../interfaces/ISpokeConfiguratorHandler.sol | 0 .../handlers/interfaces/ISpokeHandler.sol | 0 .../interfaces/ITreasurySpokeHandler.sol | 0 .../simulators/DonationAttackHandler.t.sol | 0 .../PriceFeedSimulatorHandler.t.sol | 0 .../spoke/SpokeConfiguratorHandler.t.sol | 0 .../handlers/spoke/SpokeHandler.t.sol | 0 .../handlers/spoke/TreasurySpokeHandler.t.sol | 0 .../hooks/DefaultBeforeAfterHooks.t.sol | 0 .../protocol-suite/hooks/HookAggregator.t.sol | 0 .../invariants/HubInvariants.t.sol | 0 .../invariants/SpokeInvariants.t.sol | 0 .../protocol-suite/replays/ReplayTest_1.t.sol | 0 .../protocol-suite/replays/ReplayTest_2.t.sol | 0 .../protocol-suite/replays/ReplayTest_3.t.sol | 0 .../protocol-suite/replays/ReplayTest_4.t.sol | 0 .../protocol-suite/replays/ReplayTest_5.t.sol | 0 .../protocol-suite/replays/ReplayTest_6.t.sol | 0 .../protocol-suite/replays/ReplayTest_7.t.sol | 0 .../protocol-suite/specs/InvariantsSpec.t.sol | 0 .../specs/PostconditionsSpec.t.sol | 0 .../shared/mocks/MockPriceFeedSimulator.sol | 0 .../shared/remote/DOCKERFILE | 0 .../shared/utils/Actor.sol | 0 .../shared/utils/ActorsUtils.sol | 0 .../shared/utils/CREATE3.sol | 0 .../shared/utils/DeployPermit2.sol | 0 .../shared/utils/ErrorHandlers.sol | 0 .../shared/utils/PropertiesAsserts.sol | 0 .../shared/utils/PropertiesConstants.sol | 0 .../shared/utils/StdAsserts.sol | 0 medusa.hub.json | 31 ++++++++++++++----- medusa.protocol.json | 6 ++-- tests/DeployUtils.sol | 20 +++++++----- 89 files changed, 83 insertions(+), 53 deletions(-) rename {tests/invariants => invariants}/hub-suite/HandlerAggregator.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/Invariants.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/README.md (100%) rename {tests/invariants => invariants}/hub-suite/Setup.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/SpecAggregator.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/Tester.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/_config/echidna_config.yaml (95%) rename {tests/invariants => invariants}/hub-suite/_config/echidna_config_ci.yaml (100%) rename {tests/invariants => invariants}/hub-suite/base/BaseHandler.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/base/BaseHooks.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/base/BaseStorage.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/base/BaseTest.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/base/ProtocolAssertions.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/handlers/HubConfiguratorHandler.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/handlers/HubHandler.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol (100%) rename {tests/invariants => invariants}/hub-suite/handlers/interfaces/IHubHandler.sol (100%) rename {tests/invariants => invariants}/hub-suite/handlers/simulators/DonationAttackHandler.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/hooks/HookAggregator.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/invariants/HubInvariants.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/replays/ReplayTest_1.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/replays/ReplayTest_2.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/replays/ReplayTest_3.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/replays/ReplayTest_4.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/replays/ReplayTest_5.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/replays/ReplayTest_6.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/replays/ReplayTest_7.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/replays/ReplayTest_8.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/replays/ReplayTest_9.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/specs/HubInvariantsSpec.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/specs/HubPostconditionsSpec.t.sol (100%) rename {tests/invariants => invariants}/hub-suite/utils/PropertiesConstants.sol (100%) rename {tests/invariants => invariants}/hub-suite/utils/StdAsserts.sol (100%) rename {tests/invariants => invariants}/protocol-suite/HandlerAggregator.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/Invariants.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/README.md (100%) rename {tests/invariants => invariants}/protocol-suite/Setup.t.sol (97%) rename {tests/invariants => invariants}/protocol-suite/SpecAggregator.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/TODO (100%) rename {tests/invariants => invariants}/protocol-suite/Tester.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/TesterFoundry.t.sol (94%) rename {tests/invariants => invariants}/protocol-suite/_config/echidna_config.yaml (95%) rename {tests/invariants => invariants}/protocol-suite/_config/echidna_config_ci.yaml (100%) rename {tests/invariants => invariants}/protocol-suite/base/BaseHandler.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/base/BaseHooks.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/base/BaseStorage.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/base/BaseTest.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/base/ProtocolAssertions.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/interfaces/IHubHandler.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/interfaces/ISpokeHandler.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/spoke/SpokeHandler.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/hooks/HookAggregator.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/invariants/HubInvariants.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/invariants/SpokeInvariants.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/replays/ReplayTest_1.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/replays/ReplayTest_2.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/replays/ReplayTest_3.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/replays/ReplayTest_4.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/replays/ReplayTest_5.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/replays/ReplayTest_6.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/replays/ReplayTest_7.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/specs/InvariantsSpec.t.sol (100%) rename {tests/invariants => invariants}/protocol-suite/specs/PostconditionsSpec.t.sol (100%) rename {tests/invariants => invariants}/shared/mocks/MockPriceFeedSimulator.sol (100%) rename {tests/invariants => invariants}/shared/remote/DOCKERFILE (100%) rename {tests/invariants => invariants}/shared/utils/Actor.sol (100%) rename {tests/invariants => invariants}/shared/utils/ActorsUtils.sol (100%) rename {tests/invariants => invariants}/shared/utils/CREATE3.sol (100%) rename {tests/invariants => invariants}/shared/utils/DeployPermit2.sol (100%) rename {tests/invariants => invariants}/shared/utils/ErrorHandlers.sol (100%) rename {tests/invariants => invariants}/shared/utils/PropertiesAsserts.sol (100%) rename {tests/invariants => invariants}/shared/utils/PropertiesConstants.sol (100%) rename {tests/invariants => invariants}/shared/utils/StdAsserts.sol (100%) diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml index b92d7f059..f431513f8 100644 --- a/.github/workflows/enigma-dark-invariants.yml +++ b/.github/workflows/enigma-dark-invariants.yml @@ -58,11 +58,11 @@ jobs: path: | out cache - key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/invariants/**/*.sol') }} + key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'invariants/**/*.sol') }} - name: Build contracts (skip if cache hit) if: steps.restore-build.outputs.cache-hit != 'true' - run: forge build tests/invariants/Tester.t.sol --build-info + run: forge build invariants/Tester.t.sol --build-info - name: Cache build artifacts if: steps.restore-build.outputs.cache-hit != 'true' @@ -72,7 +72,7 @@ jobs: path: | out cache - key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/invariants/**/*.sol') }} + key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'invariants/**/*.sol') }} echidna: name: Echidna-Invariants (${{ matrix.mode }}) Enigma-Dark @@ -101,12 +101,12 @@ jobs: path: | out cache - key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'tests/invariants/**/*.sol') }} + key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'invariants/**/*.sol') }} - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode uses: crytic/echidna-action@v2 with: files: . contract: Tester - config: tests/invariants/_config/echidna_config_ci.yaml + config: invariants/_config/echidna_config_ci.yaml test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} diff --git a/Makefile b/Makefile index ae41415fe..5b605f14a 100644 --- a/Makefile +++ b/Makefile @@ -32,35 +32,35 @@ coverage : # Echidna echidna: - echidna tests/invariants/protocol-suite/Tester.t.sol --contract Tester --config ./tests/invariants/protocol-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna invariants/protocol-suite/Tester.t.sol --contract Tester --config ./invariants/protocol-suite/_config/echidna_config.yaml echidna-assert: - echidna tests/invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/invariants/protocol-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./invariants/protocol-suite/_config/echidna_config.yaml echidna-explore: - echidna tests/invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/invariants/protocol-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./invariants/protocol-suite/_config/echidna_config.yaml echidna-hub: - echidna tests/invariants/hub-suite/Tester.t.sol --contract Tester --config ./tests/invariants/hub-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna invariants/hub-suite/Tester.t.sol --contract Tester --config ./invariants/hub-suite/_config/echidna_config.yaml echidna-hub-assert: - echidna tests/invariants/hub-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./tests/invariants/hub-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna invariants/hub-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./invariants/hub-suite/_config/echidna_config.yaml echidna-hub-explore: - echidna tests/invariants/hub-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./tests/invariants/hub-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna invariants/hub-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./invariants/hub-suite/_config/echidna_config.yaml # Medusa medusa: - medusa fuzz --config ./medusa.protocol.json + FOUNDRY_PROFILE=invariant medusa fuzz --config ./medusa.protocol.json medusa-hub: - medusa fuzz --config ./medusa.hub.json + FOUNDRY_PROFILE=invariant medusa fuzz --config ./medusa.hub.json foundry-invariants: - forge test --mc TesterFoundry -vvv + FOUNDRY_PROFILE=invariant forge test --mc TesterFoundry -vvv # Results runes-echidna: - runes convert ./tests/invariants/protocol-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/invariants/protocol-suite/replays + runes convert ./invariants/protocol-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./invariants/protocol-suite/replays runes-medusa: - runes convert ./tests/invariants/protocol-suite/_corpus/medusa/ --output ./tests/invariants/protocol-suite/replays + runes convert ./invariants/protocol-suite/_corpus/medusa/ --output ./invariants/protocol-suite/replays runes-echidna-hub: - runes convert ./tests/invariants/hub-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./tests/invariants/hub-suite/replays + runes convert ./invariants/hub-suite/_corpus/echidna/default/_data/corpus/reproducers --output ./invariants/hub-suite/replays runes-medusa-hub: - runes convert ./tests/invariants/hub-suite/_corpus/medusa/ --output ./tests/invariants/hub-suite/replays \ No newline at end of file + runes convert ./invariants/hub-suite/_corpus/medusa/ --output ./invariants/hub-suite/replays diff --git a/foundry.toml b/foundry.toml index 8faf2b272..7ebfec5ec 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,6 +12,7 @@ bytecode_hash = "none" gas_snapshot_check = false gas_limit = 1099511627776 dynamic_test_linking = false # https://github.com/crytic/crytic-compile/issues/651#issuecomment-3813020679 +offline = true additional_compiler_profiles = [ { name = "hub", optimizer = true, via_ir = true, optimizer_runs = 22_300 }, @@ -50,9 +51,17 @@ isolate = true [profile.coverage] optimizer = true -optimizer_runs = 444444444444 +optimizer_runs = 200 +via_ir = false +fuzz.runs = 5 +additional_compiler_profiles = [] +compilation_restrictions = [] + +[profile.invariant] +optimizer = true +optimizer_runs = 200 via_ir = false -fuzz.runs = 50 +test = 'invariants/' additional_compiler_profiles = [] compilation_restrictions = [] diff --git a/tests/invariants/hub-suite/HandlerAggregator.t.sol b/invariants/hub-suite/HandlerAggregator.t.sol similarity index 100% rename from tests/invariants/hub-suite/HandlerAggregator.t.sol rename to invariants/hub-suite/HandlerAggregator.t.sol diff --git a/tests/invariants/hub-suite/Invariants.t.sol b/invariants/hub-suite/Invariants.t.sol similarity index 100% rename from tests/invariants/hub-suite/Invariants.t.sol rename to invariants/hub-suite/Invariants.t.sol diff --git a/tests/invariants/hub-suite/README.md b/invariants/hub-suite/README.md similarity index 100% rename from tests/invariants/hub-suite/README.md rename to invariants/hub-suite/README.md diff --git a/tests/invariants/hub-suite/Setup.t.sol b/invariants/hub-suite/Setup.t.sol similarity index 100% rename from tests/invariants/hub-suite/Setup.t.sol rename to invariants/hub-suite/Setup.t.sol diff --git a/tests/invariants/hub-suite/SpecAggregator.t.sol b/invariants/hub-suite/SpecAggregator.t.sol similarity index 100% rename from tests/invariants/hub-suite/SpecAggregator.t.sol rename to invariants/hub-suite/SpecAggregator.t.sol diff --git a/tests/invariants/hub-suite/Tester.t.sol b/invariants/hub-suite/Tester.t.sol similarity index 100% rename from tests/invariants/hub-suite/Tester.t.sol rename to invariants/hub-suite/Tester.t.sol diff --git a/tests/invariants/hub-suite/_config/echidna_config.yaml b/invariants/hub-suite/_config/echidna_config.yaml similarity index 95% rename from tests/invariants/hub-suite/_config/echidna_config.yaml rename to invariants/hub-suite/_config/echidna_config.yaml index 8a0ec2b68..8b5045125 100644 --- a/tests/invariants/hub-suite/_config/echidna_config.yaml +++ b/invariants/hub-suite/_config/echidna_config.yaml @@ -42,7 +42,7 @@ coverage: true coverageFormats: ["html"] #directory to save the corpus; by default is disabled -corpusDir: "tests/invariants/hub-suite/_corpus/echidna/default/_data/corpus" +corpusDir: "invariants/hub-suite/_corpus/echidna/default/_data/corpus" # constants for corpus mutations (for experimentation only) #mutConsts: [100, 1, 1] diff --git a/tests/invariants/hub-suite/_config/echidna_config_ci.yaml b/invariants/hub-suite/_config/echidna_config_ci.yaml similarity index 100% rename from tests/invariants/hub-suite/_config/echidna_config_ci.yaml rename to invariants/hub-suite/_config/echidna_config_ci.yaml diff --git a/tests/invariants/hub-suite/base/BaseHandler.t.sol b/invariants/hub-suite/base/BaseHandler.t.sol similarity index 100% rename from tests/invariants/hub-suite/base/BaseHandler.t.sol rename to invariants/hub-suite/base/BaseHandler.t.sol diff --git a/tests/invariants/hub-suite/base/BaseHooks.t.sol b/invariants/hub-suite/base/BaseHooks.t.sol similarity index 100% rename from tests/invariants/hub-suite/base/BaseHooks.t.sol rename to invariants/hub-suite/base/BaseHooks.t.sol diff --git a/tests/invariants/hub-suite/base/BaseStorage.t.sol b/invariants/hub-suite/base/BaseStorage.t.sol similarity index 100% rename from tests/invariants/hub-suite/base/BaseStorage.t.sol rename to invariants/hub-suite/base/BaseStorage.t.sol diff --git a/tests/invariants/hub-suite/base/BaseTest.t.sol b/invariants/hub-suite/base/BaseTest.t.sol similarity index 100% rename from tests/invariants/hub-suite/base/BaseTest.t.sol rename to invariants/hub-suite/base/BaseTest.t.sol diff --git a/tests/invariants/hub-suite/base/ProtocolAssertions.t.sol b/invariants/hub-suite/base/ProtocolAssertions.t.sol similarity index 100% rename from tests/invariants/hub-suite/base/ProtocolAssertions.t.sol rename to invariants/hub-suite/base/ProtocolAssertions.t.sol diff --git a/tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol similarity index 100% rename from tests/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol rename to invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol diff --git a/tests/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol similarity index 100% rename from tests/invariants/hub-suite/handlers/HubHandler.t.sol rename to invariants/hub-suite/handlers/HubHandler.t.sol diff --git a/tests/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol similarity index 100% rename from tests/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol rename to invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol diff --git a/tests/invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubHandler.sol similarity index 100% rename from tests/invariants/hub-suite/handlers/interfaces/IHubHandler.sol rename to invariants/hub-suite/handlers/interfaces/IHubHandler.sol diff --git a/tests/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol b/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol similarity index 100% rename from tests/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol rename to invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol diff --git a/tests/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol similarity index 100% rename from tests/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol rename to invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol diff --git a/tests/invariants/hub-suite/hooks/HookAggregator.t.sol b/invariants/hub-suite/hooks/HookAggregator.t.sol similarity index 100% rename from tests/invariants/hub-suite/hooks/HookAggregator.t.sol rename to invariants/hub-suite/hooks/HookAggregator.t.sol diff --git a/tests/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol similarity index 100% rename from tests/invariants/hub-suite/invariants/HubInvariants.t.sol rename to invariants/hub-suite/invariants/HubInvariants.t.sol diff --git a/tests/invariants/hub-suite/replays/ReplayTest_1.t.sol b/invariants/hub-suite/replays/ReplayTest_1.t.sol similarity index 100% rename from tests/invariants/hub-suite/replays/ReplayTest_1.t.sol rename to invariants/hub-suite/replays/ReplayTest_1.t.sol diff --git a/tests/invariants/hub-suite/replays/ReplayTest_2.t.sol b/invariants/hub-suite/replays/ReplayTest_2.t.sol similarity index 100% rename from tests/invariants/hub-suite/replays/ReplayTest_2.t.sol rename to invariants/hub-suite/replays/ReplayTest_2.t.sol diff --git a/tests/invariants/hub-suite/replays/ReplayTest_3.t.sol b/invariants/hub-suite/replays/ReplayTest_3.t.sol similarity index 100% rename from tests/invariants/hub-suite/replays/ReplayTest_3.t.sol rename to invariants/hub-suite/replays/ReplayTest_3.t.sol diff --git a/tests/invariants/hub-suite/replays/ReplayTest_4.t.sol b/invariants/hub-suite/replays/ReplayTest_4.t.sol similarity index 100% rename from tests/invariants/hub-suite/replays/ReplayTest_4.t.sol rename to invariants/hub-suite/replays/ReplayTest_4.t.sol diff --git a/tests/invariants/hub-suite/replays/ReplayTest_5.t.sol b/invariants/hub-suite/replays/ReplayTest_5.t.sol similarity index 100% rename from tests/invariants/hub-suite/replays/ReplayTest_5.t.sol rename to invariants/hub-suite/replays/ReplayTest_5.t.sol diff --git a/tests/invariants/hub-suite/replays/ReplayTest_6.t.sol b/invariants/hub-suite/replays/ReplayTest_6.t.sol similarity index 100% rename from tests/invariants/hub-suite/replays/ReplayTest_6.t.sol rename to invariants/hub-suite/replays/ReplayTest_6.t.sol diff --git a/tests/invariants/hub-suite/replays/ReplayTest_7.t.sol b/invariants/hub-suite/replays/ReplayTest_7.t.sol similarity index 100% rename from tests/invariants/hub-suite/replays/ReplayTest_7.t.sol rename to invariants/hub-suite/replays/ReplayTest_7.t.sol diff --git a/tests/invariants/hub-suite/replays/ReplayTest_8.t.sol b/invariants/hub-suite/replays/ReplayTest_8.t.sol similarity index 100% rename from tests/invariants/hub-suite/replays/ReplayTest_8.t.sol rename to invariants/hub-suite/replays/ReplayTest_8.t.sol diff --git a/tests/invariants/hub-suite/replays/ReplayTest_9.t.sol b/invariants/hub-suite/replays/ReplayTest_9.t.sol similarity index 100% rename from tests/invariants/hub-suite/replays/ReplayTest_9.t.sol rename to invariants/hub-suite/replays/ReplayTest_9.t.sol diff --git a/tests/invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol similarity index 100% rename from tests/invariants/hub-suite/specs/HubInvariantsSpec.t.sol rename to invariants/hub-suite/specs/HubInvariantsSpec.t.sol diff --git a/tests/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol similarity index 100% rename from tests/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol rename to invariants/hub-suite/specs/HubPostconditionsSpec.t.sol diff --git a/tests/invariants/hub-suite/utils/PropertiesConstants.sol b/invariants/hub-suite/utils/PropertiesConstants.sol similarity index 100% rename from tests/invariants/hub-suite/utils/PropertiesConstants.sol rename to invariants/hub-suite/utils/PropertiesConstants.sol diff --git a/tests/invariants/hub-suite/utils/StdAsserts.sol b/invariants/hub-suite/utils/StdAsserts.sol similarity index 100% rename from tests/invariants/hub-suite/utils/StdAsserts.sol rename to invariants/hub-suite/utils/StdAsserts.sol diff --git a/tests/invariants/protocol-suite/HandlerAggregator.t.sol b/invariants/protocol-suite/HandlerAggregator.t.sol similarity index 100% rename from tests/invariants/protocol-suite/HandlerAggregator.t.sol rename to invariants/protocol-suite/HandlerAggregator.t.sol diff --git a/tests/invariants/protocol-suite/Invariants.t.sol b/invariants/protocol-suite/Invariants.t.sol similarity index 100% rename from tests/invariants/protocol-suite/Invariants.t.sol rename to invariants/protocol-suite/Invariants.t.sol diff --git a/tests/invariants/protocol-suite/README.md b/invariants/protocol-suite/README.md similarity index 100% rename from tests/invariants/protocol-suite/README.md rename to invariants/protocol-suite/README.md diff --git a/tests/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol similarity index 97% rename from tests/invariants/protocol-suite/Setup.t.sol rename to invariants/protocol-suite/Setup.t.sol index 22fd0fb16..183b0f4d9 100644 --- a/tests/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -23,15 +23,15 @@ import {MockPriceFeedSimulator} from '../shared/mocks/MockPriceFeedSimulator.sol // Contracts import {BaseTest} from './base/BaseTest.t.sol'; import {DeployUtils} from 'tests/DeployUtils.sol'; -import { - AssetInterestRateStrategy, - IAssetInterestRateStrategy -} from 'src/hub/AssetInterestRateStrategy.sol'; +import {AssetInterestRateStrategy, IAssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; import {TreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; import {AaveOracle} from 'src/spoke/AaveOracle.sol'; import {HubConfigurator, IHubConfigurator} from 'src/hub/HubConfigurator.sol'; import {SpokeConfigurator, ISpokeConfigurator} from 'src/spoke/SpokeConfigurator.sol'; +import {Hub} from 'src/hub/Hub.sol'; +import {SpokeInstance} from 'src/spoke/instances/SpokeInstance.sol'; +import {LiquidationLogic} from 'src/spoke/libraries/LiquidationLogic.sol'; /// @notice Setup contract for the invariant test Suite, inherited by Tester contract Setup is BaseTest { @@ -76,7 +76,7 @@ contract Setup is BaseTest { accessManager = new AccessManager(admin); // Hub 1 - hub1 = DeployUtils.deployHub(address(accessManager), 'hub1'); + hub1 = new Hub(address(accessManager)); irStrategy1 = new AssetInterestRateStrategy(address(hub1)); hubInfo[address(hub1)] = HubInfo({ treasurySpoke: address(treasurySpoke1), @@ -85,7 +85,7 @@ contract Setup is BaseTest { hubAddresses.push(address(hub1)); // Hub 2 - hub2 = DeployUtils.deployHub(address(accessManager), 'hub2'); + hub2 = new Hub(address(accessManager)); irStrategy2 = new AssetInterestRateStrategy(address(hub2)); hubInfo[address(hub2)] = HubInfo({ treasurySpoke: address(treasurySpoke2), @@ -132,12 +132,12 @@ contract Setup is BaseTest { vm.startPrank(deployer); IAaveOracle oracle = new AaveOracle(8, _oracleDesc); - ISpoke spoke = DeployUtils.deploySpoke( - address(oracle), - 'spoke1', - Constants.MAX_ALLOWED_USER_RESERVES_LIMIT, - proxyAdminOwner, - abi.encodeCall(ISpokeInstance.initialize, (_accessManager)) + ISpoke spoke = ISpoke( + DeployUtils.proxify( + address(new SpokeInstance(address(oracle), Constants.MAX_ALLOWED_USER_RESERVES_LIMIT)), + proxyAdminOwner, + abi.encodeCall(ISpokeInstance.initialize, (_accessManager)) + ) ); oracle.setSpoke(address(spoke)); diff --git a/tests/invariants/protocol-suite/SpecAggregator.t.sol b/invariants/protocol-suite/SpecAggregator.t.sol similarity index 100% rename from tests/invariants/protocol-suite/SpecAggregator.t.sol rename to invariants/protocol-suite/SpecAggregator.t.sol diff --git a/tests/invariants/protocol-suite/TODO b/invariants/protocol-suite/TODO similarity index 100% rename from tests/invariants/protocol-suite/TODO rename to invariants/protocol-suite/TODO diff --git a/tests/invariants/protocol-suite/Tester.t.sol b/invariants/protocol-suite/Tester.t.sol similarity index 100% rename from tests/invariants/protocol-suite/Tester.t.sol rename to invariants/protocol-suite/Tester.t.sol diff --git a/tests/invariants/protocol-suite/TesterFoundry.t.sol b/invariants/protocol-suite/TesterFoundry.t.sol similarity index 94% rename from tests/invariants/protocol-suite/TesterFoundry.t.sol rename to invariants/protocol-suite/TesterFoundry.t.sol index 485caa48b..c0c5ddeb4 100644 --- a/tests/invariants/protocol-suite/TesterFoundry.t.sol +++ b/invariants/protocol-suite/TesterFoundry.t.sol @@ -13,7 +13,7 @@ contract TesterFoundry is Invariants, Setup, StdInvariant { /// forge-config: default.invariant.fail-on-revert = false /// forge-config: default.invariant.runs = 1000 /// forge-config: default.invariant.depth = 100 - /// forge-config: default.invariant.corpus-dir = "tests/invariants/_corpus/foundry" + /// forge-config: default.invariant.corpus-dir = "invariants/_corpus/foundry" /// forge-config: default.invariant.show-solidity = true /// forge-config: default.invariant.show-metrics = true /// forge-config: default.fuzz.seed = '0x1' diff --git a/tests/invariants/protocol-suite/_config/echidna_config.yaml b/invariants/protocol-suite/_config/echidna_config.yaml similarity index 95% rename from tests/invariants/protocol-suite/_config/echidna_config.yaml rename to invariants/protocol-suite/_config/echidna_config.yaml index 9fb7e5754..6063cd0e6 100644 --- a/tests/invariants/protocol-suite/_config/echidna_config.yaml +++ b/invariants/protocol-suite/_config/echidna_config.yaml @@ -42,7 +42,7 @@ coverage: true coverageFormats: ["html"] #directory to save the corpus; by default is disabled -corpusDir: "tests/invariants/protocol-suite/_corpus/echidna/default/_data/corpus" +corpusDir: "invariants/protocol-suite/_corpus/echidna/default/_data/corpus" # constants for corpus mutations (for experimentation only) #mutConsts: [100, 1, 1] diff --git a/tests/invariants/protocol-suite/_config/echidna_config_ci.yaml b/invariants/protocol-suite/_config/echidna_config_ci.yaml similarity index 100% rename from tests/invariants/protocol-suite/_config/echidna_config_ci.yaml rename to invariants/protocol-suite/_config/echidna_config_ci.yaml diff --git a/tests/invariants/protocol-suite/base/BaseHandler.t.sol b/invariants/protocol-suite/base/BaseHandler.t.sol similarity index 100% rename from tests/invariants/protocol-suite/base/BaseHandler.t.sol rename to invariants/protocol-suite/base/BaseHandler.t.sol diff --git a/tests/invariants/protocol-suite/base/BaseHooks.t.sol b/invariants/protocol-suite/base/BaseHooks.t.sol similarity index 100% rename from tests/invariants/protocol-suite/base/BaseHooks.t.sol rename to invariants/protocol-suite/base/BaseHooks.t.sol diff --git a/tests/invariants/protocol-suite/base/BaseStorage.t.sol b/invariants/protocol-suite/base/BaseStorage.t.sol similarity index 100% rename from tests/invariants/protocol-suite/base/BaseStorage.t.sol rename to invariants/protocol-suite/base/BaseStorage.t.sol diff --git a/tests/invariants/protocol-suite/base/BaseTest.t.sol b/invariants/protocol-suite/base/BaseTest.t.sol similarity index 100% rename from tests/invariants/protocol-suite/base/BaseTest.t.sol rename to invariants/protocol-suite/base/BaseTest.t.sol diff --git a/tests/invariants/protocol-suite/base/ProtocolAssertions.t.sol b/invariants/protocol-suite/base/ProtocolAssertions.t.sol similarity index 100% rename from tests/invariants/protocol-suite/base/ProtocolAssertions.t.sol rename to invariants/protocol-suite/base/ProtocolAssertions.t.sol diff --git a/tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol b/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol rename to invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol diff --git a/tests/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol rename to invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol diff --git a/tests/invariants/protocol-suite/handlers/interfaces/IHubHandler.sol b/invariants/protocol-suite/handlers/interfaces/IHubHandler.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/interfaces/IHubHandler.sol rename to invariants/protocol-suite/handlers/interfaces/IHubHandler.sol diff --git a/tests/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol b/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol rename to invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol diff --git a/tests/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol b/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol rename to invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol diff --git a/tests/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol b/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol rename to invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol diff --git a/tests/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol b/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol rename to invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol diff --git a/tests/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol b/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol rename to invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol diff --git a/tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol rename to invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol diff --git a/tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol rename to invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol diff --git a/tests/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol similarity index 100% rename from tests/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol rename to invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol diff --git a/tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol similarity index 100% rename from tests/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol rename to invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol diff --git a/tests/invariants/protocol-suite/hooks/HookAggregator.t.sol b/invariants/protocol-suite/hooks/HookAggregator.t.sol similarity index 100% rename from tests/invariants/protocol-suite/hooks/HookAggregator.t.sol rename to invariants/protocol-suite/hooks/HookAggregator.t.sol diff --git a/tests/invariants/protocol-suite/invariants/HubInvariants.t.sol b/invariants/protocol-suite/invariants/HubInvariants.t.sol similarity index 100% rename from tests/invariants/protocol-suite/invariants/HubInvariants.t.sol rename to invariants/protocol-suite/invariants/HubInvariants.t.sol diff --git a/tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol similarity index 100% rename from tests/invariants/protocol-suite/invariants/SpokeInvariants.t.sol rename to invariants/protocol-suite/invariants/SpokeInvariants.t.sol diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_1.t.sol b/invariants/protocol-suite/replays/ReplayTest_1.t.sol similarity index 100% rename from tests/invariants/protocol-suite/replays/ReplayTest_1.t.sol rename to invariants/protocol-suite/replays/ReplayTest_1.t.sol diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_2.t.sol b/invariants/protocol-suite/replays/ReplayTest_2.t.sol similarity index 100% rename from tests/invariants/protocol-suite/replays/ReplayTest_2.t.sol rename to invariants/protocol-suite/replays/ReplayTest_2.t.sol diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_3.t.sol b/invariants/protocol-suite/replays/ReplayTest_3.t.sol similarity index 100% rename from tests/invariants/protocol-suite/replays/ReplayTest_3.t.sol rename to invariants/protocol-suite/replays/ReplayTest_3.t.sol diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_4.t.sol b/invariants/protocol-suite/replays/ReplayTest_4.t.sol similarity index 100% rename from tests/invariants/protocol-suite/replays/ReplayTest_4.t.sol rename to invariants/protocol-suite/replays/ReplayTest_4.t.sol diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_5.t.sol b/invariants/protocol-suite/replays/ReplayTest_5.t.sol similarity index 100% rename from tests/invariants/protocol-suite/replays/ReplayTest_5.t.sol rename to invariants/protocol-suite/replays/ReplayTest_5.t.sol diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_6.t.sol b/invariants/protocol-suite/replays/ReplayTest_6.t.sol similarity index 100% rename from tests/invariants/protocol-suite/replays/ReplayTest_6.t.sol rename to invariants/protocol-suite/replays/ReplayTest_6.t.sol diff --git a/tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol b/invariants/protocol-suite/replays/ReplayTest_7.t.sol similarity index 100% rename from tests/invariants/protocol-suite/replays/ReplayTest_7.t.sol rename to invariants/protocol-suite/replays/ReplayTest_7.t.sol diff --git a/tests/invariants/protocol-suite/specs/InvariantsSpec.t.sol b/invariants/protocol-suite/specs/InvariantsSpec.t.sol similarity index 100% rename from tests/invariants/protocol-suite/specs/InvariantsSpec.t.sol rename to invariants/protocol-suite/specs/InvariantsSpec.t.sol diff --git a/tests/invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol similarity index 100% rename from tests/invariants/protocol-suite/specs/PostconditionsSpec.t.sol rename to invariants/protocol-suite/specs/PostconditionsSpec.t.sol diff --git a/tests/invariants/shared/mocks/MockPriceFeedSimulator.sol b/invariants/shared/mocks/MockPriceFeedSimulator.sol similarity index 100% rename from tests/invariants/shared/mocks/MockPriceFeedSimulator.sol rename to invariants/shared/mocks/MockPriceFeedSimulator.sol diff --git a/tests/invariants/shared/remote/DOCKERFILE b/invariants/shared/remote/DOCKERFILE similarity index 100% rename from tests/invariants/shared/remote/DOCKERFILE rename to invariants/shared/remote/DOCKERFILE diff --git a/tests/invariants/shared/utils/Actor.sol b/invariants/shared/utils/Actor.sol similarity index 100% rename from tests/invariants/shared/utils/Actor.sol rename to invariants/shared/utils/Actor.sol diff --git a/tests/invariants/shared/utils/ActorsUtils.sol b/invariants/shared/utils/ActorsUtils.sol similarity index 100% rename from tests/invariants/shared/utils/ActorsUtils.sol rename to invariants/shared/utils/ActorsUtils.sol diff --git a/tests/invariants/shared/utils/CREATE3.sol b/invariants/shared/utils/CREATE3.sol similarity index 100% rename from tests/invariants/shared/utils/CREATE3.sol rename to invariants/shared/utils/CREATE3.sol diff --git a/tests/invariants/shared/utils/DeployPermit2.sol b/invariants/shared/utils/DeployPermit2.sol similarity index 100% rename from tests/invariants/shared/utils/DeployPermit2.sol rename to invariants/shared/utils/DeployPermit2.sol diff --git a/tests/invariants/shared/utils/ErrorHandlers.sol b/invariants/shared/utils/ErrorHandlers.sol similarity index 100% rename from tests/invariants/shared/utils/ErrorHandlers.sol rename to invariants/shared/utils/ErrorHandlers.sol diff --git a/tests/invariants/shared/utils/PropertiesAsserts.sol b/invariants/shared/utils/PropertiesAsserts.sol similarity index 100% rename from tests/invariants/shared/utils/PropertiesAsserts.sol rename to invariants/shared/utils/PropertiesAsserts.sol diff --git a/tests/invariants/shared/utils/PropertiesConstants.sol b/invariants/shared/utils/PropertiesConstants.sol similarity index 100% rename from tests/invariants/shared/utils/PropertiesConstants.sol rename to invariants/shared/utils/PropertiesConstants.sol diff --git a/tests/invariants/shared/utils/StdAsserts.sol b/invariants/shared/utils/StdAsserts.sol similarity index 100% rename from tests/invariants/shared/utils/StdAsserts.sol rename to invariants/shared/utils/StdAsserts.sol diff --git a/medusa.hub.json b/medusa.hub.json index cf8158f47..5b908a6ff 100644 --- a/medusa.hub.json +++ b/medusa.hub.json @@ -5,10 +5,14 @@ "timeout": 0, "testLimit": 0, "callSequenceLength": 100, - "corpusDirectory": "tests/invariants/hub-suite/_corpus/medusa", + "corpusDirectory": "invariants/hub-suite/_corpus/medusa", "coverageEnabled": true, - "deploymentOrder": ["Tester"], - "targetContracts": ["Tester"], + "deploymentOrder": [ + "Tester" + ], + "targetContracts": [ + "Tester" + ], "targetContractsBalances": [ "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" ], @@ -17,7 +21,11 @@ }, "constructorArgs": {}, "deployerAddress": "0x30000", - "senderAddresses": ["0x10000", "0x20000", "0x30000"], + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], "blockNumberDelayMax": 60480, "blockTimestampDelayMax": 604800, "blockGasLimit": 12500000000, @@ -46,13 +54,20 @@ }, "propertyTesting": { "enabled": true, - "testPrefixes": ["fuzz_", "invariant_"] + "testPrefixes": [ + "fuzz_", + "invariant_" + ] }, "optimizationTesting": { "enabled": false, - "testPrefixes": ["optimize_"] + "testPrefixes": [ + "optimize_" + ] }, - "excludeFunctionSignatures": ["Tester.checkPostConditions()"] + "excludeFunctionSignatures": [ + "Tester.checkPostConditions()" + ] }, "chainConfig": { "codeSizeCheckDisabled": true, @@ -65,7 +80,7 @@ "compilation": { "platform": "crytic-compile", "platformConfig": { - "target": "tests/invariants/hub-suite/Tester.t.sol", + "target": "invariants/hub-suite/Tester.t.sol", "solcVersion": "", "exportDirectory": "", "args": [ diff --git a/medusa.protocol.json b/medusa.protocol.json index c1e0cb662..45542dedb 100644 --- a/medusa.protocol.json +++ b/medusa.protocol.json @@ -5,7 +5,7 @@ "timeout": 0, "testLimit": 0, "callSequenceLength": 100, - "corpusDirectory": "tests/invariants/protocol-suite/_corpus/medusa", + "corpusDirectory": "invariants/protocol-suite/_corpus/medusa", "coverageEnabled": true, "deploymentOrder": [ "Tester" @@ -73,14 +73,14 @@ "codeSizeCheckDisabled": true, "cheatCodes": { "cheatCodesEnabled": true, - "enableFFI": false + "enableFFI": true } } }, "compilation": { "platform": "crytic-compile", "platformConfig": { - "target": "tests/invariants/protocol-suite/Tester.t.sol", + "target": "invariants/protocol-suite/Tester.t.sol", "solcVersion": "", "exportDirectory": "", "args": [ diff --git a/tests/DeployUtils.sol b/tests/DeployUtils.sol index 84a367b27..1d6837fab 100644 --- a/tests/DeployUtils.sol +++ b/tests/DeployUtils.sol @@ -40,7 +40,7 @@ library DeployUtils { return ISpoke( proxify( - address(deploySpokeImplementation(oracle, maxUserReservesLimit, '')), + address(ISpokeInstance(deploy(_getSpokeInstanceInitCode(oracle, maxUserReservesLimit)))), proxyAdminOwner, initData ) @@ -49,7 +49,7 @@ library DeployUtils { function deploySpoke( address oracle, - bytes32 implSalt, + bytes32 /*implSalt*/, uint16 maxUserReservesLimit, address proxyAdminOwner, bytes memory initData @@ -57,7 +57,7 @@ library DeployUtils { return ISpoke( proxify( - address(deploySpokeImplementation(oracle, maxUserReservesLimit, implSalt)), + address(ISpokeInstance(deploy(_getSpokeInstanceInitCode(oracle, maxUserReservesLimit)))), proxyAdminOwner, initData ) @@ -83,12 +83,18 @@ library DeployUtils { } function deployHub(address authority) internal returns (IHub) { - return deployHub(authority, ''); + return IHub(deploy(_getHubInitCode(authority))); } - function deployHub(address authority, bytes32 salt) internal returns (IHub hub) { - Create2Utils.loadCreate2Factory(); - return IHub(Create2Utils.create2Deploy(salt, _getHubInitCode(authority))); + function deploy(bytes memory bytecode) internal returns (address addr) { + assembly ('memory-safe') { + addr := create(0, add(bytecode, 0x20), mload(bytecode)) + } + require(addr != address(0)); + } + + function deployHub(address authority, bytes32 /*salt*/) internal returns (IHub hub) { + return IHub(deploy(_getHubInitCode(authority))); } function getDeterministicHubAddress(address authority) internal returns (address) { From 701b7bb67fa11f8957faed7ea59c00b3d4c27070 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:39:29 +0530 Subject: [PATCH 039/106] fix: cleanup, new inv, medusa fixes --- Makefile | 12 +- invariants/hub-suite/Invariants.t.sol | 1 - .../hub-suite/invariants/HubInvariants.t.sol | 18 +- invariants/protocol-suite/Invariants.t.sol | 15 +- invariants/protocol-suite/Setup.t.sol | 17 +- .../_config/echidna_config.yaml | 6 +- .../protocol-suite/base/BaseStorage.t.sol | 10 + invariants/protocol-suite/base/BaseTest.t.sol | 26 +- .../simulators/DonationAttackHandler.t.sol | 35 +++ .../handlers/spoke/SpokeHandler.t.sol | 69 +++-- .../hooks/DefaultBeforeAfterHooks.t.sol | 55 +++- .../protocol-suite/hooks/HookAggregator.t.sol | 26 +- .../invariants/HubInvariants.t.sol | 64 +++-- .../invariants/SpokeInvariants.t.sol | 14 +- .../protocol-suite/replays/ReplayTest_8.t.sol | 262 ++++++++++++++++++ .../protocol-suite/specs/InvariantsSpec.t.sol | 12 +- .../specs/PostconditionsSpec.t.sol | 2 +- medusa.hub.json | 27 +- medusa.protocol.json | 29 +- 19 files changed, 558 insertions(+), 142 deletions(-) create mode 100644 invariants/protocol-suite/replays/ReplayTest_8.t.sol diff --git a/Makefile b/Makefile index 5b605f14a..1b1df44ca 100644 --- a/Makefile +++ b/Makefile @@ -32,18 +32,18 @@ coverage : # Echidna echidna: - FOUNDRY_PROFILE=invariant echidna invariants/protocol-suite/Tester.t.sol --contract Tester --config ./invariants/protocol-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna . --contract invariants/protocol-suite/Tester.t.sol:Tester --config ./invariants/protocol-suite/_config/echidna_config.yaml echidna-assert: - FOUNDRY_PROFILE=invariant echidna invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./invariants/protocol-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna . --contract invariants/protocol-suite/Tester.t.sol:Tester --test-mode assertion --config ./invariants/protocol-suite/_config/echidna_config.yaml echidna-explore: - FOUNDRY_PROFILE=invariant echidna invariants/protocol-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./invariants/protocol-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna . --contract invariants/protocol-suite/Tester.t.sol:Tester --test-mode exploration --config ./invariants/protocol-suite/_config/echidna_config.yaml echidna-hub: - FOUNDRY_PROFILE=invariant echidna invariants/hub-suite/Tester.t.sol --contract Tester --config ./invariants/hub-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna . --contract invariants/hub-suite/Tester.t.sol:Tester --config ./invariants/hub-suite/_config/echidna_config.yaml echidna-hub-assert: - FOUNDRY_PROFILE=invariant echidna invariants/hub-suite/Tester.t.sol --contract Tester --test-mode assertion --config ./invariants/hub-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna . --contract invariants/hub-suite/Tester.t.sol:Tester --test-mode assertion --config ./invariants/hub-suite/_config/echidna_config.yaml echidna-hub-explore: - FOUNDRY_PROFILE=invariant echidna invariants/hub-suite/Tester.t.sol --contract Tester --test-mode exploration --config ./invariants/hub-suite/_config/echidna_config.yaml + FOUNDRY_PROFILE=invariant echidna . --contract invariants/hub-suite/Tester.t.sol:Tester --test-mode exploration --config ./invariants/hub-suite/_config/echidna_config.yaml # Medusa medusa: diff --git a/invariants/hub-suite/Invariants.t.sol b/invariants/hub-suite/Invariants.t.sol index 3c2129b0c..7c7a2b421 100644 --- a/invariants/hub-suite/Invariants.t.sol +++ b/invariants/hub-suite/Invariants.t.sol @@ -18,7 +18,6 @@ abstract contract Invariants is HubInvariants { assert_INV_HUB_GH(i); assert_INV_HUB_I(i); assert_INV_HUB_K(i); - assert_INV_HUB_L(i); assert_INV_HUB_O(i); assert_INV_HUB_P(i); assert_INV_HUB_N(i); diff --git a/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol index e7dd9c175..be313a821 100644 --- a/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; // Libraries import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; -import 'forge-std/console.sol'; +import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; // Interfaces import {IHub} from 'src/hub/interfaces/IHub.sol'; @@ -17,6 +17,8 @@ import {HandlerAggregator} from '../HandlerAggregator.t.sol'; /// @notice Implements Hub Invariants for the protocol /// @dev Inherits HandlerAggregator to check actions in assertion testing mode abstract contract HubInvariants is HandlerAggregator { + using SafeCast for *; + /////////////////////////////////////////////////////////////////////////////////////////////// // HUB // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -138,18 +140,6 @@ abstract contract HubInvariants is HandlerAggregator { assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); } - function assert_INV_HUB_L(uint256 assetId) internal { - // Get premium data - (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); - - // Check - assertGe( - int256(hub.previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), - premiumOffsetRay, - INV_HUB_L - ); - } - function assert_INV_HUB_O(uint256 assetId) internal { uint256 spokeCount = NUMBER_OF_ACTORS; uint256 totalDeficitRay; @@ -162,7 +152,7 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_P(uint256 assetId) internal { (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); - assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); + assertGe((premiumShares * drawnIndex).toInt256(), premiumOffsetRay, INV_HUB_P); } function assert_INV_HUB_N(uint256 assetId) internal { diff --git a/invariants/protocol-suite/Invariants.t.sol b/invariants/protocol-suite/Invariants.t.sol index e93273899..63d6ab2f4 100644 --- a/invariants/protocol-suite/Invariants.t.sol +++ b/invariants/protocol-suite/Invariants.t.sol @@ -32,10 +32,11 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { assert_INV_HUB_GH(hubAddress, j); assert_INV_HUB_I(hubAddress, j); assert_INV_HUB_K(hubAddress, j); - assert_INV_HUB_L(hubAddress, j); assert_INV_HUB_O(hubAddress, j); assert_INV_HUB_P(hubAddress, j); assert_INV_HUB_N(hubAddress, j); + assert_INV_HUB_Q(hubAddress, j); + assert_INV_HUB_R(hubAddress, j); } } @@ -63,9 +64,21 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { // Applied per actor per reserve of the spoke for (uint256 k; k < actorAddresses.length; k++) { assert_INV_SP_B(spoke, reserveId, actorAddresses[k]); + assert_INV_SP_H(spoke, reserveId, actorAddresses[k]); } } } + + // Applied per treasury spoke (only hub-sync invariant applies; + // user-level invariants don't apply since TreasurySpoke has no per-user positions) + for (uint256 i; i < treasurySpokesAddresses.length; i++) { + address spoke = treasurySpokesAddresses[i]; + for (uint256 j; j < spokeReserveIds[spoke].length; j++) { + uint256 reserveId = spokeReserveIds[spoke][j]; + assert_INV_SP_A(spoke, reserveId); + } + } + return true; } } diff --git a/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol index 183b0f4d9..62b65d415 100644 --- a/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -23,7 +23,10 @@ import {MockPriceFeedSimulator} from '../shared/mocks/MockPriceFeedSimulator.sol // Contracts import {BaseTest} from './base/BaseTest.t.sol'; import {DeployUtils} from 'tests/DeployUtils.sol'; -import {AssetInterestRateStrategy, IAssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import { + AssetInterestRateStrategy, + IAssetInterestRateStrategy +} from 'src/hub/AssetInterestRateStrategy.sol'; import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; import {TreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; import {AaveOracle} from 'src/spoke/AaveOracle.sol'; @@ -100,6 +103,8 @@ contract Setup is BaseTest { treasurySpoke2 = ITreasurySpoke(new TreasurySpoke(admin, address(hub2))); allSpokes.push(address(treasurySpoke1)); allSpokes.push(address(treasurySpoke2)); + treasurySpokesAddresses.push(address(treasurySpoke1)); + treasurySpokesAddresses.push(address(treasurySpoke2)); // Configurators hubConfigurator = new HubConfigurator(address(accessManager)); @@ -445,6 +450,16 @@ contract Setup is BaseTest { spokeReserveIds[address(treasurySpoke2)].push(hub2UsdcAssetId); spokeReserveIds[address(treasurySpoke2)].push(hub2WethAssetId); + // Map ids for treasury spokes (reserveId == assetId for treasury spokes) + reserveIdToAssetId[address(treasurySpoke1)][hub1UsdcAssetId] = hub1UsdcAssetId; + reserveIdToAssetId[address(treasurySpoke1)][hub1WethAssetId] = hub1WethAssetId; + reserveIdToHubAddress[address(treasurySpoke1)][hub1UsdcAssetId] = address(hub1); + reserveIdToHubAddress[address(treasurySpoke1)][hub1WethAssetId] = address(hub1); + reserveIdToAssetId[address(treasurySpoke2)][hub2UsdcAssetId] = hub2UsdcAssetId; + reserveIdToAssetId[address(treasurySpoke2)][hub2WethAssetId] = hub2WethAssetId; + reserveIdToHubAddress[address(treasurySpoke2)][hub2UsdcAssetId] = address(hub2); + reserveIdToHubAddress[address(treasurySpoke2)][hub2WethAssetId] = address(hub2); + // Add SPOKE 1 assets to hubs hub1.addSpoke( hub1UsdcAssetId, diff --git a/invariants/protocol-suite/_config/echidna_config.yaml b/invariants/protocol-suite/_config/echidna_config.yaml index 6063cd0e6..08155a8ad 100644 --- a/invariants/protocol-suite/_config/echidna_config.yaml +++ b/invariants/protocol-suite/_config/echidna_config.yaml @@ -11,10 +11,10 @@ balanceAddr: 0x1000000000000000000000000 balanceContract: 0x1000000000000000000000000000000000000000000000000 #testLimit is the number of test sequences to run -testLimit: 20000000 +testLimit: 1000000 #seqLen defines how many transactions are in a test sequence -seqLen: 300 +seqLen: 30 #shrinkLimit determines how much effort is spent shrinking failing sequences shrinkLimit: 2500 @@ -60,4 +60,4 @@ quiet: false format: "text" # concurrent workers -workers: 10 +workers: 30 diff --git a/invariants/protocol-suite/base/BaseStorage.t.sol b/invariants/protocol-suite/base/BaseStorage.t.sol index 4f6938428..6be112563 100644 --- a/invariants/protocol-suite/base/BaseStorage.t.sol +++ b/invariants/protocol-suite/base/BaseStorage.t.sol @@ -114,6 +114,8 @@ abstract contract BaseStorage { // SPOKES /// @notice Array of spokes addresses for the suite address[] internal spokesAddresses; + /// @notice Array of treasury spoke addresses + address[] internal treasurySpokesAddresses; /// @notice spokesAddresses + treasurySpoke address address[] internal allSpokes; /// @notice Spoke configurations @@ -127,6 +129,14 @@ abstract contract BaseStorage { /// @notice Spoke reserveIds to hub addresses mapping(address => mapping(uint256 => address)) internal reserveIdToHubAddress; + // INVARIANT TRACKING (stateful invariants) + /// @notice Last seen drawn index per hub per assetId (INV_HUB_Q) + mapping(address => mapping(uint256 => uint256)) internal lastSeenDrawnIndex; + /// @notice Last seen added assets per hub per assetId (INV_HUB_R) + mapping(address => mapping(uint256 => uint256)) internal lastSeenAssets; + /// @notice Last seen added shares per hub per assetId (INV_HUB_R) + mapping(address => mapping(uint256 => uint256)) internal lastSeenShares; + // PRICE FEEDS address[] internal priceFeeds; diff --git a/invariants/protocol-suite/base/BaseTest.t.sol b/invariants/protocol-suite/base/BaseTest.t.sol index ac83d8e56..daceb5627 100644 --- a/invariants/protocol-suite/base/BaseTest.t.sol +++ b/invariants/protocol-suite/base/BaseTest.t.sol @@ -8,7 +8,8 @@ import 'forge-std/console.sol'; import {Constants} from 'tests/Constants.sol'; // Interfaces -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; // Utils @@ -132,6 +133,29 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD; } + /// @notice Returns true if the reserve/spoke is admin-blocked for the given action + function _isReserveActionBlocked( + address spoke, + uint256 reserveId, + bool checkFrozen, + bool checkBorrowable + ) internal view returns (bool) { + ISpoke.ReserveConfig memory config = ISpoke(spoke).getReserveConfig(reserveId); + if (config.paused) return true; + if (checkFrozen && config.frozen) return true; + if (checkBorrowable && !config.borrowable) return true; + address hubAddress = _getHubAddress(spoke, reserveId); + uint256 assetId = _getAssetId(spoke, reserveId); + IHub.SpokeData memory spokeData = IHub(hubAddress).getSpoke(assetId, spoke); + if (!spokeData.active || spokeData.halted) return true; + return false; + } + + /// @notice Returns true if the current actor can act on behalf of `onBehalfOf` on the spoke + function _isAuthorized(address spoke, address onBehalfOf) internal view returns (bool) { + return ISpoke(spoke).isPositionManager(onBehalfOf, address(actor)); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol b/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol index 063084a73..f10683774 100644 --- a/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol +++ b/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol @@ -26,7 +26,12 @@ contract DonationAttackHandler is BaseHandler { // Get one of the assets IDs randomly address underlying = _getRandomBaseAsset(i); + // Register all spoke/reserve pairs that map to this hub so hub postconditions fire + _registerAllReservesForHub(hubAddress); + + _before(); TestnetERC20(underlying).mint(hubAddress, amount); + _after(); } function donateUnderlyingToSpoke(uint256 amount, uint8 i, uint8 j) external { @@ -35,10 +40,40 @@ contract DonationAttackHandler is BaseHandler { // Get one of the assets IDs randomly address underlying = _getRandomBaseAsset(i); + // Register all reserves for this spoke so hub postconditions fire + _registerAllReservesForSpoke(spoke); + + _before(); TestnetERC20(underlying).mint(spoke, amount); + _after(); } /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Registers a user-to-check entry for every reserve of every spoke that is connected + /// to the given hub, so that hub-level postconditions (GPOST_HUB_A..G) are evaluated. + /// Uses address(this) as the user since donations don't act on a specific user. + function _registerAllReservesForHub(address hubAddress) internal { + for (uint256 s; s < spokesAddresses.length; s++) { + address spoke = spokesAddresses[s]; + uint256[] storage reserveIds = spokeReserveIds[spoke]; + for (uint256 r; r < reserveIds.length; r++) { + if (reserveIdToHubAddress[spoke][reserveIds[r]] == hubAddress) { + _registerUserToCheck(spoke, reserveIds[r], address(this)); + } + } + } + } + + /// @dev Registers a user-to-check entry for every reserve of the given spoke, + /// so that hub-level postconditions are evaluated for all assets of the spoke. + /// Uses address(this) as the user since donations don't act on a specific user. + function _registerAllReservesForSpoke(address spoke) internal { + uint256[] storage reserveIds = spokeReserveIds[spoke]; + for (uint256 r; r < reserveIds.length; r++) { + _registerUserToCheck(spoke, reserveIds[r], address(this)); + } + } } diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index c43c6a14c..bbc8a7fdc 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.19; // Interfaces -import {IHub} from 'src/hub/interfaces/IHub.sol'; import {ISpoke, ISpokeBase} from 'src/spoke/interfaces/ISpoke.sol'; import {ISpokeHandler} from '../interfaces/ISpokeHandler.sol'; import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; @@ -10,7 +9,6 @@ import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; // Libraries import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {Constants} from 'tests/Constants.sol'; -import 'forge-std/console.sol'; // Test Contracts import {Actor} from '../../../shared/utils/Actor.sol'; @@ -34,8 +32,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { // Debt reserve uint256 debtReserveId; uint256 collateralReserveId; - uint256 reserveDebtBefore; - uint256 reserveDebtAfter; // Liquidation uint256 debtToCover; uint256 debtLiquidated; @@ -89,10 +85,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { // Get one of the reserves IDs randomly uint256 reserveId = _getRandomReserveId(spoke, k); - uint256 userAmount = IHub(_getHubAddress(spoke, reserveId)).previewRemoveByShares( - reserveId, - ISpoke(spoke).getUserSuppliedShares(reserveId, onBehalfOf) - ); + uint256 userAmount = ISpoke(spoke).getUserSuppliedAssets(reserveId, onBehalfOf); // Register user to check postconditions _registerUserToCheck(spoke, reserveId, onBehalfOf); @@ -103,10 +96,14 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { abi.encodeCall(ISpokeBase.withdraw, (reserveId, amount, onBehalfOf)) ); - // Implemented outside the success check to assert success + // GPOST_SP_H: debt-free user with valid auth and unblocked reserve must be able to withdraw + // todo make this "position healthy" after withdraw, add similar check on borrow if ( + _isAuthorized(spoke, onBehalfOf) && + !_isReserveActionBlocked(spoke, reserveId, false, false) && defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt == 0 && - (amount > 0 && userAmount != 0) + amount > 0 && + userAmount != 0 ) { assertTrue(success, GPOST_SP_H); } @@ -222,12 +219,9 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ); liquidationVars.totalDebtValueBefore = ISpoke(liquidationVars.spoke) - .getUserAccountData(_getRandomActor(i)) + .getUserAccountData(liquidationVars.violator) .totalDebtValueRay - .fromRayUp(); // todo fix - liquidationVars.reserveDebtBefore = ISpoke(liquidationVars.spoke).getReserveTotalDebt( - liquidationVars.debtReserveId - ); + .fromRayUp(); if (receiveShares) { liquidationVars.liquidatorCollateralBalanceBefore = ISpoke(liquidationVars.spoke) @@ -277,13 +271,16 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (success) { _after(); - // Calculate the debt liquidated - liquidationVars.reserveDebtAfter = ISpoke(liquidationVars.spoke).getReserveTotalDebt( - liquidationVars.debtReserveId - ); - liquidationVars.debtLiquidated = (liquidationVars.reserveDebtBefore > - liquidationVars.reserveDebtAfter) - ? liquidationVars.reserveDebtBefore - liquidationVars.reserveDebtAfter + // Calculate the debt liquidated from user-level snapshots (not reserve-level, which + // includes interest accrual on other users' debt and would be inaccurate) + uint256 violatorDebtBefore = defaultVarsBefore + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] + .totalDebt; + uint256 violatorDebtAfter = defaultVarsAfter + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] + .totalDebt; + liquidationVars.debtLiquidated = (violatorDebtBefore > violatorDebtAfter) + ? violatorDebtBefore - violatorDebtAfter : 0; if (receiveShares) { @@ -318,7 +315,9 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (liquidationVars.totalDebtValueBefore < Constants.DUST_LIQUIDATION_THRESHOLD) { assertEq( defaultVarsAfter - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][ + liquidationVars.violator + ] .totalDebt, 0, HSPOST_SP_LIQ_C @@ -329,7 +328,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertLt( defaultVarsBefore - .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] + .userAccountDataVars[liquidationVars.spoke][liquidationVars.violator] .healthFactor, Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, HSPOST_SP_LIQ_E @@ -337,15 +336,15 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if ( defaultVarsAfter - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][_getRandomActor(i)] + .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] .totalDebt > 0 ) { assertGt( defaultVarsAfter - .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] + .userAccountDataVars[liquidationVars.spoke][liquidationVars.violator] .healthFactor, defaultVarsBefore - .userAccountDataVars[liquidationVars.spoke][_getRandomActor(i)] + .userAccountDataVars[liquidationVars.spoke][liquidationVars.violator] .healthFactor, HSPOST_SP_LIQ_G ); @@ -401,6 +400,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { address spoke = _getRandomSpoke(i); uint256 totalDebt = _getTotalDebt(spoke, onBehalfOf); + uint256 totalPremiumDebtRay = _getTotalPremiumDebtRay(spoke, onBehalfOf); // Register user to check postconditions _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); @@ -413,8 +413,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (success) { _after(); - - ///// HSPOST ///// + assertEq(_getTotalPremiumDebtRay(spoke, onBehalfOf), totalPremiumDebtRay, HSPOST_HUB_M); assertEq(_getTotalDebt(spoke, onBehalfOf), totalDebt, HSPOST_SP_F); } else { revert('SpokeHandler: updateUserRiskPremium failed'); @@ -444,6 +443,9 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } } + // todo add updateUserPositionManager + // todo check decoded returnData + /////////////////////////////////////////////////////////////////////////////////////////////// // OWNER ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -460,4 +462,13 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } return totalDebt; } + + function _getTotalPremiumDebtRay(address spoke, address user) internal view returns (uint256) { + uint256 totalPremiumDebtRay; + uint256 reserveCount = spokeReserveIds[spoke].length; + for (uint256 i; i < reserveCount; i++) { + totalPremiumDebtRay += ISpoke(spoke).getUserPremiumDebtRay(spokeReserveIds[spoke][i], user); + } + return totalPremiumDebtRay; + } } diff --git a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index eb685733b..33b528187 100644 --- a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; // Libraries import {MathUtils} from 'src/libraries/math/MathUtils.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; +import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import 'forge-std/console.sol'; // Utils @@ -22,6 +23,8 @@ import {BaseHooks} from '../base/BaseHooks.t.sol'; /// @notice Helper contract for before and after hooks, state variable caching and postconditions /// @dev This contract is inherited by handlers abstract contract DefaultBeforeAfterHooks is BaseHooks { + using WadRayMath for uint256; + /////////////////////////////////////////////////////////////////////////////////////////////// // STRUCTS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -45,8 +48,15 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { uint256 healthFactor; } + struct SpokeAssetVars { + uint256 addedAssets; + uint256 addedShares; + uint256 owed; + } + struct DefaultVars { mapping(address hub => mapping(uint256 assetId => AssetVars)) assetVars; + mapping(address hub => mapping(uint256 assetId => mapping(address spoke => SpokeAssetVars))) spokeAssetVars; mapping(address spoke => mapping(uint256 reserveId => mapping(address user => UserVars))) userVars; mapping(address spoke => mapping(address user => UserAccountDataVars)) userAccountDataVars; } @@ -85,6 +95,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function _defaultHooksBefore() internal { // Asset values _setAssetValues(defaultVarsBefore); + // Spoke asset values + _setSpokeAssetValues(defaultVarsBefore); // User values _setUserValues(defaultVarsBefore); } @@ -92,6 +104,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function _defaultHooksAfter() internal { // Asset values _setAssetValues(defaultVarsAfter); + // Spoke asset values + _setSpokeAssetValues(defaultVarsAfter); // User values _setUserValues(defaultVarsAfter); } @@ -119,6 +133,24 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } } + function _setSpokeAssetValues(DefaultVars storage _defaultVars) internal { + for (uint256 i; i < hubAddresses.length; i++) { + address hubAddress = hubAddresses[i]; + uint256 assetCount = IHub(hubAddress).getAssetCount(); + for (uint256 j; j < assetCount; j++) { + for (uint256 k; k < allSpokes.length; k++) { + address spoke = allSpokes[k]; + _defaultVars.spokeAssetVars[hubAddress][j][spoke].addedAssets = IHub(hubAddress) + .getSpokeAddedAssets(j, spoke); + _defaultVars.spokeAssetVars[hubAddress][j][spoke].addedShares = IHub(hubAddress) + .getSpokeAddedShares(j, spoke); + (uint256 drawn, uint256 premium) = IHub(hubAddress).getSpokeOwed(j, spoke); + _defaultVars.spokeAssetVars[hubAddress][j][spoke].owed = drawn + premium; + } + } + } + } + function _setUserValues(DefaultVars storage _defaultVars) internal { // Iterate through all users to check for (uint256 i; i < usersToCheck.length; i++) { @@ -202,7 +234,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { assetId, IHub(hubAddress).getAssetLiquidity(assetId), defaultVarsAfter.assetVars[hubAddress][assetId].drawn, - 0, // Unused in the interest rate calculation + IHub(hubAddress).getAssetDeficitRay(assetId).fromRayUp(), IHub(hubAddress).getAssetSwept(assetId) ), GPOST_HUB_C @@ -223,28 +255,30 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { IHub.SpokeConfig memory spokeConfig = IHub(hubAddress).getSpokeConfig(assetId, spoke); (, uint8 decimals) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); - // GPOST_HUB_E + // GPOST_HUB_E: spoke-level addedAssets must be within addCap after an add action if ( - defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets > - defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets + defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].addedAssets > + defaultVarsBefore.spokeAssetVars[hubAddress][assetId][spoke].addedAssets && + defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].addedShares != + defaultVarsBefore.spokeAssetVars[hubAddress][assetId][spoke].addedShares /// @dev filter out interest accrual ) { if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets, + defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].addedAssets, spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_E ); } } - // GPOST_HUB_F + // GPOST_HUB_F: spoke-level owed must be within drawCap after a draw action if ( - defaultVarsAfter.assetVars[hubAddress][assetId].drawn > - defaultVarsBefore.assetVars[hubAddress][assetId].drawn + defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].owed > + defaultVarsBefore.spokeAssetVars[hubAddress][assetId][spoke].owed ) { if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - defaultVarsAfter.assetVars[hubAddress][assetId].drawn, + defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].owed, spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_F ); @@ -340,7 +374,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { assertTrue( currentActionSignature == ISpokeHandler.supply.selector || currentActionSignature == ISpokeHandler.repay.selector || - currentActionSignature == ISpokeHandler.liquidationCall.selector, + currentActionSignature == ISpokeHandler.liquidationCall.selector || + currentActionSignature == ISpokeHandler.updateUserRiskPremium.selector, GPOST_SP_LIQ_H ); } diff --git a/invariants/protocol-suite/hooks/HookAggregator.t.sol b/invariants/protocol-suite/hooks/HookAggregator.t.sol index 66f778ff9..e1f52c165 100644 --- a/invariants/protocol-suite/hooks/HookAggregator.t.sol +++ b/invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -65,16 +65,32 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { function _hubPostConditions() internal { // Iterate through all users to check for (uint256 i; i < usersToCheck.length; i++) { - // Avoid checking postconditions for CHECK_ALL_RESERVES actions - if (usersToCheck[i].reserveId != CHECK_ALL_RESERVES) { - uint256 assetId = _getAssetId(usersToCheck[i].spoke, usersToCheck[i].reserveId); - address hubAddress = _getHubAddress(usersToCheck[i].spoke, usersToCheck[i].reserveId); + address spoke = usersToCheck[i].spoke; + uint256 reserveId = usersToCheck[i].reserveId; + + // CHECK_ALL_RESERVES actions should check all reserves for that spoke + if (reserveId == CHECK_ALL_RESERVES) { + for (uint256 j; j < spokeReserveIds[spoke].length; j++) { + uint256 iterReserveId = spokeReserveIds[spoke][j]; + uint256 assetId = _getAssetId(spoke, iterReserveId); + address hubAddress = _getHubAddress(spoke, iterReserveId); + + assert_GPOST_HUB_A(hubAddress, assetId); + assert_GPOST_HUB_B(hubAddress, assetId); + assert_GPOST_HUB_C(hubAddress, assetId); + assert_GPOST_HUB_D(hubAddress, assetId); + assert_GPOST_HUB_EF(hubAddress, assetId, spoke); + assert_GPOST_HUB_G(hubAddress, assetId); + } + } else { + uint256 assetId = _getAssetId(spoke, reserveId); + address hubAddress = _getHubAddress(spoke, reserveId); assert_GPOST_HUB_A(hubAddress, assetId); assert_GPOST_HUB_B(hubAddress, assetId); assert_GPOST_HUB_C(hubAddress, assetId); assert_GPOST_HUB_D(hubAddress, assetId); - assert_GPOST_HUB_EF(hubAddress, assetId, usersToCheck[i].spoke); + assert_GPOST_HUB_EF(hubAddress, assetId, spoke); assert_GPOST_HUB_G(hubAddress, assetId); } } diff --git a/invariants/protocol-suite/invariants/HubInvariants.t.sol b/invariants/protocol-suite/invariants/HubInvariants.t.sol index 70bccf59e..127944eba 100644 --- a/invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -30,12 +30,12 @@ abstract contract HubInvariants is HandlerAggregator { } function assert_INV_HUB_B(address hubAddress, uint256 assetId) internal { - // Sum per-spoke values - uint256 spokeCount = spokesAddresses.length; + // Sum per-spoke values (use allSpokes to include treasury spokes for consistency with INV_HUB_GH/O) + uint256 spokeCount = allSpokes.length; uint256 sumDebt; for (uint256 i; i < spokeCount; i++) { - (uint256 d, uint256 p) = IHub(hubAddress).getSpokeOwed(assetId, spokesAddresses[i]); + (uint256 d, uint256 p) = IHub(hubAddress).getSpokeOwed(assetId, allSpokes[i]); sumDebt += d + p; } @@ -44,15 +44,15 @@ abstract contract HubInvariants is HandlerAggregator { } function assert_INV_HUB_C(address hubAddress, uint256 assetId) internal { - // Sum per-spoke values - uint256 spokeCount = spokesAddresses.length; + // Sum per-spoke values (use allSpokes to include treasury spokes for consistency with INV_HUB_GH/O) + uint256 spokeCount = allSpokes.length; uint256 sumDrawnShares; uint256 sumPremDrawnShares; int256 sumPremOffsetRay; for (uint256 i; i < spokeCount; i++) { - address spoke = spokesAddresses[i]; + address spoke = allSpokes[i]; sumDrawnShares += IHub(hubAddress).getSpokeDrawnShares(assetId, spoke); (uint256 premiumDrawnShares, int256 premiumOffsetRay) = IHub(hubAddress).getSpokePremiumData( assetId, @@ -79,11 +79,18 @@ abstract contract HubInvariants is HandlerAggregator { uint256 totalDebt = IHub(hubAddress).getAssetTotalOwed(assetId); uint256 accruedFees = IHub(hubAddress).getAssetAccruedFees(assetId); - // Checks //TODO check todo file INV_HUB_E + // Checks: totalAddedAssets ≈ previewRemoveByShares(totalAddedShares) + // Tolerance: the virtual offset (V=1e6) absorbs a fraction of accrued interest when + // converting all shares back to assets. The gap is ceil(V * interestAccrued / (shares + V)). + // We use the simpler bound: interestAccrued + 1, since V/(shares+V) < 1. + uint256 addedShares = IHub(hubAddress).getAddedShares(assetId); + uint256 interestAccrued = totalSuppliedAssets > addedShares + ? totalSuppliedAssets - addedShares + : 0; assertApproxEqAbs( totalSuppliedAssets, - IHub(hubAddress).previewRemoveByShares(assetId, IHub(hubAddress).getAddedShares(assetId)), - IHub(hubAddress).previewRemoveByShares(assetId, 1), // tolerance of 1 share for rounding + IHub(hubAddress).previewRemoveByShares(assetId, addedShares), + interestAccrued + 1, INV_HUB_E ); @@ -116,7 +123,7 @@ abstract contract HubInvariants is HandlerAggregator { assertApproxEqAbs( totalAddedAssets, IHub(hubAddress).getAddedAssets(assetId), - SPOKE_COUNT, + spokeCount, INV_HUB_G ); assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); @@ -143,18 +150,6 @@ abstract contract HubInvariants is HandlerAggregator { assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); } - function assert_INV_HUB_L(address hubAddress, uint256 assetId) internal { - (uint256 premiumShares, int256 premiumOffsetRay) = IHub(hubAddress).getAssetPremiumData( - assetId - ); - - assertGe( - int256(IHub(hubAddress).previewRestoreByShares(assetId, premiumShares) * WadRayMath.RAY), - premiumOffsetRay, - INV_HUB_L - ); - } - function assert_INV_HUB_O(address hubAddress, uint256 assetId) internal { uint256 spokeCount = allSpokes.length; uint256 totalDeficitRay; @@ -201,4 +196,29 @@ abstract contract HubInvariants is HandlerAggregator { uint256 tolerance = WadRayMath.RAY * PercentageMath.PERCENTAGE_FACTOR; assertApproxEqAbs(lhs, rhs, tolerance, INV_HUB_N); } + + function assert_INV_HUB_Q(address hubAddress, uint256 assetId) internal { + uint256 currentIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); + if (lastSeenDrawnIndex[hubAddress][assetId] > 0) { + assertGe(currentIndex, lastSeenDrawnIndex[hubAddress][assetId], INV_HUB_Q); + } + lastSeenDrawnIndex[hubAddress][assetId] = currentIndex; + } + + function assert_INV_HUB_R(address hubAddress, uint256 assetId) internal { + uint256 assets = IHub(hubAddress).getAddedAssets(assetId) + 1e6; + uint256 shares = IHub(hubAddress).getAddedShares(assetId) + 1e6; + if (lastSeenShares[hubAddress][assetId] > 0) { + // assets/shares >= lastAssets/lastShares <=> assets * lastShares >= lastAssets * shares + assertFullMulGe( + assets, + lastSeenShares[hubAddress][assetId], + lastSeenAssets[hubAddress][assetId], + shares, + INV_HUB_R + ); + } + lastSeenAssets[hubAddress][assetId] = assets; + lastSeenShares[hubAddress][assetId] = shares; + } } diff --git a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol index 751a84179..b8e799186 100644 --- a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol +++ b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol @@ -79,8 +79,10 @@ abstract contract SpokeInvariants is HandlerAggregator { function assert_INV_SP_D(address spoke, address user) internal { ISpoke.UserAccountData memory d = ISpoke(spoke).getUserAccountData(user); - if (d.totalCollateralValue == 0) { - assertEq(d.totalDebtValueRay, 0, INV_SP_D); // todo fix + if (d.totalDebtValueRay == 0) { + assertEq(d.healthFactor, UINT256_MAX, INV_SP_D); + } else if (d.totalCollateralValue == 0) { + assertEq(d.healthFactor, 0, INV_SP_D); } } @@ -101,4 +103,12 @@ abstract contract SpokeInvariants is HandlerAggregator { assertLe(sumUserAssets, reserveSuppliedAssets, INV_SP_F); assertApproxEqAbs(sumUserAssets, reserveSuppliedAssets, NUMBER_OF_ACTORS, INV_SP_F); } + + function assert_INV_SP_H(address spoke, uint256 reserveId, address user) internal { + uint32 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; + uint32 reserveKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; + if (userKey > 0) { + assertLe(uint256(userKey), uint256(reserveKey), INV_SP_H); + } + } } diff --git a/invariants/protocol-suite/replays/ReplayTest_8.t.sol b/invariants/protocol-suite/replays/ReplayTest_8.t.sol new file mode 100644 index 000000000..9952c1fd4 --- /dev/null +++ b/invariants/protocol-suite/replays/ReplayTest_8.t.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Libraries +import 'forge-std/Test.sol'; +import 'forge-std/console.sol'; + +// Contracts +import {Invariants} from '../Invariants.t.sol'; +import {Setup} from '../Setup.t.sol'; + +// Utils +import {Actor} from '../../shared/utils/Actor.sol'; + +contract ReplayTest7 is Invariants, Setup { + // Generated from Echidna reproducers + + // Target contract instance (you may need to adjust this) + ReplayTest7 Tester = this; + + modifier setup() override { + _; + } + + function setUp() public { + // Deploy protocol contracts + _setUp(); + + /// @dev fixes the actor to the first user + actor = actors[USER1]; + + vm.warp(101007); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY TESTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function test_replay_withdraw() public { + _setUpActor(USER2); + _delay(151486); + Tester.updateUserRiskPremium(127); + _setUpActor(USER3); + _delay(1030565); + Tester.setUsingAsCollateral(true, 0, 0); + _setUpActor(USER2); + _delay(70441); + Tester.updatePaused(true, 0, 152); + _delay(140369); + Tester.updateSpokeRiskPremiumThreshold(0, 13, 20, 75); + Tester.updateUserRiskPremium(156); + _setUpActor(USER1); + _delay(71842); + Tester.updateUserRiskPremium(7); + _setUpActor(USER2); + _delay(3481); + Tester.updateUserDynamicConfig(127); + Tester.updatePaused(true, 118, 165); + _setUpActor(USER1); + Tester.updateUserDynamicConfig(13); + _delay(340229); + Tester.updatePaused(false, 134, 157); + _setUpActor(USER2); + _delay(267059); + Tester.updateFrozen(true, 121, 96); + _delay(70398); + Tester.donateUnderlyingToSpoke(10, 26, 123); + _delay(570296); + Tester.updateSpokeHalted(false, 225, 36, 0); + _setUpActor(USER3); + _delay(40); + Tester.setPrice(528, 24); + _setUpActor(USER2); + Tester.supply(255, 190, 60, 95); + _setUpActor(USER3); + Tester.updateUserDynamicConfig(61); + _delay(28399); + Tester.freezeAllReserves(209); + _delay(352625); + Tester.updateUserRiskPremium(126); + _setUpActor(USER1); + Tester.pauseAllReserves(17); + _setUpActor(USER3); + _delay(585234); + Tester.updateFrozen(true, 190, 214); + _setUpActor(USER2); + _delay(395602); + Tester.updatePaused(false, 211, 11); + _delay(420078); + Tester.setPrice( + 32073901804028723412800996385287954179043683908620921829695310985447157891024, + 32 + ); + _setUpActor(USER1); + Tester.updateUserDynamicConfig(32); + _setUpActor(USER3); + _delay(233249); + Tester.freezeAllReserves(31); + _setUpActor(USER1); + _delay(147712); + Tester.freezeAllReserves(0); + _setUpActor(USER3); + Tester.updatePaused(false, 69, 126); + _setUpActor(USER1); + Tester.updatePaused(false, 204, 59); + _setUpActor(USER2); + _delay(3); + Tester.freezeAllReserves(208); + _setUpActor(USER1); + _delay(563779); + Tester.updateSpokeHalted(false, 24, 223, 0); + _setUpActor(USER3); + Tester.freezeAllReserves(78); + _setUpActor(USER2); + Tester.updateSpokeHalted(true, 144, 0, 21); + _setUpActor(USER1); + Tester.updateUserDynamicConfig(248); + _setUpActor(USER3); + _delay(434606); + Tester.updateSpokeHalted(true, 38, 5, 100); + _setUpActor(USER1); + _delay(260725); + Tester.updateBorrowable(false, 0, 174); + _setUpActor(USER3); + _delay(23626); + Tester.donateUnderlyingToHub( + 57896044618658097711785492504343953926547177528453588222701002057468963542481, + 121, + 149 + ); + _setUpActor(USER1); + Tester.setPrice(-2500, 70); + _setUpActor(USER3); + _delay(207320); + Tester.donateUnderlyingToHub( + 57896044618658097711785492504343953926464851149359812787997104700240680714240, + 0, + 22 + ); + _setUpActor(USER2); + Tester.updateBorrowable(true, 64, 0); + _setUpActor(USER3); + Tester.setPrice( + 13855700271963159636037022216593078218492230319803981905413115251605142940095, + 31 + ); + Tester.updateUserRiskPremium(80); + _setUpActor(USER2); + Tester.updatePaused(true, 104, 80); + _setUpActor(USER3); + _delay(448805); + Tester.freezeAllReserves(90); + _setUpActor(USER2); + Tester.updateFrozen(true, 0, 0); + _delay(432000); + Tester.updateUserDynamicConfig(86); + Tester.updateBorrowable(false, 225, 96); + _setUpActor(USER1); + _delay(333625); + Tester.updateUserRiskPremium(0); + _setUpActor(USER3); + _delay(531501); + Tester.withdraw( + 28019993473610222170674694923366466910776419356246130299415631191766869810267, + 1, + 188, + 147 + ); + } + + function test_replay_withdraw_2() public { + _setUpActor(USER2); + _delay(48); + Tester.supply(5765, 214, 57, 0); + Tester.pauseAllReserves(229); + Tester.withdraw( + 79270905586291627497307400106420653318261579396350751610744730950627405265, + 190, + 189, + 0 + ); + } + + function test_replay_withdraw_3() public { + _setUpActor(USER1); + _delay(48); + Tester.updatePaused(false, 43, 5); + _setUpActor(USER2); + Tester.donateUnderlyingToSpoke( + 4453226477229950780488458817391925832298660130277646469228882959756527864210, + 82, + 35 + ); + Tester.updateBorrowable(true, 102, 225); + Tester.updateUserRiskPremium(232); + _setUpActor(USER3); + Tester.updatePaused(true, 92, 0); + _setUpActor(USER1); + _delay(184974); + Tester.updateFrozen(false, 5, 198); + _setUpActor(USER2); + Tester.setPrice(-1, 56); + _setUpActor(USER1); + Tester.updateUserDynamicConfig(22); + _delay(12646); + Tester.supply(50000000, 0, 27, 0); + _setUpActor(USER3); + _delay(367615); + Tester.pauseAllReserves(137); + _setUpActor(USER1); + _delay(187876); + Tester.updateUserDynamicConfig(35); + Tester.donateUnderlyingToHub( + 15900516933484199723709268303309061460856563576000487395170929283679828504510, + 100, + 0 + ); + _setUpActor(USER2); + Tester.updatePaused(false, 153, 223); + _setUpActor(USER1); + Tester.withdraw( + 34926635329146824736853381233931534329217246589975803687784392641014182723417, + 0, + 201, + 12 + ); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Fast forward the time and set up an actor, + /// @dev Use for ECHIDNA call-traces + function _delay(uint256 _seconds) internal { + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up an actor + function _setUpActor(address _origin) internal { + actor = actors[_origin]; + } + + /// @notice Set up an actor and fast forward the time + /// @dev Use for ECHIDNA call-traces + function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { + actor = actors[_origin]; + vm.warp(block.timestamp + _seconds); + } + + /// @notice Set up a specific block and actor + function _setUpBlockAndActor(uint256 _block, address _user) internal { + vm.roll(_block); + actor = actors[_user]; + } + + /// @notice Set up a specific timestamp and actor + function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { + vm.warp(_timestamp); + actor = actors[_user]; + } +} diff --git a/invariants/protocol-suite/specs/InvariantsSpec.t.sol b/invariants/protocol-suite/specs/InvariantsSpec.t.sol index e6147de6c..b805db112 100644 --- a/invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -46,9 +46,6 @@ abstract contract InvariantsSpec { string constant INV_HUB_K = 'INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset'; - string constant INV_HUB_L = - 'INV_HUB_L: Asset.premiumShares.toDrawnAssetsUp() * WadRayMath.RAY >= Asset.premiumOffsetRay * WadRayMath.RAY'; - string constant INV_HUB_N = 'INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares'; @@ -58,6 +55,12 @@ abstract contract InvariantsSpec { string constant INV_HUB_P = 'INV_HUB_P: Premium offset should not exceed premium shares * drawnIndex'; + string constant INV_HUB_Q = + 'INV_HUB_Q: Drawn index must be monotonically non-decreasing across invariant checks'; + + string constant INV_HUB_R = + 'INV_HUB_R: Supply share price (addedAssets/addedShares) must be monotonically non-decreasing across invariant checks'; + /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -78,4 +81,7 @@ abstract contract InvariantsSpec { string constant INV_SP_F = 'INV_SP_F: Sum of user supplied assets on a spoke for a given asset == spoke supplied assets (hub spoke added assets)'; + + string constant INV_SP_H = + 'INV_SP_H: User dynamicConfigKey must never exceed the reserve dynamicConfigKey (no future config reference)'; } diff --git a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol index 3e0613d84..1b58f957e 100644 --- a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -105,5 +105,5 @@ abstract contract PostconditionsSpec { 'GPOST_SP_LIQ_G: Only liquidations can deteriorate the health factor of an already unhealthy account'; string constant GPOST_SP_LIQ_H = - 'GPOST_SP_LIQ_H: Only a supply, repay & liquidationCall can leave an account in an unhealthy state'; + 'GPOST_SP_LIQ_H: Only a supply, repay, liquidationCall & updateUserRiskPremium can leave an account in an unhealthy state'; } diff --git a/medusa.hub.json b/medusa.hub.json index 5b908a6ff..d67bc6b04 100644 --- a/medusa.hub.json +++ b/medusa.hub.json @@ -7,12 +7,8 @@ "callSequenceLength": 100, "corpusDirectory": "invariants/hub-suite/_corpus/medusa", "coverageEnabled": true, - "deploymentOrder": [ - "Tester" - ], - "targetContracts": [ - "Tester" - ], + "deploymentOrder": ["Tester"], + "targetContracts": ["Tester"], "targetContractsBalances": [ "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" ], @@ -21,11 +17,7 @@ }, "constructorArgs": {}, "deployerAddress": "0x30000", - "senderAddresses": [ - "0x10000", - "0x20000", - "0x30000" - ], + "senderAddresses": ["0x10000", "0x20000", "0x30000"], "blockNumberDelayMax": 60480, "blockTimestampDelayMax": 604800, "blockGasLimit": 12500000000, @@ -54,20 +46,13 @@ }, "propertyTesting": { "enabled": true, - "testPrefixes": [ - "fuzz_", - "invariant_" - ] + "testPrefixes": ["fuzz_", "invariant_"] }, "optimizationTesting": { "enabled": false, - "testPrefixes": [ - "optimize_" - ] + "testPrefixes": ["optimize_"] }, - "excludeFunctionSignatures": [ - "Tester.checkPostConditions()" - ] + "excludeFunctionSignatures": ["Tester.checkPostConditions()"] }, "chainConfig": { "codeSizeCheckDisabled": true, diff --git a/medusa.protocol.json b/medusa.protocol.json index 45542dedb..a84ddc77e 100644 --- a/medusa.protocol.json +++ b/medusa.protocol.json @@ -1,18 +1,14 @@ { "fuzzing": { - "workers": 10, + "workers": 30, "workerResetLimit": 50, "timeout": 0, "testLimit": 0, "callSequenceLength": 100, "corpusDirectory": "invariants/protocol-suite/_corpus/medusa", "coverageEnabled": true, - "deploymentOrder": [ - "Tester" - ], - "targetContracts": [ - "Tester" - ], + "deploymentOrder": ["Tester"], + "targetContracts": ["Tester"], "targetContractsBalances": [ "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" ], @@ -21,11 +17,7 @@ }, "constructorArgs": {}, "deployerAddress": "0x30000", - "senderAddresses": [ - "0x10000", - "0x20000", - "0x30000" - ], + "senderAddresses": ["0x10000", "0x20000", "0x30000"], "blockNumberDelayMax": 60480, "blockTimestampDelayMax": 604800, "blockGasLimit": 12500000000, @@ -54,20 +46,13 @@ }, "propertyTesting": { "enabled": true, - "testPrefixes": [ - "fuzz_", - "invariant_" - ] + "testPrefixes": ["fuzz_", "invariant_"] }, "optimizationTesting": { "enabled": false, - "testPrefixes": [ - "optimize_" - ] + "testPrefixes": ["optimize_"] }, - "excludeFunctionSignatures": [ - "Tester.checkPostConditions()" - ] + "excludeFunctionSignatures": ["Tester.checkPostConditions()"] }, "chainConfig": { "codeSizeCheckDisabled": true, From 047c5180744fce92d3c51de360eddff2719f4041 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:44:33 +0530 Subject: [PATCH 040/106] chore: ci cleanup --- .github/workflows/enigma-dark-invariants.yml | 112 ------------------- .github/workflows/invariants.yml | 42 +++++++ 2 files changed, 42 insertions(+), 112 deletions(-) delete mode 100644 .github/workflows/enigma-dark-invariants.yml create mode 100644 .github/workflows/invariants.yml diff --git a/.github/workflows/enigma-dark-invariants.yml b/.github/workflows/enigma-dark-invariants.yml deleted file mode 100644 index f431513f8..000000000 --- a/.github/workflows/enigma-dark-invariants.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: Echidna-Invariants Enigma-Dark - -on: - push: - branches: - - main - pull_request: - -env: - FOUNDRY_PROFILE: ci - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - setup: - name: Setup - runs-on: aave-latest - outputs: - corpus-sha: ${{ steps.clone-corpus.outputs.sha }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Generate GitHub App Token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: aave - repositories: aave-v4-invariants-corpus - - - name: Clone Corpus - id: clone-corpus - run: | - git clone https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/aave/aave-v4-invariants-corpus.git corpus - echo "sha=$(git -C corpus rev-parse HEAD)" >> $GITHUB_OUTPUT - - - name: Cache corpus - uses: actions/cache@v4 - with: - path: corpus - key: corpus-${{ steps.clone-corpus.outputs.sha }} - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly - - - name: Restore build cache - id: restore-build - uses: actions/cache@v4 - with: - path: | - out - cache - key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'invariants/**/*.sol') }} - - - name: Build contracts (skip if cache hit) - if: steps.restore-build.outputs.cache-hit != 'true' - run: forge build invariants/Tester.t.sol --build-info - - - name: Cache build artifacts - if: steps.restore-build.outputs.cache-hit != 'true' - id: save-build - uses: actions/cache@v4 - with: - path: | - out - cache - key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'invariants/**/*.sol') }} - - echidna: - name: Echidna-Invariants (${{ matrix.mode }}) Enigma-Dark - runs-on: ubuntu-latest - needs: setup - - strategy: - matrix: - mode: [property, assertion] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Restore corpus - uses: actions/cache@v4 - with: - path: corpus - key: corpus-${{ needs.setup.outputs.corpus-sha }} - - - name: Restore build cache - uses: actions/cache@v4 - with: - path: | - out - cache - key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml', 'invariants/**/*.sol') }} - - - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode - uses: crytic/echidna-action@v2 - with: - files: . - contract: Tester - config: invariants/_config/echidna_config_ci.yaml - test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml new file mode 100644 index 000000000..9c43d6c10 --- /dev/null +++ b/.github/workflows/invariants.yml @@ -0,0 +1,42 @@ +name: Echidna-Invariants Enigma-Dark + +on: + push: + branches: + - main + pull_request: + +env: + FOUNDRY_PROFILE: invariant + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + echidna: + name: Echidna-Invariants (${{ matrix.mode }}) + runs-on: ubuntu-latest + + strategy: + matrix: + mode: [property, assertion] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Run Foundry setup + uses: bgd-labs/github-workflows/.github/actions/foundry-setup@main + with: + FOUNDRY_VERSION: nightly + + - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode + uses: crytic/echidna-action@v2 + with: + files: . + contract: Tester + config: invariants/_config/echidna_config_ci.yaml + test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} From 1f36d03306e305147787157f2cb12c069d1503c8 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:52:10 +0530 Subject: [PATCH 041/106] chore: ci cleanup --- .github/workflows/invariants.yml | 2 +- invariants/protocol-suite/_config/echidna_config_ci.yaml | 2 +- invariants/protocol-suite/invariants/SpokeInvariants.t.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index 9c43d6c10..a1ad101a8 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -38,5 +38,5 @@ jobs: with: files: . contract: Tester - config: invariants/_config/echidna_config_ci.yaml + config: invariants/protocol-suite/_config/echidna_config_ci.yaml test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} diff --git a/invariants/protocol-suite/_config/echidna_config_ci.yaml b/invariants/protocol-suite/_config/echidna_config_ci.yaml index 11027a560..add954592 100644 --- a/invariants/protocol-suite/_config/echidna_config_ci.yaml +++ b/invariants/protocol-suite/_config/echidna_config_ci.yaml @@ -47,7 +47,7 @@ corpusDir: "corpus" #mutConsts: [100, 1, 1] #remappings -cryticArgs: ["--ignore-compile", "--compile-libraries=(LiquidationLogic,0xf01)"] +cryticArgs: ["--compile-libraries=(LiquidationLogic,0xf01)"] deployContracts: [["0xf01", "LiquidationLogic"]] diff --git a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol index b8e799186..055bac241 100644 --- a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol +++ b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol @@ -80,7 +80,7 @@ abstract contract SpokeInvariants is HandlerAggregator { function assert_INV_SP_D(address spoke, address user) internal { ISpoke.UserAccountData memory d = ISpoke(spoke).getUserAccountData(user); if (d.totalDebtValueRay == 0) { - assertEq(d.healthFactor, UINT256_MAX, INV_SP_D); + assertEq(d.healthFactor, type(uint256).max, INV_SP_D); } else if (d.totalCollateralValue == 0) { assertEq(d.healthFactor, 0, INV_SP_D); } From defc15c924864f959da344462e442bcfb555ff66 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:53:44 +0530 Subject: [PATCH 042/106] chore: ci cleanup --- foundry.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 7ebfec5ec..636ac1bb3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,7 +12,6 @@ bytecode_hash = "none" gas_snapshot_check = false gas_limit = 1099511627776 dynamic_test_linking = false # https://github.com/crytic/crytic-compile/issues/651#issuecomment-3813020679 -offline = true additional_compiler_profiles = [ { name = "hub", optimizer = true, via_ir = true, optimizer_runs = 22_300 }, From f881f981459629869b79436b54e9e234e125cb6a Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:21:43 +0530 Subject: [PATCH 043/106] chore: revert deploy utils, target ci --- .github/workflows/invariants.yml | 2 +- tests/DeployUtils.sol | 33 +++++--------------------------- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index a1ad101a8..a459ae903 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -37,6 +37,6 @@ jobs: uses: crytic/echidna-action@v2 with: files: . - contract: Tester + contract: invariants/protocol-suite/Tester.t.sol:Tester config: invariants/protocol-suite/_config/echidna_config_ci.yaml test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} diff --git a/tests/DeployUtils.sol b/tests/DeployUtils.sol index 1d6837fab..e928effd6 100644 --- a/tests/DeployUtils.sol +++ b/tests/DeployUtils.sol @@ -40,24 +40,7 @@ library DeployUtils { return ISpoke( proxify( - address(ISpokeInstance(deploy(_getSpokeInstanceInitCode(oracle, maxUserReservesLimit)))), - proxyAdminOwner, - initData - ) - ); - } - - function deploySpoke( - address oracle, - bytes32 /*implSalt*/, - uint16 maxUserReservesLimit, - address proxyAdminOwner, - bytes memory initData - ) internal returns (ISpoke) { - return - ISpoke( - proxify( - address(ISpokeInstance(deploy(_getSpokeInstanceInitCode(oracle, maxUserReservesLimit)))), + address(deploySpokeImplementation(oracle, maxUserReservesLimit, '')), proxyAdminOwner, initData ) @@ -83,18 +66,12 @@ library DeployUtils { } function deployHub(address authority) internal returns (IHub) { - return IHub(deploy(_getHubInitCode(authority))); - } - - function deploy(bytes memory bytecode) internal returns (address addr) { - assembly ('memory-safe') { - addr := create(0, add(bytecode, 0x20), mload(bytecode)) - } - require(addr != address(0)); + return deployHub(authority, ''); } - function deployHub(address authority, bytes32 /*salt*/) internal returns (IHub hub) { - return IHub(deploy(_getHubInitCode(authority))); + function deployHub(address authority, bytes32 salt) internal returns (IHub hub) { + Create2Utils.loadCreate2Factory(); + return IHub(Create2Utils.create2Deploy(salt, _getHubInitCode(authority))); } function getDeterministicHubAddress(address authority) internal returns (address) { From 8d3544600bb48f5f57309dd299a3fd981ff83369 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:59:40 +0530 Subject: [PATCH 044/106] chore: run specified file --- .github/workflows/invariants.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index a459ae903..037cda0b7 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -1,4 +1,4 @@ -name: Echidna-Invariants Enigma-Dark +name: Echidna-Invariants on: push: @@ -36,7 +36,7 @@ jobs: - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode uses: crytic/echidna-action@v2 with: - files: . - contract: invariants/protocol-suite/Tester.t.sol:Tester + files: invariants/protocol-suite/Tester.t.sol + contract: Tester config: invariants/protocol-suite/_config/echidna_config_ci.yaml test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} From 1460bbb05f98913860f9777c81186935102b54ff Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:12:52 +0530 Subject: [PATCH 045/106] chore: force precompile so it picks up correct profile --- .github/workflows/invariants.yml | 3 +++ invariants/protocol-suite/_config/echidna_config.yaml | 9 +++++++++ .../protocol-suite/_config/echidna_config_ci.yaml | 11 ++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index 037cda0b7..7fa902dcd 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -33,6 +33,9 @@ jobs: with: FOUNDRY_VERSION: nightly + - name: Run Forge Build + run: forge build --build-info + - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode uses: crytic/echidna-action@v2 with: diff --git a/invariants/protocol-suite/_config/echidna_config.yaml b/invariants/protocol-suite/_config/echidna_config.yaml index 08155a8ad..95384aeea 100644 --- a/invariants/protocol-suite/_config/echidna_config.yaml +++ b/invariants/protocol-suite/_config/echidna_config.yaml @@ -51,6 +51,15 @@ cryticArgs: ["--compile-libraries=(LiquidationLogic,0xf01)"] deployContracts: [["0xf01", "LiquidationLogic"]] +# create2factory +deployBytecodes: + [ + [ + "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7", + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3", + ], + ] + # maximum value to send to payable functions maxValue: 1e+30 # 100000000000 eth diff --git a/invariants/protocol-suite/_config/echidna_config_ci.yaml b/invariants/protocol-suite/_config/echidna_config_ci.yaml index add954592..9c5293647 100644 --- a/invariants/protocol-suite/_config/echidna_config_ci.yaml +++ b/invariants/protocol-suite/_config/echidna_config_ci.yaml @@ -47,10 +47,19 @@ corpusDir: "corpus" #mutConsts: [100, 1, 1] #remappings -cryticArgs: ["--compile-libraries=(LiquidationLogic,0xf01)"] +cryticArgs: ["--ignore-compile", "--compile-libraries=(LiquidationLogic,0xf01)"] deployContracts: [["0xf01", "LiquidationLogic"]] +# create2factory +deployBytecodes: + [ + [ + "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7", + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3", + ], + ] + # maximum value to send to payable functions maxValue: 1e+30 # 100000000000 eth From 358cf00e85603e3c01e882660b1546abca0aaba4 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:28:27 +0530 Subject: [PATCH 046/106] chore: adapt to latest --- invariants/protocol-suite/Setup.t.sol | 41 +++++++++++++-------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol index 62b65d415..cbc052884 100644 --- a/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -614,32 +614,31 @@ contract Setup is BaseTest { } { - bytes4[] memory selectors = new bytes4[](25); + bytes4[] memory selectors = new bytes4[](24); selectors[0] = ISpokeConfigurator.updateReservePriceSource.selector; selectors[1] = ISpokeConfigurator.updateLiquidationTargetHealthFactor.selector; selectors[2] = ISpokeConfigurator.updateHealthFactorForMaxBonus.selector; selectors[3] = ISpokeConfigurator.updateLiquidationBonusFactor.selector; selectors[4] = ISpokeConfigurator.updateLiquidationConfig.selector; - selectors[5] = ISpokeConfigurator.updateMaxReserves.selector; - selectors[6] = ISpokeConfigurator.addReserve.selector; - selectors[7] = ISpokeConfigurator.updatePaused.selector; - selectors[8] = ISpokeConfigurator.updateFrozen.selector; - selectors[9] = ISpokeConfigurator.updateBorrowable.selector; - selectors[10] = ISpokeConfigurator.updateReceiveSharesEnabled.selector; - selectors[11] = ISpokeConfigurator.updateCollateralRisk.selector; - selectors[12] = ISpokeConfigurator.addCollateralFactor.selector; - selectors[13] = ISpokeConfigurator.updateCollateralFactor.selector; - selectors[14] = ISpokeConfigurator.addMaxLiquidationBonus.selector; - selectors[15] = ISpokeConfigurator.updateMaxLiquidationBonus.selector; - selectors[16] = ISpokeConfigurator.addLiquidationFee.selector; - selectors[17] = ISpokeConfigurator.updateLiquidationFee.selector; - selectors[18] = ISpokeConfigurator.addDynamicReserveConfig.selector; - selectors[19] = ISpokeConfigurator.updateDynamicReserveConfig.selector; - selectors[20] = ISpokeConfigurator.pauseAllReserves.selector; - selectors[21] = ISpokeConfigurator.freezeAllReserves.selector; - selectors[22] = ISpokeConfigurator.pauseReserve.selector; - selectors[23] = ISpokeConfigurator.freezeReserve.selector; - selectors[24] = ISpokeConfigurator.updatePositionManager.selector; + selectors[5] = ISpokeConfigurator.addReserve.selector; + selectors[6] = ISpokeConfigurator.updatePaused.selector; + selectors[7] = ISpokeConfigurator.updateFrozen.selector; + selectors[8] = ISpokeConfigurator.updateBorrowable.selector; + selectors[9] = ISpokeConfigurator.updateReceiveSharesEnabled.selector; + selectors[10] = ISpokeConfigurator.updateCollateralRisk.selector; + selectors[11] = ISpokeConfigurator.addCollateralFactor.selector; + selectors[12] = ISpokeConfigurator.updateCollateralFactor.selector; + selectors[13] = ISpokeConfigurator.addMaxLiquidationBonus.selector; + selectors[14] = ISpokeConfigurator.updateMaxLiquidationBonus.selector; + selectors[15] = ISpokeConfigurator.addLiquidationFee.selector; + selectors[16] = ISpokeConfigurator.updateLiquidationFee.selector; + selectors[17] = ISpokeConfigurator.addDynamicReserveConfig.selector; + selectors[18] = ISpokeConfigurator.updateDynamicReserveConfig.selector; + selectors[19] = ISpokeConfigurator.pauseAllReserves.selector; + selectors[20] = ISpokeConfigurator.freezeAllReserves.selector; + selectors[21] = ISpokeConfigurator.pauseReserve.selector; + selectors[22] = ISpokeConfigurator.freezeReserve.selector; + selectors[23] = ISpokeConfigurator.updatePositionManager.selector; accessManager.setTargetFunctionRole( address(spokeConfigurator), From 5ab024eddca291d947dfc5f3e9f51bba5bca034a Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:48:06 +0530 Subject: [PATCH 047/106] chore: rm unnecessary --- invariants/protocol-suite/_config/echidna_config.yaml | 9 --------- invariants/protocol-suite/_config/echidna_config_ci.yaml | 9 --------- 2 files changed, 18 deletions(-) diff --git a/invariants/protocol-suite/_config/echidna_config.yaml b/invariants/protocol-suite/_config/echidna_config.yaml index 95384aeea..08155a8ad 100644 --- a/invariants/protocol-suite/_config/echidna_config.yaml +++ b/invariants/protocol-suite/_config/echidna_config.yaml @@ -51,15 +51,6 @@ cryticArgs: ["--compile-libraries=(LiquidationLogic,0xf01)"] deployContracts: [["0xf01", "LiquidationLogic"]] -# create2factory -deployBytecodes: - [ - [ - "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7", - "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3", - ], - ] - # maximum value to send to payable functions maxValue: 1e+30 # 100000000000 eth diff --git a/invariants/protocol-suite/_config/echidna_config_ci.yaml b/invariants/protocol-suite/_config/echidna_config_ci.yaml index 9c5293647..11027a560 100644 --- a/invariants/protocol-suite/_config/echidna_config_ci.yaml +++ b/invariants/protocol-suite/_config/echidna_config_ci.yaml @@ -51,15 +51,6 @@ cryticArgs: ["--ignore-compile", "--compile-libraries=(LiquidationLogic,0xf01)"] deployContracts: [["0xf01", "LiquidationLogic"]] -# create2factory -deployBytecodes: - [ - [ - "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7", - "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3", - ], - ] - # maximum value to send to payable functions maxValue: 1e+30 # 100000000000 eth From c623ef371a06cb709b4d06064c9fa690ce08cff0 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:19:15 +0530 Subject: [PATCH 048/106] chore: rm create2 --- invariants/hub-suite/Setup.t.sol | 41 +++++++++++++++++++++++---- invariants/protocol-suite/Setup.t.sol | 1 - 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/invariants/hub-suite/Setup.t.sol b/invariants/hub-suite/Setup.t.sol index 7a4eccd64..906b1456b 100644 --- a/invariants/hub-suite/Setup.t.sol +++ b/invariants/hub-suite/Setup.t.sol @@ -6,10 +6,6 @@ import {ActorsUtils} from '../shared/utils/ActorsUtils.sol'; import {Constants} from 'tests/Constants.sol'; import {Roles} from 'src/libraries/types/Roles.sol'; import {Actor} from '../shared/utils/Actor.sol'; -import 'forge-std/console.sol'; - -// Interfaces -import {IHub} from 'src/hub/interfaces/IHub.sol'; // Test Contracts import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; @@ -20,7 +16,8 @@ import {DeployUtils} from 'tests/DeployUtils.sol'; import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; -import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; +import {HubConfigurator, IHubConfigurator} from 'src/hub/HubConfigurator.sol'; +import {Hub, IHub} from 'src/hub/Hub.sol'; /// @notice Setup contract for the invariant test Suite, inherited by Tester contract Setup is BaseTest { @@ -68,7 +65,7 @@ contract Setup is BaseTest { accessManager = new AccessManager(admin); // Hub 1 - hub = DeployUtils.deployHub(address(accessManager)); + hub = new Hub(address(accessManager)); irStrategy = new AssetInterestRateStrategy(address(hub)); // Configurators @@ -294,6 +291,38 @@ contract Setup is BaseTest { selectors[2] = IHub.updateAssetConfig.selector; accessManager.setTargetFunctionRole(address(hub), selectors, Roles.HUB_ADMIN_ROLE); } + + { + bytes4[] memory selectors = new bytes4[](22); + selectors[0] = IHubConfigurator.updateLiquidityFee.selector; + selectors[1] = IHubConfigurator.updateFeeReceiver.selector; + selectors[2] = IHubConfigurator.updateFeeConfig.selector; + selectors[3] = IHubConfigurator.updateInterestRateStrategy.selector; + selectors[4] = IHubConfigurator.updateReinvestmentController.selector; + selectors[5] = IHubConfigurator.resetAssetCaps.selector; + selectors[6] = IHubConfigurator.deactivateAsset.selector; + selectors[7] = IHubConfigurator.haltAsset.selector; + selectors[8] = IHubConfigurator.addSpoke.selector; + selectors[9] = IHubConfigurator.addSpokeToAssets.selector; + selectors[10] = IHubConfigurator.updateSpokeActive.selector; + selectors[11] = IHubConfigurator.updateSpokeHalted.selector; + selectors[12] = IHubConfigurator.updateSpokeSupplyCap.selector; + selectors[13] = IHubConfigurator.updateSpokeDrawCap.selector; + selectors[14] = IHubConfigurator.updateSpokeRiskPremiumThreshold.selector; + selectors[15] = IHubConfigurator.updateSpokeCaps.selector; + selectors[16] = IHubConfigurator.deactivateSpoke.selector; + selectors[17] = IHubConfigurator.haltSpoke.selector; + selectors[18] = IHubConfigurator.resetSpokeCaps.selector; + selectors[19] = IHubConfigurator.updateInterestRateData.selector; + selectors[20] = IHubConfigurator.addAsset.selector; + selectors[21] = IHubConfigurator.addAssetWithDecimals.selector; + + accessManager.setTargetFunctionRole( + address(hubConfigurator), + selectors, + accessManager.PUBLIC_ROLE() + ); + } } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol index cbc052884..0a321ebee 100644 --- a/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -6,7 +6,6 @@ import {CREATE3} from '../shared/utils/CREATE3.sol'; import {ActorsUtils} from '../shared/utils/ActorsUtils.sol'; import {Constants} from 'tests/Constants.sol'; import {Roles} from 'src/libraries/types/Roles.sol'; -import 'forge-std/console.sol'; // Interfaces import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; From e9f047c9a9c6ebcb177a6bf39620ffc7c417e1f9 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:20:28 +0530 Subject: [PATCH 049/106] chore: typo --- invariants/hub-suite/Setup.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invariants/hub-suite/Setup.t.sol b/invariants/hub-suite/Setup.t.sol index 906b1456b..462236b47 100644 --- a/invariants/hub-suite/Setup.t.sol +++ b/invariants/hub-suite/Setup.t.sol @@ -69,7 +69,7 @@ contract Setup is BaseTest { irStrategy = new AssetInterestRateStrategy(address(hub)); // Configurators - hubConfigurator = new HubConfigurator(admin); + hubConfigurator = new HubConfigurator(address(accessManager)); _setUpConfiguratorRoles(); vm.label(address(accessManager), 'accessManager'); From 3759bf03e4769cc30c2e9a36f331264c25a93984 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:53:18 +0530 Subject: [PATCH 050/106] chore: fix failing hub invariants --- invariants/hub-suite/Invariants.t.sol | 3 +- invariants/hub-suite/base/BaseTest.t.sol | 2 +- .../hub-suite/hooks/HookAggregator.t.sol | 1 - .../hub-suite/invariants/HubInvariants.t.sol | 60 +++++++++++------ .../hub-suite/specs/HubInvariantsSpec.t.sol | 4 +- invariants/protocol-suite/Invariants.t.sol | 3 +- .../protocol-suite/hooks/HookAggregator.t.sol | 1 - .../invariants/HubInvariants.t.sol | 66 +++++++++++-------- .../protocol-suite/specs/InvariantsSpec.t.sol | 9 ++- 9 files changed, 91 insertions(+), 58 deletions(-) diff --git a/invariants/hub-suite/Invariants.t.sol b/invariants/hub-suite/Invariants.t.sol index 7c7a2b421..6897977b7 100644 --- a/invariants/hub-suite/Invariants.t.sol +++ b/invariants/hub-suite/Invariants.t.sol @@ -14,7 +14,8 @@ abstract contract Invariants is HubInvariants { assert_INV_HUB_A(i); assert_INV_HUB_B(i); assert_INV_HUB_C(i); - assert_INV_HUB_EF(i); + assert_INV_HUB_E(i); + assert_INV_HUB_F(i); assert_INV_HUB_GH(i); assert_INV_HUB_I(i); assert_INV_HUB_K(i); diff --git a/invariants/hub-suite/base/BaseTest.t.sol b/invariants/hub-suite/base/BaseTest.t.sol index 53311db8c..041b90500 100644 --- a/invariants/hub-suite/base/BaseTest.t.sol +++ b/invariants/hub-suite/base/BaseTest.t.sol @@ -37,7 +37,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @dev Solves medusa backward time warp issue modifier monotonicTimestamp() virtual { - /// @dev: Implement monotonic timestamp if needed + // @dev Implement monotonic timestamp if needed _; } diff --git a/invariants/hub-suite/hooks/HookAggregator.t.sol b/invariants/hub-suite/hooks/HookAggregator.t.sol index b8e5b9ae0..48c1e7fe5 100644 --- a/invariants/hub-suite/hooks/HookAggregator.t.sol +++ b/invariants/hub-suite/hooks/HookAggregator.t.sol @@ -6,7 +6,6 @@ import {DefaultBeforeAfterHooks} from './DefaultBeforeAfterHooks.t.sol'; // Utils import {ErrorHandlers} from '../../shared/utils/ErrorHandlers.sol'; -import 'forge-std/console.sol'; /// @title HookAggregator /// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler diff --git a/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol index be313a821..e853243f1 100644 --- a/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.19; // Libraries +import {Premium} from 'src/hub/libraries/Premium.sol'; +import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; @@ -18,6 +20,7 @@ import {HandlerAggregator} from '../HandlerAggregator.t.sol'; /// @dev Inherits HandlerAggregator to check actions in assertion testing mode abstract contract HubInvariants is HandlerAggregator { using SafeCast for *; + using WadRayMath for *; /////////////////////////////////////////////////////////////////////////////////////////////// // HUB // @@ -72,32 +75,47 @@ abstract contract HubInvariants is HandlerAggregator { assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); } - function assert_INV_HUB_EF(uint256 assetId) internal { - // Total amounts - uint256 totalSuppliedAssets = hub.getAddedAssets(assetId); + function assert_INV_HUB_E(uint256 assetId) internal { + uint256 totalAssets = hub.getAddedAssets(assetId); + uint256 totalShares = hub.getAddedShares(assetId); - IHub.Asset memory asset = hub.getAsset(assetId); - uint256 totalDebt = hub.getAssetTotalOwed(assetId); + // interest accrued on virtual shares + uint256 burntInterest = totalAssets - hub.previewRemoveByShares(assetId, totalShares); - // Checks + // Checks: totalAddedAssets ≈ previewRemoveByShares(totalAddedShares) + // Tolerance: the virtual offset (V=1e6) absorbs a fraction of accrued interest when + // converting all shares back to assets assertApproxEqAbs( - //TODO check todo file INV_HUB_E - totalSuppliedAssets, - hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)), - hub.previewRemoveByShares(assetId, 1), // tolerance of 1 share for rounding - INV_HUB_E + totalAssets, + hub.previewRemoveByShares(assetId, totalShares), // round down + burntInterest, + INV_HUB_E_1 ); - assertEq( - totalSuppliedAssets * WadRayMath.RAY, - asset.liquidity * WadRayMath.RAY + - totalDebt * WadRayMath.RAY + - asset.deficitRay + - asset.swept * WadRayMath.RAY, - INV_HUB_F + assertGe( + totalAssets + SharesMath.VIRTUAL_ASSETS, + hub.previewRemoveByShares(assetId, totalShares + SharesMath.VIRTUAL_ASSETS), + INV_HUB_E_2 ); } + function assert_INV_HUB_F(uint256 assetId) internal { + uint256 totalAssets = hub.getAddedAssets(assetId); + uint256 accruedFees = hub.getAssetAccruedFees(assetId); + + IHub.Asset memory asset = hub.getAsset(assetId); + uint256 index = hub.getAssetDrawnIndex(assetId); + uint256 premiumRay = Premium.calculatePremiumRay({ + premiumShares: asset.premiumShares, + premiumOffsetRay: asset.premiumOffsetRay, + drawnIndex: index + }); + uint256 drawnRay = asset.drawnShares * index; + uint256 aggregatedOwed = (drawnRay + premiumRay + asset.deficitRay).fromRayUp(); + + assertEq(totalAssets + accruedFees, asset.liquidity + aggregatedOwed + asset.swept, INV_HUB_F); + } + function assert_INV_HUB_GH(uint256 assetId) internal { uint256 spokeCount = NUMBER_OF_ACTORS; @@ -188,7 +206,8 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_ERC4626_A(uint256 assetId, address spoke) internal { uint256 addedAssets = hub.getSpokeAddedAssets(assetId, spoke); uint256 addedShares = hub.getSpokeAddedShares(assetId, spoke); - if (addedAssets != 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_A); + uint256 premiumShares = hub.getSpoke(assetId, spoke).premiumShares; + if (addedAssets != 0 && premiumShares == 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_A); } function assert_INV_HUB_ERC4626_B(uint256 assetId, address spoke) internal { @@ -200,7 +219,8 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_ERC4626_C(uint256 assetId) internal { uint256 addedAssets = hub.getAddedAssets(assetId); uint256 addedShares = hub.getAddedShares(assetId); - if (addedAssets != 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_C); + uint256 premiumShares = hub.getAsset(assetId).premiumShares; + if (addedAssets != 0 && premiumShares == 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_C); } function assert_INV_HUB_ERC4626_D(uint256 assetId) internal { diff --git a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol index 1468dadce..1ccf0a702 100644 --- a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -56,13 +56,13 @@ abstract contract HubInvariantsSpec is InvariantsSpec { // GENERIC string constant INV_HUB_ERC4626_A = - 'INV_HUB_ERC4626_A: Spoke cannot have non-zero assets and zero shares in add side'; + 'INV_HUB_ERC4626_A: Spoke cannot have non-zero assets and zero shares in add side without any premium'; string constant INV_HUB_ERC4626_B = 'INV_HUB_ERC4626_B: Spoke cannot have non-zero assets and zero shares in draw side'; string constant INV_HUB_ERC4626_C = - 'INV_HUB_ERC4626_C: Asset cannot have non-zero assets and zero shares in add side'; + 'INV_HUB_ERC4626_C: Asset cannot have non-zero assets and zero shares in add side without any premium'; string constant INV_HUB_ERC4626_D = 'INV_HUB_ERC4626_D: Asset cannot have non-zero assets and zero shares in draw side'; diff --git a/invariants/protocol-suite/Invariants.t.sol b/invariants/protocol-suite/Invariants.t.sol index 63d6ab2f4..70ca3dd08 100644 --- a/invariants/protocol-suite/Invariants.t.sol +++ b/invariants/protocol-suite/Invariants.t.sol @@ -28,7 +28,8 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { assert_INV_HUB_A(hubAddress, j); assert_INV_HUB_B(hubAddress, j); assert_INV_HUB_C(hubAddress, j); - assert_INV_HUB_EF(hubAddress, j); + assert_INV_HUB_E(hubAddress, j); + assert_INV_HUB_F(hubAddress, j); assert_INV_HUB_GH(hubAddress, j); assert_INV_HUB_I(hubAddress, j); assert_INV_HUB_K(hubAddress, j); diff --git a/invariants/protocol-suite/hooks/HookAggregator.t.sol b/invariants/protocol-suite/hooks/HookAggregator.t.sol index e1f52c165..e85ceec8c 100644 --- a/invariants/protocol-suite/hooks/HookAggregator.t.sol +++ b/invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -6,7 +6,6 @@ import {DefaultBeforeAfterHooks} from './DefaultBeforeAfterHooks.t.sol'; // Utils import {ErrorHandlers} from '../../shared/utils/ErrorHandlers.sol'; -import 'forge-std/console.sol'; /// @title HookAggregator /// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler diff --git a/invariants/protocol-suite/invariants/HubInvariants.t.sol b/invariants/protocol-suite/invariants/HubInvariants.t.sol index 127944eba..1f0a8e4b8 100644 --- a/invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -2,9 +2,11 @@ pragma solidity ^0.8.19; // Libraries +import {Premium} from 'src/hub/libraries/Premium.sol'; +import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; -import 'forge-std/console.sol'; +import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; // Interfaces import {IHub} from 'src/hub/interfaces/IHub.sol'; @@ -17,6 +19,9 @@ import {HandlerAggregator} from '../HandlerAggregator.t.sol'; /// @notice Implements Hub Invariants for the protocol /// @dev Inherits HandlerAggregator to check actions in assertion testing mode abstract contract HubInvariants is HandlerAggregator { + using SafeCast for *; + using WadRayMath for *; + /////////////////////////////////////////////////////////////////////////////////////////////// // HUB // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -71,43 +76,48 @@ abstract contract HubInvariants is HandlerAggregator { assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); } - function assert_INV_HUB_EF(address hubAddress, uint256 assetId) internal { - // Total amounts - uint256 totalSuppliedAssets = IHub(hubAddress).getAddedAssets(assetId); + function assert_INV_HUB_E(address hubAddress, uint256 assetId) internal { + uint256 totalAssets = IHub(hubAddress).getAddedAssets(assetId); + uint256 totalShares = IHub(hubAddress).getAddedShares(assetId); - IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); - uint256 totalDebt = IHub(hubAddress).getAssetTotalOwed(assetId); - uint256 accruedFees = IHub(hubAddress).getAssetAccruedFees(assetId); + // interest accrued on virtual shares + uint256 burntInterest = totalAssets - + IHub(hubAddress).previewRemoveByShares(assetId, totalShares); // Checks: totalAddedAssets ≈ previewRemoveByShares(totalAddedShares) // Tolerance: the virtual offset (V=1e6) absorbs a fraction of accrued interest when - // converting all shares back to assets. The gap is ceil(V * interestAccrued / (shares + V)). - // We use the simpler bound: interestAccrued + 1, since V/(shares+V) < 1. - uint256 addedShares = IHub(hubAddress).getAddedShares(assetId); - uint256 interestAccrued = totalSuppliedAssets > addedShares - ? totalSuppliedAssets - addedShares - : 0; + // converting all shares back to assets. assertApproxEqAbs( - totalSuppliedAssets, - IHub(hubAddress).previewRemoveByShares(assetId, addedShares), - interestAccrued + 1, - INV_HUB_E + totalAssets, + IHub(hubAddress).previewRemoveByShares(assetId, totalShares), // round down + burntInterest, + INV_HUB_E_1 ); - // totalAddedAssets + fees = liquidity + totalDebt + deficit + swept - // Note: uses approx equality due to rounding differences between totalOwed (rounds twice) - // and aggregatedOwedRay.fromRayUp() (rounds once) - assertApproxEqAbs( - (totalSuppliedAssets + accruedFees) * WadRayMath.RAY, - asset.liquidity * WadRayMath.RAY + - totalDebt * WadRayMath.RAY + - asset.deficitRay + - asset.swept * WadRayMath.RAY, - 2 * WadRayMath.RAY, // tolerance of 2 units for rounding - INV_HUB_F + assertGe( + totalAssets + SharesMath.VIRTUAL_ASSETS, + IHub(hubAddress).previewRemoveByShares(assetId, totalShares + SharesMath.VIRTUAL_ASSETS), + INV_HUB_E_2 ); } + function assert_INV_HUB_F(address hubAddress, uint256 assetId) internal { + uint256 totalAssets = IHub(hubAddress).getAddedAssets(assetId); + uint256 accruedFees = IHub(hubAddress).getAssetAccruedFees(assetId); + + IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); + uint256 index = IHub(hubAddress).getAssetDrawnIndex(assetId); + uint256 premiumRay = Premium.calculatePremiumRay({ + premiumShares: asset.premiumShares, + premiumOffsetRay: asset.premiumOffsetRay, + drawnIndex: index + }); + uint256 drawnRay = asset.drawnShares * index; + uint256 aggregatedOwed = (drawnRay + premiumRay + asset.deficitRay).fromRayUp(); + + assertEq(totalAssets + accruedFees, asset.liquidity + aggregatedOwed + asset.swept, INV_HUB_F); + } + function assert_INV_HUB_GH(address hubAddress, uint256 assetId) internal { uint256 spokeCount = allSpokes.length; diff --git a/invariants/protocol-suite/specs/InvariantsSpec.t.sol b/invariants/protocol-suite/specs/InvariantsSpec.t.sol index b805db112..5f57f2c73 100644 --- a/invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -28,11 +28,14 @@ abstract contract InvariantsSpec { string constant INV_HUB_C = 'INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub'; - string constant INV_HUB_E = - 'INV_HUB_E: hub.getAddedAssets and hub.previewRemoveByShares(hub.getAddedShares(assetId)) should match at any time, should not be off by more than 1 share worth of assets due to division precision loss'; + string constant INV_HUB_E_1 = + 'INV_HUB_E: total assets is equal to or greater than the supplied amount without taking into account the virtual assets and shares up to the burn interest to virtual shares'; + + string constant INV_HUB_E_2 = + 'INV_HUB_E: total assets is equal to the supplied amount when taking into account the virtual assets and shares'; string constant INV_HUB_F = - 'INV_HUB_F: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + totalDebt + deficit + swept'; + 'INV_HUB_F: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + (totalDebtRay + deficitRay).fromRayUp + swept'; string constant INV_HUB_G = 'INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT'; // TODO check if tolerance is correct From 5acd24e70113deac3594d3eeab42c3c25f0ea3f3 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:10:00 +0530 Subject: [PATCH 051/106] chore: grant spoke3 deficit eliminator role --- invariants/hub-suite/Setup.t.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/invariants/hub-suite/Setup.t.sol b/invariants/hub-suite/Setup.t.sol index 462236b47..a1745aab8 100644 --- a/invariants/hub-suite/Setup.t.sol +++ b/invariants/hub-suite/Setup.t.sol @@ -173,6 +173,7 @@ contract Setup is BaseTest { // Spoke 1: usdc, weth and wbtc // Spoke 2: weth and wbtc // Spoke 3: usdc, weth and wbtc + // Only Spoke 3 is granted DEFICIT_ELIMINATOR Role // Add SPOKE 1 assets to hub hub.addSpoke( @@ -276,6 +277,13 @@ contract Setup is BaseTest { usdc.approve(address(hub), type(uint256).max); weth.approve(address(hub), type(uint256).max); wbtc.approve(address(hub), type(uint256).max); + + accessManager.grantRole(Roles.DEFICIT_ELIMINATOR_ROLE, address(actors[USER3]), 0); + { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IHub.eliminateDeficit.selector; + accessManager.setTargetFunctionRole(address(hub), selectors, Roles.DEFICIT_ELIMINATOR_ROLE); + } } /// @notice Set up roles for the configurators From 2d560be4ed7844697a9e77b2a6f410d8aae1b9d1 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:37:39 +0530 Subject: [PATCH 052/106] fix: more inv --- invariants/hub-suite/handlers/HubHandler.t.sol | 4 ++-- .../hub-suite/handlers/interfaces/IHubHandler.sol | 12 +++++++++++- invariants/hub-suite/invariants/HubInvariants.t.sol | 8 +++++++- invariants/hub-suite/replays/ReplayTest_2.t.sol | 2 +- invariants/hub-suite/replays/ReplayTest_8.t.sol | 12 ++++++------ invariants/hub-suite/replays/ReplayTest_9.t.sol | 12 ------------ .../protocol-suite/invariants/HubInvariants.t.sol | 8 ++++---- 7 files changed, 31 insertions(+), 27 deletions(-) diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index 4c2aad51d..0bc9fe394 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -217,11 +217,11 @@ contract HubHandler is BaseHandler, IHubHandler { } } - function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external setup { + function eliminateDeficit(uint256 amount, uint8 i) external setup { bool success; bytes memory returnData; targetAssetId = _getRandomBaseAssetId(i); - address spoke = _getRandomActor(j); + address spoke = address(actors[USER3]); // todo move this to an auth handler? _before(); (success, returnData) = actor.proxy( diff --git a/invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubHandler.sol index f718c4a44..6b9bacab5 100644 --- a/invariants/hub-suite/handlers/interfaces/IHubHandler.sol +++ b/invariants/hub-suite/handlers/interfaces/IHubHandler.sol @@ -5,24 +5,34 @@ pragma solidity ^0.8.19; /// @notice Interface for hub handler actions interface IHubHandler { function add(uint256 amount, uint8 i) external returns (uint256 addedShares); + function remove(uint256 amount, uint8 i) external returns (uint256 removedShares); + function draw(uint256 amount, uint8 i) external returns (uint256 drawnShares); + function restore( uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i ) external returns (uint256 restoredDrawnShares); + function reportDeficit( uint256 drawnAmount, uint256 premiumAmount, int256 sharesDelta, uint8 i ) external; - function eliminateDeficit(uint256 amount, uint8 i, uint8 j) external; + + function eliminateDeficit(uint256 amount, uint8 i) external; + function refreshPremium(int256 sharesDelta, uint8 i) external; + function payFeeShares(uint256 shares, uint8 i) external; + function transferShares(uint256 shares, uint8 i, uint8 j) external; + function sweep(uint256 amount, uint8 i) external; + function reclaim(uint256 amount, uint8 i) external; } diff --git a/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol index e853243f1..281734774 100644 --- a/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -118,6 +118,7 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_GH(uint256 assetId) internal { uint256 spokeCount = NUMBER_OF_ACTORS; + uint256 tolerancePerActor = hub.previewAddByShares(assetId, 1); // Sum per-spoke values uint256 totalAddedAssets; @@ -132,7 +133,12 @@ abstract contract HubInvariants is HandlerAggregator { // Checks uint256 addedShares = hub.getAddedShares(assetId); if (addedShares > 0) { - assertApproxEqAbs(totalAddedAssets, hub.getAddedAssets(assetId), SPOKE_COUNT, INV_HUB_G); // TODO check if tolerance is correct + assertApproxEqAbs( + totalAddedAssets, + hub.getAddedAssets(assetId), + SPOKE_COUNT * tolerancePerActor, + INV_HUB_G + ); } assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); } diff --git a/invariants/hub-suite/replays/ReplayTest_2.t.sol b/invariants/hub-suite/replays/ReplayTest_2.t.sol index aff11b4e9..c5ddd9e06 100644 --- a/invariants/hub-suite/replays/ReplayTest_2.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_2.t.sol @@ -63,7 +63,7 @@ contract ReplayTest2Hub is Invariants, Setup { Tester.add(2, 1); Tester.draw(1, 1); _delay(1); - Tester.eliminateDeficit(1974577205127400860, 0, 0); + Tester.eliminateDeficit(1974577205127400860, 0); } /// @dev PASS diff --git a/invariants/hub-suite/replays/ReplayTest_8.t.sol b/invariants/hub-suite/replays/ReplayTest_8.t.sol index 7446384c5..4d9d272de 100644 --- a/invariants/hub-suite/replays/ReplayTest_8.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_8.t.sol @@ -36,12 +36,12 @@ contract ReplayTest8 is Invariants, Setup { // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// - function test_replay_8_eliminateDeficit() public { - _setUpActor(USER1); - Tester.refreshPremium(1, 0); - _delay(1); - Tester.eliminateDeficit(1, 0, 0); - } + // function test_replay_8_eliminateDeficit() public { + // _setUpActor(USER1); + // Tester.refreshPremium(1, 0); + // _delay(1); + // Tester.eliminateDeficit(1, 0, 0); + // } /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // diff --git a/invariants/hub-suite/replays/ReplayTest_9.t.sol b/invariants/hub-suite/replays/ReplayTest_9.t.sol index 8efe42615..6411d5a1e 100644 --- a/invariants/hub-suite/replays/ReplayTest_9.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_9.t.sol @@ -32,18 +32,6 @@ contract ReplayTest9 is Invariants, Setup { vm.warp(101007); } - /////////////////////////////////////////////////////////////////////////////////////////////// - // REPLAY TESTS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function test_replay_9_eliminateDeficit() public { - _setUpActor(USER1); - Tester.refreshPremium(1, 0); - _delay(1); - Tester.eliminateDeficit(1, 0, 0); - invariant_INV_HUB(); - } - /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/protocol-suite/invariants/HubInvariants.t.sol b/invariants/protocol-suite/invariants/HubInvariants.t.sol index 1f0a8e4b8..61ed474fd 100644 --- a/invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -40,11 +40,10 @@ abstract contract HubInvariants is HandlerAggregator { uint256 sumDebt; for (uint256 i; i < spokeCount; i++) { - (uint256 d, uint256 p) = IHub(hubAddress).getSpokeOwed(assetId, allSpokes[i]); - sumDebt += d + p; + sumDebt += IHub(hubAddress).getSpokeTotalOwed(assetId, allSpokes[i]); } - uint256 assetTotal = IHub(hubAddress).getAssetTotalOwed(assetId); // drawn + premium + uint256 assetTotal = IHub(hubAddress).getAssetTotalOwed(assetId); assertGe(sumDebt, assetTotal, INV_HUB_B); } @@ -120,6 +119,7 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_GH(address hubAddress, uint256 assetId) internal { uint256 spokeCount = allSpokes.length; + uint256 tolerancePerActor = IHub(hubAddress).previewAddByShares(assetId, 1); // Sum per-spoke values uint256 totalAddedAssets; @@ -133,7 +133,7 @@ abstract contract HubInvariants is HandlerAggregator { assertApproxEqAbs( totalAddedAssets, IHub(hubAddress).getAddedAssets(assetId), - spokeCount, + spokeCount * tolerancePerActor, INV_HUB_G ); assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); From 9e2ae79d082ca99f1c1ebc0b75684717fb22f65f Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:48:41 +0530 Subject: [PATCH 053/106] fix: sum of all invariants by accounting for burnt interest --- invariants/hub-suite/base/BaseTest.t.sol | 21 ++++++++++--------- .../hub-suite/invariants/HubInvariants.t.sol | 9 +++++--- .../hub-suite/replays/ReplayTest_2.t.sol | 9 -------- .../hub-suite/replays/ReplayTest_9.t.sol | 9 ++++++++ invariants/protocol-suite/base/BaseTest.t.sol | 17 ++++++++------- .../invariants/HubInvariants.t.sol | 4 +++- 6 files changed, 38 insertions(+), 31 deletions(-) diff --git a/invariants/hub-suite/base/BaseTest.t.sol b/invariants/hub-suite/base/BaseTest.t.sol index 041b90500..aaf4b965f 100644 --- a/invariants/hub-suite/base/BaseTest.t.sol +++ b/invariants/hub-suite/base/BaseTest.t.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.19; // Libraries import {Vm} from 'forge-std/Base.sol'; import {StdUtils} from 'forge-std/StdUtils.sol'; -import 'forge-std/console.sol'; // Interfaces -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; // Utils @@ -109,10 +109,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU function _transferByActor(address token, address to, uint256 amount) internal { bool success; bytes memory returnData; - (success, returnData) = actor.proxy( - token, - abi.encodeWithSelector(IERC20.transfer.selector, to, amount) - ); + (success, returnData) = actor.proxy(token, abi.encodeCall(IERC20.transfer, (to, amount))); require(success, string(returnData)); } @@ -120,10 +117,14 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU function _approveByActor(address token, address spender, uint256 amount) internal { bool success; bytes memory returnData; - (success, returnData) = actor.proxy( - token, - abi.encodeWithSelector(IERC20.approve.selector, spender, amount) - ); + (success, returnData) = actor.proxy(token, abi.encodeCall(IERC20.approve, (spender, amount))); require(success, string(returnData)); } + + /// @notice Helper function to calculate burnt interest in assets terms (originating from virtual shares) + function _calculateBurntInterest(IHub hub_, uint256 assetId_) internal view returns (uint256) { + uint256 totalAssets = hub_.getAddedAssets(assetId_); + uint256 totalShares = hub_.getAddedShares(assetId_); + return totalAssets - hub_.previewRemoveByShares(assetId_, totalShares); + } } diff --git a/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol index 281734774..ea5f71d61 100644 --- a/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -127,8 +127,11 @@ abstract contract HubInvariants is HandlerAggregator { totalAddedAssets += hub.getSpokeAddedAssets(assetId, actorAddresses[i]); totalAddedShares += hub.getSpokeAddedShares(assetId, actorAddresses[i]); } - totalAddedAssets += hub.getSpokeAddedAssets(assetId, address(this)); - totalAddedShares += hub.getSpokeAddedShares(assetId, address(this)); + address feeReceiver = hub.getAsset(assetId).feeReceiver; + totalAddedAssets += hub.getSpokeAddedAssets(assetId, feeReceiver); + totalAddedShares += hub.getSpokeAddedShares(assetId, feeReceiver); + + totalAddedAssets += _calculateBurntInterest(hub, assetId); // Checks uint256 addedShares = hub.getAddedShares(assetId); @@ -136,7 +139,7 @@ abstract contract HubInvariants is HandlerAggregator { assertApproxEqAbs( totalAddedAssets, hub.getAddedAssets(assetId), - SPOKE_COUNT * tolerancePerActor, + (spokeCount + 2) * tolerancePerActor, INV_HUB_G ); } diff --git a/invariants/hub-suite/replays/ReplayTest_2.t.sol b/invariants/hub-suite/replays/ReplayTest_2.t.sol index c5ddd9e06..f88b02629 100644 --- a/invariants/hub-suite/replays/ReplayTest_2.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_2.t.sol @@ -57,15 +57,6 @@ contract ReplayTest2Hub is Invariants, Setup { invariant_INV_HUB(); } - /// @dev PASS - function test_replay_2_eliminateDeficit() public { - _setUpActor(USER1); - Tester.add(2, 1); - Tester.draw(1, 1); - _delay(1); - Tester.eliminateDeficit(1974577205127400860, 0); - } - /// @dev PASS function test_replay_2_remove() public { _setUpActor(USER1); diff --git a/invariants/hub-suite/replays/ReplayTest_9.t.sol b/invariants/hub-suite/replays/ReplayTest_9.t.sol index 6411d5a1e..91094372d 100644 --- a/invariants/hub-suite/replays/ReplayTest_9.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_9.t.sol @@ -32,6 +32,15 @@ contract ReplayTest9 is Invariants, Setup { vm.warp(101007); } + function test_replay_9_sum_of_balances() public { + Tester.add(1, 2); + Tester.refreshPremium(445022, 2); + _delay(18880); + _delay(23650); + Tester.donateUnderlyingToHub(0, 0); + invariant_INV_HUB(); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/protocol-suite/base/BaseTest.t.sol b/invariants/protocol-suite/base/BaseTest.t.sol index daceb5627..198022cca 100644 --- a/invariants/protocol-suite/base/BaseTest.t.sol +++ b/invariants/protocol-suite/base/BaseTest.t.sol @@ -170,10 +170,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU function _transferByActor(address token, address to, uint256 amount) internal { bool success; bytes memory returnData; - (success, returnData) = actor.proxy( - token, - abi.encodeWithSelector(IERC20.transfer.selector, to, amount) - ); + (success, returnData) = actor.proxy(token, abi.encodeCall(IERC20.transfer, (to, amount))); require(success, string(returnData)); } @@ -181,10 +178,14 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU function _approveByActor(address token, address spender, uint256 amount) internal { bool success; bytes memory returnData; - (success, returnData) = actor.proxy( - token, - abi.encodeWithSelector(IERC20.approve.selector, spender, amount) - ); + (success, returnData) = actor.proxy(token, abi.encodeCall(IERC20.approve, (spender, amount))); require(success, string(returnData)); } + + /// @notice Helper function to calculate burnt interest in assets terms (originating from virtual shares) + function _calculateBurntInterest(IHub hub_, uint256 assetId_) internal view returns (uint256) { + uint256 totalAssets = hub_.getAddedAssets(assetId_); + uint256 totalShares = hub_.getAddedShares(assetId_); + return totalAssets - hub_.previewRemoveByShares(assetId_, totalShares); + } } diff --git a/invariants/protocol-suite/invariants/HubInvariants.t.sol b/invariants/protocol-suite/invariants/HubInvariants.t.sol index 61ed474fd..0f08bdf27 100644 --- a/invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -129,11 +129,13 @@ abstract contract HubInvariants is HandlerAggregator { totalAddedShares += IHub(hubAddress).getSpokeAddedShares(assetId, allSpokes[i]); } + totalAddedAssets += _calculateBurntInterest(IHub(hubAddress), assetId); + // Checks assertApproxEqAbs( totalAddedAssets, IHub(hubAddress).getAddedAssets(assetId), - spokeCount * tolerancePerActor, + (spokeCount + 2) * tolerancePerActor, INV_HUB_G ); assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); From bf392bc35e3a54309d520b1a0dd9fea5d3ab68de Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:32:40 +0530 Subject: [PATCH 054/106] chore: fix premium cond --- .../hub-suite/invariants/HubInvariants.t.sol | 14 ++++++++++---- invariants/hub-suite/replays/ReplayTest_8.t.sol | 14 ++++++++------ medusa.hub.json | 10 ++-------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol index ea5f71d61..9aad4c58f 100644 --- a/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -215,8 +215,11 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_ERC4626_A(uint256 assetId, address spoke) internal { uint256 addedAssets = hub.getSpokeAddedAssets(assetId, spoke); uint256 addedShares = hub.getSpokeAddedShares(assetId, spoke); - uint256 premiumShares = hub.getSpoke(assetId, spoke).premiumShares; - if (addedAssets != 0 && premiumShares == 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_A); + uint256 premiumRay = hub.getAssetPremiumRay(assetId); + // since premium can be incurred without drawing liquidity + if (addedAssets != 0 && premiumRay == 0) { + assertTrue(addedShares != 0, INV_HUB_ERC4626_A); + } } function assert_INV_HUB_ERC4626_B(uint256 assetId, address spoke) internal { @@ -228,8 +231,11 @@ abstract contract HubInvariants is HandlerAggregator { function assert_INV_HUB_ERC4626_C(uint256 assetId) internal { uint256 addedAssets = hub.getAddedAssets(assetId); uint256 addedShares = hub.getAddedShares(assetId); - uint256 premiumShares = hub.getAsset(assetId).premiumShares; - if (addedAssets != 0 && premiumShares == 0) assertTrue(addedShares != 0, INV_HUB_ERC4626_C); + uint256 premiumRay = hub.getAssetPremiumRay(assetId); + // since premium can be incurred without drawing liquidity + if (addedAssets != 0 && premiumRay == 0) { + assertTrue(addedShares != 0, INV_HUB_ERC4626_C); + } } function assert_INV_HUB_ERC4626_D(uint256 assetId) internal { diff --git a/invariants/hub-suite/replays/ReplayTest_8.t.sol b/invariants/hub-suite/replays/ReplayTest_8.t.sol index 4d9d272de..915f01df3 100644 --- a/invariants/hub-suite/replays/ReplayTest_8.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_8.t.sol @@ -36,12 +36,14 @@ contract ReplayTest8 is Invariants, Setup { // REPLAY TESTS // /////////////////////////////////////////////////////////////////////////////////////////////// - // function test_replay_8_eliminateDeficit() public { - // _setUpActor(USER1); - // Tester.refreshPremium(1, 0); - // _delay(1); - // Tester.eliminateDeficit(1, 0, 0); - // } + function test_replay_8() public { + _setUpActor(USER1); + _delay(35); + Tester.refreshPremium(3, 0); + _delay(1); + Tester.refreshPremium(-3, 0); + invariant_INV_HUB(); + } /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // diff --git a/medusa.hub.json b/medusa.hub.json index d67bc6b04..2364bcf54 100644 --- a/medusa.hub.json +++ b/medusa.hub.json @@ -12,9 +12,7 @@ "targetContractsBalances": [ "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" ], - "predeployedContracts": { - "LiquidationLogic": "0xf01" - }, + "predeployedContracts": {}, "constructorArgs": {}, "deployerAddress": "0x30000", "senderAddresses": ["0x10000", "0x20000", "0x30000"], @@ -68,11 +66,7 @@ "target": "invariants/hub-suite/Tester.t.sol", "solcVersion": "", "exportDirectory": "", - "args": [ - "--solc-remaps", - "forge-std/=../../../lib/forge-std/src/", - "--compile-libraries=(LiquidationLogic,0xf01)" - ] + "args": ["--solc-remaps", "forge-std/=../../../lib/forge-std/src/"] } }, "logging": { From ef40cc786302ed229455c1abdf37f1ec3770e6fe Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:57:31 +0530 Subject: [PATCH 055/106] feat: broader refresh premium target --- .../hub-suite/handlers/HubHandler.t.sol | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index 0bc9fe394..c7461fe34 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -2,13 +2,12 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; import {IHub, IHubBase} from 'src/hub/interfaces/IHub.sol'; import {IHubHandler} from './interfaces/IHubHandler.sol'; // Libraries import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; -import 'forge-std/console.sol'; // Test Contracts import {Actor} from '../../shared/utils/Actor.sol'; @@ -268,6 +267,32 @@ contract HubHandler is BaseHandler, IHubHandler { } } + // @dev broader `refreshPremium` to cover edge cases, above case exists for narrow happy path + function refreshPremium(IHubBase.PremiumDelta memory premiumDelta, uint8 i) external setup { + bool success; + bytes memory returnData; + targetAssetId = _getRandomBaseAssetId(i); + + _before(); + (success, returnData) = actor.proxy( + address(hub), + abi.encodeCall(IHubBase.refreshPremium, (targetAssetId, premiumDelta)) + ); + + if (success) { + _after(); + + // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) + assertEq( + defaultVarsAfter.assetVars[targetAssetId].premium, + defaultVarsBefore.assetVars[targetAssetId].premium, + HSPOST_HUB_M + ); + } else { + revert('HubHandler: refreshPremium failed'); + } + } + function payFeeShares(uint256 shares, uint8 i) external setup { bool success; bytes memory returnData; From f2a8d4db5de48e030fa339629e57065c294fa94b Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:58:00 +0530 Subject: [PATCH 056/106] temp: clamp ci normal tests --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 636ac1bb3..687de909f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -38,7 +38,7 @@ runs = 1000 seed = "0x640" [profile.pr.fuzz] -runs = 5000 +runs = 5 # todo revert [profile.ci.fuzz] runs = 10000 From 2e8c3536625557c2f3aa75e132603b13042f2af0 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:58:14 +0530 Subject: [PATCH 057/106] feat: fuzz parser utility --- fuzz_parser.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 fuzz_parser.py diff --git a/fuzz_parser.py b/fuzz_parser.py new file mode 100644 index 000000000..fc8c7ce2b --- /dev/null +++ b/fuzz_parser.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +import re + +def parse_echidna_trace(trace): + """Parse Echidna trace and extract function calls, parameters, delays, and 'from' addresses.""" + calls = [] + last_address = None # Track the last 'from' address + + for line in trace.strip().splitlines(): + # Parse function calls, including those with underscores + func_call = re.match(r"Tester\.([a-zA-Z0-9_]+)\(([^)]*)\)", line) + if func_call: + func_name = func_call.group(1) + params = func_call.group(2) + + # Check for 'from' address + from_address = re.search(r"from: (0x[a-fA-F0-9]+)", line) + time_delay = re.search(r"Time delay: (\d+)", line) + + # If we have a 'from' address, we need to set it up + if from_address: + address = from_address.group(1) + if address != last_address: + # Only set up the actor if the address has changed + calls.append(f"_setUpActor({address});") + last_address = address + + # Add the delay if it exists + if time_delay: + delay_time = time_delay.group(1) + calls.append(f"_delay({delay_time});") + + # Add the function call + calls.append(f"Tester.{func_name}({params});") + + # Handle special case of "*wait*" for a delay + elif "*wait*" in line: + time_delay = re.search(r"Time delay: (\d+)", line) + if time_delay: + delay_time = time_delay.group(1) + calls.append(f"_delay({delay_time});") + + return calls + +def parse_medusa_trace(trace): + """Parse Medusa trace and extract function calls, parameters, block, time, and sender information.""" + calls = [] + last_address = None # Track the last 'from' address + last_block = 0 + last_time = 0 + + for line in trace.strip().splitlines(): + # Skip empty lines + if not line.strip(): + continue + + # Parse Medusa format: number) Contract.function(types)(values) (metadata) + medusa_pattern = r"(?:\d+\))?\s*Tester\.([a-zA-Z0-9_]+)\([^)]*\)\(([^)]*)\)\s*\(block=(\d+),\s*time=(\d+).*sender=(0x[a-fA-F0-9]+)\)" + match = re.search(medusa_pattern, line) + + if match: + func_name = match.group(1) + params = match.group(2) + block = int(match.group(3)) + time = int(match.group(4)) + address = match.group(5) + + if (address == "0x10000"): + actor = "USER1" + elif (address == "0x20000"): + actor = "USER2" + elif (address == "0x30000"): + actor = "USER3" + + # If sender address changed, set up the new actor + if address != last_address: + calls.append(f"_setUpActor({actor});") + last_address = address + + # Add block/time delay if needed + if time > last_time: + # Medusa reports absolute timestamps, so we calculate the difference + # between consecutive transactions to determine the delay + time_diff = time - last_time + if time_diff > 0: + calls.append(f"_delay({time_diff});") + last_time = time + last_block = block + + # Add the function call + calls.append(f"Tester.{func_name}({params});") + + return calls + +def detect_trace_format(trace): + """Detect whether the trace is in Echidna or Medusa format.""" + # Check for Medusa format indicators (block=X, time=Y, sender=0xZ) + if re.search(r"\(block=\d+,\s*time=\d+.*sender=0x[a-fA-F0-9]+\)", trace): + return "medusa" + # Default to Echidna format + return "echidna" + +def generate_foundry_test(calls, test_name="test_replay"): + """Generate the Solidity test function code.""" + test_code = [f"function {test_name}() public {{"] + test_code.extend(f" {call}" for call in calls) + test_code.append("}") + + return "\n".join(test_code) + +# Ask user to paste the trace +print("Paste your Echidna or Medusa call trace below. Press Enter twice to finish:") +trace = [] +while True: + line = input() + if line: + trace.append(line.strip()) + else: + break +trace = "\n".join(trace) + +# Detect format and parse the trace +format_type = detect_trace_format(trace) +if format_type == "medusa": + print("\nDetected Medusa trace format") + parsed_calls = parse_medusa_trace(trace) +else: + print("\nDetected Echidna trace format") + parsed_calls = parse_echidna_trace(trace) + +# Generate the test +solidity_test = generate_foundry_test(parsed_calls) + +# Output the generated Solidity test +print("\nGenerated Foundry Test Function:\n") +print(solidity_test) From 7ccbdb441a697ad8f23ae1d8ae2e0e201e5b0e9e Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:28:30 +0530 Subject: [PATCH 058/106] feat: run for longer --- .github/workflows/invariants_hub.yml | 45 +++++++++++++++++++ .../hub-suite/_config/echidna_config_ci.yaml | 2 +- .../_config/echidna_config_ci.yaml | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/invariants_hub.yml diff --git a/.github/workflows/invariants_hub.yml b/.github/workflows/invariants_hub.yml new file mode 100644 index 000000000..b772f36f8 --- /dev/null +++ b/.github/workflows/invariants_hub.yml @@ -0,0 +1,45 @@ +name: Echidna-Invariants + +on: + push: + branches: + - main + pull_request: + +env: + FOUNDRY_PROFILE: invariant + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + echidna: + name: Echidna-Invariants (${{ matrix.mode }}) + runs-on: ubuntu-latest + + strategy: + matrix: + mode: [property, assertion] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Run Foundry setup + uses: bgd-labs/github-workflows/.github/actions/foundry-setup@main + with: + FOUNDRY_VERSION: nightly + + - name: Run Forge Build + run: forge build --build-info + + - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode + uses: crytic/echidna-action@v2 + with: + files: invariants/hub-suite/Tester.t.sol + contract: Tester + config: invariants/hub-suite/_config/echidna_config_ci.yaml + test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} diff --git a/invariants/hub-suite/_config/echidna_config_ci.yaml b/invariants/hub-suite/_config/echidna_config_ci.yaml index 11027a560..964e975fa 100644 --- a/invariants/hub-suite/_config/echidna_config_ci.yaml +++ b/invariants/hub-suite/_config/echidna_config_ci.yaml @@ -11,7 +11,7 @@ balanceAddr: 0x1000000000000000000000000 balanceContract: 0x1000000000000000000000000000000000000000000000000 #testLimit is the number of test sequences to run -testLimit: 80000 +testLimit: 200000 #seqLen defines how many transactions are in a test sequence seqLen: 300 diff --git a/invariants/protocol-suite/_config/echidna_config_ci.yaml b/invariants/protocol-suite/_config/echidna_config_ci.yaml index 11027a560..964e975fa 100644 --- a/invariants/protocol-suite/_config/echidna_config_ci.yaml +++ b/invariants/protocol-suite/_config/echidna_config_ci.yaml @@ -11,7 +11,7 @@ balanceAddr: 0x1000000000000000000000000 balanceContract: 0x1000000000000000000000000000000000000000000000000 #testLimit is the number of test sequences to run -testLimit: 80000 +testLimit: 200000 #seqLen defines how many transactions are in a test sequence seqLen: 300 From e6a663f1e3dca4f2f3004c1b868d434bcc082817 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:53:57 +0530 Subject: [PATCH 059/106] fix: update workflow name to Echidna-Invariants-Hub --- .github/workflows/invariants_hub.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/invariants_hub.yml b/.github/workflows/invariants_hub.yml index b772f36f8..13833a94b 100644 --- a/.github/workflows/invariants_hub.yml +++ b/.github/workflows/invariants_hub.yml @@ -1,4 +1,4 @@ -name: Echidna-Invariants +name: Echidna-Invariants-Hub on: push: From 2a3f7bce7dc7ed563557b755db3116d7e7d69391 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:21:24 +0530 Subject: [PATCH 060/106] feat: hub cleanup --- invariants/hub-suite/Invariants.t.sol | 1 - invariants/hub-suite/base/BaseHandler.t.sol | 2 +- .../handlers/HubConfiguratorHandler.t.sol | 5 +- .../hub-suite/handlers/HubHandler.t.sol | 21 ++- .../simulators/DonationAttackHandler.t.sol | 2 + .../hooks/DefaultBeforeAfterHooks.t.sol | 158 ++++++++++-------- .../hub-suite/invariants/HubInvariants.t.sol | 45 +---- .../hub-suite/replays/ReplayTest_8.t.sol | 9 + invariants/protocol-suite/Invariants.t.sol | 1 - .../invariants/HubInvariants.t.sol | 30 ---- .../protocol-suite/specs/InvariantsSpec.t.sol | 3 - 11 files changed, 123 insertions(+), 154 deletions(-) diff --git a/invariants/hub-suite/Invariants.t.sol b/invariants/hub-suite/Invariants.t.sol index 6897977b7..2fd14d0b5 100644 --- a/invariants/hub-suite/Invariants.t.sol +++ b/invariants/hub-suite/Invariants.t.sol @@ -21,7 +21,6 @@ abstract contract Invariants is HubInvariants { assert_INV_HUB_K(i); assert_INV_HUB_O(i); assert_INV_HUB_P(i); - assert_INV_HUB_N(i); for (uint256 j; j < NUMBER_OF_ACTORS; j++) { assert_INV_HUB_ERC4626_A(i, actorAddresses[j]); assert_INV_HUB_ERC4626_B(i, actorAddresses[j]); diff --git a/invariants/hub-suite/base/BaseHandler.t.sol b/invariants/hub-suite/base/BaseHandler.t.sol index a5e422751..573007136 100644 --- a/invariants/hub-suite/base/BaseHandler.t.sol +++ b/invariants/hub-suite/base/BaseHandler.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; // Libraries import {MockERC20} from 'tests/mocks/MockERC20.sol'; diff --git a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol index fd6ce3a49..3378e7ff3 100644 --- a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol +++ b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol @@ -2,12 +2,9 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; import {IHubConfiguratorHandler} from './interfaces/IHubConfiguratorHandler.sol'; -// Libraries -import 'forge-std/console.sol'; - // Test Contracts import {Actor} from '../../shared/utils/Actor.sol'; import {BaseHandler} from '../base/BaseHandler.t.sol'; diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index c7461fe34..8c0c0eb54 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -174,7 +174,18 @@ contract HubHandler is BaseHandler, IHubHandler { (uint256 drawnAfter, ) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); - assertEq(drawnBefore, drawnAfter + drawnAmount, HSPOST_HUB_ERC4626_RESTORE_A); + if (restoredDrawnShares > 0) { + uint256 tolerance = hub.previewRestoreByShares(cachedTargetAssetId, 1); + assertApproxEqAbs( + drawnBefore, + drawnAfter + drawnAmount, + tolerance, + HSPOST_HUB_ERC4626_RESTORE_A + ); + } else { + // dust case, all restored assets donated + assertEq(drawnAfter, drawnBefore, HSPOST_HUB_ERC4626_RESTORE_A); + } assertEq( drawnSharesBefore, hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)) + restoredDrawnShares, @@ -258,8 +269,8 @@ contract HubHandler is BaseHandler, IHubHandler { // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) assertEq( - defaultVarsAfter.assetVars[targetAssetId].premium, - defaultVarsBefore.assetVars[targetAssetId].premium, + _assetVarsAfter(targetAssetId).debt.premium, + _assetVarsBefore(targetAssetId).debt.premium, HSPOST_HUB_M ); } else { @@ -284,8 +295,8 @@ contract HubHandler is BaseHandler, IHubHandler { // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) assertEq( - defaultVarsAfter.assetVars[targetAssetId].premium, - defaultVarsBefore.assetVars[targetAssetId].premium, + _assetVarsAfter(targetAssetId).debt.premium, + _assetVarsBefore(targetAssetId).debt.premium, HSPOST_HUB_M ); } else { diff --git a/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol b/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol index 1af52dcd9..ad927e37b 100644 --- a/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol +++ b/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol @@ -23,7 +23,9 @@ contract DonationAttackHandler is BaseHandler { function donateUnderlyingToHub(uint256 amount, uint8 i) external { address underlying = _getRandomBaseAsset(i); + _before(); TestnetERC20(underlying).mint(address(hub), amount); + _after(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index 8a3e88e52..7c2c93936 100644 --- a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.19; // Libraries +import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; import {MathUtils} from 'src/libraries/math/MathUtils.sol'; +import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; -import 'forge-std/console.sol'; // Utils import {Actor} from '../../shared/utils/Actor.sol'; @@ -23,33 +24,32 @@ import {BaseHooks} from '../base/BaseHooks.t.sol'; /// @notice Helper contract for before and after hooks, state variable caching and postconditions /// @dev This contract is inherited by handlers abstract contract DefaultBeforeAfterHooks is BaseHooks { + using WadRayMath for *; + /////////////////////////////////////////////////////////////////////////////////////////////// // STRUCTS // /////////////////////////////////////////////////////////////////////////////////////////////// + struct Debt { + uint256 drawn; + uint256 premium; + uint256 owed; + } + struct AssetVars { + IHub.Asset asset; + uint256 drawnRate; uint256 drawnIndex; uint256 totalAssets; uint256 totalShares; - uint256 drawn; - uint256 premium; - uint256 liquidity; - uint256 deficitRay; - uint256 swept; - uint256 lastUpdateTimestamp; - uint256 drawnRate; + Debt debt; } struct SpokeDataVars { + IHub.SpokeData spokeData; uint256 addedAssets; uint256 addedShares; - uint256 drawnShares; - uint256 premiumShares; - int256 premiumOffsetRay; - uint256 deficitRay; - uint256 drawn; - uint256 premium; - uint256 owed; + Debt debt; } struct DefaultVars { @@ -97,42 +97,33 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// - function _setAssetValues(DefaultVars storage _defaultVars) internal { + function _setAssetValues(DefaultVars storage vars) internal { uint256 assetCount = hub.getAssetCount(); - for (uint256 j; j < assetCount; j++) { - _defaultVars.assetVars[j].drawnIndex = hub.getAssetDrawnIndex(j); - _defaultVars.assetVars[j].totalAssets = hub.getAddedAssets(j); - _defaultVars.assetVars[j].totalShares = hub.getAddedShares(j); - (_defaultVars.assetVars[j].drawn, _defaultVars.assetVars[j].premium) = hub.getAssetOwed(j); - _defaultVars.assetVars[j].lastUpdateTimestamp = hub.getAsset(j).lastUpdateTimestamp; - _defaultVars.assetVars[j].drawnRate = hub.getAssetDrawnRate(j); - _defaultVars.assetVars[j].liquidity = hub.getAssetLiquidity(j); - _defaultVars.assetVars[j].deficitRay = hub.getAssetDeficitRay(j); - _defaultVars.assetVars[j].swept = hub.getAssetSwept(j); + for (uint256 i; i < assetCount; ++i) { + (uint256 drawn, uint256 premium) = hub.getAssetOwed(i); + vars.assetVars[i] = AssetVars({ + asset: hub.getAsset(i), + drawnRate: hub.getAssetDrawnRate(i), + drawnIndex: hub.getAssetDrawnIndex(i), + totalAssets: hub.getAddedAssets(i), + totalShares: hub.getAddedShares(i), + debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + }); } } - function _setSpokeDataValues(DefaultVars storage _defaultVars) internal { + function _setSpokeDataValues(DefaultVars storage vars) internal { uint256 assetCount = hub.getAssetCount(); - - for (uint256 i; i < assetCount; i++) { - for (uint256 j; j < NUMBER_OF_ACTORS; j++) { + for (uint256 i; i < assetCount; ++i) { + for (uint256 j; j < NUMBER_OF_ACTORS; ++j) { address spoke = actorAddresses[j]; - - _defaultVars.spokeDataVars[i][spoke].addedShares = hub.getSpokeAddedShares(i, spoke); - _defaultVars.spokeDataVars[i][spoke].addedAssets = hub.getSpokeAddedAssets(i, spoke); - _defaultVars.spokeDataVars[i][spoke].drawnShares = hub.getSpokeDrawnShares(i, spoke); - ( - _defaultVars.spokeDataVars[i][spoke].premiumShares, - _defaultVars.spokeDataVars[i][spoke].premiumOffsetRay - ) = hub.getSpokePremiumData(i, spoke); - _defaultVars.spokeDataVars[i][spoke].deficitRay = hub.getSpokeDeficitRay(i, spoke); - ( - _defaultVars.spokeDataVars[i][spoke].drawn, - _defaultVars.spokeDataVars[i][spoke].premium - ) = hub.getSpokeOwed(i, spoke); - _defaultVars.spokeDataVars[i][spoke].owed = - _defaultVars.spokeDataVars[i][spoke].drawn + _defaultVars.spokeDataVars[i][spoke].premium; + (uint256 drawn, uint256 premium) = hub.getSpokeOwed(i, spoke); + vars.spokeDataVars[i][spoke] = SpokeDataVars({ + spokeData: hub.getSpoke(i, spoke), + addedAssets: hub.getSpokeAddedAssets(i, spoke), + addedShares: hub.getSpokeAddedShares(i, spoke), + debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + }); } } } @@ -142,19 +133,20 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { /////////////////////////////////////////////////////////////////////////////////////////////// function assert_GPOST_HUB_A(uint256 assetId) internal { - assertGe( - defaultVarsAfter.assetVars[assetId].drawnIndex, - defaultVarsBefore.assetVars[assetId].drawnIndex, - GPOST_HUB_A - ); + AssetVars memory varsBefore = _assetVarsBefore(assetId); + AssetVars memory varsAfter = _assetVarsAfter(assetId); + assertGe(varsAfter.drawnIndex, varsBefore.drawnIndex, GPOST_HUB_A); } function assert_GPOST_HUB_B(uint256 assetId) internal { + AssetVars memory varsBefore = _assetVarsBefore(assetId); + AssetVars memory varsAfter = _assetVarsAfter(assetId); + assertFullMulGe( - defaultVarsAfter.assetVars[assetId].totalAssets + 1e6, - defaultVarsBefore.assetVars[assetId].totalShares + 1e6, - defaultVarsBefore.assetVars[assetId].totalAssets + 1e6, - defaultVarsAfter.assetVars[assetId].totalShares + 1e6, + varsAfter.totalAssets + SharesMath.VIRTUAL_ASSETS, + varsBefore.totalShares + SharesMath.VIRTUAL_SHARES, + varsBefore.totalAssets + SharesMath.VIRTUAL_ASSETS, + varsAfter.totalShares + SharesMath.VIRTUAL_SHARES, GPOST_HUB_B ); } @@ -175,14 +167,15 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { signature == IHubHandler.payFeeShares.selector || signature == IHubHandler.transferShares.selector ) { + AssetVars memory vars = _assetVarsAfter(assetId); assertEq( - hub.getAssetDrawnRate(assetId), + vars.drawnRate, IAssetInterestRateStrategy(irStrategy).calculateInterestRate( assetId, - hub.getAssetLiquidity(assetId), - defaultVarsAfter.assetVars[assetId].drawn, - 0, // Unused in the interest rate calculation - hub.getAssetSwept(assetId) + vars.asset.liquidity, + vars.debt.drawn, + vars.asset.deficitRay.fromRayUp(), + vars.asset.swept ), GPOST_HUB_C ); @@ -190,7 +183,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } function assert_GPOST_HUB_D(uint256 assetId) internal { - assertLe(defaultVarsAfter.assetVars[assetId].lastUpdateTimestamp, block.timestamp, GPOST_HUB_D); + assertLe(_assetVarsAfter(assetId).asset.lastUpdateTimestamp, block.timestamp, GPOST_HUB_D); } function assert_GPOST_HUB_EF(uint256 assetId, address spoke) internal { @@ -199,15 +192,17 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { (, uint8 decimals) = hub.getAssetUnderlyingAndDecimals(assetId); // GPOST_HUB_E + SpokeDataVars memory spokeDataBefore = _spokeDataVarsBefore(assetId, spoke); + SpokeDataVars memory spokeDataAfter = _spokeDataVarsAfter(assetId, spoke); + if ( - defaultVarsAfter.spokeDataVars[assetId][spoke].addedAssets > - defaultVarsBefore.spokeDataVars[assetId][spoke].addedAssets && - defaultVarsAfter.spokeDataVars[assetId][spoke].addedShares != - defaultVarsBefore.spokeDataVars[assetId][spoke].addedShares /// @dev required to avoid interest accrual detection + spokeDataAfter.addedAssets > spokeDataBefore.addedAssets && + spokeDataAfter.addedShares != spokeDataBefore.addedShares && + spokeDataBefore.addedShares != 0 /// @dev required to avoid interest accrual detection ) { if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - defaultVarsAfter.spokeDataVars[assetId][spoke].addedAssets, + spokeDataAfter.addedAssets, spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_E ); @@ -215,13 +210,10 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } // GPOST_HUB_F - if ( - defaultVarsAfter.spokeDataVars[assetId][spoke].owed > - defaultVarsBefore.spokeDataVars[assetId][spoke].owed - ) { + if (spokeDataAfter.debt.owed > spokeDataBefore.debt.owed) { if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - defaultVarsAfter.spokeDataVars[assetId][spoke].owed, + spokeDataAfter.debt.owed, spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_F ); @@ -231,8 +223,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function assert_GPOST_HUB_G(uint256 assetId) internal { assertGe( - defaultVarsAfter.assetVars[assetId].lastUpdateTimestamp, - defaultVarsBefore.assetVars[assetId].lastUpdateTimestamp, + _assetVarsAfter(assetId).asset.lastUpdateTimestamp, + _assetVarsBefore(assetId).asset.lastUpdateTimestamp, GPOST_HUB_G ); } @@ -243,4 +235,26 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function _cacheCurrentActionSignature() internal { currentActionSignature = bytes4(msg.sig); } + + function _assetVarsBefore(uint256 assetId) internal view returns (AssetVars memory) { + return defaultVarsBefore.assetVars[assetId]; + } + + function _assetVarsAfter(uint256 assetId) internal view returns (AssetVars memory) { + return defaultVarsAfter.assetVars[assetId]; + } + + function _spokeDataVarsBefore( + uint256 assetId, + address spoke + ) internal view returns (SpokeDataVars memory) { + return defaultVarsBefore.spokeDataVars[assetId][spoke]; + } + + function _spokeDataVarsAfter( + uint256 assetId, + address spoke + ) internal view returns (SpokeDataVars memory) { + return defaultVarsAfter.spokeDataVars[assetId][spoke]; + } } diff --git a/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol index 9aad4c58f..686edf96d 100644 --- a/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -67,12 +67,12 @@ abstract contract HubInvariants is HandlerAggregator { } // Asset totals - IHub.Asset memory a = hub.getAsset(assetId); + IHub.Asset memory asset = hub.getAsset(assetId); // Checks - assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); - assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); - assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); + assertEq(sumDrawnShares, asset.drawnShares, INV_HUB_C); + assertEq(sumPremDrawnShares, asset.premiumShares, INV_HUB_C); + assertEq(sumPremOffsetRay, asset.premiumOffsetRay, INV_HUB_C); } function assert_INV_HUB_E(uint256 assetId) internal { @@ -104,13 +104,14 @@ abstract contract HubInvariants is HandlerAggregator { uint256 accruedFees = hub.getAssetAccruedFees(assetId); IHub.Asset memory asset = hub.getAsset(assetId); - uint256 index = hub.getAssetDrawnIndex(assetId); + uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); + uint256 premiumRay = Premium.calculatePremiumRay({ premiumShares: asset.premiumShares, premiumOffsetRay: asset.premiumOffsetRay, - drawnIndex: index + drawnIndex: drawnIndex }); - uint256 drawnRay = asset.drawnShares * index; + uint256 drawnRay = asset.drawnShares * drawnIndex; uint256 aggregatedOwed = (drawnRay + premiumRay + asset.deficitRay).fromRayUp(); assertEq(totalAssets + accruedFees, asset.liquidity + aggregatedOwed + asset.swept, INV_HUB_F); @@ -182,36 +183,6 @@ abstract contract HubInvariants is HandlerAggregator { assertGe((premiumShares * drawnIndex).toInt256(), premiumOffsetRay, INV_HUB_P); } - function assert_INV_HUB_N(uint256 assetId) internal { - IHub.Asset memory asset = hub.getAsset(assetId); - - // Skip if no debt (no interest can accrue) - if (asset.drawnShares == 0 && asset.premiumShares == 0) return; - - // Get current index (includes unrealized interest) vs stored index - uint256 currentIndex = hub.getAssetDrawnIndex(assetId); - uint256 storedIndex = asset.drawnIndex; - - // Skip if no index growth (no interest accrued) - if (currentIndex == storedIndex) return; - - // Calculate accrued interest from index growth - uint256 indexGrowth = currentIndex - storedIndex; - uint256 totalDebtShares = uint256(asset.drawnShares) + uint256(asset.premiumShares); - uint256 accruedInterestRay = (totalDebtShares * indexGrowth); - - // Get unrealized fees - uint256 unrealizedFees = hub.getAssetAccruedFees(assetId) - asset.realizedFees; - - // Invariant: fees × PERCENTAGE_FACTOR × RAY ≈ accruedInterestRay × liquidityFee - uint256 lhs = unrealizedFees * PercentageMath.PERCENTAGE_FACTOR * WadRayMath.RAY; - uint256 rhs = accruedInterestRay * asset.liquidityFee; - - // Tolerance: 1 wei rounding scaled up - uint256 tolerance = WadRayMath.RAY * PercentageMath.PERCENTAGE_FACTOR; - assertApproxEqAbs(lhs, rhs, tolerance, INV_HUB_N); - } - function assert_INV_HUB_ERC4626_A(uint256 assetId, address spoke) internal { uint256 addedAssets = hub.getSpokeAddedAssets(assetId, spoke); uint256 addedShares = hub.getSpokeAddedShares(assetId, spoke); diff --git a/invariants/hub-suite/replays/ReplayTest_8.t.sol b/invariants/hub-suite/replays/ReplayTest_8.t.sol index 915f01df3..f3025aa0c 100644 --- a/invariants/hub-suite/replays/ReplayTest_8.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_8.t.sol @@ -45,6 +45,15 @@ contract ReplayTest8 is Invariants, Setup { invariant_INV_HUB(); } + function test_replay_8_2() public { + _setUpActor(USER1); + Tester.add(1, 0); + Tester.draw(1, 0); + _delay(5); + Tester.restore(1, 0, 0, 0); + invariant_INV_HUB(); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/protocol-suite/Invariants.t.sol b/invariants/protocol-suite/Invariants.t.sol index 70ca3dd08..d57406ac5 100644 --- a/invariants/protocol-suite/Invariants.t.sol +++ b/invariants/protocol-suite/Invariants.t.sol @@ -35,7 +35,6 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { assert_INV_HUB_K(hubAddress, j); assert_INV_HUB_O(hubAddress, j); assert_INV_HUB_P(hubAddress, j); - assert_INV_HUB_N(hubAddress, j); assert_INV_HUB_Q(hubAddress, j); assert_INV_HUB_R(hubAddress, j); } diff --git a/invariants/protocol-suite/invariants/HubInvariants.t.sol b/invariants/protocol-suite/invariants/HubInvariants.t.sol index 0f08bdf27..44aab85bb 100644 --- a/invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -179,36 +179,6 @@ abstract contract HubInvariants is HandlerAggregator { assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); } - function assert_INV_HUB_N(address hubAddress, uint256 assetId) internal { - IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); - - // Skip if no debt (no interest can accrue) - if (asset.drawnShares == 0 && asset.premiumShares == 0) return; - - // Get current index (includes unrealized interest) vs stored index - uint256 currentIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); - uint256 storedIndex = asset.drawnIndex; - - // Skip if no index growth (no interest accrued) - if (currentIndex == storedIndex) return; - - // Calculate accrued interest from index growth - uint256 indexGrowth = currentIndex - storedIndex; - uint256 totalDebtShares = uint256(asset.drawnShares) + uint256(asset.premiumShares); - uint256 accruedInterestRay = (totalDebtShares * indexGrowth); - - // Get unrealized fees - uint256 unrealizedFees = IHub(hubAddress).getAssetAccruedFees(assetId) - asset.realizedFees; - - // Invariant: fees × PERCENTAGE_FACTOR × RAY ≈ accruedInterestRay × liquidityFee - uint256 lhs = unrealizedFees * PercentageMath.PERCENTAGE_FACTOR * WadRayMath.RAY; - uint256 rhs = accruedInterestRay * asset.liquidityFee; - - // Tolerance: 1 wei rounding scaled up - uint256 tolerance = WadRayMath.RAY * PercentageMath.PERCENTAGE_FACTOR; - assertApproxEqAbs(lhs, rhs, tolerance, INV_HUB_N); - } - function assert_INV_HUB_Q(address hubAddress, uint256 assetId) internal { uint256 currentIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); if (lastSeenDrawnIndex[hubAddress][assetId] > 0) { diff --git a/invariants/protocol-suite/specs/InvariantsSpec.t.sol b/invariants/protocol-suite/specs/InvariantsSpec.t.sol index 5f57f2c73..26e6304f4 100644 --- a/invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -49,9 +49,6 @@ abstract contract InvariantsSpec { string constant INV_HUB_K = 'INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset'; - string constant INV_HUB_N = - 'INV_HUB_N: Liquidity growth (ie accrued interest) = AccruedFees + sum of Accrued for all spokes with non zero addedShares'; - string constant INV_HUB_O = 'INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay'; From e4e835b0a1519185678a316ed2437bba294a744b Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:30:06 +0530 Subject: [PATCH 061/106] feat: begin protocol suite cleanup --- invariants/hub-suite/base/BaseHandler.t.sol | 9 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 3 + invariants/protocol-suite/Invariants.t.sol | 75 ++--- invariants/protocol-suite/Setup.t.sol | 56 ++-- .../protocol-suite/base/BaseHandler.t.sol | 19 +- .../protocol-suite/base/BaseStorage.t.sol | 39 +-- invariants/protocol-suite/base/BaseTest.t.sol | 23 +- .../handlers/hub/HubConfiguratorHandler.t.sol | 5 +- .../simulators/DonationAttackHandler.t.sol | 33 +-- .../spoke/SpokeConfiguratorHandler.t.sol | 2 +- .../handlers/spoke/SpokeHandler.t.sol | 99 +++---- .../hooks/DefaultBeforeAfterHooks.t.sol | 279 ++++++++++-------- .../protocol-suite/hooks/HookAggregator.t.sol | 51 ++-- .../invariants/HubInvariants.t.sol | 15 +- .../invariants/SpokeInvariants.t.sol | 16 +- .../protocol-suite/replays/ReplayTest_1.t.sol | 10 +- .../protocol-suite/replays/ReplayTest_2.t.sol | 10 +- .../protocol-suite/replays/ReplayTest_3.t.sol | 18 +- .../protocol-suite/replays/ReplayTest_4.t.sol | 18 +- .../protocol-suite/replays/ReplayTest_5.t.sol | 10 +- .../protocol-suite/replays/ReplayTest_6.t.sol | 23 +- .../protocol-suite/replays/ReplayTest_7.t.sol | 34 +-- .../protocol-suite/replays/ReplayTest_8.t.sol | 32 +- invariants/shared/utils/Actor.sol | 55 ++-- invariants/shared/utils/ActorsUtils.sol | 23 +- 25 files changed, 477 insertions(+), 480 deletions(-) diff --git a/invariants/hub-suite/base/BaseHandler.t.sol b/invariants/hub-suite/base/BaseHandler.t.sol index 573007136..d5b739f46 100644 --- a/invariants/hub-suite/base/BaseHandler.t.sol +++ b/invariants/hub-suite/base/BaseHandler.t.sol @@ -52,7 +52,6 @@ contract BaseHandler is HookAggregator { } /// @notice Helper function to safely approve an amount of tokens to a spender - function _approve(address token, address owner, address spender, uint256 amount) internal { vm.prank(owner); _safeApprove(token, spender, 0); @@ -63,11 +62,9 @@ contract BaseHandler is HookAggregator { /// @notice Helper function to safely approve an amount of tokens to a spender /// @dev This function is used to revert on failed approvals function _safeApprove(address token, address spender, uint256 amount) internal { - (bool success, bytes memory retdata) = token.call( - abi.encodeWithSelector(IERC20.approve.selector, spender, amount) - ); - assert(success); - if (retdata.length > 0) assert(abi.decode(retdata, (bool))); + (bool ok, bytes memory ret) = token.call(abi.encodeCall(IERC20.approve, (spender, amount))); + assert(ok); + if (ret.length > 0) assert(abi.decode(ret, (bool))); } /// @notice Helper function to mint an amount of tokens to an address diff --git a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index 7c2c93936..3768e377f 100644 --- a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -232,6 +232,9 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// + /// @dev Only use these helpers in postConditions, do NOT rely on them at Invariants because they not be populated + /// when the fuzzer has updated env (eg block.timestamp) which does not invoke any Handler + function _cacheCurrentActionSignature() internal { currentActionSignature = bytes4(msg.sig); } diff --git a/invariants/protocol-suite/Invariants.t.sol b/invariants/protocol-suite/Invariants.t.sol index d57406ac5..7ebef16f3 100644 --- a/invariants/protocol-suite/Invariants.t.sol +++ b/invariants/protocol-suite/Invariants.t.sol @@ -1,7 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; + // Interfaces +import {ITreasurySpoke} from 'src/spoke/interfaces/ITreasurySpoke.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IHub} from 'src/hub/interfaces/IHub.sol'; // Invariant Contracts @@ -10,33 +14,35 @@ import {SpokeInvariants} from './invariants/SpokeInvariants.t.sol'; /// @title Invariants /// @notice Wrappers for the protocol invariants implemented in each invariants contract -/// @dev recognised by Echidna when property mode is activated +/// @dev recognized by Echidna when property mode is activated /// @dev Inherits HubInvariants, SpokeInvariants abstract contract Invariants is HubInvariants, SpokeInvariants { + using EnumerableSet for EnumerableSet.AddressSet; + /////////////////////////////////////////////////////////////////////////////////////////////// // BASE INVARIANTS // /////////////////////////////////////////////////////////////////////////////////////////////// function invariant_INV_HUB() public returns (bool) { // Applied per hub - for (uint256 i; i < hubAddresses.length; i++) { - address hubAddress = hubAddresses[i]; + for (uint256 i; i < hubs.length(); i++) { + address hub = hubs.at(i); // Applied per asset of the hub - uint256 assetCount = IHub(hubAddress).getAssetCount(); + uint256 assetCount = IHub(hub).getAssetCount(); for (uint256 j; j < assetCount; j++) { - assert_INV_HUB_A(hubAddress, j); - assert_INV_HUB_B(hubAddress, j); - assert_INV_HUB_C(hubAddress, j); - assert_INV_HUB_E(hubAddress, j); - assert_INV_HUB_F(hubAddress, j); - assert_INV_HUB_GH(hubAddress, j); - assert_INV_HUB_I(hubAddress, j); - assert_INV_HUB_K(hubAddress, j); - assert_INV_HUB_O(hubAddress, j); - assert_INV_HUB_P(hubAddress, j); - assert_INV_HUB_Q(hubAddress, j); - assert_INV_HUB_R(hubAddress, j); + assert_INV_HUB_A(hub, j); + assert_INV_HUB_B(hub, j); + assert_INV_HUB_C(hub, j); + assert_INV_HUB_E(hub, j); + assert_INV_HUB_F(hub, j); + assert_INV_HUB_GH(hub, j); + assert_INV_HUB_I(hub, j); + assert_INV_HUB_K(hub, j); + assert_INV_HUB_O(hub, j); + assert_INV_HUB_P(hub, j); + assert_INV_HUB_Q(hub, j); + assert_INV_HUB_R(hub, j); } } @@ -45,37 +51,38 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { function invariant_INV_SP() public returns (bool) { // Applied per spoke - for (uint256 i; i < spokesAddresses.length; i++) { - address spoke = spokesAddresses[i]; + for (uint256 i; i < spokes.length(); i++) { + address spoke = spokes.at(i); // Applied per actor on the spoke - for (uint256 j; j < actorAddresses.length; j++) { - assert_INV_SP_D(spoke, actorAddresses[j]); + for (uint256 j; j < actors.length(); j++) { + assert_INV_SP_D(spoke, actors.at(j)); } // Applied per reserve of the spoke - for (uint256 j; j < spokeReserveIds[spoke].length; j++) { - uint256 reserveId = spokeReserveIds[spoke][j]; - assert_INV_SP_A(spoke, reserveId); - assert_INV_SP_C(spoke, reserveId); - assert_INV_SP_E(spoke, reserveId); - assert_INV_SP_F(spoke, reserveId); + uint256 reserveCount = ISpoke(spoke).getReserveCount(); + for (uint256 j; j < reserveCount; j++) { + assert_INV_SP_A(spoke, j); + assert_INV_SP_C(spoke, j); + assert_INV_SP_E(spoke, j); + assert_INV_SP_F(spoke, j); // Applied per actor per reserve of the spoke - for (uint256 k; k < actorAddresses.length; k++) { - assert_INV_SP_B(spoke, reserveId, actorAddresses[k]); - assert_INV_SP_H(spoke, reserveId, actorAddresses[k]); + for (uint256 k; k < actors.length(); k++) { + assert_INV_SP_B(spoke, j, actors.at(k)); + assert_INV_SP_H(spoke, j, actors.at(k)); } } } // Applied per treasury spoke (only hub-sync invariant applies; // user-level invariants don't apply since TreasurySpoke has no per-user positions) - for (uint256 i; i < treasurySpokesAddresses.length; i++) { - address spoke = treasurySpokesAddresses[i]; - for (uint256 j; j < spokeReserveIds[spoke].length; j++) { - uint256 reserveId = spokeReserveIds[spoke][j]; - assert_INV_SP_A(spoke, reserveId); + for (uint256 i; i < treasurySpokes.length(); i++) { + address spoke = treasurySpokes.at(i); + // reserveId == assetId for treasury spoke + uint256 reserveCount = IHub(address(ITreasurySpoke(spoke).HUB())).getAssetCount(); + for (uint256 j; j < reserveCount; j++) { + assert_INV_SP_A(spoke, j); } } diff --git a/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol index 0a321ebee..91935e872 100644 --- a/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.19; // Libraries +import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; import {CREATE3} from '../shared/utils/CREATE3.sol'; import {ActorsUtils} from '../shared/utils/ActorsUtils.sol'; import {Constants} from 'tests/Constants.sol'; @@ -26,7 +27,7 @@ import { AssetInterestRateStrategy, IAssetInterestRateStrategy } from 'src/hub/AssetInterestRateStrategy.sol'; -import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; +import {AccessManagerEnumerable} from 'src/access/AccessManagerEnumerable.sol'; import {TreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; import {AaveOracle} from 'src/spoke/AaveOracle.sol'; import {HubConfigurator, IHubConfigurator} from 'src/hub/HubConfigurator.sol'; @@ -37,6 +38,9 @@ import {LiquidationLogic} from 'src/spoke/libraries/LiquidationLogic.sol'; /// @notice Setup contract for the invariant test Suite, inherited by Tester contract Setup is BaseTest { + using EnumerableSet for EnumerableSet.AddressSet; + using ActorsUtils for *; + /// @notice Number of actors to deploy function _setUp() internal { // Deploy the suite assets @@ -75,7 +79,7 @@ contract Setup is BaseTest { /// @notice Deploy protocol core contracts function _deployProtocolCore() internal { // Access manager - accessManager = new AccessManager(admin); + accessManager = new AccessManagerEnumerable(admin); // Hub 1 hub1 = new Hub(address(accessManager)); @@ -84,7 +88,7 @@ contract Setup is BaseTest { treasurySpoke: address(treasurySpoke1), irStrategy: address(irStrategy1) }); - hubAddresses.push(address(hub1)); + hubs.add(address(hub1)); // Hub 2 hub2 = new Hub(address(accessManager)); @@ -93,7 +97,7 @@ contract Setup is BaseTest { treasurySpoke: address(treasurySpoke2), irStrategy: address(irStrategy2) }); - hubAddresses.push(address(hub2)); + hubs.add(address(hub2)); // Spokes (spoke1, oracle1) = _deploySpokeWithOracle(admin, address(accessManager), 'Spoke 1 (USD)'); @@ -102,8 +106,8 @@ contract Setup is BaseTest { treasurySpoke2 = ITreasurySpoke(new TreasurySpoke(admin, address(hub2))); allSpokes.push(address(treasurySpoke1)); allSpokes.push(address(treasurySpoke2)); - treasurySpokesAddresses.push(address(treasurySpoke1)); - treasurySpokesAddresses.push(address(treasurySpoke2)); + treasurySpokes.add(address(treasurySpoke1)); + treasurySpokes.add(address(treasurySpoke2)); // Configurators hubConfigurator = new HubConfigurator(address(accessManager)); @@ -150,7 +154,7 @@ contract Setup is BaseTest { assertEq(spoke.ORACLE(), address(oracle)); assertEq(oracle.SPOKE(), address(spoke)); - spokesAddresses.push(address(spoke)); + spokes.add(address(spoke)); allSpokes.push(address(spoke)); return (spoke, oracle); @@ -433,22 +437,6 @@ contract Setup is BaseTest { reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].weth.reserveId] = address(hub1); reserveIdToHubAddress[address(spoke2)][spokeInfo[spoke2].weth2.reserveId] = address(hub2); - // Store spoke reserve ids on array - spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].usdc.reserveId); - spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].usdc2.reserveId); - spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].weth.reserveId); - spokeReserveIds[address(spoke1)].push(spokeInfo[spoke1].weth2.reserveId); - spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].usdc.reserveId); - spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].usdc2.reserveId); - spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].weth.reserveId); - spokeReserveIds[address(spoke2)].push(spokeInfo[spoke2].weth2.reserveId); - - // TreasurySpoke uses assetIds directly as reserveIds - spokeReserveIds[address(treasurySpoke1)].push(hub1UsdcAssetId); - spokeReserveIds[address(treasurySpoke1)].push(hub1WethAssetId); - spokeReserveIds[address(treasurySpoke2)].push(hub2UsdcAssetId); - spokeReserveIds[address(treasurySpoke2)].push(hub2WethAssetId); - // Map ids for treasury spokes (reserveId == assetId for treasury spokes) reserveIdToAssetId[address(treasurySpoke1)][hub1UsdcAssetId] = hub1UsdcAssetId; reserveIdToAssetId[address(treasurySpoke1)][hub1WethAssetId] = hub1WethAssetId; @@ -608,7 +596,7 @@ contract Setup is BaseTest { accessManager.setTargetFunctionRole( address(hubConfigurator), selectors, - accessManager.PUBLIC_ROLE() + type(uint64).max // !todo public role, fix this ); } @@ -642,7 +630,7 @@ contract Setup is BaseTest { accessManager.setTargetFunctionRole( address(spokeConfigurator), selectors, - accessManager.PUBLIC_ROLE() + type(uint64).max // !todo public role, fix this ); } } @@ -654,12 +642,11 @@ contract Setup is BaseTest { /// @notice Deploy protocol actors and initialize their balances function _setUpActors() internal { // Initialize the three actors of the fuzzers - address[] memory addresses = new address[](3); - addresses[0] = USER1; - addresses[1] = USER2; - addresses[2] = USER3; + address[] memory users = new address[](3); + users[0] = USER1; + users[1] = USER2; + users[2] = USER3; - // Initialize the tokens array address[] memory tokens = new address[](2); tokens[0] = address(usdc); tokens[1] = address(weth); @@ -670,9 +657,10 @@ contract Setup is BaseTest { contracts[2] = address(spoke1); contracts[3] = address(spoke2); - actorAddresses = ActorsUtils.setUpActors(addresses, tokens, contracts); - actors[USER1] = Actor(payable(actorAddresses[0])); - actors[USER2] = Actor(payable(actorAddresses[1])); - actors[USER3] = Actor(payable(actorAddresses[2])); + address[] memory actorAddresses = users.setUpActors(tokens, contracts); + for (uint256 i; i < actorAddresses.length; ++i) { + userToActor[users[i]] = Actor(payable(actorAddresses[i])); + actors.add(actorAddresses[i]); + } } } diff --git a/invariants/protocol-suite/base/BaseHandler.t.sol b/invariants/protocol-suite/base/BaseHandler.t.sol index a5e422751..22bbcd830 100644 --- a/invariants/protocol-suite/base/BaseHandler.t.sol +++ b/invariants/protocol-suite/base/BaseHandler.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; // Libraries import {MockERC20} from 'tests/mocks/MockERC20.sol'; @@ -42,17 +42,14 @@ contract BaseHandler is HookAggregator { /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor function _approve(address token, Actor actor_, address spender, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor_.proxy( + (bool ok, bytes memory returnData) = actor_.proxy( token, - abi.encodeWithSelector(0x095ea7b3, spender, amount) + abi.encodeCall(IERC20.approve, (spender, amount)) ); - require(success, string(returnData)); + require(ok, string(ret)); } /// @notice Helper function to safely approve an amount of tokens to a spender - function _approve(address token, address owner, address spender, uint256 amount) internal { vm.prank(owner); _safeApprove(token, spender, 0); @@ -63,11 +60,9 @@ contract BaseHandler is HookAggregator { /// @notice Helper function to safely approve an amount of tokens to a spender /// @dev This function is used to revert on failed approvals function _safeApprove(address token, address spender, uint256 amount) internal { - (bool success, bytes memory retdata) = token.call( - abi.encodeWithSelector(IERC20.approve.selector, spender, amount) - ); - assert(success); - if (retdata.length > 0) assert(abi.decode(retdata, (bool))); + (bool ok, bytes memory ret) = token.call(abi.encodeCall(IERC20.approve, (spender, amount))); + assert(ok); + if (ret.length > 0) assert(abi.decode(ret, (bool))); } /// @notice Helper function to mint an amount of tokens to an address diff --git a/invariants/protocol-suite/base/BaseStorage.t.sol b/invariants/protocol-suite/base/BaseStorage.t.sol index 6be112563..161f8aee2 100644 --- a/invariants/protocol-suite/base/BaseStorage.t.sol +++ b/invariants/protocol-suite/base/BaseStorage.t.sol @@ -1,22 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -// Contracts +import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; import {IHub} from 'src/hub/interfaces/IHub.sol'; import {ITreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; -import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; -import {AccessManager} from 'src/dependencies/openzeppelin/AccessManager.sol'; -import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; -import {SpokeConfigurator} from 'src/spoke/SpokeConfigurator.sol'; +import {IAssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import {IAccessManagerEnumerable} from 'src/access/interfaces/IAccessManagerEnumerable.sol'; +import {IHubConfigurator} from 'src/hub/interfaces/IHubConfigurator.sol'; +import {ISpokeConfigurator} from 'src/spoke/interfaces/ISpokeConfigurator.sol'; -// Mock Contracts - -// Test Contracts - -// Utils import {Actor} from '../../shared/utils/Actor.sol'; /// @notice BaseStorage contract for all test contracts, works in tandem with BaseTest @@ -28,8 +23,8 @@ abstract contract BaseStorage { uint256 constant MAX_TOKEN_AMOUNT = 1e29; uint256 constant ONE_DAY = 1 days; - uint256 constant ONE_MONTH = ONE_YEAR / 12; uint256 constant ONE_YEAR = 365 days; + uint256 constant ONE_MONTH = ONE_YEAR / 12; uint256 internal constant NUMBER_OF_ACTORS = 3; uint256 internal constant INITIAL_COLL_BALANCE = 1e21; @@ -45,10 +40,10 @@ abstract contract BaseStorage { Actor internal actor; /// @notice Mapping of fuzzer user addresses to actors - mapping(address => Actor) internal actors; + mapping(address user => Actor) internal userToActor; /// @notice Array of all actor addresses - address[] internal actorAddresses; + EnumerableSet.AddressSet internal actors; /// @notice The address that is targeted when executing an action (OPTIONAL) address internal targetActor; @@ -72,23 +67,23 @@ abstract contract BaseStorage { // HUB CONTRACTS IHub internal hub1; IHub internal hub2; - AssetInterestRateStrategy internal irStrategy1; - AssetInterestRateStrategy internal irStrategy2; - HubConfigurator internal hubConfigurator; + IAssetInterestRateStrategy internal irStrategy1; + IAssetInterestRateStrategy internal irStrategy2; + IHubConfigurator internal hubConfigurator; // SPOKE CONTRACTS ITreasurySpoke internal treasurySpoke1; ITreasurySpoke internal treasurySpoke2; ISpoke internal spoke1; ISpoke internal spoke2; - SpokeConfigurator internal spokeConfigurator; + ISpokeConfigurator internal spokeConfigurator; // ORACLES IAaveOracle internal oracle1; IAaveOracle internal oracle2; // CONFIGURATION - AccessManager internal accessManager; + IAccessManagerEnumerable internal accessManager; /////////////////////////////////////////////////////////////////////////////////////////////// // EXTRA VARIABLES // @@ -105,7 +100,7 @@ abstract contract BaseStorage { uint256 internal hub2UsdcAssetId; /// @notice Array of hub addresses for the suite - address[] internal hubAddresses; + EnumerableSet.AddressSet internal hubs; /// @notice Spoke configurations mapping(address => HubInfo) internal hubInfo; /// @notice Hub assetIds @@ -113,15 +108,13 @@ abstract contract BaseStorage { // SPOKES /// @notice Array of spokes addresses for the suite - address[] internal spokesAddresses; + EnumerableSet.AddressSet internal spokes; /// @notice Array of treasury spoke addresses - address[] internal treasurySpokesAddresses; + EnumerableSet.AddressSet internal treasurySpokes; /// @notice spokesAddresses + treasurySpoke address address[] internal allSpokes; /// @notice Spoke configurations mapping(ISpoke => SpokeInfo) internal spokeInfo; - /// @notice Spoke reserveIds - mapping(address => uint256[]) internal spokeReserveIds; /// @notice Spoke reserveIds to global assetIds mapping(address => mapping(uint256 => uint256)) internal reserveIdToAssetId; /// @notice Spoke assetIds to reserveIds info diff --git a/invariants/protocol-suite/base/BaseTest.t.sol b/invariants/protocol-suite/base/BaseTest.t.sol index 198022cca..11ab9d79f 100644 --- a/invariants/protocol-suite/base/BaseTest.t.sol +++ b/invariants/protocol-suite/base/BaseTest.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; // Libraries import {Vm} from 'forge-std/Base.sol'; import {StdUtils} from 'forge-std/StdUtils.sol'; -import 'forge-std/console.sol'; +import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; import {Constants} from 'tests/Constants.sol'; // Interfaces @@ -24,7 +24,9 @@ import {BaseStorage} from './BaseStorage.t.sol'; /// @dev Provides setup modifier and cheat code setup /// @dev inherits Storage, Testing constants assertions and utils needed for testing abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdUtils { - bool internal IS_TEST = true; + using EnumerableSet for EnumerableSet.AddressSet; + + bool internal IS_TEST = true; // todo! make public /////////////////////////////////////////////////////////////////////////////////////////////// // ACTOR PROXY MECHANISM // @@ -32,7 +34,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @dev Actor proxy mechanism modifier setup() virtual { - actor = actors[msg.sender]; + actor = userToActor[msg.sender]; _; delete actor; } @@ -65,8 +67,8 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @notice Get a random actor proxy address function _getRandomActor(uint256 _i) internal view returns (address) { - uint256 _actorIndex = _i % NUMBER_OF_ACTORS; - return actorAddresses[_actorIndex]; + uint256 _actorIndex = _i % actors.length(); + return actors.at(_actorIndex); } /// @notice Helper function to get a random base asset @@ -89,14 +91,13 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @notice Helper function to get a random spoke address function _getRandomSpoke(uint256 i) internal view returns (address) { - uint256 _spokeIndex = i % spokesAddresses.length; - return spokesAddresses[_spokeIndex]; + uint256 _spokeIndex = i % spokes.length(); + return spokes.at(_spokeIndex); } /// @notice Helper function to get a random reserve id from a spoke function _getRandomReserveId(address spoke, uint256 i) internal view returns (uint256) { - uint256 _reserveIndex = i % spokeReserveIds[spoke].length; - return spokeReserveIds[spoke][_reserveIndex]; + return i % ISpoke(spoke).getReserveCount(); } /// @notice Helper function to get a random price feed address @@ -107,8 +108,8 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @notice Helper function to get a random hub address function _getRandomHub(uint256 i) internal view returns (address) { - uint256 _hubIndex = i % hubAddresses.length; - return hubAddresses[_hubIndex]; + uint256 _hubIndex = i % hubs.length(); + return hubs.at(_hubIndex); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol b/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol index 98a8b5f29..21c3dbafe 100644 --- a/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol +++ b/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol @@ -2,12 +2,9 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; import {IHubConfiguratorHandler} from '../interfaces/IHubConfiguratorHandler.sol'; -// Libraries -import 'forge-std/console.sol'; - // Test Contracts import {Actor} from '../../../shared/utils/Actor.sol'; import {BaseHandler} from '../../base/BaseHandler.t.sol'; diff --git a/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol b/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol index f10683774..7eb58d02e 100644 --- a/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol +++ b/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol @@ -1,13 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -// Test Contracts +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; import {BaseHandler} from '../../base/BaseHandler.t.sol'; import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; /// @title DonationAttackHandler /// @notice Handler test contract for a set of actions contract DonationAttackHandler is BaseHandler { + using EnumerableSet for EnumerableSet.AddressSet; + /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -21,23 +24,19 @@ contract DonationAttackHandler is BaseHandler { /////////////////////////////////////////////////////////////////////////////////////////////// function donateUnderlyingToHub(uint256 amount, uint8 i, uint8 j) external { - // Get one of the hub addresses randomly - address hubAddress = _getRandomHub(j); - // Get one of the assets IDs randomly + address hub = _getRandomHub(j); address underlying = _getRandomBaseAsset(i); // Register all spoke/reserve pairs that map to this hub so hub postconditions fire - _registerAllReservesForHub(hubAddress); + _registerAllReservesForHub(hub); _before(); - TestnetERC20(underlying).mint(hubAddress, amount); + TestnetERC20(underlying).mint(hub, amount); _after(); } function donateUnderlyingToSpoke(uint256 amount, uint8 i, uint8 j) external { - // Get one of the spoke addresses randomly address spoke = _getRandomSpoke(j); - // Get one of the assets IDs randomly address underlying = _getRandomBaseAsset(i); // Register all reserves for this spoke so hub postconditions fire @@ -56,12 +55,12 @@ contract DonationAttackHandler is BaseHandler { /// to the given hub, so that hub-level postconditions (GPOST_HUB_A..G) are evaluated. /// Uses address(this) as the user since donations don't act on a specific user. function _registerAllReservesForHub(address hubAddress) internal { - for (uint256 s; s < spokesAddresses.length; s++) { - address spoke = spokesAddresses[s]; - uint256[] storage reserveIds = spokeReserveIds[spoke]; - for (uint256 r; r < reserveIds.length; r++) { - if (reserveIdToHubAddress[spoke][reserveIds[r]] == hubAddress) { - _registerUserToCheck(spoke, reserveIds[r], address(this)); + for (uint256 s; s < spokes.length(); s++) { + address spoke = spokes.at(s); + uint256 reserveCount = ISpoke(spoke).getReserveCount(); + for (uint256 r; r < reserveCount; r++) { + if (reserveIdToHubAddress[spoke][r] == hubAddress) { + _registerUserToCheck(spoke, r, address(this)); } } } @@ -71,9 +70,9 @@ contract DonationAttackHandler is BaseHandler { /// so that hub-level postconditions are evaluated for all assets of the spoke. /// Uses address(this) as the user since donations don't act on a specific user. function _registerAllReservesForSpoke(address spoke) internal { - uint256[] storage reserveIds = spokeReserveIds[spoke]; - for (uint256 r; r < reserveIds.length; r++) { - _registerUserToCheck(spoke, reserveIds[r], address(this)); + uint256 reserveCount = ISpoke(spoke).getReserveCount(); + for (uint256 r; r < reserveCount; ++r) { + _registerUserToCheck(spoke, r, address(this)); } } } diff --git a/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol index 2ff1f2c21..d7ec13187 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; import {ISpokeConfiguratorHandler} from '../interfaces/ISpokeConfiguratorHandler.sol'; // Libraries diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index bbc8a7fdc..d64b1c9d1 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -8,6 +8,7 @@ import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; // Libraries import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; +import {MathUtils} from 'src/libraries/math/MathUtils.sol'; import {Constants} from 'tests/Constants.sol'; // Test Contracts @@ -18,6 +19,7 @@ import {BaseHandler} from '../../base/BaseHandler.t.sol'; /// @notice Handler test contract for a set of actions contract SpokeHandler is BaseHandler, ISpokeHandler { using WadRayMath for uint256; + using MathUtils for uint256; /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // @@ -101,7 +103,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if ( _isAuthorized(spoke, onBehalfOf) && !_isReserveActionBlocked(spoke, reserveId, false, false) && - defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt == 0 && + _userVarsBefore(spoke, reserveId, onBehalfOf).debt.owed == 0 && amount > 0 && userAmount != 0 ) { @@ -177,8 +179,8 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ///// HSPOST ///// assertLe( - defaultVarsAfter.userVars[spoke][reserveId][onBehalfOf].totalDebt, - defaultVarsBefore.userVars[spoke][reserveId][onBehalfOf].totalDebt, + _userVarsAfter(spoke, reserveId, onBehalfOf).debt.owed, + _userVarsBefore(spoke, reserveId, onBehalfOf).debt.owed, HSPOST_SP_C ); } else { @@ -273,15 +275,19 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { // Calculate the debt liquidated from user-level snapshots (not reserve-level, which // includes interest accrual on other users' debt and would be inaccurate) - uint256 violatorDebtBefore = defaultVarsBefore - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] - .totalDebt; - uint256 violatorDebtAfter = defaultVarsAfter - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] - .totalDebt; - liquidationVars.debtLiquidated = (violatorDebtBefore > violatorDebtAfter) - ? violatorDebtBefore - violatorDebtAfter - : 0; + UserVars memory violatorDebtVarsBefore = _userVarsBefore( + liquidationVars.spoke, + liquidationVars.debtReserveId, + liquidationVars.violator + ); + UserVars memory violatorDebtVarsAfter = _userVarsAfter( + liquidationVars.spoke, + liquidationVars.debtReserveId, + liquidationVars.violator + ); + liquidationVars.debtLiquidated = violatorDebtVarsBefore.debt.owed.zeroFloorSub( + violatorDebtVarsAfter.debt.owed + ); if (receiveShares) { liquidationVars.liquidatorCollateralBalanceAfter = ISpoke(liquidationVars.spoke) @@ -292,13 +298,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } ///// HSPOST ///// - assertLe( - liquidationVars.debtLiquidated, - defaultVarsBefore - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] - .totalDebt, - HSPOST_SP_LIQ_A - ); + assertLe(liquidationVars.debtLiquidated, violatorDebtVarsBefore.debt.owed, HSPOST_SP_LIQ_A); if ( liquidationVars.liquidatorCollateralBalanceAfter > @@ -313,38 +313,26 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } if (liquidationVars.totalDebtValueBefore < Constants.DUST_LIQUIDATION_THRESHOLD) { - assertEq( - defaultVarsAfter - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][ - liquidationVars.violator - ] - .totalDebt, - 0, - HSPOST_SP_LIQ_C - ); + assertEq(violatorDebtVarsAfter.debt.owed, 0, HSPOST_SP_LIQ_C); } assertGe(liquidationVars.debtToCover, liquidationVars.debtLiquidated, HSPOST_SP_LIQ_D); assertLt( - defaultVarsBefore - .userAccountDataVars[liquidationVars.spoke][liquidationVars.violator] + _userAccountDataVarsBefore(liquidationVars.spoke, liquidationVars.violator) + .data .healthFactor, Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, HSPOST_SP_LIQ_E ); - if ( - defaultVarsAfter - .userVars[liquidationVars.spoke][liquidationVars.debtReserveId][liquidationVars.violator] - .totalDebt > 0 - ) { + if (violatorDebtVarsAfter.debt.owed > 0) { assertGt( - defaultVarsAfter - .userAccountDataVars[liquidationVars.spoke][liquidationVars.violator] + _userAccountDataVarsAfter(liquidationVars.spoke, liquidationVars.violator) + .data .healthFactor, - defaultVarsBefore - .userAccountDataVars[liquidationVars.spoke][liquidationVars.violator] + _userAccountDataVarsBefore(liquidationVars.spoke, liquidationVars.violator) + .data .healthFactor, HSPOST_SP_LIQ_G ); @@ -365,7 +353,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { uint256 reserveId = _getRandomReserveId(spoke, j); (bool isUsingAsCollateral, ) = ISpoke(spoke).getUserReserveStatus(reserveId, onBehalfOf); - + // !todo remove this require( usingAsCollateral != isUsingAsCollateral, 'SpokeHandler: usingAsCollateral already set' @@ -399,9 +387,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { address spoke = _getRandomSpoke(i); - uint256 totalDebt = _getTotalDebt(spoke, onBehalfOf); - uint256 totalPremiumDebtRay = _getTotalPremiumDebtRay(spoke, onBehalfOf); - // Register user to check postconditions _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); @@ -413,8 +398,14 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (success) { _after(); - assertEq(_getTotalPremiumDebtRay(spoke, onBehalfOf), totalPremiumDebtRay, HSPOST_HUB_M); - assertEq(_getTotalDebt(spoke, onBehalfOf), totalDebt, HSPOST_SP_F); + ///// HSPOST ///// + uint256 reserveCount = ISpoke(spoke).getReserveCount(); + for (uint256 j; j < reserveCount; j++) { + UserVars memory varsBefore = _userVarsBefore(spoke, j, onBehalfOf); + UserVars memory varsAfter = _userVarsAfter(spoke, j, onBehalfOf); + assertEq(varsBefore.debt.premium, varsAfter.debt.premium, HSPOST_HUB_M); // !todo add premium ray in those getters and check that here? + assertEq(varsBefore.debt.owed, varsAfter.debt.owed, HSPOST_SP_F); + } } else { revert('SpokeHandler: updateUserRiskPremium failed'); } @@ -453,22 +444,4 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// - - function _getTotalDebt(address spoke, address user) internal view returns (uint256) { - uint256 totalDebt; - uint256 reserveCount = spokeReserveIds[spoke].length; - for (uint256 i; i < reserveCount; i++) { - totalDebt += ISpoke(spoke).getUserTotalDebt(spokeReserveIds[spoke][i], user); - } - return totalDebt; - } - - function _getTotalPremiumDebtRay(address spoke, address user) internal view returns (uint256) { - uint256 totalPremiumDebtRay; - uint256 reserveCount = spokeReserveIds[spoke].length; - for (uint256 i; i < reserveCount; i++) { - totalPremiumDebtRay += ISpoke(spoke).getUserPremiumDebtRay(spokeReserveIds[spoke][i], user); - } - return totalPremiumDebtRay; - } } diff --git a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index 33b528187..e9ea44149 100644 --- a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -2,10 +2,11 @@ pragma solidity ^0.8.19; // Libraries +import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; +import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; import {MathUtils} from 'src/libraries/math/MathUtils.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; -import 'forge-std/console.sol'; // Utils import {Constants} from 'tests/Constants.sol'; @@ -23,40 +24,47 @@ import {BaseHooks} from '../base/BaseHooks.t.sol'; /// @notice Helper contract for before and after hooks, state variable caching and postconditions /// @dev This contract is inherited by handlers abstract contract DefaultBeforeAfterHooks is BaseHooks { - using WadRayMath for uint256; + using WadRayMath for *; + using EnumerableSet for EnumerableSet.AddressSet; /////////////////////////////////////////////////////////////////////////////////////////////// // STRUCTS // /////////////////////////////////////////////////////////////////////////////////////////////// + struct Debt { + uint256 drawn; + uint256 premium; + uint256 owed; + } + struct AssetVars { + IHub.Asset asset; + uint256 drawnRate; uint256 drawnIndex; uint256 totalAssets; uint256 totalShares; - uint256 drawn; - uint256 premium; - uint256 lastUpdateTimestamp; + Debt debt; } struct UserVars { - uint256 drawnDebt; - uint256 premiumDebt; - uint256 totalDebt; + ISpoke.UserPosition position; + Debt debt; } struct UserAccountDataVars { - uint256 healthFactor; + ISpoke.UserAccountData data; } - struct SpokeAssetVars { + struct SpokeVars { + IHub.SpokeData spokeData; uint256 addedAssets; uint256 addedShares; - uint256 owed; + Debt debt; } struct DefaultVars { mapping(address hub => mapping(uint256 assetId => AssetVars)) assetVars; - mapping(address hub => mapping(uint256 assetId => mapping(address spoke => SpokeAssetVars))) spokeAssetVars; + mapping(address hub => mapping(uint256 assetId => mapping(address spoke => SpokeVars))) spokeVars; mapping(address spoke => mapping(uint256 reserveId => mapping(address user => UserVars))) userVars; mapping(address spoke => mapping(address user => UserAccountDataVars)) userAccountDataVars; } @@ -114,83 +122,71 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// - function _setAssetValues(DefaultVars storage _defaultVars) internal { - for (uint256 i; i < hubAddresses.length; i++) { - address hubAddress = hubAddresses[i]; - uint256 assetCount = IHub(hubAddress).getAssetCount(); + /// @dev Only use these helpers in postConditions, do NOT rely on them at Invariants because they not be populated + /// when the fuzzer has updated env (eg block.timestamp) which does not invoke any Handler + + function _setAssetValues(DefaultVars storage defaultVars) internal { + for (uint256 i; i < hubs.length(); i++) { + IHub hub = IHub(hubs.at(i)); + uint256 assetCount = hub.getAssetCount(); for (uint256 j; j < assetCount; j++) { - _defaultVars.assetVars[hubAddress][j].drawnIndex = IHub(hubAddress).getAssetDrawnIndex(j); - _defaultVars.assetVars[hubAddress][j].totalAssets = IHub(hubAddress).getAddedAssets(j); - _defaultVars.assetVars[hubAddress][j].totalShares = IHub(hubAddress).getAddedShares(j); - ( - _defaultVars.assetVars[hubAddress][j].drawn, - _defaultVars.assetVars[hubAddress][j].premium - ) = IHub(hubAddress).getAssetOwed(j); - _defaultVars.assetVars[hubAddress][j].lastUpdateTimestamp = IHub(hubAddress) - .getAsset(j) - .lastUpdateTimestamp; + (uint256 drawn, uint256 premium) = hub.getAssetOwed(j); + defaultVars.assetVars[address(hub)][j] = AssetVars({ + asset: hub.getAsset(j), + drawnRate: hub.getAssetDrawnRate(j), + drawnIndex: hub.getAssetDrawnIndex(j), + totalAssets: hub.getAddedAssets(j), + totalShares: hub.getAddedShares(j), + debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + }); } } } - function _setSpokeAssetValues(DefaultVars storage _defaultVars) internal { - for (uint256 i; i < hubAddresses.length; i++) { - address hubAddress = hubAddresses[i]; - uint256 assetCount = IHub(hubAddress).getAssetCount(); + function _setSpokeAssetValues(DefaultVars storage defaultVars) internal { + for (uint256 i; i < hubs.length(); i++) { + IHub hub = IHub(hubs.at(i)); + uint256 assetCount = hub.getAssetCount(); for (uint256 j; j < assetCount; j++) { for (uint256 k; k < allSpokes.length; k++) { address spoke = allSpokes[k]; - _defaultVars.spokeAssetVars[hubAddress][j][spoke].addedAssets = IHub(hubAddress) - .getSpokeAddedAssets(j, spoke); - _defaultVars.spokeAssetVars[hubAddress][j][spoke].addedShares = IHub(hubAddress) - .getSpokeAddedShares(j, spoke); - (uint256 drawn, uint256 premium) = IHub(hubAddress).getSpokeOwed(j, spoke); - _defaultVars.spokeAssetVars[hubAddress][j][spoke].owed = drawn + premium; + (uint256 drawn, uint256 premium) = hub.getSpokeOwed(j, spoke); + defaultVars.spokeVars[address(hub)][j][spoke] = SpokeVars({ + spokeData: hub.getSpoke(j, spoke), + addedAssets: hub.getSpokeAddedAssets(j, spoke), + addedShares: hub.getSpokeAddedShares(j, spoke), + debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + }); } } } } - function _setUserValues(DefaultVars storage _defaultVars) internal { - // Iterate through all users to check - for (uint256 i; i < usersToCheck.length; i++) { + function _setUserValues(DefaultVars storage defaultVars) internal { + for (uint256 i; i < usersToCheck.length; ++i) { UserInfo memory userInfo = usersToCheck[i]; - - // Cache values for the user's account data - ISpoke.UserAccountData memory userAccountData = ISpoke(userInfo.spoke).getUserAccountData( - userInfo.user - ); - _defaultVars.userAccountDataVars[userInfo.spoke][userInfo.user].healthFactor = userAccountData - .healthFactor; + ISpoke spoke = ISpoke(userInfo.spoke); + defaultVars.userAccountDataVars[userInfo.spoke][userInfo.user].data = spoke + .getUserAccountData(userInfo.user); // Cache values for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig if (userInfo.reserveId == CHECK_ALL_RESERVES) { - // Iterate through all reserves of the spoke - for (uint256 j; j < spokeReserveIds[userInfo.spoke].length; j++) { - ( - _defaultVars - .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] - .drawnDebt, - _defaultVars - .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] - .premiumDebt - ) = ISpoke(userInfo.spoke).getUserDebt(spokeReserveIds[userInfo.spoke][j], userInfo.user); - _defaultVars - .userVars[userInfo.spoke][spokeReserveIds[userInfo.spoke][j]][userInfo.user] - .totalDebt = ISpoke(userInfo.spoke).getUserTotalDebt( - spokeReserveIds[userInfo.spoke][j], - userInfo.user - ); + uint256 reserveCount = spoke.getReserveCount(); + for (uint256 j; j < reserveCount; ++j) { + (uint256 drawn, uint256 premium) = spoke.getUserDebt(j, userInfo.user); + defaultVars.userVars[userInfo.spoke][j][userInfo.user] = UserVars({ + position: spoke.getUserPosition(j, userInfo.user), + debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + }); } } else { // Cache values for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral - ( - _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].drawnDebt, - _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].premiumDebt - ) = ISpoke(userInfo.spoke).getUserDebt(userInfo.reserveId, userInfo.user); - _defaultVars.userVars[userInfo.spoke][userInfo.reserveId][userInfo.user].totalDebt = ISpoke( - userInfo.spoke - ).getUserTotalDebt(userInfo.reserveId, userInfo.user); + uint256 reserveId = userInfo.reserveId; + (uint256 drawn, uint256 premium) = spoke.getUserDebt(reserveId, userInfo.user); + defaultVars.userVars[userInfo.spoke][reserveId][userInfo.user] = UserVars({ + position: spoke.getUserPosition(reserveId, userInfo.user), + debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + }); } } } @@ -200,19 +196,20 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { /////////////////////////////////////////////////////////////////////////////////////////////// function assert_GPOST_HUB_A(address hubAddress, uint256 assetId) internal { - assertGe( - defaultVarsAfter.assetVars[hubAddress][assetId].drawnIndex, - defaultVarsBefore.assetVars[hubAddress][assetId].drawnIndex, - GPOST_HUB_A - ); + AssetVars memory varsBefore = _assetVarsBefore(hubAddress, assetId); + AssetVars memory varsAfter = _assetVarsAfter(hubAddress, assetId); + assertGe(varsAfter.drawnIndex, varsBefore.drawnIndex, GPOST_HUB_A); } function assert_GPOST_HUB_B(address hubAddress, uint256 assetId) internal { + AssetVars memory varsBefore = _assetVarsBefore(hubAddress, assetId); + AssetVars memory varsAfter = _assetVarsAfter(hubAddress, assetId); + assertFullMulGe( - defaultVarsAfter.assetVars[hubAddress][assetId].totalAssets + 1e6, - defaultVarsBefore.assetVars[hubAddress][assetId].totalShares + 1e6, - defaultVarsBefore.assetVars[hubAddress][assetId].totalAssets + 1e6, - defaultVarsAfter.assetVars[hubAddress][assetId].totalShares + 1e6, + varsAfter.totalAssets + SharesMath.VIRTUAL_ASSETS, + varsBefore.totalShares + SharesMath.VIRTUAL_SHARES, + varsBefore.totalAssets + SharesMath.VIRTUAL_ASSETS, + varsAfter.totalShares + SharesMath.VIRTUAL_SHARES, GPOST_HUB_B ); } @@ -228,14 +225,15 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { signature == ISpokeHandler.updateUserRiskPremium.selector || signature == ISpokeHandler.liquidationCall.selector ) { + AssetVars memory vars = _assetVarsAfter(hubAddress, assetId); assertEq( - IHub(hubAddress).getAssetDrawnRate(assetId), + vars.drawnRate, IAssetInterestRateStrategy(hubInfo[hubAddress].irStrategy).calculateInterestRate( assetId, - IHub(hubAddress).getAssetLiquidity(assetId), - defaultVarsAfter.assetVars[hubAddress][assetId].drawn, - IHub(hubAddress).getAssetDeficitRay(assetId).fromRayUp(), - IHub(hubAddress).getAssetSwept(assetId) + vars.asset.liquidity, + vars.debt.drawn, + vars.asset.deficitRay.fromRayUp(), + vars.asset.swept ), GPOST_HUB_C ); @@ -244,7 +242,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function assert_GPOST_HUB_D(address hubAddress, uint256 assetId) internal { assertLe( - defaultVarsAfter.assetVars[hubAddress][assetId].lastUpdateTimestamp, + _assetVarsAfter(hubAddress, assetId).asset.lastUpdateTimestamp, block.timestamp, GPOST_HUB_D ); @@ -255,16 +253,17 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { IHub.SpokeConfig memory spokeConfig = IHub(hubAddress).getSpokeConfig(assetId, spoke); (, uint8 decimals) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); + SpokeVars memory spokeDataBefore = _spokeVarsBefore(hubAddress, assetId, spoke); + SpokeVars memory spokeDataAfter = _spokeVarsAfter(hubAddress, assetId, spoke); + // GPOST_HUB_E: spoke-level addedAssets must be within addCap after an add action if ( - defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].addedAssets > - defaultVarsBefore.spokeAssetVars[hubAddress][assetId][spoke].addedAssets && - defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].addedShares != - defaultVarsBefore.spokeAssetVars[hubAddress][assetId][spoke].addedShares /// @dev filter out interest accrual + spokeDataAfter.addedAssets > spokeDataBefore.addedAssets && + spokeDataAfter.addedShares != spokeDataBefore.addedShares /// @dev required to avoid interest accrual detection ) { if (spokeConfig.addCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].addedAssets, + spokeDataAfter.addedAssets, spokeConfig.addCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_E ); @@ -272,13 +271,10 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } // GPOST_HUB_F: spoke-level owed must be within drawCap after a draw action - if ( - defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].owed > - defaultVarsBefore.spokeAssetVars[hubAddress][assetId][spoke].owed - ) { + if (spokeDataAfter.debt.owed > spokeDataBefore.debt.owed) { if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - defaultVarsAfter.spokeAssetVars[hubAddress][assetId][spoke].owed, + spokeDataAfter.debt.owed, spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_F ); @@ -288,8 +284,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function assert_GPOST_HUB_G(address hubAddress, uint256 assetId) internal { assertGe( - defaultVarsAfter.assetVars[hubAddress][assetId].lastUpdateTimestamp, - defaultVarsBefore.assetVars[hubAddress][assetId].lastUpdateTimestamp, + _assetVarsAfter(hubAddress, assetId).asset.lastUpdateTimestamp, + _assetVarsBefore(hubAddress, assetId).asset.lastUpdateTimestamp, GPOST_HUB_G ); } @@ -307,10 +303,10 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } function assert_GPOST_SP_B(address spoke, uint256 reserveId, address user) internal { - if ( - defaultVarsAfter.userVars[spoke][reserveId][user].premiumDebt < - defaultVarsBefore.userVars[spoke][reserveId][user].premiumDebt - ) { + UserVars memory userVarsBefore = _userVarsBefore(spoke, reserveId, user); + UserVars memory userVarsAfter = _userVarsAfter(spoke, reserveId, user); + + if (userVarsAfter.debt.premium < userVarsBefore.debt.premium) { assertTrue( currentActionSignature == ISpokeHandler.repay.selector || currentActionSignature == ISpokeHandler.liquidationCall.selector, @@ -318,26 +314,19 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { ); } - if ( - defaultVarsAfter.userVars[spoke][reserveId][user].drawnDebt < - defaultVarsBefore.userVars[spoke][reserveId][user].drawnDebt - ) { + if (userVarsAfter.debt.drawn < userVarsBefore.debt.drawn) { assertTrue( currentActionSignature == ISpokeHandler.repay.selector || currentActionSignature == ISpokeHandler.liquidationCall.selector, GPOST_SP_B2 ); - assertEq(defaultVarsAfter.userVars[spoke][reserveId][user].premiumDebt, 0, GPOST_SP_B2); + assertEq(userVarsAfter.debt.premium, 0, GPOST_SP_B2); } } function assert_GPOST_SP_E(address spoke, uint256 reserveId, address user) internal { - // latest reserve key uint32 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; - // user-stored key uint32 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; - - // Read the cached signature of the current action bytes4 signature = currentActionSignature; if ( @@ -351,16 +340,14 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { } function assert_GPOST_LIQ_G(address spoke, address user) internal { - // Read the cached values of the user's health factor - uint256 healthFactorBefore = defaultVarsBefore.userAccountDataVars[spoke][user].healthFactor; - uint256 healthFactorAfter = defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor; + UserAccountDataVars memory dataBefore = _userAccountDataVarsBefore(spoke, user); + UserAccountDataVars memory dataAfter = _userAccountDataVarsAfter(spoke, user); - // Read the cached signature of the current action bytes4 signature = currentActionSignature; if ( - healthFactorBefore < Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD && - healthFactorAfter < healthFactorBefore + dataBefore.data.healthFactor < Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD && + dataAfter.data.healthFactor < dataBefore.data.healthFactor ) { assertTrue(signature == ISpokeHandler.liquidationCall.selector, GPOST_SP_LIQ_G); } @@ -368,7 +355,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function assert_GPOST_SP_LIQ_H(address spoke, address user) internal { if ( - defaultVarsAfter.userAccountDataVars[spoke][user].healthFactor < + _userAccountDataVarsAfter(spoke, user).data.healthFactor < Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD ) { assertTrue( @@ -392,4 +379,64 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { function _cacheCurrentActionSignature() internal { currentActionSignature = bytes4(msg.sig); } + + function _assetVarsBefore( + address hubAddress, + uint256 assetId + ) internal view returns (AssetVars memory) { + return defaultVarsBefore.assetVars[hubAddress][assetId]; + } + + function _assetVarsAfter( + address hubAddress, + uint256 assetId + ) internal view returns (AssetVars memory) { + return defaultVarsAfter.assetVars[hubAddress][assetId]; + } + + function _spokeVarsBefore( + address hubAddress, + uint256 assetId, + address spoke + ) internal view returns (SpokeVars memory) { + return defaultVarsBefore.spokeVars[hubAddress][assetId][spoke]; + } + + function _spokeVarsAfter( + address hubAddress, + uint256 assetId, + address spoke + ) internal view returns (SpokeVars memory) { + return defaultVarsAfter.spokeVars[hubAddress][assetId][spoke]; + } + + function _userVarsBefore( + address spoke, + uint256 reserveId, + address user + ) internal view returns (UserVars memory) { + return defaultVarsBefore.userVars[spoke][reserveId][user]; + } + + function _userVarsAfter( + address spoke, + uint256 reserveId, + address user + ) internal view returns (UserVars memory) { + return defaultVarsAfter.userVars[spoke][reserveId][user]; + } + + function _userAccountDataVarsBefore( + address spoke, + address user + ) internal view returns (UserAccountDataVars memory) { + return defaultVarsBefore.userAccountDataVars[spoke][user]; + } + + function _userAccountDataVarsAfter( + address spoke, + address user + ) internal view returns (UserAccountDataVars memory) { + return defaultVarsAfter.userAccountDataVars[spoke][user]; + } } diff --git a/invariants/protocol-suite/hooks/HookAggregator.t.sol b/invariants/protocol-suite/hooks/HookAggregator.t.sol index e85ceec8c..20966c3e3 100644 --- a/invariants/protocol-suite/hooks/HookAggregator.t.sol +++ b/invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -// Hook Contracts +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {DefaultBeforeAfterHooks} from './DefaultBeforeAfterHooks.t.sol'; // Utils @@ -69,28 +69,28 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { // CHECK_ALL_RESERVES actions should check all reserves for that spoke if (reserveId == CHECK_ALL_RESERVES) { - for (uint256 j; j < spokeReserveIds[spoke].length; j++) { - uint256 iterReserveId = spokeReserveIds[spoke][j]; - uint256 assetId = _getAssetId(spoke, iterReserveId); - address hubAddress = _getHubAddress(spoke, iterReserveId); - - assert_GPOST_HUB_A(hubAddress, assetId); - assert_GPOST_HUB_B(hubAddress, assetId); - assert_GPOST_HUB_C(hubAddress, assetId); - assert_GPOST_HUB_D(hubAddress, assetId); - assert_GPOST_HUB_EF(hubAddress, assetId, spoke); - assert_GPOST_HUB_G(hubAddress, assetId); + uint256 reserveCount = ISpoke(spoke).getReserveCount(); + for (uint256 j; j < reserveCount; j++) { + uint256 assetId = _getAssetId(spoke, j); + address hub = _getHubAddress(spoke, j); + + assert_GPOST_HUB_A(hub, assetId); + assert_GPOST_HUB_B(hub, assetId); + assert_GPOST_HUB_C(hub, assetId); + assert_GPOST_HUB_D(hub, assetId); + assert_GPOST_HUB_EF(hub, assetId, spoke); + assert_GPOST_HUB_G(hub, assetId); } } else { uint256 assetId = _getAssetId(spoke, reserveId); - address hubAddress = _getHubAddress(spoke, reserveId); - - assert_GPOST_HUB_A(hubAddress, assetId); - assert_GPOST_HUB_B(hubAddress, assetId); - assert_GPOST_HUB_C(hubAddress, assetId); - assert_GPOST_HUB_D(hubAddress, assetId); - assert_GPOST_HUB_EF(hubAddress, assetId, spoke); - assert_GPOST_HUB_G(hubAddress, assetId); + address hub = _getHubAddress(spoke, reserveId); + + assert_GPOST_HUB_A(hub, assetId); + assert_GPOST_HUB_B(hub, assetId); + assert_GPOST_HUB_C(hub, assetId); + assert_GPOST_HUB_D(hub, assetId); + assert_GPOST_HUB_EF(hub, assetId, spoke); + assert_GPOST_HUB_G(hub, assetId); } } } @@ -98,8 +98,8 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { function _spokePostConditions() internal { // Iterate through all users to check for (uint256 i; i < usersToCheck.length; i++) { - uint256 reserveId = usersToCheck[i].reserveId; address spoke = usersToCheck[i].spoke; + uint256 reserveId = usersToCheck[i].reserveId; address user = usersToCheck[i].user; // Check properties for the spoke @@ -108,11 +108,12 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { // Check properties for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig if (reserveId == CHECK_ALL_RESERVES) { + uint256 reserveCount = ISpoke(spoke).getReserveCount(); // Iterate through all reserves of the spoke - for (uint256 j; j < spokeReserveIds[spoke].length; j++) { - assert_GPOST_SP_A(spoke, spokeReserveIds[spoke][j], user); - assert_GPOST_SP_B(spoke, spokeReserveIds[spoke][j], user); - assert_GPOST_SP_E(spoke, spokeReserveIds[spoke][j], user); + for (uint256 j; j < reserveCount; j++) { + assert_GPOST_SP_A(spoke, j, user); + assert_GPOST_SP_B(spoke, j, user); + assert_GPOST_SP_E(spoke, j, user); } } else { // Check properties for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral diff --git a/invariants/protocol-suite/invariants/HubInvariants.t.sol b/invariants/protocol-suite/invariants/HubInvariants.t.sol index 44aab85bb..8dcc3ff6e 100644 --- a/invariants/protocol-suite/invariants/HubInvariants.t.sol +++ b/invariants/protocol-suite/invariants/HubInvariants.t.sol @@ -67,12 +67,12 @@ abstract contract HubInvariants is HandlerAggregator { } // Asset totals - IHub.Asset memory a = IHub(hubAddress).getAsset(assetId); + AssetVars memory vars = _assetVarsAfter(hubAddress, assetId); // Checks - assertEq(sumDrawnShares, a.drawnShares, INV_HUB_C); - assertEq(sumPremDrawnShares, a.premiumShares, INV_HUB_C); - assertEq(sumPremOffsetRay, a.premiumOffsetRay, INV_HUB_C); + assertEq(sumDrawnShares, vars.asset.drawnShares, INV_HUB_C); + assertEq(sumPremDrawnShares, vars.asset.premiumShares, INV_HUB_C); + assertEq(sumPremOffsetRay, vars.asset.premiumOffsetRay, INV_HUB_C); } function assert_INV_HUB_E(address hubAddress, uint256 assetId) internal { @@ -105,13 +105,14 @@ abstract contract HubInvariants is HandlerAggregator { uint256 accruedFees = IHub(hubAddress).getAssetAccruedFees(assetId); IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); - uint256 index = IHub(hubAddress).getAssetDrawnIndex(assetId); + uint256 drawnIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); + uint256 premiumRay = Premium.calculatePremiumRay({ premiumShares: asset.premiumShares, premiumOffsetRay: asset.premiumOffsetRay, - drawnIndex: index + drawnIndex: drawnIndex }); - uint256 drawnRay = asset.drawnShares * index; + uint256 drawnRay = asset.drawnShares * drawnIndex; uint256 aggregatedOwed = (drawnRay + premiumRay + asset.deficitRay).fromRayUp(); assertEq(totalAssets + accruedFees, asset.liquidity + aggregatedOwed + asset.swept, INV_HUB_F); diff --git a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol index 055bac241..aa7562c1f 100644 --- a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol +++ b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; + // Interfaces import {ISpokeBase} from 'src/spoke/interfaces/ISpokeBase.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; @@ -13,6 +15,8 @@ import {HandlerAggregator} from '../HandlerAggregator.t.sol'; /// @notice Implements Spoke Invariants for the protocol /// @dev Inherits HandlerAggregator to check actions in assertion testing mode abstract contract SpokeInvariants is HandlerAggregator { + using EnumerableSet for EnumerableSet.AddressSet; + /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -71,8 +75,8 @@ abstract contract SpokeInvariants is HandlerAggregator { function assert_INV_SP_C(address spoke, uint256 reserveId) internal { uint256 sumSpokeDebts; - for (uint256 i; i < actorAddresses.length; i++) { - sumSpokeDebts += ISpokeBase(spoke).getUserTotalDebt(reserveId, actorAddresses[i]); + for (uint256 i; i < actors.length(); i++) { + sumSpokeDebts += ISpokeBase(spoke).getUserTotalDebt(reserveId, actors.at(i)); } assertGe(sumSpokeDebts, ISpokeBase(spoke).getReserveTotalDebt(reserveId), INV_SP_C); } @@ -88,16 +92,16 @@ abstract contract SpokeInvariants is HandlerAggregator { function assert_INV_SP_E(address spoke, uint256 reserveId) internal { uint256 sumUserShares; - for (uint256 i; i < actorAddresses.length; i++) { - sumUserShares += ISpokeBase(spoke).getUserSuppliedShares(reserveId, actorAddresses[i]); + for (uint256 i; i < actors.length(); i++) { + sumUserShares += ISpokeBase(spoke).getUserSuppliedShares(reserveId, actors.at(i)); } assertEq(sumUserShares, ISpokeBase(spoke).getReserveSuppliedShares(reserveId), INV_SP_E); } function assert_INV_SP_F(address spoke, uint256 reserveId) internal { uint256 sumUserAssets; - for (uint256 i; i < actorAddresses.length; i++) { - sumUserAssets += ISpokeBase(spoke).getUserSuppliedAssets(reserveId, actorAddresses[i]); + for (uint256 i; i < actors.length(); i++) { + sumUserAssets += ISpokeBase(spoke).getUserSuppliedAssets(reserveId, actors.at(i)); } uint256 reserveSuppliedAssets = ISpokeBase(spoke).getReserveSuppliedAssets(reserveId); assertLe(sumUserAssets, reserveSuppliedAssets, INV_SP_F); diff --git a/invariants/protocol-suite/replays/ReplayTest_1.t.sol b/invariants/protocol-suite/replays/ReplayTest_1.t.sol index d92ab34f6..d088905ec 100644 --- a/invariants/protocol-suite/replays/ReplayTest_1.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_1.t.sol @@ -27,7 +27,7 @@ contract ReplayTest1 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -72,25 +72,25 @@ contract ReplayTest1 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/protocol-suite/replays/ReplayTest_2.t.sol b/invariants/protocol-suite/replays/ReplayTest_2.t.sol index 4b4def943..92b0da3c9 100644 --- a/invariants/protocol-suite/replays/ReplayTest_2.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_2.t.sol @@ -27,7 +27,7 @@ contract ReplayTest2 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -63,25 +63,25 @@ contract ReplayTest2 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/protocol-suite/replays/ReplayTest_3.t.sol b/invariants/protocol-suite/replays/ReplayTest_3.t.sol index 4718fff90..f6a58d389 100644 --- a/invariants/protocol-suite/replays/ReplayTest_3.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_3.t.sol @@ -27,7 +27,7 @@ contract ReplayTest3 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -38,12 +38,12 @@ contract ReplayTest3 is Invariants, Setup { function test_replay_3_setUsingAsCollateral() public { _setUpActor(USER3); - Tester.supply(790, 128, 71, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 203, 9); + Tester.supply(790, 128, 71, 203); + Tester.setUsingAsCollateral(true, 111, 255); + Tester.borrow(527, 68, 203, 11); _setUpActor(USER1); _delay(689004); - Tester.setUsingAsCollateral(false, 15, 13); + Tester.setUsingAsCollateral(false, 15, 15); invariant_INV_HUB(); } @@ -59,25 +59,25 @@ contract ReplayTest3 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/protocol-suite/replays/ReplayTest_4.t.sol b/invariants/protocol-suite/replays/ReplayTest_4.t.sol index 792cfd6df..cf358757c 100644 --- a/invariants/protocol-suite/replays/ReplayTest_4.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_4.t.sol @@ -27,7 +27,7 @@ contract ReplayTest4 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -38,12 +38,12 @@ contract ReplayTest4 is Invariants, Setup { function test_replay_4_supply() public { _setUpActor(USER3); - Tester.supply(790, 128, 71, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 95, 9); + Tester.supply(790, 128, 71, 203); + Tester.setUsingAsCollateral(true, 111, 255); + Tester.borrow(527, 68, 95, 11); _setUpActor(USER1); _delay(434894); - Tester.supply(423, 66, 149, 45); + Tester.supply(423, 66, 149, 47); } /////////////////////////////////////////////////////////////////////////////////////////////// @@ -58,25 +58,25 @@ contract ReplayTest4 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/protocol-suite/replays/ReplayTest_5.t.sol b/invariants/protocol-suite/replays/ReplayTest_5.t.sol index 94fb3e87b..43a263414 100644 --- a/invariants/protocol-suite/replays/ReplayTest_5.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_5.t.sol @@ -27,7 +27,7 @@ contract ReplayTest5 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -48,25 +48,25 @@ contract ReplayTest5 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/protocol-suite/replays/ReplayTest_6.t.sol b/invariants/protocol-suite/replays/ReplayTest_6.t.sol index 1be745481..1f7eb94fc 100644 --- a/invariants/protocol-suite/replays/ReplayTest_6.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_6.t.sol @@ -27,7 +27,7 @@ contract ReplayTest6 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -38,9 +38,8 @@ contract ReplayTest6 is Invariants, Setup { function test_replay_6_freezeAllReserves() public { _setUpActor(USER3); - Tester.supply(673, 128, 1, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 203, 9); + Tester.supply(673, 128, 1, 203); + Tester.setUsingAsCollateral(true, 111, 255); _setUpActor(USER1); _delay(338347); Tester.freezeAllReserves(32); @@ -49,12 +48,12 @@ contract ReplayTest6 is Invariants, Setup { function test_replay_6_supply() public { _setUpActor(USER3); - Tester.supply(790, 197, 87, 201); - Tester.setUsingAsCollateral(true, 197, 253); - Tester.borrow(527, 68, 65, 9); + Tester.supply(790, 197, 87, 203); + Tester.setUsingAsCollateral(true, 197, 255); + Tester.borrow(527, 68, 65, 11); _setUpActor(USER1); _delay(467); - Tester.supply(1327428228, 3, 151, 97); + Tester.supply(1327428228, 3, 151, 99); invariant_INV_SP(); } @@ -70,25 +69,25 @@ contract ReplayTest6 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/protocol-suite/replays/ReplayTest_7.t.sol b/invariants/protocol-suite/replays/ReplayTest_7.t.sol index 35acda80c..56c5b204f 100644 --- a/invariants/protocol-suite/replays/ReplayTest_7.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_7.t.sol @@ -27,7 +27,7 @@ contract ReplayTest7 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -38,13 +38,13 @@ contract ReplayTest7 is Invariants, Setup { function test_replay_7_updateUserRiskPremium() public { _setUpActor(USER3); - Tester.supply(790, 128, 71, 201); - Tester.setUsingAsCollateral(true, 111, 253); - Tester.borrow(527, 68, 203, 9); + Tester.supply(790, 128, 71, 203); + Tester.setUsingAsCollateral(true, 111, 255); + Tester.borrow(527, 68, 203, 11); _setUpActor(USER1); _delay(1812425); _delay(320876); - Tester.updateFrozen(false, 59, 85); + Tester.updateFrozen(false, 59, 87); _setUpActor(USER2); _delay(343393); Tester.updateHealthFactorForMaxBonus(105, 146); @@ -55,14 +55,14 @@ contract ReplayTest7 is Invariants, Setup { Tester.updateUserRiskPremium(101); _setUpActor(USER3); _delay(590687); - Tester.setUsingAsCollateral(true, 111, 253); + Tester.setUsingAsCollateral(true, 111, 255); _delay(323286); Tester.donateUnderlyingToHub(194, 231, 231); _delay(505810); - Tester.borrow(56, 77, 155, 21); + Tester.borrow(56, 77, 155, 23); _setUpActor(USER2); _delay(112744); - Tester.updateBorrowable(false, 223, 157); + Tester.updateBorrowable(false, 223, 159); _setUpActor(USER1); _delay(1632525); _setUpActor(USER2); @@ -85,7 +85,7 @@ contract ReplayTest7 is Invariants, Setup { _delay(928317); _setUpActor(USER1); _delay(23908); - Tester.updateBorrowable(true, 217, 53); + Tester.updateBorrowable(true, 217, 55); _setUpActor(USER3); _delay(582766); Tester.updateUserRiskPremium(25); @@ -94,11 +94,11 @@ contract ReplayTest7 is Invariants, Setup { function test_replay_7_repay() public { _setUpActor(USER3); _delay(321376); - Tester.supply(790, 197, 87, 201); + Tester.supply(790, 197, 87, 203); Tester.supply(100000000000000000000000002, 50, 228, 214); - Tester.setUsingAsCollateral(true, 197, 253); + Tester.setUsingAsCollateral(true, 197, 255); _delay(20833); - Tester.borrow(2973933138, 65, 107, 14); + Tester.borrow(2973933138, 65, 107, 12); _setUpActor(USER1); Tester.updateSpokeSupplyCap(172, 180, 253, 253); _setUpActor(USER3); @@ -107,7 +107,7 @@ contract ReplayTest7 is Invariants, Setup { 1209722426464509529070304541882533570806727645123886685504166337177518312, 62, 253, - 254 + 252 ); } @@ -123,25 +123,25 @@ contract ReplayTest7 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/protocol-suite/replays/ReplayTest_8.t.sol b/invariants/protocol-suite/replays/ReplayTest_8.t.sol index 9952c1fd4..df32f8d6b 100644 --- a/invariants/protocol-suite/replays/ReplayTest_8.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_8.t.sol @@ -27,7 +27,7 @@ contract ReplayTest7 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -62,7 +62,7 @@ contract ReplayTest7 is Invariants, Setup { Tester.updatePaused(false, 134, 157); _setUpActor(USER2); _delay(267059); - Tester.updateFrozen(true, 121, 96); + Tester.updateFrozen(true, 121, 98); _delay(70398); Tester.donateUnderlyingToSpoke(10, 26, 123); _delay(570296); @@ -85,7 +85,7 @@ contract ReplayTest7 is Invariants, Setup { Tester.updateFrozen(true, 190, 214); _setUpActor(USER2); _delay(395602); - Tester.updatePaused(false, 211, 11); + Tester.updatePaused(false, 211, 9); _delay(420078); Tester.setPrice( 32073901804028723412800996385287954179043683908620921829695310985447157891024, @@ -100,7 +100,7 @@ contract ReplayTest7 is Invariants, Setup { _delay(147712); Tester.freezeAllReserves(0); _setUpActor(USER3); - Tester.updatePaused(false, 69, 126); + Tester.updatePaused(false, 69, 124); _setUpActor(USER1); Tester.updatePaused(false, 204, 59); _setUpActor(USER2); @@ -154,7 +154,7 @@ contract ReplayTest7 is Invariants, Setup { Tester.updateFrozen(true, 0, 0); _delay(432000); Tester.updateUserDynamicConfig(86); - Tester.updateBorrowable(false, 225, 96); + Tester.updateBorrowable(false, 225, 98); _setUpActor(USER1); _delay(333625); Tester.updateUserRiskPremium(0); @@ -171,20 +171,20 @@ contract ReplayTest7 is Invariants, Setup { function test_replay_withdraw_2() public { _setUpActor(USER2); _delay(48); - Tester.supply(5765, 214, 57, 0); + Tester.supply(5765, 214, 57, 2); Tester.pauseAllReserves(229); Tester.withdraw( 79270905586291627497307400106420653318261579396350751610744730950627405265, 190, 189, - 0 + 2 ); } function test_replay_withdraw_3() public { _setUpActor(USER1); _delay(48); - Tester.updatePaused(false, 43, 5); + Tester.updatePaused(false, 43, 7); _setUpActor(USER2); Tester.donateUnderlyingToSpoke( 4453226477229950780488458817391925832298660130277646469228882959756527864210, @@ -197,13 +197,13 @@ contract ReplayTest7 is Invariants, Setup { Tester.updatePaused(true, 92, 0); _setUpActor(USER1); _delay(184974); - Tester.updateFrozen(false, 5, 198); + Tester.updateFrozen(false, 5, 196); _setUpActor(USER2); Tester.setPrice(-1, 56); _setUpActor(USER1); Tester.updateUserDynamicConfig(22); _delay(12646); - Tester.supply(50000000, 0, 27, 0); + Tester.supply(50000000, 0, 27, 2); _setUpActor(USER3); _delay(367615); Tester.pauseAllReserves(137); @@ -216,13 +216,13 @@ contract ReplayTest7 is Invariants, Setup { 0 ); _setUpActor(USER2); - Tester.updatePaused(false, 153, 223); + Tester.updatePaused(false, 153, 221); _setUpActor(USER1); Tester.withdraw( 34926635329146824736853381233931534329217246589975803687784392641014182723417, 0, 201, - 12 + 10 ); } @@ -238,25 +238,25 @@ contract ReplayTest7 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/shared/utils/Actor.sol b/invariants/shared/utils/Actor.sol index 4b5bdbc47..180d3c195 100644 --- a/invariants/shared/utils/Actor.sol +++ b/invariants/shared/utils/Actor.sol @@ -2,56 +2,49 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol'; /// @notice Proxy contract for invariant suite actors to avoid aTester calling contracts contract Actor { - /// @notice list of tokens to approve - address[] internal tokens; - /// @notice list of contracts to approve tokens to - address[] internal contracts; + using SafeERC20 for IERC20; - constructor(address[] memory _tokens, address[] memory _contracts) payable { - tokens = _tokens; - contracts = _contracts; - for (uint256 i = 0; i < tokens.length; i++) { - for (uint256 j = 0; j < contracts.length; j++) { - IERC20(tokens[i]).approve(contracts[j], type(uint256).max); + /// @dev Constructor approves the maximum amount of all tokens to all protocol contracts to avoid needing to approve in handlers + constructor(address[] memory tokens, address[] memory contracts) payable { + for (uint256 i = 0; i < tokens.length; ++i) { + for (uint256 j = 0; j < contracts.length; ++j) { + IERC20(tokens[i]).forceApprove(contracts[j], type(uint256).max); } } } /// @notice Helper function to proxy a call to a target contract, used to avoid Tester calling contracts - function proxy( - address _target, - bytes memory _calldata - ) public returns (bool success, bytes memory returnData) { - (success, returnData) = address(_target).call(_calldata); - - handleAssertionError(success, returnData); + function proxy(address target, bytes memory callData) public returns (bool, bytes memory) { + (bool ok, bytes memory ret) = address(target).call(callData); + _handleAssertionError(ok, ret); + return (ok, ret); } /// @notice Helper function to proxy a call and value to a target contract, used to avoid Tester calling contracts function proxy( - address _target, - bytes memory _calldata, + address target, + bytes memory callData, uint256 value - ) public returns (bool success, bytes memory returnData) { - (success, returnData) = address(_target).call{value: value}(_calldata); - - handleAssertionError(success, returnData); + ) public payable returns (bool, bytes memory) { + (bool ok, bytes memory ret) = address(target).call{value: value}(callData); + _handleAssertionError(ok, ret); + return (ok, ret); } /// @notice Checks if a call failed due to an assertion error and propagates the error if found. - /// @param success Indicates whether the call was successful. - /// @param returnData The data returned from the call. - function handleAssertionError(bool success, bytes memory returnData) internal pure { - if (!success && returnData.length == 36) { + /// @param ok Indicates whether the call was successful. + /// @param ret The data returned from the call. + function _handleAssertionError(bool ok, bytes memory ret) internal pure { + if (!ok && ret.length == 36) { bytes4 selector; uint256 code; - assembly { - selector := mload(add(returnData, 0x20)) - code := mload(add(returnData, 0x24)) + assembly ('memory-safe') { + selector := mload(add(ret, 0x20)) + code := mload(add(ret, 0x24)) } if (selector == bytes4(0x4e487b71) && code == 1) { diff --git a/invariants/shared/utils/ActorsUtils.sol b/invariants/shared/utils/ActorsUtils.sol index 342cdf51f..9620af9d4 100644 --- a/invariants/shared/utils/ActorsUtils.sol +++ b/invariants/shared/utils/ActorsUtils.sol @@ -24,30 +24,29 @@ library ActorsUtils { // Initialize the three actors of the fuzzers for (uint256 i; i < addresses.length; i++) { // Deploy actor proxies and approve system contracts - address _actor = setUpActor(tokens, contracts); + address actor = setUpActor(tokens, contracts); // Mint initial balances to actors for (uint256 j = 0; j < tokens.length; j++) { - TestnetERC20 _token = TestnetERC20(tokens[j]); - uint256 decimals = _token.decimals(); - _token.mint(_actor, INITIAL_BALANCE * 10 ** decimals); + TestnetERC20 token = TestnetERC20(tokens[j]); + uint256 decimals = token.decimals(); + token.mint(actor, INITIAL_BALANCE * 10 ** decimals); } - actorAddresses[i] = _actor; + actorAddresses[i] = actor; } } /// @notice Deploy an actor proxy contract /// @param tokens Array of token addresses /// @param contracts Array of contract addresses to aprove tokens to - /// @return actorAddress Address of the deployed actor + /// @return Address of the deployed actor function setUpActor( address[] memory tokens, address[] memory contracts - ) internal returns (address actorAddress) { - bool success; - Actor _actor = new Actor(tokens, contracts); - (success, ) = address(_actor).call{value: INITIAL_ETH_BALANCE}(''); - assert(success); - actorAddress = address(_actor); + ) internal returns (address) { + Actor actor = new Actor(tokens, contracts); + (bool ok, ) = address(actor).call{value: INITIAL_ETH_BALANCE}(''); + assert(ok); + return address(actor); } } From 2d7ac817331cdc6cfb9c7ad100a99abade637740 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:31:45 +0530 Subject: [PATCH 062/106] chore: fix compile --- invariants/protocol-suite/base/BaseHandler.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invariants/protocol-suite/base/BaseHandler.t.sol b/invariants/protocol-suite/base/BaseHandler.t.sol index 22bbcd830..e32c9bc5c 100644 --- a/invariants/protocol-suite/base/BaseHandler.t.sol +++ b/invariants/protocol-suite/base/BaseHandler.t.sol @@ -42,7 +42,7 @@ contract BaseHandler is HookAggregator { /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor function _approve(address token, Actor actor_, address spender, uint256 amount) internal { - (bool ok, bytes memory returnData) = actor_.proxy( + (bool ok, bytes memory ret) = actor_.proxy( token, abi.encodeCall(IERC20.approve, (spender, amount)) ); From 07fdc968b1d6b89aa4bafc999066cc514970be1f Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:11:07 +0530 Subject: [PATCH 063/106] feat: cleanup + new users invariant --- invariants/hub-suite/Invariants.t.sol | 24 +- invariants/hub-suite/README.md | 46 +++- .../invariants/HubInvariantAssertions.t.sol | 240 ++++++++++++++++++ .../hub-suite/invariants/HubInvariants.t.sol | 187 ++------------ .../hub-suite/specs/HubInvariantsSpec.t.sol | 67 ++++- .../specs/HubPostconditionsSpec.t.sol | 40 ++- invariants/protocol-suite/Invariants.t.sol | 33 ++- invariants/protocol-suite/README.md | 40 ++- .../protocol-suite/base/BaseStorage.t.sol | 8 - .../invariants/HubInvariants.t.sol | 207 --------------- .../invariants/SpokeInvariants.t.sol | 77 +++--- .../protocol-suite/specs/InvariantsSpec.t.sol | 65 +---- .../specs/PostconditionsSpec.t.sol | 53 +--- 13 files changed, 509 insertions(+), 578 deletions(-) create mode 100644 invariants/hub-suite/invariants/HubInvariantAssertions.t.sol delete mode 100644 invariants/protocol-suite/invariants/HubInvariants.t.sol diff --git a/invariants/hub-suite/Invariants.t.sol b/invariants/hub-suite/Invariants.t.sol index 2fd14d0b5..7b1c46203 100644 --- a/invariants/hub-suite/Invariants.t.sol +++ b/invariants/hub-suite/Invariants.t.sol @@ -10,17 +10,19 @@ abstract contract Invariants is HubInvariants { uint256 assetCount = hub.getAssetCount(); for (uint256 i; i < assetCount; i++) { - // HUB - assert_INV_HUB_A(i); - assert_INV_HUB_B(i); - assert_INV_HUB_C(i); - assert_INV_HUB_E(i); - assert_INV_HUB_F(i); - assert_INV_HUB_GH(i); - assert_INV_HUB_I(i); - assert_INV_HUB_K(i); - assert_INV_HUB_O(i); - assert_INV_HUB_P(i); + // HUB (parameterized assertions from HubInvariantAssertions) + assert_INV_HUB_A(hub, i); + assert_INV_HUB_B(hub, i); + assert_INV_HUB_C(hub, i); + assert_INV_HUB_E(hub, i); + assert_INV_HUB_F(hub, i); + assert_INV_HUB_GH(hub, i); + assert_INV_HUB_I(hub, i); + assert_INV_HUB_K(hub, i); + assert_INV_HUB_O(hub, i); + assert_INV_HUB_P(hub, i); + assert_INV_HUB_Q(hub, i); + assert_INV_HUB_R(hub, i); for (uint256 j; j < NUMBER_OF_ACTORS; j++) { assert_INV_HUB_ERC4626_A(i, actorAddresses[j]); assert_INV_HUB_ERC4626_B(i, actorAddresses[j]); diff --git a/invariants/hub-suite/README.md b/invariants/hub-suite/README.md index edc1ae221..ebbed6b37 100644 --- a/invariants/hub-suite/README.md +++ b/invariants/hub-suite/README.md @@ -1,6 +1,8 @@ -# Enigma Dark – Hub-Focused Fuzzing & Invariant Testing Suite +# Hub-Focused Fuzzing & Invariant Testing Suite -A simplified handler-based invariant testing suite focused exclusively on the **Hub** component of the Aave v4 protocol. This suite performs deep stateful fuzzing against a single hub with actors simulating spokes, validating critical hub system properties through automated property checking and postcondition verification. +A handler-based invariant testing suite focused exclusively on the **Hub** component of the Aave v4 protocol. This suite performs deep stateful fuzzing against a single hub with actors simulating spokes, validating critical hub system properties through automated property checking and postcondition verification. + +Hub-suite is the **canonical source** for all hub-related specs and invariant assertions. The protocol-suite imports from hub-suite — hub-suite has zero protocol-suite dependencies. ## Overview @@ -23,24 +25,42 @@ All protocol actions are monitored by hooks that snapshot state and verify postc - Deploys a single Hub with a deterministic interest rate strategy - Configures 2 base assets (USDC, WETH) for simplicity and performance - Initializes multiple actors with spoke permissions registered on the hub -- Simplified configuration compared to the full multi-hub, multi-spoke suite + +**Spec Layer** (`specs/`) + +- `HubInvariantsSpec` – canonical hub invariant string constants (INV*HUB*\_, ERC4626\_\_, AVAILABILITY\_\*) +- `HubPostconditionsSpec` – canonical hub postcondition string constants (GPOST*HUB*_, HSPOST*HUB*_) +- Protocol-suite inherits these specs; they are defined here only **Handler Layer** (`handlers/`) -- `HubHandler` – hub liquidity operations (supply, draw, repay) through actor-spokes +- `HubHandler` – hub liquidity operations (add, remove, draw, restore, etc.) through actor-spokes - `HubConfiguratorHandler` – admin operations (spoke cap updates, risk parameter changes) -- Handlers expose hub interface to actors registered as spokes +- `DonationAttackHandler` – simulates direct token transfers to the hub + +**Invariant Layer** (`invariants/`) + +- `HubInvariantAssertions` – abstract parameterized hub invariant assertion logic (INV_HUB_A through P). Importable by other suites +- `HubInvariants` – concrete invariants extending `HubInvariantAssertions`, adds ERC4626 and AVAILABILITY assertions specific to the hub-suite -**Verification Layer** (`hooks/`, `invariants/`) +**Verification Layer** (`hooks/`) - Before/after hooks with state snapshots - Global and handler-specific postcondition assertions -- Hub invariants (liquidity accounting, share calculations, interest accrual) -**Utilities** (`utils/`) +### Reuse by Protocol-Suite + +Hub-suite exports reusable abstracts that protocol-suite imports: + +``` +protocol-suite → hub-suite → shared/ +``` -- Constants and assertion helpers -- Random value generation for fuzzing inputs +| Hub-suite export | Protocol-suite usage | +| ------------------------ | ------------------------------------------------------ | +| `HubInvariantsSpec` | Inherited by `InvariantsSpec` for hub string constants | +| `HubPostconditionsSpec` | Inherited by `PostconditionsSpec` for hub strings | +| `HubInvariantAssertions` | Inherited by `Invariants.t.sol` for hub assert logic | ## How It Works @@ -62,12 +82,12 @@ make echidna-hub-assert ## Key Features -- **Simplified hub-focused testing** for specific hub component validation +- **Canonical hub logic** — single source of truth for hub specs and invariant assertions - **Actor-based spoke simulation** – no custom spoke deployments, just actors as spokes - **Comprehensive postcondition checking** after every hub state transition - **Performance optimized** – minimal asset and spoke count for faster fuzzing -- **Hub-specific invariants** validating liquidity accounting, interest calculations, and spoken cap constraints +- **Reusable invariant abstracts** – `HubInvariantAssertions` importable by any suite --- -**Note:** This suite complements the full multi-hub, multi-spoke suite by providing deep, focused testing of hub core functionality in isolation. +**Note:** This suite complements the full multi-hub, multi-spoke protocol-suite by providing deep, focused testing of hub core functionality in isolation. diff --git a/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol b/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol new file mode 100644 index 000000000..ed1b216c6 --- /dev/null +++ b/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Libraries +import {Premium} from 'src/hub/libraries/Premium.sol'; +import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; +import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; +import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; + +// Interfaces +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; + +// Specs +import {HubInvariantsSpec} from '../specs/HubInvariantsSpec.t.sol'; + +// Assertions +import {StdAsserts} from '../../shared/utils/StdAsserts.sol'; + +/// @title HubInvariantAssertions +/// @notice Abstract hub invariant assertion logic, importable by any suite. +/// @dev Does not inherit any suite-specific base class. Concrete suites override +/// `_getSpokesForAsset` to supply the spoke list for iteration. +abstract contract HubInvariantAssertions is StdAsserts, HubInvariantsSpec { + using SafeCast for *; + using WadRayMath for *; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATEFUL INVARIANT STORAGE // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Last seen drawn index per hub per assetId (INV_HUB_Q) + mapping(IHub => mapping(uint256 => uint256)) internal _lastSeenDebtSharePrice; + /// @notice Last seen added assets (+ virtual) per hub per assetId (INV_HUB_R) + mapping(IHub => mapping(uint256 => uint256)) internal _lastSeenAssets; + /// @notice Last seen added shares (+ virtual) per hub per assetId (INV_HUB_R) + mapping(IHub => mapping(uint256 => uint256)) internal _lastSeenShares; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // VIRTUAL HOOKS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Returns the list of spokes to iterate for a given hub and asset. + /// Hub-suite returns actorAddresses + feeReceiver; protocol-suite returns allSpokes. + function _getSpokesForAsset( + IHub hub, + uint256 assetId + ) internal view virtual returns (address[] memory); + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_INV_HUB_A(IHub hub, uint256 assetId) internal { + uint256 assets = hub.getAddedAssets(assetId); + + if (assets == 0) { + assertEq(hub.getAddedShares(assetId), 0, INV_HUB_A2); + } + } + + function assert_INV_HUB_B(IHub hub, uint256 assetId) internal { + address[] memory spokes = _getSpokesForAsset(hub, assetId); + uint256 spokeCount = spokes.length; + uint256 sumDebt; + + for (uint256 i; i < spokeCount; i++) { + sumDebt += hub.getSpokeTotalOwed(assetId, spokes[i]); + } + + uint256 assetTotal = hub.getAssetTotalOwed(assetId); + assertGe(sumDebt, assetTotal, INV_HUB_B); + } + + function assert_INV_HUB_C(IHub hub, uint256 assetId) internal { + address[] memory spokes = _getSpokesForAsset(hub, assetId); + uint256 spokeCount = spokes.length; + + uint256 sumDrawnShares; + uint256 sumPremDrawnShares; + int256 sumPremOffsetRay; + + for (uint256 i; i < spokeCount; i++) { + address spoke = spokes[i]; + sumDrawnShares += hub.getSpokeDrawnShares(assetId, spoke); + (uint256 premiumDrawnShares, int256 premiumOffsetRay) = hub.getSpokePremiumData( + assetId, + spoke + ); + sumPremDrawnShares += premiumDrawnShares; + sumPremOffsetRay += premiumOffsetRay; + } + + // Asset totals + IHub.Asset memory asset = hub.getAsset(assetId); + + // Checks + assertEq(sumDrawnShares, asset.drawnShares, INV_HUB_C); + assertEq(sumPremDrawnShares, asset.premiumShares, INV_HUB_C); + assertEq(sumPremOffsetRay, asset.premiumOffsetRay, INV_HUB_C); + } + + function assert_INV_HUB_E(IHub hub, uint256 assetId) internal { + uint256 totalAssets = hub.getAddedAssets(assetId); + uint256 totalShares = hub.getAddedShares(assetId); + + // interest accrued on virtual shares + uint256 burntInterest = totalAssets - hub.previewRemoveByShares(assetId, totalShares); + + // Checks: totalAddedAssets ≈ previewRemoveByShares(totalAddedShares) + // Tolerance: the virtual offset (V=1e6) absorbs a fraction of accrued interest when + // converting all shares back to assets. + assertApproxEqAbs( + totalAssets, + hub.previewRemoveByShares(assetId, totalShares), // round down + burntInterest, + INV_HUB_E_1 + ); + + assertGe( + totalAssets + SharesMath.VIRTUAL_ASSETS, + hub.previewRemoveByShares(assetId, totalShares + SharesMath.VIRTUAL_ASSETS), + INV_HUB_E_2 + ); + } + + function assert_INV_HUB_F(IHub hub, uint256 assetId) internal { + uint256 totalAssets = hub.getAddedAssets(assetId); + uint256 accruedFees = hub.getAssetAccruedFees(assetId); + + IHub.Asset memory asset = hub.getAsset(assetId); + uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); + + uint256 premiumRay = Premium.calculatePremiumRay({ + premiumShares: asset.premiumShares, + premiumOffsetRay: asset.premiumOffsetRay, + drawnIndex: drawnIndex + }); + uint256 drawnRay = asset.drawnShares * drawnIndex; + uint256 aggregatedOwed = (drawnRay + premiumRay + asset.deficitRay).fromRayUp(); + + assertEq(totalAssets + accruedFees, asset.liquidity + aggregatedOwed + asset.swept, INV_HUB_F); + } + + function assert_INV_HUB_GH(IHub hub, uint256 assetId) internal { + address[] memory spokes = _getSpokesForAsset(hub, assetId); + uint256 spokeCount = spokes.length; + uint256 tolerancePerActor = hub.previewAddByShares(assetId, 1); + + // Sum per-spoke values + uint256 totalAddedAssets; + uint256 totalAddedShares; + for (uint256 i; i < spokeCount; i++) { + totalAddedAssets += hub.getSpokeAddedAssets(assetId, spokes[i]); + totalAddedShares += hub.getSpokeAddedShares(assetId, spokes[i]); + } + + // Inline burnt interest: interest accrued on virtual shares + { + uint256 totalAssets = hub.getAddedAssets(assetId); + uint256 totalShares = hub.getAddedShares(assetId); + totalAddedAssets += totalAssets - hub.previewRemoveByShares(assetId, totalShares); + } + + // Checks + uint256 addedShares = hub.getAddedShares(assetId); + if (addedShares > 0) { + assertApproxEqAbs( + totalAddedAssets, + hub.getAddedAssets(assetId), + (spokeCount + 2) * tolerancePerActor, + INV_HUB_G + ); + } + assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); + } + + function assert_INV_HUB_I(IHub hub, uint256 assetId) internal { + // Get underlying from assetId + (address underlying, ) = hub.getAssetUnderlyingAndDecimals(assetId); + + // Query values + uint256 liquidity = hub.getAssetLiquidity(assetId); + uint256 swept = hub.getAssetSwept(assetId); + uint256 underlyingBalance = IERC20(underlying).balanceOf(address(hub)); + + // Checks + assertGe(underlyingBalance + swept, liquidity, INV_HUB_I); + } + + function assert_INV_HUB_K(IHub hub, uint256 assetId) internal { + IHub.AssetConfig memory assetConfig = hub.getAssetConfig(assetId); + + // Checks + assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); + } + + function assert_INV_HUB_O(IHub hub, uint256 assetId) internal { + address[] memory spokes = _getSpokesForAsset(hub, assetId); + uint256 spokeCount = spokes.length; + uint256 totalDeficitRay; + for (uint256 i; i < spokeCount; i++) { + totalDeficitRay += hub.getSpokeDeficitRay(assetId, spokes[i]); + } + assertEq(totalDeficitRay, hub.getAssetDeficitRay(assetId), INV_HUB_O); + } + + function assert_INV_HUB_P(IHub hub, uint256 assetId) internal { + (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); + uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); + assertGe((premiumShares * drawnIndex).toInt256(), premiumOffsetRay, INV_HUB_P); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // STATEFUL HUB INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function assert_INV_HUB_Q(IHub hub, uint256 assetId) internal { + uint256 lastIndex = _lastSeenDebtSharePrice[hub][assetId]; + uint256 currentIndex = hub.getAssetDrawnIndex(assetId); + assertGe(currentIndex, WadRayMath.RAY, INV_HUB_Q); + if (lastIndex > 0) { + assertGe(currentIndex, lastIndex, INV_HUB_Q); + } + _lastSeenDebtSharePrice[hub][assetId] = currentIndex; + } + + function assert_INV_HUB_R(IHub hub, uint256 assetId) internal { + uint256 lastAssets = _lastSeenAssets[hub][assetId]; + uint256 lastShares = _lastSeenShares[hub][assetId]; + uint256 assets = hub.getAddedAssets(assetId) + SharesMath.VIRTUAL_ASSETS; + uint256 shares = hub.getAddedShares(assetId) + SharesMath.VIRTUAL_SHARES; + if (lastShares > 0) { + // assets/shares >= lastAssets/lastShares <=> assets * lastShares >= lastAssets * shares + assertFullMulGe(assets, lastShares, lastAssets, shares, INV_HUB_R); + } + _lastSeenAssets[hub][assetId] = assets; + _lastSeenShares[hub][assetId] = shares; + } +} diff --git a/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol index 686edf96d..c4328444c 100644 --- a/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -1,187 +1,40 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -// Libraries -import {Premium} from 'src/hub/libraries/Premium.sol'; -import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; -import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; -import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; -import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; - // Interfaces import {IHub} from 'src/hub/interfaces/IHub.sol'; -import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; // Contracts +import {HubInvariantAssertions} from './HubInvariantAssertions.t.sol'; import {HandlerAggregator} from '../HandlerAggregator.t.sol'; /// @title HubInvariants -/// @notice Implements Hub Invariants for the protocol -/// @dev Inherits HandlerAggregator to check actions in assertion testing mode -abstract contract HubInvariants is HandlerAggregator { - using SafeCast for *; - using WadRayMath for *; - +/// @notice Implements Hub Invariants for the hub-suite. +/// @dev Common invariant assertions are inherited from HubInvariantAssertions. +/// This contract adds ERC4626 and AVAILABILITY invariants specific to the hub-suite. +abstract contract HubInvariants is HandlerAggregator, HubInvariantAssertions { /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // + // VIRTUAL OVERRIDES // /////////////////////////////////////////////////////////////////////////////////////////////// - function assert_INV_HUB_A(uint256 assetId) internal { - uint256 assets = hub.getAddedAssets(assetId); - - if (assets == 0) { - assertEq(hub.getAddedShares(assetId), 0, INV_HUB_A2); - } - } - - function assert_INV_HUB_B(uint256 assetId) internal { - // Sum per-spoke values - uint256 spokeCount = NUMBER_OF_ACTORS; - uint256 sumDebt; - - for (uint256 i; i < spokeCount; i++) { - (uint256 d, uint256 p) = hub.getSpokeOwed(assetId, actorAddresses[i]); - sumDebt += d + p; - } - - uint256 assetTotal = hub.getAssetTotalOwed(assetId); // drawn + premium - assertGe(sumDebt, assetTotal, INV_HUB_B); - } - - function assert_INV_HUB_C(uint256 assetId) internal { - // Sum per-spoke values - uint256 spokeCount = NUMBER_OF_ACTORS; - - uint256 sumDrawnShares; - uint256 sumPremDrawnShares; - int256 sumPremOffsetRay; - - for (uint256 i; i < spokeCount; i++) { - address spoke = actorAddresses[i]; - sumDrawnShares += hub.getSpokeDrawnShares(assetId, spoke); - (uint256 premiumDrawnShares, int256 premiumOffsetRay) = hub.getSpokePremiumData( - assetId, - spoke - ); - sumPremDrawnShares += premiumDrawnShares; - sumPremOffsetRay += premiumOffsetRay; - } - - // Asset totals - IHub.Asset memory asset = hub.getAsset(assetId); - - // Checks - assertEq(sumDrawnShares, asset.drawnShares, INV_HUB_C); - assertEq(sumPremDrawnShares, asset.premiumShares, INV_HUB_C); - assertEq(sumPremOffsetRay, asset.premiumOffsetRay, INV_HUB_C); - } - - function assert_INV_HUB_E(uint256 assetId) internal { - uint256 totalAssets = hub.getAddedAssets(assetId); - uint256 totalShares = hub.getAddedShares(assetId); - - // interest accrued on virtual shares - uint256 burntInterest = totalAssets - hub.previewRemoveByShares(assetId, totalShares); - - // Checks: totalAddedAssets ≈ previewRemoveByShares(totalAddedShares) - // Tolerance: the virtual offset (V=1e6) absorbs a fraction of accrued interest when - // converting all shares back to assets - assertApproxEqAbs( - totalAssets, - hub.previewRemoveByShares(assetId, totalShares), // round down - burntInterest, - INV_HUB_E_1 - ); - - assertGe( - totalAssets + SharesMath.VIRTUAL_ASSETS, - hub.previewRemoveByShares(assetId, totalShares + SharesMath.VIRTUAL_ASSETS), - INV_HUB_E_2 - ); - } - - function assert_INV_HUB_F(uint256 assetId) internal { - uint256 totalAssets = hub.getAddedAssets(assetId); - uint256 accruedFees = hub.getAssetAccruedFees(assetId); - IHub.Asset memory asset = hub.getAsset(assetId); - uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); - - uint256 premiumRay = Premium.calculatePremiumRay({ - premiumShares: asset.premiumShares, - premiumOffsetRay: asset.premiumOffsetRay, - drawnIndex: drawnIndex - }); - uint256 drawnRay = asset.drawnShares * drawnIndex; - uint256 aggregatedOwed = (drawnRay + premiumRay + asset.deficitRay).fromRayUp(); - - assertEq(totalAssets + accruedFees, asset.liquidity + aggregatedOwed + asset.swept, INV_HUB_F); - } - - function assert_INV_HUB_GH(uint256 assetId) internal { - uint256 spokeCount = NUMBER_OF_ACTORS; - uint256 tolerancePerActor = hub.previewAddByShares(assetId, 1); - - // Sum per-spoke values - uint256 totalAddedAssets; - uint256 totalAddedShares; - for (uint256 i; i < spokeCount; i++) { - totalAddedAssets += hub.getSpokeAddedAssets(assetId, actorAddresses[i]); - totalAddedShares += hub.getSpokeAddedShares(assetId, actorAddresses[i]); - } + /// @dev Returns actorAddresses + feeReceiver for the given hub and asset. + function _getSpokesForAsset( + IHub hub, + uint256 assetId + ) internal view override returns (address[] memory) { address feeReceiver = hub.getAsset(assetId).feeReceiver; - totalAddedAssets += hub.getSpokeAddedAssets(assetId, feeReceiver); - totalAddedShares += hub.getSpokeAddedShares(assetId, feeReceiver); - - totalAddedAssets += _calculateBurntInterest(hub, assetId); - - // Checks - uint256 addedShares = hub.getAddedShares(assetId); - if (addedShares > 0) { - assertApproxEqAbs( - totalAddedAssets, - hub.getAddedAssets(assetId), - (spokeCount + 2) * tolerancePerActor, - INV_HUB_G - ); - } - assertEq(totalAddedShares, hub.getAddedShares(assetId), INV_HUB_H); - } - - function assert_INV_HUB_I(uint256 assetId) internal { - // Get underlying from assetId - (address underlying, ) = hub.getAssetUnderlyingAndDecimals(assetId); - - // Query values - uint256 liquidity = hub.getAssetLiquidity(assetId); - uint256 swept = hub.getAssetSwept(assetId); - uint256 underlyingBalance = IERC20(underlying).balanceOf(address(hub)); - - // Checks - assertGe(underlyingBalance + swept, liquidity, INV_HUB_I); - } - - function assert_INV_HUB_K(uint256 assetId) internal { - /// @dev for this check to be meaningful, strategy configuration operations have to be integrated - IHub.AssetConfig memory assetConfig = hub.getAssetConfig(assetId); - - // Checks - assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); - } - - function assert_INV_HUB_O(uint256 assetId) internal { - uint256 spokeCount = NUMBER_OF_ACTORS; - uint256 totalDeficitRay; - for (uint256 i; i < spokeCount; i++) { - totalDeficitRay += hub.getSpokeDeficitRay(assetId, actorAddresses[i]); + uint256 count = NUMBER_OF_ACTORS; + address[] memory spokes = new address[](count + 1); + for (uint256 i; i < count; i++) { + spokes[i] = actorAddresses[i]; } - assertEq(totalDeficitRay, hub.getAssetDeficitRay(assetId), INV_HUB_O); + spokes[count] = feeReceiver; + return spokes; } - function assert_INV_HUB_P(uint256 assetId) internal { - (uint256 premiumShares, int256 premiumOffsetRay) = hub.getAssetPremiumData(assetId); - uint256 drawnIndex = hub.getAssetDrawnIndex(assetId); - assertGe((premiumShares * drawnIndex).toInt256(), premiumOffsetRay, INV_HUB_P); - } + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB: ERC4626 // + /////////////////////////////////////////////////////////////////////////////////////////////// function assert_INV_HUB_ERC4626_A(uint256 assetId, address spoke) internal { uint256 addedAssets = hub.getSpokeAddedAssets(assetId, spoke); diff --git a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol index 1ccf0a702..197ecd38b 100644 --- a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -1,13 +1,66 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {InvariantsSpec} from '../../protocol-suite/specs/InvariantsSpec.t.sol'; +/// @title HubInvariantsSpec +/// @notice Invariants specification for the hub +/// @dev Contains pseudo code and description for the invariant properties in the hub. +/// This is the canonical source for all hub invariant strings. +/// Protocol-suite imports these via inheritance. +abstract contract HubInvariantsSpec { + /*///////////////////////////////////////////////////////////////////////////////////////////// + // PROPERTY TYPES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// - INVARIANTS (INV): + /// - Properties that should always hold true in the system. + /// - Implemented in the /invariants folder. + + /////////////////////////////////////////////////////////////////////////////////////////////*/ -/// @title InvariantsSpec -/// @notice Invariants specification for the protocol -/// @dev Contains pseudo code and description for the invariant properties in the protocol -abstract contract HubInvariantsSpec is InvariantsSpec { - /////////////////////////////////////////////////////////////////////////////////////////////*/ + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant INV_HUB_A2 = 'INV_HUB_A2: If hub assets = 0 => shares 0'; + + string constant INV_HUB_B = + 'INV_HUB_B: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset'; + + string constant INV_HUB_C = + 'INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub'; + + string constant INV_HUB_E_1 = + 'INV_HUB_E: total assets is equal to or greater than the supplied amount without taking into account the virtual assets and shares up to the burn interest to virtual shares'; + + string constant INV_HUB_E_2 = + 'INV_HUB_E: total assets is equal to the supplied amount when taking into account the virtual assets and shares'; + + string constant INV_HUB_F = + 'INV_HUB_F: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + (totalDebtRay + deficitRay).fromRayUp + swept'; + + string constant INV_HUB_G = + 'INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT'; // TODO check if tolerance is correct + + string constant INV_HUB_H = + 'INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes'; + + string constant INV_HUB_I = + 'INV_HUB_I: asset.underlying.balanceOf(hub) + asset.swept >= asset.liquidity'; + + string constant INV_HUB_K = + 'INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset'; + + string constant INV_HUB_O = + 'INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay'; + + string constant INV_HUB_P = + 'INV_HUB_P: Premium offset should not exceed premium shares * drawnIndex'; + + string constant INV_HUB_Q = + 'INV_HUB_Q: Drawn index must be monotonically non-decreasing across invariant checks'; + + string constant INV_HUB_R = + 'INV_HUB_R: Supply share price (addedAssets/addedShares) must be monotonically non-decreasing across invariant checks'; /////////////////////////////////////////////////////////////////////////////////////////////// // ERC4626 // @@ -15,7 +68,7 @@ abstract contract HubInvariantsSpec is InvariantsSpec { // Add string constant HSPOST_HUB_ERC4626_ADD_A = - 'HSPOST_HUB_ERC4626_ADD_A: After add, spoke addedAssets must increase by at at most addedAmount'; + 'HSPOST_HUB_ERC4626_ADD_A: After add, spoke addedAssets must increase by at most addedAmount'; string constant HSPOST_HUB_ERC4626_ADD_B = 'HSPOST_HUB_ERC4626_ADD_B: After add, spoke addedShares must increase by addedSharesAmount'; diff --git a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol index bd11d05c2..4712906b1 100644 --- a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol +++ b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {PostconditionsSpec} from '../../protocol-suite/specs/PostconditionsSpec.t.sol'; - -/// @title PostconditionsSpec -/// @notice Postcoditions specification for the protocol -/// @dev Contains pseudo code and description for the postcondition properties in the protocol -abstract contract HubPostconditionsSpec is PostconditionsSpec { +/// @title HubPostconditionsSpec +/// @notice Postconditions specification for the hub +/// @dev Contains pseudo code and description for the postcondition properties in the hub. +/// This is the canonical source for all hub postcondition strings. +/// Protocol-suite imports these via inheritance. +abstract contract HubPostconditionsSpec { /*///////////////////////////////////////////////////////////////////////////////////////////// // PROPERTY TYPES // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -23,4 +23,32 @@ abstract contract HubPostconditionsSpec is PostconditionsSpec { /// - Implemented within each handler function, under the HANDLER-SPECIFIC POSTCONDITIONS section. /////////////////////////////////////////////////////////////////////////////////////////////*/ + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant GPOST_HUB_A = + 'GPOST_HUB_A: Drawn index cannot decrease (remains constant or increases). If no time passes, it stays constant. only increases due to interest accumulation'; + + string constant GPOST_HUB_B = + "GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding)."; + + string constant GPOST_HUB_C = + 'GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block.'; + + string constant GPOST_HUB_D = + 'GPOST_HUB_D: lastUpdateTimestamp must be <= block.timestamp after any action (timestamps cannot be in the future).'; + + string constant GPOST_HUB_E = + 'GPOST_HUB_E: if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)'; + + string constant GPOST_HUB_F = + 'GPOST_HUB_F: if owed for a spoke & assetId increase, owed <= drawCap * precision (when cap != MAX)'; // TODO-ok take into account the deficit, use Owed instead of drawn -> review fix + + string constant GPOST_HUB_G = + 'GPOST_HUB_G: lastUpdateTimestamp is monotonic non-decreasing across actions (time does not go backwards)'; + + string constant HSPOST_HUB_M = + 'HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution)'; } diff --git a/invariants/protocol-suite/Invariants.t.sol b/invariants/protocol-suite/Invariants.t.sol index 7ebef16f3..04c62f5b8 100644 --- a/invariants/protocol-suite/Invariants.t.sol +++ b/invariants/protocol-suite/Invariants.t.sol @@ -8,17 +8,28 @@ import {ITreasurySpoke} from 'src/spoke/interfaces/ITreasurySpoke.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IHub} from 'src/hub/interfaces/IHub.sol'; -// Invariant Contracts -import {HubInvariants} from './invariants/HubInvariants.t.sol'; +// Hub invariant assertions (imported from hub-suite) +import {HubInvariantAssertions} from '../hub-suite/invariants/HubInvariantAssertions.t.sol'; + +// Spoke invariants (protocol-suite) import {SpokeInvariants} from './invariants/SpokeInvariants.t.sol'; /// @title Invariants /// @notice Wrappers for the protocol invariants implemented in each invariants contract /// @dev recognized by Echidna when property mode is activated -/// @dev Inherits HubInvariants, SpokeInvariants -abstract contract Invariants is HubInvariants, SpokeInvariants { +/// @dev Inherits HubInvariantAssertions, SpokeInvariants +abstract contract Invariants is SpokeInvariants, HubInvariantAssertions { using EnumerableSet for EnumerableSet.AddressSet; + /////////////////////////////////////////////////////////////////////////////////////////////// + // VIRTUAL OVERRIDES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Returns allSpokes for the given hub (includes treasury spokes). + function _getSpokesForAsset(IHub, uint256) internal view override returns (address[] memory) { + return allSpokes; + } + /////////////////////////////////////////////////////////////////////////////////////////////// // BASE INVARIANTS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -26,11 +37,12 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { function invariant_INV_HUB() public returns (bool) { // Applied per hub for (uint256 i; i < hubs.length(); i++) { - address hub = hubs.at(i); + IHub hub = IHub(hubs.at(i)); // Applied per asset of the hub - uint256 assetCount = IHub(hub).getAssetCount(); + uint256 assetCount = hub.getAssetCount(); for (uint256 j; j < assetCount; j++) { + // Common hub invariants (from HubInvariantAssertions) assert_INV_HUB_A(hub, j); assert_INV_HUB_B(hub, j); assert_INV_HUB_C(hub, j); @@ -52,7 +64,7 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { function invariant_INV_SP() public returns (bool) { // Applied per spoke for (uint256 i; i < spokes.length(); i++) { - address spoke = spokes.at(i); + ISpoke spoke = ISpoke(spokes.at(i)); // Applied per actor on the spoke for (uint256 j; j < actors.length(); j++) { @@ -60,7 +72,7 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { } // Applied per reserve of the spoke - uint256 reserveCount = ISpoke(spoke).getReserveCount(); + uint256 reserveCount = spoke.getReserveCount(); for (uint256 j; j < reserveCount; j++) { assert_INV_SP_A(spoke, j); assert_INV_SP_C(spoke, j); @@ -71,6 +83,7 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { for (uint256 k; k < actors.length(); k++) { assert_INV_SP_B(spoke, j, actors.at(k)); assert_INV_SP_H(spoke, j, actors.at(k)); + assert_INV_SP_I(spoke, j, actors.at(k)); } } } @@ -78,9 +91,9 @@ abstract contract Invariants is HubInvariants, SpokeInvariants { // Applied per treasury spoke (only hub-sync invariant applies; // user-level invariants don't apply since TreasurySpoke has no per-user positions) for (uint256 i; i < treasurySpokes.length(); i++) { - address spoke = treasurySpokes.at(i); + ISpoke spoke = ISpoke(treasurySpokes.at(i)); // reserveId == assetId for treasury spoke - uint256 reserveCount = IHub(address(ITreasurySpoke(spoke).HUB())).getAssetCount(); + uint256 reserveCount = IHub(address(ITreasurySpoke(address(spoke)).HUB())).getAssetCount(); for (uint256 j; j < reserveCount; j++) { assert_INV_SP_A(spoke, j); } diff --git a/invariants/protocol-suite/README.md b/invariants/protocol-suite/README.md index c98c91aa9..135a35ded 100644 --- a/invariants/protocol-suite/README.md +++ b/invariants/protocol-suite/README.md @@ -1,4 +1,4 @@ -# Enigma Dark – Fuzzing & Invariant Testing Suite +# Fuzzing & Invariant Testing Suite A comprehensive handler-based invariant testing suite for the Aave v4 protocol. This suite performs deep stateful fuzzing across multiple hubs and spokes, validating critical system properties through automated property checking and postcondition verification. @@ -23,6 +23,20 @@ Compatible with industry-standard fuzzing tools: ## Architecture +### Dependencies + +Hub invariant assertions and spec strings are imported from hub-suite — not duplicated. Spoke-specific code is self-contained. + +``` +protocol-suite → hub-suite → shared/ +``` + +| Imported from hub-suite | Local file | +| ------------------------ | -------------------------------- | +| `HubInvariantsSpec` | `specs/InvariantsSpec.t.sol` | +| `HubPostconditionsSpec` | `specs/PostconditionsSpec.t.sol` | +| `HubInvariantAssertions` | `Invariants.t.sol` | + ### Core Components **Setup Layer** (`Setup.t.sol`, `base/`) @@ -31,20 +45,29 @@ Compatible with industry-standard fuzzing tools: - Configures distinct collateral factors, liquidation parameters, and interest rate curves - Initializes multiple actors with protocol permissions +**Spec Layer** (`specs/`) + +- `InvariantsSpec` – inherits `HubInvariantsSpec` from hub-suite, adds spoke-specific strings (INV*SP*\*) +- `PostconditionsSpec` – inherits `HubPostconditionsSpec` from hub-suite, adds spoke-specific strings (GPOST*SP*_, HSPOST*SP*_) + **Handler Layer** (`handlers/`) - `SpokeHandler` – user operations (supply, borrow, repay, withdraw, liquidations) -- `HubHandler` – liquidity management and treasury operations -- `TreasurySpoke` – fee collection and distribution -- `Configurator Handlers` – admin operations (reserve updates, risk parameter changes) -- `Simulator Handlers` – price feeds, donation attacks +- `TreasurySpokeHandler` – fee collection and distribution +- `HubConfiguratorHandler`, `SpokeConfiguratorHandler` – admin operations +- `PriceFeedSimulatorHandler`, `DonationAttackHandler` – simulation handlers + +**Invariant Layer** (`invariants/`) + +- Hub invariant assertions imported from hub-suite via `HubInvariantAssertions` (INV_HUB_A through P) +- `SpokeInvariants` – spoke-specific invariant assertions (INV_SP_A through H) +- Protocol-suite-only stateful invariants Q and R defined inline in `Invariants.t.sol` -**Verification Layer** (`hooks/`, `invariants/`) +**Verification Layer** (`hooks/`) - Before/after hooks with state snapshots - Global and handler-specific postcondition assertions -- Hub invariants (liquidity accounting, share calculations, interest accrual) -- Spoke invariants (position tracking, collateralization, debt limits) +- Hub and spoke postconditions **Replay Layer** (`replays/`) @@ -100,6 +123,7 @@ forge test --mc ReplayTest_1 -vvv ## Key Features - Multi-hub, multi-spoke testing for cross-protocol interactions +- Hub invariant logic imported from hub-suite (single source of truth, no duplication) - Comprehensive postcondition checking after every state transition - Actor-based modeling for realistic multi-user scenarios - Admin operation fuzzing (config updates, parameter changes) diff --git a/invariants/protocol-suite/base/BaseStorage.t.sol b/invariants/protocol-suite/base/BaseStorage.t.sol index 161f8aee2..ce29ac03f 100644 --- a/invariants/protocol-suite/base/BaseStorage.t.sol +++ b/invariants/protocol-suite/base/BaseStorage.t.sol @@ -122,14 +122,6 @@ abstract contract BaseStorage { /// @notice Spoke reserveIds to hub addresses mapping(address => mapping(uint256 => address)) internal reserveIdToHubAddress; - // INVARIANT TRACKING (stateful invariants) - /// @notice Last seen drawn index per hub per assetId (INV_HUB_Q) - mapping(address => mapping(uint256 => uint256)) internal lastSeenDrawnIndex; - /// @notice Last seen added assets per hub per assetId (INV_HUB_R) - mapping(address => mapping(uint256 => uint256)) internal lastSeenAssets; - /// @notice Last seen added shares per hub per assetId (INV_HUB_R) - mapping(address => mapping(uint256 => uint256)) internal lastSeenShares; - // PRICE FEEDS address[] internal priceFeeds; diff --git a/invariants/protocol-suite/invariants/HubInvariants.t.sol b/invariants/protocol-suite/invariants/HubInvariants.t.sol deleted file mode 100644 index 8dcc3ff6e..000000000 --- a/invariants/protocol-suite/invariants/HubInvariants.t.sol +++ /dev/null @@ -1,207 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -// Libraries -import {Premium} from 'src/hub/libraries/Premium.sol'; -import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; -import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; -import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; -import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; - -// Interfaces -import {IHub} from 'src/hub/interfaces/IHub.sol'; -import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; - -// Contracts -import {HandlerAggregator} from '../HandlerAggregator.t.sol'; - -/// @title HubInvariants -/// @notice Implements Hub Invariants for the protocol -/// @dev Inherits HandlerAggregator to check actions in assertion testing mode -abstract contract HubInvariants is HandlerAggregator { - using SafeCast for *; - using WadRayMath for *; - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // - /////////////////////////////////////////////////////////////////////////////////////////////// - - function assert_INV_HUB_A(address hubAddress, uint256 assetId) internal { - uint256 assets = IHub(hubAddress).getAddedAssets(assetId); - - if (assets == 0) { - assertEq(IHub(hubAddress).getAddedShares(assetId), 0, INV_HUB_A2); - } - } - - function assert_INV_HUB_B(address hubAddress, uint256 assetId) internal { - // Sum per-spoke values (use allSpokes to include treasury spokes for consistency with INV_HUB_GH/O) - uint256 spokeCount = allSpokes.length; - uint256 sumDebt; - - for (uint256 i; i < spokeCount; i++) { - sumDebt += IHub(hubAddress).getSpokeTotalOwed(assetId, allSpokes[i]); - } - - uint256 assetTotal = IHub(hubAddress).getAssetTotalOwed(assetId); - assertGe(sumDebt, assetTotal, INV_HUB_B); - } - - function assert_INV_HUB_C(address hubAddress, uint256 assetId) internal { - // Sum per-spoke values (use allSpokes to include treasury spokes for consistency with INV_HUB_GH/O) - uint256 spokeCount = allSpokes.length; - - uint256 sumDrawnShares; - uint256 sumPremDrawnShares; - int256 sumPremOffsetRay; - - for (uint256 i; i < spokeCount; i++) { - address spoke = allSpokes[i]; - sumDrawnShares += IHub(hubAddress).getSpokeDrawnShares(assetId, spoke); - (uint256 premiumDrawnShares, int256 premiumOffsetRay) = IHub(hubAddress).getSpokePremiumData( - assetId, - spoke - ); - sumPremDrawnShares += premiumDrawnShares; - sumPremOffsetRay += premiumOffsetRay; - } - - // Asset totals - AssetVars memory vars = _assetVarsAfter(hubAddress, assetId); - - // Checks - assertEq(sumDrawnShares, vars.asset.drawnShares, INV_HUB_C); - assertEq(sumPremDrawnShares, vars.asset.premiumShares, INV_HUB_C); - assertEq(sumPremOffsetRay, vars.asset.premiumOffsetRay, INV_HUB_C); - } - - function assert_INV_HUB_E(address hubAddress, uint256 assetId) internal { - uint256 totalAssets = IHub(hubAddress).getAddedAssets(assetId); - uint256 totalShares = IHub(hubAddress).getAddedShares(assetId); - - // interest accrued on virtual shares - uint256 burntInterest = totalAssets - - IHub(hubAddress).previewRemoveByShares(assetId, totalShares); - - // Checks: totalAddedAssets ≈ previewRemoveByShares(totalAddedShares) - // Tolerance: the virtual offset (V=1e6) absorbs a fraction of accrued interest when - // converting all shares back to assets. - assertApproxEqAbs( - totalAssets, - IHub(hubAddress).previewRemoveByShares(assetId, totalShares), // round down - burntInterest, - INV_HUB_E_1 - ); - - assertGe( - totalAssets + SharesMath.VIRTUAL_ASSETS, - IHub(hubAddress).previewRemoveByShares(assetId, totalShares + SharesMath.VIRTUAL_ASSETS), - INV_HUB_E_2 - ); - } - - function assert_INV_HUB_F(address hubAddress, uint256 assetId) internal { - uint256 totalAssets = IHub(hubAddress).getAddedAssets(assetId); - uint256 accruedFees = IHub(hubAddress).getAssetAccruedFees(assetId); - - IHub.Asset memory asset = IHub(hubAddress).getAsset(assetId); - uint256 drawnIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); - - uint256 premiumRay = Premium.calculatePremiumRay({ - premiumShares: asset.premiumShares, - premiumOffsetRay: asset.premiumOffsetRay, - drawnIndex: drawnIndex - }); - uint256 drawnRay = asset.drawnShares * drawnIndex; - uint256 aggregatedOwed = (drawnRay + premiumRay + asset.deficitRay).fromRayUp(); - - assertEq(totalAssets + accruedFees, asset.liquidity + aggregatedOwed + asset.swept, INV_HUB_F); - } - - function assert_INV_HUB_GH(address hubAddress, uint256 assetId) internal { - uint256 spokeCount = allSpokes.length; - uint256 tolerancePerActor = IHub(hubAddress).previewAddByShares(assetId, 1); - - // Sum per-spoke values - uint256 totalAddedAssets; - uint256 totalAddedShares; - for (uint256 i; i < spokeCount; i++) { - totalAddedAssets += IHub(hubAddress).getSpokeAddedAssets(assetId, allSpokes[i]); - totalAddedShares += IHub(hubAddress).getSpokeAddedShares(assetId, allSpokes[i]); - } - - totalAddedAssets += _calculateBurntInterest(IHub(hubAddress), assetId); - - // Checks - assertApproxEqAbs( - totalAddedAssets, - IHub(hubAddress).getAddedAssets(assetId), - (spokeCount + 2) * tolerancePerActor, - INV_HUB_G - ); - assertEq(totalAddedShares, IHub(hubAddress).getAddedShares(assetId), INV_HUB_H); - } - - function assert_INV_HUB_I(address hubAddress, uint256 assetId) internal { - // Get underlying from assetId - (address underlying, ) = IHub(hubAddress).getAssetUnderlyingAndDecimals(assetId); - - // Query values - uint256 liquidity = IHub(hubAddress).getAssetLiquidity(assetId); - uint256 swept = IHub(hubAddress).getAssetSwept(assetId); - uint256 underlyingBalance = IERC20(underlying).balanceOf(address(IHub(hubAddress))); - - // Checks - assertGe(underlyingBalance + swept, liquidity, INV_HUB_I); - } - - function assert_INV_HUB_K(address hubAddress, uint256 assetId) internal { - /// @dev TODO for this check to be meaningful, strategy configuration operations have to be integrated - IHub.AssetConfig memory assetConfig = IHub(hubAddress).getAssetConfig(assetId); - - // Checks - assertTrue(assetConfig.irStrategy != address(0), INV_HUB_K); - } - - function assert_INV_HUB_O(address hubAddress, uint256 assetId) internal { - uint256 spokeCount = allSpokes.length; - uint256 totalDeficitRay; - for (uint256 i; i < spokeCount; i++) { - totalDeficitRay += IHub(hubAddress).getSpokeDeficitRay(assetId, allSpokes[i]); - } - assertEq(totalDeficitRay, IHub(hubAddress).getAssetDeficitRay(assetId), INV_HUB_O); - } - - function assert_INV_HUB_P(address hubAddress, uint256 assetId) internal { - (uint256 premiumShares, int256 premiumOffsetRay) = IHub(hubAddress).getAssetPremiumData( - assetId - ); - uint256 drawnIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); - assertGe(int256(premiumShares * drawnIndex), premiumOffsetRay, INV_HUB_P); - } - - function assert_INV_HUB_Q(address hubAddress, uint256 assetId) internal { - uint256 currentIndex = IHub(hubAddress).getAssetDrawnIndex(assetId); - if (lastSeenDrawnIndex[hubAddress][assetId] > 0) { - assertGe(currentIndex, lastSeenDrawnIndex[hubAddress][assetId], INV_HUB_Q); - } - lastSeenDrawnIndex[hubAddress][assetId] = currentIndex; - } - - function assert_INV_HUB_R(address hubAddress, uint256 assetId) internal { - uint256 assets = IHub(hubAddress).getAddedAssets(assetId) + 1e6; - uint256 shares = IHub(hubAddress).getAddedShares(assetId) + 1e6; - if (lastSeenShares[hubAddress][assetId] > 0) { - // assets/shares >= lastAssets/lastShares <=> assets * lastShares >= lastAssets * shares - assertFullMulGe( - assets, - lastSeenShares[hubAddress][assetId], - lastSeenAssets[hubAddress][assetId], - shares, - INV_HUB_R - ); - } - lastSeenAssets[hubAddress][assetId] = assets; - lastSeenShares[hubAddress][assetId] = shares; - } -} diff --git a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol index aa7562c1f..362cbd09a 100644 --- a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol +++ b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.19; import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; // Interfaces -import {ISpokeBase} from 'src/spoke/interfaces/ISpokeBase.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IHub} from 'src/hub/interfaces/IHub.sol'; @@ -21,68 +20,68 @@ abstract contract SpokeInvariants is HandlerAggregator { // SPOKE // /////////////////////////////////////////////////////////////////////////////////////////////// - function assert_INV_SP_A(address spoke, uint256 reserveId) internal { + function assert_INV_SP_A(ISpoke spoke, uint256 reserveId) internal { // Get the assetId related to the reserveId of the spoke - uint256 assetId = _getAssetId(spoke, reserveId); + uint256 assetId = _getAssetId(address(spoke), reserveId); - IHub hub = IHub(_getHubAddress(spoke, reserveId)); + IHub hub = IHub(_getHubAddress(address(spoke), reserveId)); // supply assertEq( - ISpokeBase(spoke).getReserveSuppliedShares(reserveId), - hub.getSpokeAddedShares(assetId, spoke), + spoke.getReserveSuppliedShares(reserveId), + hub.getSpokeAddedShares(assetId, address(spoke)), INV_SP_A ); assertEq( - ISpokeBase(spoke).getReserveSuppliedAssets(reserveId), - hub.getSpokeAddedAssets(assetId, spoke), + spoke.getReserveSuppliedAssets(reserveId), + hub.getSpokeAddedAssets(assetId, address(spoke)), INV_SP_A ); // debt - (uint256 d1, uint256 p1) = hub.getSpokeOwed(assetId, spoke); - (uint256 d2, uint256 p2) = ISpokeBase(spoke).getReserveDebt(reserveId); + (uint256 d1, uint256 p1) = hub.getSpokeOwed(assetId, address(spoke)); + (uint256 d2, uint256 p2) = spoke.getReserveDebt(reserveId); assertEq(d2, d1, INV_SP_A); assertEq(p2, p1, INV_SP_A); } - function assert_INV_SP_B(address spoke, uint256 reserveId, address user) internal { + function assert_INV_SP_B(ISpoke spoke, uint256 reserveId, address user) internal { // reserve supply - if (ISpokeBase(spoke).getReserveSuppliedAssets(reserveId) > 0) { - assertGt(ISpokeBase(spoke).getReserveSuppliedShares(reserveId), 0, INV_SP_B); + if (spoke.getReserveSuppliedAssets(reserveId) > 0) { + assertGt(spoke.getReserveSuppliedShares(reserveId), 0, INV_SP_B); } // reserve debt - if (ISpokeBase(spoke).getReserveTotalDebt(reserveId) > 0) { + if (spoke.getReserveTotalDebt(reserveId) > 0) { assertGt( - IHub(_getHubAddress(spoke, reserveId)).getSpokeDrawnShares( - _getAssetId(spoke, reserveId), - spoke + IHub(_getHubAddress(address(spoke), reserveId)).getSpokeDrawnShares( + _getAssetId(address(spoke), reserveId), + address(spoke) ), 0, INV_SP_B ); } // user supply - if (ISpokeBase(spoke).getUserSuppliedAssets(reserveId, user) > 0) { - assertGt(ISpokeBase(spoke).getUserSuppliedShares(reserveId, user), 0, INV_SP_B); + if (spoke.getUserSuppliedAssets(reserveId, user) > 0) { + assertGt(spoke.getUserSuppliedShares(reserveId, user), 0, INV_SP_B); } // user debt - if (ISpokeBase(spoke).getUserTotalDebt(reserveId, user) > 0) { - ISpoke.UserPosition memory up = ISpoke(spoke).getUserPosition(reserveId, user); + if (spoke.getUserTotalDebt(reserveId, user) > 0) { + ISpoke.UserPosition memory up = spoke.getUserPosition(reserveId, user); assertTrue(up.drawnShares > 0 || up.premiumShares > 0, INV_SP_B); } } - function assert_INV_SP_C(address spoke, uint256 reserveId) internal { + function assert_INV_SP_C(ISpoke spoke, uint256 reserveId) internal { uint256 sumSpokeDebts; for (uint256 i; i < actors.length(); i++) { - sumSpokeDebts += ISpokeBase(spoke).getUserTotalDebt(reserveId, actors.at(i)); + sumSpokeDebts += spoke.getUserTotalDebt(reserveId, actors.at(i)); } - assertGe(sumSpokeDebts, ISpokeBase(spoke).getReserveTotalDebt(reserveId), INV_SP_C); + assertGe(sumSpokeDebts, spoke.getReserveTotalDebt(reserveId), INV_SP_C); } - function assert_INV_SP_D(address spoke, address user) internal { - ISpoke.UserAccountData memory d = ISpoke(spoke).getUserAccountData(user); + function assert_INV_SP_D(ISpoke spoke, address user) internal { + ISpoke.UserAccountData memory d = spoke.getUserAccountData(user); if (d.totalDebtValueRay == 0) { assertEq(d.healthFactor, type(uint256).max, INV_SP_D); } else if (d.totalCollateralValue == 0) { @@ -90,29 +89,37 @@ abstract contract SpokeInvariants is HandlerAggregator { } } - function assert_INV_SP_E(address spoke, uint256 reserveId) internal { + function assert_INV_SP_E(ISpoke spoke, uint256 reserveId) internal { uint256 sumUserShares; for (uint256 i; i < actors.length(); i++) { - sumUserShares += ISpokeBase(spoke).getUserSuppliedShares(reserveId, actors.at(i)); + sumUserShares += spoke.getUserSuppliedShares(reserveId, actors.at(i)); } - assertEq(sumUserShares, ISpokeBase(spoke).getReserveSuppliedShares(reserveId), INV_SP_E); + assertEq(sumUserShares, spoke.getReserveSuppliedShares(reserveId), INV_SP_E); } - function assert_INV_SP_F(address spoke, uint256 reserveId) internal { + function assert_INV_SP_F(ISpoke spoke, uint256 reserveId) internal { uint256 sumUserAssets; for (uint256 i; i < actors.length(); i++) { - sumUserAssets += ISpokeBase(spoke).getUserSuppliedAssets(reserveId, actors.at(i)); + sumUserAssets += spoke.getUserSuppliedAssets(reserveId, actors.at(i)); } - uint256 reserveSuppliedAssets = ISpokeBase(spoke).getReserveSuppliedAssets(reserveId); + uint256 reserveSuppliedAssets = spoke.getReserveSuppliedAssets(reserveId); assertLe(sumUserAssets, reserveSuppliedAssets, INV_SP_F); assertApproxEqAbs(sumUserAssets, reserveSuppliedAssets, NUMBER_OF_ACTORS, INV_SP_F); } - function assert_INV_SP_H(address spoke, uint256 reserveId, address user) internal { - uint32 userKey = ISpoke(spoke).getUserPosition(reserveId, user).dynamicConfigKey; - uint32 reserveKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; + function assert_INV_SP_H(ISpoke spoke, uint256 reserveId, address user) internal { + uint32 userKey = spoke.getUserPosition(reserveId, user).dynamicConfigKey; + uint32 reserveKey = spoke.getReserve(reserveId).dynamicConfigKey; if (userKey > 0) { assertLe(uint256(userKey), uint256(reserveKey), INV_SP_H); } } + + function assert_INV_SP_I(ISpoke spoke, uint256 reserveId, address user) internal { + ISpoke.UserPosition memory up = spoke.getUserPosition(reserveId, user); + if (up.drawnShares == 0) { + assertEq(up.premiumShares, 0, INV_SP_I); + assertEq(up.premiumOffsetRay, 0, INV_SP_I); + } + } } diff --git a/invariants/protocol-suite/specs/InvariantsSpec.t.sol b/invariants/protocol-suite/specs/InvariantsSpec.t.sol index 26e6304f4..1737c9b98 100644 --- a/invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -1,66 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +import {HubInvariantsSpec} from '../../hub-suite/specs/HubInvariantsSpec.t.sol'; + /// @title InvariantsSpec /// @notice Invariants specification for the protocol -/// @dev Contains pseudo code and description for the invariant properties in the protocol -abstract contract InvariantsSpec { - // TODO: check invariant overlap - /*///////////////////////////////////////////////////////////////////////////////////////////// - // PROPERTY TYPES // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// - INVARIANTS (INV): - /// - Properties that should always hold true in the system. - /// - Implemented in the /invariants folder. - - /////////////////////////////////////////////////////////////////////////////////////////////*/ - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // - /////////////////////////////////////////////////////////////////////////////////////////////// - - string constant INV_HUB_A2 = 'INV_HUB_A2: If hub assets = 0 => shares 0'; - - string constant INV_HUB_B = - 'INV_HUB_B: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset'; - - string constant INV_HUB_C = - 'INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub'; - - string constant INV_HUB_E_1 = - 'INV_HUB_E: total assets is equal to or greater than the supplied amount without taking into account the virtual assets and shares up to the burn interest to virtual shares'; - - string constant INV_HUB_E_2 = - 'INV_HUB_E: total assets is equal to the supplied amount when taking into account the virtual assets and shares'; - - string constant INV_HUB_F = - 'INV_HUB_F: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + (totalDebtRay + deficitRay).fromRayUp + swept'; - - string constant INV_HUB_G = - 'INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT'; // TODO check if tolerance is correct - - string constant INV_HUB_H = - 'INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes'; - - string constant INV_HUB_I = - 'INV_HUB_I: asset.underlying.balanceOf(hub) + asset.swept >= asset.liquidity'; - - string constant INV_HUB_K = - 'INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset'; - - string constant INV_HUB_O = - 'INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay'; - - string constant INV_HUB_P = - 'INV_HUB_P: Premium offset should not exceed premium shares * drawnIndex'; - - string constant INV_HUB_Q = - 'INV_HUB_Q: Drawn index must be monotonically non-decreasing across invariant checks'; - - string constant INV_HUB_R = - 'INV_HUB_R: Supply share price (addedAssets/addedShares) must be monotonically non-decreasing across invariant checks'; - +/// @dev Contains spoke invariant strings. Hub invariant strings are inherited from HubInvariantsSpec. +abstract contract InvariantsSpec is HubInvariantsSpec { /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -84,4 +30,7 @@ abstract contract InvariantsSpec { string constant INV_SP_H = 'INV_SP_H: User dynamicConfigKey must never exceed the reserve dynamicConfigKey (no future config reference)'; + + string constant INV_SP_I = + 'INV_SP_I: User cannot have premium shares/offset without drawn shares (premium debt is always repaid first, and can only be created when drawn shares exist)'; } diff --git a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol index 1b58f957e..e94d56e6a 100644 --- a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -1,55 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -/// @title PostconditionsSpec -/// @notice Postcoditions specification for the protocol -/// @dev Contains pseudo code and description for the postcondition properties in the protocol -abstract contract PostconditionsSpec { - /*///////////////////////////////////////////////////////////////////////////////////////////// - // PROPERTY TYPES // - /////////////////////////////////////////////////////////////////////////////////////////////// - - /// - POSTCONDITIONS: - /// - Properties that should hold true after an action is executed. - /// - Implemented in the /hooks and /handlers folders. - /// - There are two types of POSTCONDITIONS: - /// - GLOBAL POSTCONDITIONS (GPOST): - /// - Properties that should always hold true after any action is executed. - /// - Checked in the `_checkPostConditions` function within the HookAggregator contract. - /// - HANDLER-SPECIFIC POSTCONDITIONS (HSPOST): - /// - Properties that should hold true after a specific action is executed in a specific context. - /// - Implemented within each handler function, under the HANDLER-SPECIFIC POSTCONDITIONS section. - - /////////////////////////////////////////////////////////////////////////////////////////////*/ - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // - ///////////////////////////////////////////////////////////////////////////////////////////////// - - string constant GPOST_HUB_A = - 'GPOST_HUB_A: Drawn index cannot decrease (remains constant or increases). If no time passes, it stays constant. only increases due to interest accumulation'; - - string constant GPOST_HUB_B = - "GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding)."; - - string constant GPOST_HUB_C = - 'GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block.'; - - string constant GPOST_HUB_D = - 'GPOST_HUB_D: lastUpdateTimestamp must be <= block.timestamp after any action (timestamps cannot be in the future).'; - - string constant GPOST_HUB_E = - 'GPOST_HUB_E: if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)'; - - string constant GPOST_HUB_F = - 'GPOST_HUB_F: if owed for a spoke & assetId increase, owed <= drawCap * precision (when cap != MAX)'; // TODO-ok take into account the deficit, use Owed instead of drawn -> review fix - - string constant GPOST_HUB_G = - 'GPOST_HUB_G: lastUpdateTimestamp is monotonic non-decreasing across actions (time does not go backwards)'; - - string constant HSPOST_HUB_M = - 'HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution)'; +import {HubPostconditionsSpec} from '../../hub-suite/specs/HubPostconditionsSpec.t.sol'; +/// @title PostconditionsSpec +/// @notice Postconditions specification for the protocol +/// @dev Contains spoke postcondition strings. Hub postcondition strings are inherited from HubPostconditionsSpec. +abstract contract PostconditionsSpec is HubPostconditionsSpec { /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE // /////////////////////////////////////////////////////////////////////////////////////////////// From 5586fba8f3796606ff44d8726a639f93d701baee Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:13:35 +0530 Subject: [PATCH 064/106] chore: update docs --- invariants/hub-suite/README.md | 10 +++++----- invariants/protocol-suite/README.md | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/invariants/hub-suite/README.md b/invariants/hub-suite/README.md index ebbed6b37..d49e0b374 100644 --- a/invariants/hub-suite/README.md +++ b/invariants/hub-suite/README.md @@ -10,7 +10,7 @@ The suite tests a simplified hub-centric deployment with: - **1 Hub** with a single interest rate strategy - **Multiple Actors** simulating spoke behavior (registered as spokes in the hub) -- **2 Base Assets** (USDC, WETH) to keep the asset surface simple and performant +- **3 Base Assets** (USDC, WETH, WBTC) with varying decimals (6, 18, 8) - **Direct Hub Interactions** through handlers that expose hub functions - **Hub Configuration Management** through the HubConfigurator handler @@ -23,13 +23,13 @@ All protocol actions are monitored by hooks that snapshot state and verify postc **Setup Layer** (`Setup.t.sol`, `base/`) - Deploys a single Hub with a deterministic interest rate strategy -- Configures 2 base assets (USDC, WETH) for simplicity and performance +- Configures 3 base assets (USDC, WETH, WBTC) with varying decimals - Initializes multiple actors with spoke permissions registered on the hub **Spec Layer** (`specs/`) -- `HubInvariantsSpec` – canonical hub invariant string constants (INV*HUB*\_, ERC4626\_\_, AVAILABILITY\_\*) -- `HubPostconditionsSpec` – canonical hub postcondition string constants (GPOST*HUB*_, HSPOST*HUB*_) +- `HubInvariantsSpec` – canonical hub invariant string constants (`INV_HUB_*`, `ERC4626_*`, `AVAILABILITY_*`) +- `HubPostconditionsSpec` – canonical hub postcondition string constants (`GPOST_HUB_*`, `HSPOST_HUB_*`) - Protocol-suite inherits these specs; they are defined here only **Handler Layer** (`handlers/`) @@ -40,7 +40,7 @@ All protocol actions are monitored by hooks that snapshot state and verify postc **Invariant Layer** (`invariants/`) -- `HubInvariantAssertions` – abstract parameterized hub invariant assertion logic (INV_HUB_A through P). Importable by other suites +- `HubInvariantAssertions` – abstract parameterized hub invariant assertion logic (INV_HUB_A through R). Importable by other suites - `HubInvariants` – concrete invariants extending `HubInvariantAssertions`, adds ERC4626 and AVAILABILITY assertions specific to the hub-suite **Verification Layer** (`hooks/`) diff --git a/invariants/protocol-suite/README.md b/invariants/protocol-suite/README.md index 135a35ded..57fa948b9 100644 --- a/invariants/protocol-suite/README.md +++ b/invariants/protocol-suite/README.md @@ -41,14 +41,14 @@ protocol-suite → hub-suite → shared/ **Setup Layer** (`Setup.t.sol`, `base/`) -- Deploys 2-hub, 2-spoke architecture with deterministic addresses (CREATE3) +- Deploys 2-hub, 2-spoke architecture with 2 treasury spokes - Configures distinct collateral factors, liquidation parameters, and interest rate curves - Initializes multiple actors with protocol permissions **Spec Layer** (`specs/`) -- `InvariantsSpec` – inherits `HubInvariantsSpec` from hub-suite, adds spoke-specific strings (INV*SP*\*) -- `PostconditionsSpec` – inherits `HubPostconditionsSpec` from hub-suite, adds spoke-specific strings (GPOST*SP*_, HSPOST*SP*_) +- `InvariantsSpec` – inherits `HubInvariantsSpec` from hub-suite, adds spoke-specific strings (`INV_SP_*`) +- `PostconditionsSpec` – inherits `HubPostconditionsSpec` from hub-suite, adds spoke-specific strings (`GPOST_SP_*`, `HSPOST_SP_*`) **Handler Layer** (`handlers/`) @@ -59,9 +59,9 @@ protocol-suite → hub-suite → shared/ **Invariant Layer** (`invariants/`) -- Hub invariant assertions imported from hub-suite via `HubInvariantAssertions` (INV_HUB_A through P) -- `SpokeInvariants` – spoke-specific invariant assertions (INV_SP_A through H) -- Protocol-suite-only stateful invariants Q and R defined inline in `Invariants.t.sol` +- Hub invariant assertions imported from hub-suite via `HubInvariantAssertions` (INV_HUB_A through R) +- `SpokeInvariants` – spoke-specific invariant assertions (INV_SP_A through I) +- Stateful hub invariants Q and R imported from hub-suite's `HubInvariantAssertions` **Verification Layer** (`hooks/`) From 8720895f218d1059688ddebad3ebe57ecaf254b4 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:10:14 +0530 Subject: [PATCH 065/106] feat: fix 4626 rt handlers --- .github/workflows/invariants.yml | 7 +- .github/workflows/invariants_hub.yml | 7 +- invariants/hub-suite/Invariants.t.sol | 1 - .../hub-suite/handlers/HubHandler.t.sol | 73 +++++++++++++------ .../hub-suite/specs/HubInvariantsSpec.t.sol | 40 +++++++--- invariants/protocol-suite/Invariants.t.sol | 1 - 6 files changed, 90 insertions(+), 39 deletions(-) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index 7fa902dcd..ff7e9c6d3 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -9,9 +9,10 @@ on: env: FOUNDRY_PROFILE: invariant -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true +# ! todo uncomment before merging +# concurrency: +# group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} +# cancel-in-progress: true jobs: echidna: diff --git a/.github/workflows/invariants_hub.yml b/.github/workflows/invariants_hub.yml index 13833a94b..5b6b669ac 100644 --- a/.github/workflows/invariants_hub.yml +++ b/.github/workflows/invariants_hub.yml @@ -9,9 +9,10 @@ on: env: FOUNDRY_PROFILE: invariant -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true +# ! todo uncomment before merging +# concurrency: +# group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} +# cancel-in-progress: true jobs: echidna: diff --git a/invariants/hub-suite/Invariants.t.sol b/invariants/hub-suite/Invariants.t.sol index 7b1c46203..4361d8dc7 100644 --- a/invariants/hub-suite/Invariants.t.sol +++ b/invariants/hub-suite/Invariants.t.sol @@ -10,7 +10,6 @@ abstract contract Invariants is HubInvariants { uint256 assetCount = hub.getAssetCount(); for (uint256 i; i < assetCount; i++) { - // HUB (parameterized assertions from HubInvariantAssertions) assert_INV_HUB_A(hub, i); assert_INV_HUB_B(hub, i); assert_INV_HUB_C(hub, i); diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index 8c0c0eb54..d3fbca4d6 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -363,44 +363,73 @@ contract HubHandler is BaseHandler, IHubHandler { } /////////////////////////////////////////////////////////////////////////////////////////////// - // ROUNDTRIP // + // ERC4626 ROUNDTRIP (STATELESS) // /////////////////////////////////////////////////////////////////////////////////////////////// + /// @dev Stateless roundtrip checks — pure preview calls, no state changes. + + /// @dev A: previewRemoveByShares(previewAddByAssets(a)) <= a function roundtrip_ERC4626_RT_A(uint256 amount, uint8 i) external { uint256 assetId = _getRandomBaseAssetId(i); - - uint256 previewSharesToAdd = hub.previewAddByAssets(assetId, amount); - uint256 previewAssetsToRemove = hub.previewRemoveByShares(assetId, previewSharesToAdd); - - assertLe(previewAssetsToRemove, amount, HSPOST_HUB_ERC4626_RT_A); + uint256 shares = hub.previewAddByAssets(assetId, amount); + uint256 assets = hub.previewRemoveByShares(assetId, shares); + assertLe(assets, amount, INV_HUB_ERC4626_RT_A); } + /// @dev B: previewRemoveByAssets(a) >= previewAddByAssets(a) function roundtrip_ERC4626_RT_B(uint256 amount, uint8 i) external { - uint256 sharesAdded = add(amount, i); - uint256 previewAssetsToRemove = hub.previewRemoveByShares( - _getRandomBaseAssetId(i), - sharesAdded - ); - uint256 sharesRemoved = remove(previewAssetsToRemove, i); - - assertGe(sharesRemoved, sharesAdded, HSPOST_HUB_ERC4626_RT_B); + uint256 assetId = _getRandomBaseAssetId(i); + uint256 sDeposit = hub.previewAddByAssets(assetId, amount); + uint256 sWithdraw = hub.previewRemoveByAssets(assetId, amount); + assertGe(sWithdraw, sDeposit, INV_HUB_ERC4626_RT_B); } + /// @dev C: previewAddByAssets(previewRemoveByShares(s)) <= s function roundtrip_ERC4626_RT_C(uint256 shares, uint8 i) external { uint256 assetId = _getRandomBaseAssetId(i); + uint256 assets = hub.previewRemoveByShares(assetId, shares); + uint256 resultShares = hub.previewAddByAssets(assetId, assets); + assertLe(resultShares, shares, INV_HUB_ERC4626_RT_C); + } + + /// @dev D: previewAddByShares(s) >= previewRemoveByShares(s) + function roundtrip_ERC4626_RT_D(uint256 shares, uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + uint256 aRedeem = hub.previewRemoveByShares(assetId, shares); + uint256 aMint = hub.previewAddByShares(assetId, shares); + assertGe(aMint, aRedeem, INV_HUB_ERC4626_RT_D); + } - uint256 previewAssetsToRemove = hub.previewRemoveByShares(assetId, shares); - uint256 previewShares = hub.previewAddByAssets(assetId, previewAssetsToRemove); + /// @dev E: previewRemoveByAssets(previewAddByShares(s)) >= s + function roundtrip_ERC4626_RT_E(uint256 shares, uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + uint256 assets = hub.previewAddByShares(assetId, shares); + uint256 resultShares = hub.previewRemoveByAssets(assetId, assets); + assertGe(resultShares, shares, INV_HUB_ERC4626_RT_E); + } - assertLe(previewShares, shares, HSPOST_HUB_ERC4626_RT_C); + /// @dev F: previewRemoveByShares(s) <= previewAddByShares(s) + function roundtrip_ERC4626_RT_F(uint256 shares, uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + uint256 aMint = hub.previewAddByShares(assetId, shares); + uint256 aRedeem = hub.previewRemoveByShares(assetId, shares); + assertLe(aRedeem, aMint, INV_HUB_ERC4626_RT_F); } - function roundtrip_ERC4626_RT_D(uint256 amount, uint8 i) external { - uint256 sharesRemoved = remove(amount, i); - uint256 previewAssetsToAdd = hub.previewAddByShares(_getRandomBaseAssetId(i), sharesRemoved); - uint256 sharesAdded = add(previewAssetsToAdd, i); + /// @dev G: previewAddByShares(previewRemoveByAssets(a)) >= a + function roundtrip_ERC4626_RT_G(uint256 amount, uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + uint256 shares = hub.previewRemoveByAssets(assetId, amount); + uint256 assets = hub.previewAddByShares(assetId, shares); + assertGe(assets, amount, INV_HUB_ERC4626_RT_G); + } - assertLe(sharesAdded, sharesRemoved, HSPOST_HUB_ERC4626_RT_D); + /// @dev H: previewAddByAssets(a) <= previewRemoveByAssets(a) + function roundtrip_ERC4626_RT_H(uint256 amount, uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + uint256 sWithdraw = hub.previewRemoveByAssets(assetId, amount); + uint256 sDeposit = hub.previewAddByAssets(assetId, amount); + assertLe(sDeposit, sWithdraw, INV_HUB_ERC4626_RT_H); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol index 197ecd38b..b29a659ee 100644 --- a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -104,7 +104,7 @@ abstract contract HubInvariantsSpec { 'HSPOST_HUB_ERC4626_RESTORE_B: After restore, spoke drawnShares must decrease by restoredSharesAmount'; string constant HSPOST_HUB_ERC4626_RESTORE_C = - 'HSPOST_HUB_SP_RESTORE_C: previewRestoredShares must be less than or equal to the restored shares after the action'; + 'HSPOST_HUB_ERC4626_RESTORE_C: previewRestoredShares must be less than or equal to the restored shares after the action'; // GENERIC @@ -124,17 +124,39 @@ abstract contract HubInvariantsSpec { // ERC4626 ROUNDTRIP // /////////////////////////////////////////////////////////////////////////////////////////////// - string constant HSPOST_HUB_ERC4626_RT_A = - 'HSPOST_HUB_SP_ERC4626_RT_A: previewRemoveByShares(previewAddByAssets(amount)) <= shares added'; + /// @notice ROUNDTRIP - string constant HSPOST_HUB_ERC4626_RT_B = - 'HSPOST_HUB_SP_ERC4626_RT_B: remove(previewRemoveByShares(add(amount))) >= add(amount)'; + /// @dev ERC4626: redeem(deposit(a)) <= a + string constant INV_HUB_ERC4626_RT_A = + 'INV_HUB_ERC4626_RT_A: previewRemoveByShares(previewAddByAssets(a)) <= a'; - string constant HSPOST_HUB_ERC4626_RT_C = - 'HSPOST_HUB_SP_ERC4626_RT_C: previewAddByAssets(previewRemoveByShares(shares)) >= shares'; + /// @dev ERC4626: s = deposit(a), s' = withdraw(a), s' >= s + string constant INV_HUB_ERC4626_RT_B = + 'INV_HUB_ERC4626_RT_B: previewRemoveByAssets(a) >= previewAddByAssets(a)'; - string constant HSPOST_HUB_ERC4626_RT_D = - 'HSPOST_HUB_SP_ERC4626_RT_D: add(previewAddByShares(remove(amount))) <= shares removed'; + /// @dev ERC4626: deposit(redeem(s)) <= s + string constant INV_HUB_ERC4626_RT_C = + 'INV_HUB_ERC4626_RT_C: previewAddByAssets(previewRemoveByShares(s)) <= s'; + + /// @dev ERC4626: a = redeem(s), a' = mint(s), a' >= a + string constant INV_HUB_ERC4626_RT_D = + 'INV_HUB_ERC4626_RT_D: previewAddByShares(s) >= previewRemoveByShares(s)'; + + /// @dev ERC4626: withdraw(mint(s)) >= s + string constant INV_HUB_ERC4626_RT_E = + 'INV_HUB_ERC4626_RT_E: previewRemoveByAssets(previewAddByShares(s)) >= s'; + + /// @dev ERC4626: a = mint(s), a' = redeem(s), a' <= a + string constant INV_HUB_ERC4626_RT_F = + 'INV_HUB_ERC4626_RT_F: previewRemoveByShares(s) <= previewAddByShares(s)'; + + /// @dev ERC4626: mint(withdraw(a)) >= a + string constant INV_HUB_ERC4626_RT_G = + 'INV_HUB_ERC4626_RT_G: previewAddByShares(previewRemoveByAssets(a)) >= a'; + + /// @dev ERC4626: s = withdraw(a), s' = deposit(a), s' <= s + string constant INV_HUB_ERC4626_RT_H = + 'INV_HUB_ERC4626_RT_H: previewAddByAssets(a) <= previewRemoveByAssets(a)'; /////////////////////////////////////////////////////////////////////////////////////////////// // HUB: AVAILABILITY // diff --git a/invariants/protocol-suite/Invariants.t.sol b/invariants/protocol-suite/Invariants.t.sol index 04c62f5b8..8b086ec2e 100644 --- a/invariants/protocol-suite/Invariants.t.sol +++ b/invariants/protocol-suite/Invariants.t.sol @@ -42,7 +42,6 @@ abstract contract Invariants is SpokeInvariants, HubInvariantAssertions { // Applied per asset of the hub uint256 assetCount = hub.getAssetCount(); for (uint256 j; j < assetCount; j++) { - // Common hub invariants (from HubInvariantAssertions) assert_INV_HUB_A(hub, j); assert_INV_HUB_B(hub, j); assert_INV_HUB_C(hub, j); From dc112ba603b18bf51ff19a509f005f8334181704 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:10:51 +0530 Subject: [PATCH 066/106] fix: liq fee handler --- .../hub-suite/handlers/HubConfiguratorHandler.t.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol index 3378e7ff3..f80ffb78d 100644 --- a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol +++ b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +// Libraries +import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; + // Interfaces import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; import {IHubConfiguratorHandler} from './interfaces/IHubConfiguratorHandler.sol'; @@ -55,6 +58,12 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { hubConfigurator.updateSpokeHalted(address(hub), assetId, spoke, halted); } + function updateLiquidityFee(uint256 liquidityFee, uint8 i) external setup { + uint256 assetId = _getRandomBaseAssetId(i); + liquidityFee = liquidityFee % PercentageMath.PERCENTAGE_FACTOR; + hubConfigurator.updateLiquidityFee(address(hub), assetId, liquidityFee); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// From ec164d68d6fc16ec6338a6f9ef0029ad6fd8c6da Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:48:58 +0530 Subject: [PATCH 067/106] chore: fix minting --- invariants/hub-suite/Setup.t.sol | 11 +++-------- invariants/hub-suite/handlers/HubHandler.t.sol | 15 +++++++-------- .../handlers/spoke/SpokeHandler.t.sol | 13 +++++++++++++ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/invariants/hub-suite/Setup.t.sol b/invariants/hub-suite/Setup.t.sol index a1745aab8..95ef51940 100644 --- a/invariants/hub-suite/Setup.t.sol +++ b/invariants/hub-suite/Setup.t.sol @@ -116,7 +116,7 @@ contract Setup is BaseTest { liquidityFee: 5_00, feeReceiver: address(this), irStrategy: address(irStrategy), - reinvestmentController: address(0) + reinvestmentController: address(this) }), new bytes(0) ); @@ -138,7 +138,7 @@ contract Setup is BaseTest { liquidityFee: 10_00, feeReceiver: address(this), irStrategy: address(irStrategy), - reinvestmentController: address(0) + reinvestmentController: address(this) }), new bytes(0) ); @@ -160,7 +160,7 @@ contract Setup is BaseTest { liquidityFee: 5_00, feeReceiver: address(this), irStrategy: address(irStrategy), - reinvestmentController: address(0) + reinvestmentController: address(this) }), new bytes(0) ); @@ -269,11 +269,6 @@ contract Setup is BaseTest { }) ); - // Set reinvestment controller - hubConfigurator.updateReinvestmentController(address(hub), usdcAssetId, address(this)); - hubConfigurator.updateReinvestmentController(address(hub), wethAssetId, address(this)); - hubConfigurator.updateReinvestmentController(address(hub), wbtcAssetId, address(this)); - usdc.approve(address(hub), type(uint256).max); weth.approve(address(hub), type(uint256).max); wbtc.approve(address(hub), type(uint256).max); diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index d3fbca4d6..11e80615d 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.19; // Interfaces -import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; import {IHub, IHubBase} from 'src/hub/interfaces/IHub.sol'; import {IHubHandler} from './interfaces/IHubHandler.sol'; @@ -31,9 +30,9 @@ contract HubHandler is BaseHandler, IHubHandler { uint256 assetsBefore = hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)); uint256 sharesBefore = hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)); + _mint(underlying, address(hub), amount); + _before(); - vm.prank(address(actor)); - IERC20(underlying).transfer(address(hub), amount); (success, returnData) = actor.proxy( address(hub), abi.encodeCall(IHubBase.add, (targetAssetId, amount)) @@ -156,12 +155,9 @@ contract HubHandler is BaseHandler, IHubHandler { targetAssetId ); + _mint(assetIdToUnderlying[cachedTargetAssetId], address(hub), drawnAmount + premiumAmount); + _before(); - vm.prank(address(actor)); - IERC20(assetIdToUnderlying[cachedTargetAssetId]).transfer( - address(hub), - drawnAmount + premiumAmount - ); (success, returnData) = actor.proxy( address(hub), abi.encodeCall(IHubBase.restore, (cachedTargetAssetId, drawnAmount, premiumDelta)) @@ -353,6 +349,9 @@ contract HubHandler is BaseHandler, IHubHandler { function reclaim(uint256 amount, uint8 i) external { targetAssetId = _getRandomBaseAssetId(i); + address underlying = assetIdToUnderlying[targetAssetId]; + + _mint(underlying, address(hub), amount); _before(); try hub.reclaim(targetAssetId, amount) { diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index d64b1c9d1..ff0bbd51f 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -62,6 +62,9 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { // Register user to check postconditions _registerUserToCheck(spoke, reserveId, onBehalfOf); + address underlying = ISpoke(spoke).getReserve(reserveId).underlying; + _mintAndApprove(underlying, address(actor), spoke, amount); + _before(); (success, returnData) = actor.proxy( spoke, @@ -167,6 +170,9 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { // Register user to check postconditions _registerUserToCheck(spoke, reserveId, onBehalfOf); + address underlying = ISpoke(spoke).getReserve(reserveId).underlying; + _mintAndApprove(underlying, address(actor), spoke, amount); + _before(); (success, returnData) = actor.proxy( spoke, @@ -255,6 +261,13 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { liquidationVars.liquidator ); + _mintAndApprove( + ISpoke(liquidationVars.spoke).getReserve(liquidationVars.debtReserveId).underlying, + address(actor), + liquidationVars.spoke, + debtToCover + ); + _before(); (success, returnData) = actor.proxy( liquidationVars.spoke, From 17d3dc9b79018f660d958d6797ed10710b75de4c Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:55:28 +0530 Subject: [PATCH 068/106] fix: unnecessary cached target asset id --- invariants/hub-suite/base/BaseStorage.t.sol | 2 - .../hub-suite/handlers/HubHandler.t.sol | 115 +++++++++--------- .../hub-suite/hooks/HookAggregator.t.sol | 7 +- 3 files changed, 60 insertions(+), 64 deletions(-) diff --git a/invariants/hub-suite/base/BaseStorage.t.sol b/invariants/hub-suite/base/BaseStorage.t.sol index 604076cf0..6e21605a5 100644 --- a/invariants/hub-suite/base/BaseStorage.t.sol +++ b/invariants/hub-suite/base/BaseStorage.t.sol @@ -58,8 +58,6 @@ abstract contract BaseStorage { /// @notice The WBTC token TestnetERC20 internal wbtc; - uint256 internal targetAssetId; - /////////////////////////////////////////////////////////////////////////////////////////////// // HUB STORAGE // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index 11e80615d..cf031ed25 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -22,20 +22,20 @@ contract HubHandler is BaseHandler, IHubHandler { function add(uint256 amount, uint8 i) public setup returns (uint256 addedShares) { bool success; bytes memory returnData; - uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); - address underlying = assetIdToUnderlying[cachedTargetAssetId]; + uint256 assetId = _getRandomBaseAssetId(i); + address underlying = assetIdToUnderlying[assetId]; - uint256 previewAddedShares = hub.previewAddByAssets(cachedTargetAssetId, amount); + uint256 previewAddedShares = hub.previewAddByAssets(assetId, amount); - uint256 assetsBefore = hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)); - uint256 sharesBefore = hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)); + uint256 assetsBefore = hub.getSpokeAddedAssets(assetId, address(actor)); + uint256 sharesBefore = hub.getSpokeAddedShares(assetId, address(actor)); _mint(underlying, address(hub), amount); _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHubBase.add, (targetAssetId, amount)) + abi.encodeCall(IHubBase.add, (assetId, amount)) ); if (success) { @@ -45,12 +45,12 @@ contract HubHandler is BaseHandler, IHubHandler { assertGe( assetsBefore + amount, - hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)), + hub.getSpokeAddedAssets(assetId, address(actor)), HSPOST_HUB_ERC4626_ADD_A ); assertEq( sharesBefore + addedShares, - hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)), + hub.getSpokeAddedShares(assetId, address(actor)), HSPOST_HUB_ERC4626_ADD_B ); @@ -63,17 +63,17 @@ contract HubHandler is BaseHandler, IHubHandler { function remove(uint256 amount, uint8 i) public setup returns (uint256 removedShares) { bool success; bytes memory returnData; - uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); + uint256 assetId = _getRandomBaseAssetId(i); - uint256 previewRemovedShares = hub.previewRemoveByAssets(cachedTargetAssetId, amount); + uint256 previewRemovedShares = hub.previewRemoveByAssets(assetId, amount); - uint256 assetsBefore = hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)); - uint256 sharesBefore = hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)); + uint256 assetsBefore = hub.getSpokeAddedAssets(assetId, address(actor)); + uint256 sharesBefore = hub.getSpokeAddedShares(assetId, address(actor)); _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHubBase.remove, (cachedTargetAssetId, amount, address(actor))) + abi.encodeCall(IHubBase.remove, (assetId, amount, address(actor))) ); if (success) { @@ -83,12 +83,12 @@ contract HubHandler is BaseHandler, IHubHandler { assertGe( assetsBefore, - hub.getSpokeAddedAssets(cachedTargetAssetId, address(actor)) + amount, + hub.getSpokeAddedAssets(assetId, address(actor)) + amount, HSPOST_HUB_ERC4626_REMOVE_A ); assertEq( sharesBefore, - hub.getSpokeAddedShares(cachedTargetAssetId, address(actor)) + removedShares, + hub.getSpokeAddedShares(assetId, address(actor)) + removedShares, HSPOST_HUB_ERC4626_REMOVE_B ); @@ -101,17 +101,17 @@ contract HubHandler is BaseHandler, IHubHandler { function draw(uint256 amount, uint8 i) public setup returns (uint256 drawnShares) { bool success; bytes memory returnData; - uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); + uint256 assetId = _getRandomBaseAssetId(i); - uint256 previewDrawnShares = hub.previewDrawByAssets(cachedTargetAssetId, amount); + uint256 previewDrawnShares = hub.previewDrawByAssets(assetId, amount); - (uint256 drawnBefore, ) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); - uint256 sharesBefore = hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)); + (uint256 drawnBefore, ) = hub.getSpokeOwed(assetId, address(actor)); + uint256 sharesBefore = hub.getSpokeDrawnShares(assetId, address(actor)); _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHubBase.draw, (cachedTargetAssetId, amount, address(actor))) + abi.encodeCall(IHubBase.draw, (assetId, amount, address(actor))) ); if (success) { @@ -119,12 +119,12 @@ contract HubHandler is BaseHandler, IHubHandler { drawnShares = uint256(abi.decode(returnData, (uint256))); - (uint256 drawnAfter, ) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); + (uint256 drawnAfter, ) = hub.getSpokeOwed(assetId, address(actor)); assertLe(drawnBefore + amount, drawnAfter, HSPOST_HUB_ERC4626_DRAW_A); assertEq( sharesBefore + drawnShares, - hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)), + hub.getSpokeDrawnShares(assetId, address(actor)), HSPOST_HUB_ERC4626_DRAW_B ); @@ -142,25 +142,25 @@ contract HubHandler is BaseHandler, IHubHandler { ) public setup returns (uint256 restoredDrawnShares) { bool success; bytes memory returnData; - uint256 cachedTargetAssetId = targetAssetId = _getRandomBaseAssetId(i); + uint256 assetId = _getRandomBaseAssetId(i); - uint256 previewRestoredShares = hub.previewRestoreByAssets(cachedTargetAssetId, drawnAmount); + uint256 previewRestoredShares = hub.previewRestoreByAssets(assetId, drawnAmount); - (uint256 drawnBefore, ) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); - uint256 drawnSharesBefore = hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)); + (uint256 drawnBefore, ) = hub.getSpokeOwed(assetId, address(actor)); + uint256 drawnSharesBefore = hub.getSpokeDrawnShares(assetId, address(actor)); IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta( sharesDelta, premiumAmount, - targetAssetId + assetId ); - _mint(assetIdToUnderlying[cachedTargetAssetId], address(hub), drawnAmount + premiumAmount); + _mint(assetIdToUnderlying[assetId], address(hub), drawnAmount + premiumAmount); _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHubBase.restore, (cachedTargetAssetId, drawnAmount, premiumDelta)) + abi.encodeCall(IHubBase.restore, (assetId, drawnAmount, premiumDelta)) ); if (success) { @@ -168,10 +168,10 @@ contract HubHandler is BaseHandler, IHubHandler { restoredDrawnShares = uint256(abi.decode(returnData, (uint256))); - (uint256 drawnAfter, ) = hub.getSpokeOwed(cachedTargetAssetId, address(actor)); + (uint256 drawnAfter, ) = hub.getSpokeOwed(assetId, address(actor)); if (restoredDrawnShares > 0) { - uint256 tolerance = hub.previewRestoreByShares(cachedTargetAssetId, 1); + uint256 tolerance = hub.previewRestoreByShares(assetId, 1); assertApproxEqAbs( drawnBefore, drawnAfter + drawnAmount, @@ -184,7 +184,7 @@ contract HubHandler is BaseHandler, IHubHandler { } assertEq( drawnSharesBefore, - hub.getSpokeDrawnShares(cachedTargetAssetId, address(actor)) + restoredDrawnShares, + hub.getSpokeDrawnShares(assetId, address(actor)) + restoredDrawnShares, HSPOST_HUB_ERC4626_RESTORE_B ); @@ -202,18 +202,18 @@ contract HubHandler is BaseHandler, IHubHandler { ) external setup { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); + uint256 assetId = _getRandomBaseAssetId(i); IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta( sharesDelta, premiumAmount, - targetAssetId + assetId ); _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHubBase.reportDeficit, (targetAssetId, drawnAmount, premiumDelta)) + abi.encodeCall(IHubBase.reportDeficit, (assetId, drawnAmount, premiumDelta)) ); if (success) { @@ -226,13 +226,14 @@ contract HubHandler is BaseHandler, IHubHandler { function eliminateDeficit(uint256 amount, uint8 i) external setup { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); - address spoke = address(actors[USER3]); // todo move this to an auth handler? + uint256 assetId = _getRandomBaseAssetId(i); + // only spoke3 is given deficit eliminator role + address spoke = address(actors[USER3]); _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHub.eliminateDeficit, (targetAssetId, amount, spoke)) + abi.encodeCall(IHub.eliminateDeficit, (assetId, amount, spoke)) ); if (success) { @@ -245,9 +246,9 @@ contract HubHandler is BaseHandler, IHubHandler { function refreshPremium(int256 sharesDelta, uint8 i) external setup { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); + uint256 assetId = _getRandomBaseAssetId(i); - int256 offsetRayDelta = sharesDelta * int256(hub.getAssetDrawnIndex(targetAssetId)); + int256 offsetRayDelta = sharesDelta * int256(hub.getAssetDrawnIndex(assetId)); IHubBase.PremiumDelta memory premiumDelta = IHubBase.PremiumDelta({ sharesDelta: sharesDelta, offsetRayDelta: offsetRayDelta, @@ -257,7 +258,7 @@ contract HubHandler is BaseHandler, IHubHandler { _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHubBase.refreshPremium, (targetAssetId, premiumDelta)) + abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta)) ); if (success) { @@ -265,8 +266,8 @@ contract HubHandler is BaseHandler, IHubHandler { // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) assertEq( - _assetVarsAfter(targetAssetId).debt.premium, - _assetVarsBefore(targetAssetId).debt.premium, + _assetVarsAfter(assetId).debt.premium, + _assetVarsBefore(assetId).debt.premium, HSPOST_HUB_M ); } else { @@ -278,12 +279,12 @@ contract HubHandler is BaseHandler, IHubHandler { function refreshPremium(IHubBase.PremiumDelta memory premiumDelta, uint8 i) external setup { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); + uint256 assetId = _getRandomBaseAssetId(i); _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHubBase.refreshPremium, (targetAssetId, premiumDelta)) + abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta)) ); if (success) { @@ -291,8 +292,8 @@ contract HubHandler is BaseHandler, IHubHandler { // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) assertEq( - _assetVarsAfter(targetAssetId).debt.premium, - _assetVarsBefore(targetAssetId).debt.premium, + _assetVarsAfter(assetId).debt.premium, + _assetVarsBefore(assetId).debt.premium, HSPOST_HUB_M ); } else { @@ -303,12 +304,12 @@ contract HubHandler is BaseHandler, IHubHandler { function payFeeShares(uint256 shares, uint8 i) external setup { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); + uint256 assetId = _getRandomBaseAssetId(i); _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHubBase.payFeeShares, (targetAssetId, shares)) + abi.encodeCall(IHubBase.payFeeShares, (assetId, shares)) ); if (success) { _after(); @@ -320,13 +321,13 @@ contract HubHandler is BaseHandler, IHubHandler { function transferShares(uint256 shares, uint8 i, uint8 j) external setup { bool success; bytes memory returnData; - targetAssetId = _getRandomBaseAssetId(i); + uint256 assetId = _getRandomBaseAssetId(i); address toSpoke = _getRandomActor(j); _before(); (success, returnData) = actor.proxy( address(hub), - abi.encodeCall(IHub.transferShares, (targetAssetId, shares, toSpoke)) + abi.encodeCall(IHub.transferShares, (assetId, shares, toSpoke)) ); if (success) { @@ -337,10 +338,11 @@ contract HubHandler is BaseHandler, IHubHandler { } function sweep(uint256 amount, uint8 i) external { - targetAssetId = _getRandomBaseAssetId(i); + uint256 assetId = _getRandomBaseAssetId(i); _before(); - try hub.sweep(targetAssetId, amount) { + // handler is the reinvestmentController + try hub.sweep(assetId, amount) { _after(); } catch { revert('HubHandler: sweep failed'); @@ -348,13 +350,14 @@ contract HubHandler is BaseHandler, IHubHandler { } function reclaim(uint256 amount, uint8 i) external { - targetAssetId = _getRandomBaseAssetId(i); - address underlying = assetIdToUnderlying[targetAssetId]; + uint256 assetId = _getRandomBaseAssetId(i); + address underlying = assetIdToUnderlying[assetId]; _mint(underlying, address(hub), amount); _before(); - try hub.reclaim(targetAssetId, amount) { + // handler is the reinvestmentController + try hub.reclaim(assetId, amount) { _after(); } catch { revert('HubHandler: reclaim failed'); diff --git a/invariants/hub-suite/hooks/HookAggregator.t.sol b/invariants/hub-suite/hooks/HookAggregator.t.sol index 48c1e7fe5..51660eee5 100644 --- a/invariants/hub-suite/hooks/HookAggregator.t.sol +++ b/invariants/hub-suite/hooks/HookAggregator.t.sol @@ -55,16 +55,12 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { /// @dev postconditions checks entrypoint, should be self-called function checkPostConditions() external { - // Target asset postconditions - if (targetAssetId != 0) { - assert_GPOST_HUB_C(targetAssetId); - } - // Protocol-wide postconditions uint256 assetCount = hub.getAssetCount(); for (uint256 i; i < assetCount; i++) { assert_GPOST_HUB_A(i); assert_GPOST_HUB_B(i); + assert_GPOST_HUB_C(i); assert_GPOST_HUB_D(i); assert_GPOST_HUB_G(i); @@ -82,6 +78,5 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { /// @notice Resets the state of the handlers function _resetState() internal { delete currentActionSignature; - delete targetAssetId; } } From 644ed1a042512b92dd3ce9287ddda5e2570c3996 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:00:44 +0530 Subject: [PATCH 069/106] chore: reword low level ret vars --- invariants/hub-suite/base/BaseHandler.t.sol | 6 +- invariants/hub-suite/base/BaseTest.t.sol | 15 ++-- .../hub-suite/handlers/HubHandler.t.sol | 68 +++++++------------ .../hub-suite/hooks/HookAggregator.t.sol | 4 +- invariants/protocol-suite/base/BaseTest.t.sol | 15 ++-- .../handlers/spoke/SpokeHandler.t.sol | 60 +++++----------- .../protocol-suite/hooks/HookAggregator.t.sol | 4 +- 7 files changed, 62 insertions(+), 110 deletions(-) diff --git a/invariants/hub-suite/base/BaseHandler.t.sol b/invariants/hub-suite/base/BaseHandler.t.sol index d5b739f46..c81f7d89d 100644 --- a/invariants/hub-suite/base/BaseHandler.t.sol +++ b/invariants/hub-suite/base/BaseHandler.t.sol @@ -42,13 +42,11 @@ contract BaseHandler is HookAggregator { /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor function _approve(address token, Actor actor_, address spender, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor_.proxy( + (bool ok, bytes memory ret) = actor_.proxy( token, abi.encodeWithSelector(0x095ea7b3, spender, amount) ); - require(success, string(returnData)); + require(ok, string(ret)); } /// @notice Helper function to safely approve an amount of tokens to a spender diff --git a/invariants/hub-suite/base/BaseTest.t.sol b/invariants/hub-suite/base/BaseTest.t.sol index aaf4b965f..ce3ba9db4 100644 --- a/invariants/hub-suite/base/BaseTest.t.sol +++ b/invariants/hub-suite/base/BaseTest.t.sol @@ -107,18 +107,17 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @notice Helper function to transfer tokens by actor function _transferByActor(address token, address to, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor.proxy(token, abi.encodeCall(IERC20.transfer, (to, amount))); - require(success, string(returnData)); + (bool ok, bytes memory ret) = actor.proxy(token, abi.encodeCall(IERC20.transfer, (to, amount))); + require(ok, string(ret)); } /// @notice Helper function to approve tokens by actor function _approveByActor(address token, address spender, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor.proxy(token, abi.encodeCall(IERC20.approve, (spender, amount))); - require(success, string(returnData)); + (bool ok, bytes memory ret) = actor.proxy( + token, + abi.encodeCall(IERC20.approve, (spender, amount)) + ); + require(ok, string(ret)); } /// @notice Helper function to calculate burnt interest in assets terms (originating from virtual shares) diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index cf031ed25..17dc5d535 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -20,8 +20,6 @@ contract HubHandler is BaseHandler, IHubHandler { /////////////////////////////////////////////////////////////////////////////////////////////// function add(uint256 amount, uint8 i) public setup returns (uint256 addedShares) { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); address underlying = assetIdToUnderlying[assetId]; @@ -33,15 +31,15 @@ contract HubHandler is BaseHandler, IHubHandler { _mint(underlying, address(hub), amount); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHubBase.add, (assetId, amount)) ); - if (success) { + if (ok) { _after(); - addedShares = uint256(abi.decode(returnData, (uint256))); + addedShares = uint256(abi.decode(ret, (uint256))); assertGe( assetsBefore + amount, @@ -61,8 +59,6 @@ contract HubHandler is BaseHandler, IHubHandler { } function remove(uint256 amount, uint8 i) public setup returns (uint256 removedShares) { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); uint256 previewRemovedShares = hub.previewRemoveByAssets(assetId, amount); @@ -71,15 +67,15 @@ contract HubHandler is BaseHandler, IHubHandler { uint256 sharesBefore = hub.getSpokeAddedShares(assetId, address(actor)); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHubBase.remove, (assetId, amount, address(actor))) ); - if (success) { + if (ok) { _after(); - removedShares = uint256(abi.decode(returnData, (uint256))); + removedShares = uint256(abi.decode(ret, (uint256))); assertGe( assetsBefore, @@ -99,8 +95,6 @@ contract HubHandler is BaseHandler, IHubHandler { } function draw(uint256 amount, uint8 i) public setup returns (uint256 drawnShares) { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); uint256 previewDrawnShares = hub.previewDrawByAssets(assetId, amount); @@ -109,15 +103,15 @@ contract HubHandler is BaseHandler, IHubHandler { uint256 sharesBefore = hub.getSpokeDrawnShares(assetId, address(actor)); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHubBase.draw, (assetId, amount, address(actor))) ); - if (success) { + if (ok) { _after(); - drawnShares = uint256(abi.decode(returnData, (uint256))); + drawnShares = uint256(abi.decode(ret, (uint256))); (uint256 drawnAfter, ) = hub.getSpokeOwed(assetId, address(actor)); @@ -140,8 +134,6 @@ contract HubHandler is BaseHandler, IHubHandler { int256 sharesDelta, uint8 i ) public setup returns (uint256 restoredDrawnShares) { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); uint256 previewRestoredShares = hub.previewRestoreByAssets(assetId, drawnAmount); @@ -158,15 +150,15 @@ contract HubHandler is BaseHandler, IHubHandler { _mint(assetIdToUnderlying[assetId], address(hub), drawnAmount + premiumAmount); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHubBase.restore, (assetId, drawnAmount, premiumDelta)) ); - if (success) { + if (ok) { _after(); - restoredDrawnShares = uint256(abi.decode(returnData, (uint256))); + restoredDrawnShares = uint256(abi.decode(ret, (uint256))); (uint256 drawnAfter, ) = hub.getSpokeOwed(assetId, address(actor)); @@ -200,8 +192,6 @@ contract HubHandler is BaseHandler, IHubHandler { int256 sharesDelta, uint8 i ) external setup { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); IHubBase.PremiumDelta memory premiumDelta = _calculatePremiumDelta( @@ -211,12 +201,12 @@ contract HubHandler is BaseHandler, IHubHandler { ); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHubBase.reportDeficit, (assetId, drawnAmount, premiumDelta)) ); - if (success) { + if (ok) { _after(); } else { revert('HubHandler: reportDeficit failed'); @@ -224,19 +214,17 @@ contract HubHandler is BaseHandler, IHubHandler { } function eliminateDeficit(uint256 amount, uint8 i) external setup { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); // only spoke3 is given deficit eliminator role address spoke = address(actors[USER3]); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHub.eliminateDeficit, (assetId, amount, spoke)) ); - if (success) { + if (ok) { _after(); } else { revert('HubHandler: eliminateDeficit failed'); @@ -244,8 +232,6 @@ contract HubHandler is BaseHandler, IHubHandler { } function refreshPremium(int256 sharesDelta, uint8 i) external setup { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); int256 offsetRayDelta = sharesDelta * int256(hub.getAssetDrawnIndex(assetId)); @@ -256,12 +242,12 @@ contract HubHandler is BaseHandler, IHubHandler { }); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta)) ); - if (success) { + if (ok) { _after(); // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) @@ -277,17 +263,15 @@ contract HubHandler is BaseHandler, IHubHandler { // @dev broader `refreshPremium` to cover edge cases, above case exists for narrow happy path function refreshPremium(IHubBase.PremiumDelta memory premiumDelta, uint8 i) external setup { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta)) ); - if (success) { + if (ok) { _after(); // HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution) @@ -302,16 +286,14 @@ contract HubHandler is BaseHandler, IHubHandler { } function payFeeShares(uint256 shares, uint8 i) external setup { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHubBase.payFeeShares, (assetId, shares)) ); - if (success) { + if (ok) { _after(); } else { revert('HubHandler: payFeeShares failed'); @@ -319,18 +301,16 @@ contract HubHandler is BaseHandler, IHubHandler { } function transferShares(uint256 shares, uint8 i, uint8 j) external setup { - bool success; - bytes memory returnData; uint256 assetId = _getRandomBaseAssetId(i); address toSpoke = _getRandomActor(j); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( address(hub), abi.encodeCall(IHub.transferShares, (assetId, shares, toSpoke)) ); - if (success) { + if (ok) { _after(); } else { revert('HubHandler: transferShares failed'); diff --git a/invariants/hub-suite/hooks/HookAggregator.t.sol b/invariants/hub-suite/hooks/HookAggregator.t.sol index 51660eee5..6a824ffd7 100644 --- a/invariants/hub-suite/hooks/HookAggregator.t.sol +++ b/invariants/hub-suite/hooks/HookAggregator.t.sol @@ -48,8 +48,8 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { // Store the message signature to avoid losing it inside the checkPostConditions call context _cacheCurrentActionSignature(); - try this.checkPostConditions() {} catch (bytes memory returnData) { - ErrorHandlers.handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); + try this.checkPostConditions() {} catch (bytes memory ret) { + ErrorHandlers.handleAssertionError(false, ret, true, GPOST_CHECK_FAILED); } } diff --git a/invariants/protocol-suite/base/BaseTest.t.sol b/invariants/protocol-suite/base/BaseTest.t.sol index 11ab9d79f..a3b8be8e0 100644 --- a/invariants/protocol-suite/base/BaseTest.t.sol +++ b/invariants/protocol-suite/base/BaseTest.t.sol @@ -169,18 +169,17 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @notice Helper function to transfer tokens by actor function _transferByActor(address token, address to, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor.proxy(token, abi.encodeCall(IERC20.transfer, (to, amount))); - require(success, string(returnData)); + (bool ok, bytes memory ret) = actor.proxy(token, abi.encodeCall(IERC20.transfer, (to, amount))); + require(ok, string(ret)); } /// @notice Helper function to approve tokens by actor function _approveByActor(address token, address spender, uint256 amount) internal { - bool success; - bytes memory returnData; - (success, returnData) = actor.proxy(token, abi.encodeCall(IERC20.approve, (spender, amount))); - require(success, string(returnData)); + (bool ok, bytes memory ret) = actor.proxy( + token, + abi.encodeCall(IERC20.approve, (spender, amount)) + ); + require(ok, string(ret)); } /// @notice Helper function to calculate burnt interest in assets terms (originating from virtual shares) diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index ff0bbd51f..c79a73aac 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -48,9 +48,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { /////////////////////////////////////////////////////////////////////////////////////////////// function supply(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - bool success; - bytes memory returnData; - // Get one of the three actors randomly address onBehalfOf = _getRandomActor(i); @@ -66,12 +63,12 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _mintAndApprove(underlying, address(actor), spoke, amount); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( spoke, abi.encodeCall(ISpokeBase.supply, (reserveId, amount, onBehalfOf)) ); - if (success) { + if (ok) { _after(); } else { revert('SpokeHandler: supply failed'); @@ -79,9 +76,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } function withdraw(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - bool success; - bytes memory returnData; - // Get one of the three actors randomly address onBehalfOf = _getRandomActor(i); @@ -96,7 +90,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, reserveId, onBehalfOf); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( spoke, abi.encodeCall(ISpokeBase.withdraw, (reserveId, amount, onBehalfOf)) ); @@ -110,10 +104,10 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { amount > 0 && userAmount != 0 ) { - assertTrue(success, GPOST_SP_H); + assertTrue(ok, GPOST_SP_H); } - if (success) { + if (ok) { _after(); } else { revert('SpokeHandler: withdraw failed'); @@ -121,9 +115,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } function borrow(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - bool success; - bytes memory returnData; - // Get one of the three actors randomly address onBehalfOf = _getRandomActor(i); @@ -139,12 +130,12 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { bool isHealthy = _isHealthy(spoke, onBehalfOf); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( spoke, abi.encodeCall(ISpokeBase.borrow, (reserveId, amount, onBehalfOf)) ); - if (success) { + if (ok) { _after(); ///// HSPOST ///// @@ -156,9 +147,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } function repay(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - bool success; - bytes memory returnData; - // Get one of the three actors randomly address onBehalfOf = _getRandomActor(i); @@ -174,12 +162,12 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _mintAndApprove(underlying, address(actor), spoke, amount); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( spoke, abi.encodeCall(ISpokeBase.repay, (reserveId, amount, onBehalfOf)) ); - if (success) { + if (ok) { _after(); ///// HSPOST ///// @@ -202,9 +190,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { uint8 k, uint8 l ) external setup { - bool success; - bytes memory returnData; - LiquidationVars memory liquidationVars; // Get one of the three actors randomly @@ -269,7 +254,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( liquidationVars.spoke, abi.encodeCall( ISpokeBase.liquidationCall, @@ -283,7 +268,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ) ); - if (success) { + if (ok) { _after(); // Calculate the debt liquidated from user-level snapshots (not reserve-level, which @@ -356,9 +341,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external setup { - bool success; - bytes memory returnData; - address onBehalfOf = address(actor); address spoke = _getRandomSpoke(i); @@ -380,12 +362,12 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, (usingAsCollateral ? reserveId : CHECK_ALL_RESERVES), onBehalfOf); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( spoke, abi.encodeCall(ISpoke.setUsingAsCollateral, (reserveId, usingAsCollateral, onBehalfOf)) ); - if (success) { + if (ok) { _after(); } else { revert('SpokeHandler: setUsingAsCollateral failed'); @@ -393,9 +375,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } function updateUserRiskPremium(uint8 i) external setup { - bool success; - bytes memory returnData; - address onBehalfOf = address(actor); address spoke = _getRandomSpoke(i); @@ -404,12 +383,12 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( spoke, abi.encodeCall(ISpoke.updateUserRiskPremium, (onBehalfOf)) ); - if (success) { + if (ok) { _after(); ///// HSPOST ///// uint256 reserveCount = ISpoke(spoke).getReserveCount(); @@ -425,9 +404,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } function updateUserDynamicConfig(uint8 i) external setup { - bool success; - bytes memory returnData; - address onBehalfOf = address(actor); address spoke = _getRandomSpoke(i); @@ -435,12 +411,12 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); _before(); - (success, returnData) = actor.proxy( + (bool ok, bytes memory ret) = actor.proxy( spoke, abi.encodeCall(ISpoke.updateUserDynamicConfig, (onBehalfOf)) ); - if (success) { + if (ok) { _after(); } else { revert('SpokeHandler: updateUserDynamicConfig failed'); @@ -448,7 +424,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } // todo add updateUserPositionManager - // todo check decoded returnData + // todo check decoded ret /////////////////////////////////////////////////////////////////////////////////////////////// // OWNER ACTIONS // diff --git a/invariants/protocol-suite/hooks/HookAggregator.t.sol b/invariants/protocol-suite/hooks/HookAggregator.t.sol index 20966c3e3..b92db04bf 100644 --- a/invariants/protocol-suite/hooks/HookAggregator.t.sol +++ b/invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -48,8 +48,8 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { // Store the message signature to avoid losing it inside the checkPostConditions call context _cacheCurrentActionSignature(); - try this.checkPostConditions() {} catch (bytes memory returnData) { - ErrorHandlers.handleAssertionError(false, returnData, true, GPOST_CHECK_FAILED); + try this.checkPostConditions() {} catch (bytes memory ret) { + ErrorHandlers.handleAssertionError(false, ret, true, GPOST_CHECK_FAILED); } } From 7a2c1bbe909f713222e840f2145d5cf45c6032de Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:08:51 +0530 Subject: [PATCH 070/106] chore: minor cleanup --- invariants/hub-suite/invariants/HubInvariantAssertions.t.sol | 2 +- invariants/hub-suite/specs/HubInvariantsSpec.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol b/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol index ed1b216c6..fbb5775c3 100644 --- a/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol +++ b/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol @@ -55,7 +55,7 @@ abstract contract HubInvariantAssertions is StdAsserts, HubInvariantsSpec { uint256 assets = hub.getAddedAssets(assetId); if (assets == 0) { - assertEq(hub.getAddedShares(assetId), 0, INV_HUB_A2); + assertEq(hub.getAddedShares(assetId), 0, INV_HUB_A); } } diff --git a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol index b29a659ee..b09408c2e 100644 --- a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -21,7 +21,7 @@ abstract contract HubInvariantsSpec { // HUB // /////////////////////////////////////////////////////////////////////////////////////////////// - string constant INV_HUB_A2 = 'INV_HUB_A2: If hub assets = 0 => shares 0'; + string constant INV_HUB_A = 'INV_HUB_A: If hub assets = 0 => shares 0'; string constant INV_HUB_B = 'INV_HUB_B: Sum of spoke debts on a single asset must be greater or equal than the total debt of the asset'; From 385a675ea2711c509b8557cb9af0ba0d79c372d9 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:13:22 +0530 Subject: [PATCH 071/106] feat: safe mint for spoke --- .../protocol-suite/base/BaseHandler.t.sol | 22 +++++++++++++++++++ .../handlers/spoke/SpokeHandler.t.sol | 6 ++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/invariants/protocol-suite/base/BaseHandler.t.sol b/invariants/protocol-suite/base/BaseHandler.t.sol index e32c9bc5c..6e7021426 100644 --- a/invariants/protocol-suite/base/BaseHandler.t.sol +++ b/invariants/protocol-suite/base/BaseHandler.t.sol @@ -79,4 +79,26 @@ contract BaseHandler is HookAggregator { _mint(token, owner, amount); _approve(token, owner, spender, amount); } + + /// @notice Best-effort mint — silently swallows overflow so the handler can proceed. + /// @dev Using a low-level call prevents the handler from reverting on mint failure + /// (e.g. totalSupply overflow), which would cause the fuzzer to discard the + /// entire call sequence. Spoke functions bound the passed amount to the user's + /// actual balance (e.g. repay(type(uint256).max) repays only the owed debt), + /// so even if minting the full amount overflows, the spoke will operate on + /// whatever balance exists. + function _tryMint(address token, address receiver, uint256 amount) internal { + token.call(abi.encodeCall(MockERC20.mint, (receiver, amount))); + } + + /// @notice Best-effort mint + approve for spoke handlers + function _tryMintAndApprove( + address token, + address owner, + address spender, + uint256 amount + ) internal { + _tryMint(token, owner, amount); + _approve(token, owner, spender, amount); + } } diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index c79a73aac..74397cc8e 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -60,7 +60,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, reserveId, onBehalfOf); address underlying = ISpoke(spoke).getReserve(reserveId).underlying; - _mintAndApprove(underlying, address(actor), spoke, amount); + _tryMintAndApprove(underlying, address(actor), spoke, amount); _before(); (bool ok, bytes memory ret) = actor.proxy( @@ -159,7 +159,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, reserveId, onBehalfOf); address underlying = ISpoke(spoke).getReserve(reserveId).underlying; - _mintAndApprove(underlying, address(actor), spoke, amount); + _tryMintAndApprove(underlying, address(actor), spoke, amount); _before(); (bool ok, bytes memory ret) = actor.proxy( @@ -246,7 +246,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { liquidationVars.liquidator ); - _mintAndApprove( + _tryMintAndApprove( ISpoke(liquidationVars.spoke).getReserve(liquidationVars.debtReserveId).underlying, address(actor), liquidationVars.spoke, From 2fea6f390af10290be523c6122367b173033bed7 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:41:43 +0530 Subject: [PATCH 072/106] chore: group invariants --- invariants/hub-suite/Invariants.t.sol | 64 ++++++++- .../hub-suite/replays/ReplayTest_2.t.sol | 2 +- .../hub-suite/replays/ReplayTest_3.t.sol | 2 +- .../hub-suite/replays/ReplayTest_5.t.sol | 2 +- .../hub-suite/replays/ReplayTest_8.t.sol | 4 +- .../hub-suite/replays/ReplayTest_9.t.sol | 2 +- .../hub-suite/specs/HubInvariantsSpec.t.sol | 78 +++-------- .../specs/HubPostconditionsSpec.t.sol | 66 ++++++++- invariants/protocol-suite/Invariants.t.sol | 128 ++++++++++++++---- .../protocol-suite/replays/ReplayTest_2.t.sol | 2 +- .../protocol-suite/replays/ReplayTest_3.t.sol | 2 +- .../protocol-suite/replays/ReplayTest_6.t.sol | 4 +- .../protocol-suite/specs/InvariantsSpec.t.sol | 32 +---- .../specs/PostconditionsSpec.t.sol | 20 ++- .../specs/SpokeInvariantsSpec.t.sol | 43 ++++++ 15 files changed, 307 insertions(+), 144 deletions(-) create mode 100644 invariants/protocol-suite/specs/SpokeInvariantsSpec.t.sol diff --git a/invariants/hub-suite/Invariants.t.sol b/invariants/hub-suite/Invariants.t.sol index 4361d8dc7..20786da3e 100644 --- a/invariants/hub-suite/Invariants.t.sol +++ b/invariants/hub-suite/Invariants.t.sol @@ -6,22 +6,58 @@ import {HubInvariants} from './invariants/HubInvariants.t.sol'; /// @title Invariants /// @notice Aggregator for hub invariants abstract contract Invariants is HubInvariants { - function invariant_INV_HUB() public returns (bool) { - uint256 assetCount = hub.getAssetCount(); + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACCOUNTING // + /////////////////////////////////////////////////////////////////////////////////////////////// + function invariant_INV_HUB_ACCOUNTING() public returns (bool) { + uint256 assetCount = hub.getAssetCount(); for (uint256 i; i < assetCount; i++) { assert_INV_HUB_A(hub, i); assert_INV_HUB_B(hub, i); assert_INV_HUB_C(hub, i); + assert_INV_HUB_GH(hub, i); + assert_INV_HUB_O(hub, i); + assert_INV_HUB_P(hub, i); + } + return true; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SOLVENCY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function invariant_INV_HUB_SOLVENCY() public returns (bool) { + uint256 assetCount = hub.getAssetCount(); + for (uint256 i; i < assetCount; i++) { assert_INV_HUB_E(hub, i); assert_INV_HUB_F(hub, i); - assert_INV_HUB_GH(hub, i); assert_INV_HUB_I(hub, i); assert_INV_HUB_K(hub, i); - assert_INV_HUB_O(hub, i); - assert_INV_HUB_P(hub, i); + } + return true; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // MONOTONICITY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function invariant_INV_HUB_MONOTONICITY() public returns (bool) { + uint256 assetCount = hub.getAssetCount(); + for (uint256 i; i < assetCount; i++) { assert_INV_HUB_Q(hub, i); assert_INV_HUB_R(hub, i); + } + return true; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ERC4626 // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function invariant_INV_HUB_ERC4626() public returns (bool) { + uint256 assetCount = hub.getAssetCount(); + for (uint256 i; i < assetCount; i++) { for (uint256 j; j < NUMBER_OF_ACTORS; j++) { assert_INV_HUB_ERC4626_A(i, actorAddresses[j]); assert_INV_HUB_ERC4626_B(i, actorAddresses[j]); @@ -29,10 +65,13 @@ abstract contract Invariants is HubInvariants { assert_INV_HUB_ERC4626_C(i); assert_INV_HUB_ERC4626_D(i); } - return true; } + /////////////////////////////////////////////////////////////////////////////////////////////// + // AVAILABILITY // + /////////////////////////////////////////////////////////////////////////////////////////////// + function invariant_INV_HUB_AVAILABILITY() public returns (bool) { uint256 assetCount = hub.getAssetCount(); for (uint256 i; i < assetCount; i++) { @@ -48,7 +87,18 @@ abstract contract Invariants is HubInvariants { assert_INV_HUB_AVAILABILITY_I(i, actorAddresses[j]); } } - return true; } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _checkAllHubInvariants() internal { + assertTrue(invariant_INV_HUB_ACCOUNTING()); + assertTrue(invariant_INV_HUB_SOLVENCY()); + assertTrue(invariant_INV_HUB_MONOTONICITY()); + assertTrue(invariant_INV_HUB_ERC4626()); + assertTrue(invariant_INV_HUB_AVAILABILITY()); + } } diff --git a/invariants/hub-suite/replays/ReplayTest_2.t.sol b/invariants/hub-suite/replays/ReplayTest_2.t.sol index f88b02629..98bb5fb4a 100644 --- a/invariants/hub-suite/replays/ReplayTest_2.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_2.t.sol @@ -54,7 +54,7 @@ contract ReplayTest2Hub is Invariants, Setup { Tester.draw(1, 1); _delay(1); Tester.payFeeShares(1, 0); - invariant_INV_HUB(); + _checkAllHubInvariants(); } /// @dev PASS diff --git a/invariants/hub-suite/replays/ReplayTest_3.t.sol b/invariants/hub-suite/replays/ReplayTest_3.t.sol index 3218f73fb..b51c91017 100644 --- a/invariants/hub-suite/replays/ReplayTest_3.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_3.t.sol @@ -43,7 +43,7 @@ contract ReplayTest3Hub is Invariants, Setup { Tester.draw(1, 0); _delay(6343); Tester.add(6, 0); - invariant_INV_HUB(); + _checkAllHubInvariants(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/hub-suite/replays/ReplayTest_5.t.sol b/invariants/hub-suite/replays/ReplayTest_5.t.sol index 191e6f9a2..696ebc68e 100644 --- a/invariants/hub-suite/replays/ReplayTest_5.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_5.t.sol @@ -41,7 +41,7 @@ contract ReplayTest5Hub is Invariants, Setup { _setUpActor(USER1); Tester.refreshPremium(9472849991, 0); _delay(1); - invariant_INV_HUB(); + _checkAllHubInvariants(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/hub-suite/replays/ReplayTest_8.t.sol b/invariants/hub-suite/replays/ReplayTest_8.t.sol index f3025aa0c..a12c4d844 100644 --- a/invariants/hub-suite/replays/ReplayTest_8.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_8.t.sol @@ -42,7 +42,7 @@ contract ReplayTest8 is Invariants, Setup { Tester.refreshPremium(3, 0); _delay(1); Tester.refreshPremium(-3, 0); - invariant_INV_HUB(); + _checkAllHubInvariants(); } function test_replay_8_2() public { @@ -51,7 +51,7 @@ contract ReplayTest8 is Invariants, Setup { Tester.draw(1, 0); _delay(5); Tester.restore(1, 0, 0, 0); - invariant_INV_HUB(); + _checkAllHubInvariants(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/hub-suite/replays/ReplayTest_9.t.sol b/invariants/hub-suite/replays/ReplayTest_9.t.sol index 91094372d..8546899a4 100644 --- a/invariants/hub-suite/replays/ReplayTest_9.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_9.t.sol @@ -38,7 +38,7 @@ contract ReplayTest9 is Invariants, Setup { _delay(18880); _delay(23650); Tester.donateUnderlyingToHub(0, 0); - invariant_INV_HUB(); + _checkAllHubInvariants(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol index b09408c2e..f1a6106dc 100644 --- a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -18,7 +18,7 @@ abstract contract HubInvariantsSpec { /////////////////////////////////////////////////////////////////////////////////////////////*/ /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // + // ACCOUNTING // /////////////////////////////////////////////////////////////////////////////////////////////// string constant INV_HUB_A = 'INV_HUB_A: If hub assets = 0 => shares 0'; @@ -29,6 +29,22 @@ abstract contract HubInvariantsSpec { string constant INV_HUB_C = 'INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub'; + string constant INV_HUB_G = + 'INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT'; // TODO check if tolerance is correct + + string constant INV_HUB_H = + 'INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes'; + + string constant INV_HUB_O = + 'INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay'; + + string constant INV_HUB_P = + 'INV_HUB_P: Premium offset should not exceed premium shares * drawnIndex'; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SOLVENCY // + /////////////////////////////////////////////////////////////////////////////////////////////// + string constant INV_HUB_E_1 = 'INV_HUB_E: total assets is equal to or greater than the supplied amount without taking into account the virtual assets and shares up to the burn interest to virtual shares'; @@ -38,23 +54,15 @@ abstract contract HubInvariantsSpec { string constant INV_HUB_F = 'INV_HUB_F: hub.getTotalSuppliedAssets = totalAssets() = availableLiquidity + (totalDebtRay + deficitRay).fromRayUp + swept'; - string constant INV_HUB_G = - 'INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT'; // TODO check if tolerance is correct - - string constant INV_HUB_H = - 'INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes'; - string constant INV_HUB_I = 'INV_HUB_I: asset.underlying.balanceOf(hub) + asset.swept >= asset.liquidity'; string constant INV_HUB_K = 'INV_HUB_K: Asset.irStrategy should never be address(0) for any (currently/previously) registered asset'; - string constant INV_HUB_O = - 'INV_HUB_O: sum of deficitRay across spokes for a given asset == total asset deficitRay'; - - string constant INV_HUB_P = - 'INV_HUB_P: Premium offset should not exceed premium shares * drawnIndex'; + /////////////////////////////////////////////////////////////////////////////////////////////// + // MONOTONICITY // + /////////////////////////////////////////////////////////////////////////////////////////////// string constant INV_HUB_Q = 'INV_HUB_Q: Drawn index must be monotonically non-decreasing across invariant checks'; @@ -66,48 +74,6 @@ abstract contract HubInvariantsSpec { // ERC4626 // /////////////////////////////////////////////////////////////////////////////////////////////// - // Add - string constant HSPOST_HUB_ERC4626_ADD_A = - 'HSPOST_HUB_ERC4626_ADD_A: After add, spoke addedAssets must increase by at most addedAmount'; - - string constant HSPOST_HUB_ERC4626_ADD_B = - 'HSPOST_HUB_ERC4626_ADD_B: After add, spoke addedShares must increase by addedSharesAmount'; - - string constant HSPOST_HUB_ERC4626_ADD_C = - 'HSPOST_HUB_ERC4626_ADD_C: previewAddedShares must be less than or equal to the added shares after the action'; - - // Remove - string constant HSPOST_HUB_ERC4626_REMOVE_A = - 'HSPOST_HUB_ERC4626_REMOVE_A: After remove, spoke addedAssets must decrease by at least removedAmount'; - - string constant HSPOST_HUB_ERC4626_REMOVE_B = - 'HSPOST_HUB_ERC4626_REMOVE_B: After remove, spoke addedShares must decrease by removedSharesAmount'; - - string constant HSPOST_HUB_ERC4626_REMOVE_C = - 'HSPOST_HUB_ERC4626_REMOVE_C: previewRemovedShares must be greater than or equal to the removed shares after the action'; - - // Draw - string constant HSPOST_HUB_ERC4626_DRAW_A = - 'HSPOST_HUB_ERC4626_DRAW_A: After draw, spoke drawnAssets must increase by at least drawnAmount'; - - string constant HSPOST_HUB_ERC4626_DRAW_B = - 'HSPOST_HUB_ERC4626_DRAW_B: After draw, spoke drawnShares must increase by drawnSharesAmount'; - - string constant HSPOST_HUB_ERC4626_DRAW_C = - 'HSPOST_HUB_ERC4626_DRAW_C: previewDrawnShares must be greater than or equal to the drawn shares after the action'; - - // Restore - string constant HSPOST_HUB_ERC4626_RESTORE_A = - 'HSPOST_HUB_ERC4626_RESTORE_A: After restore, spoke drawnAssets must increase by at most drawnAmount'; - - string constant HSPOST_HUB_ERC4626_RESTORE_B = - 'HSPOST_HUB_ERC4626_RESTORE_B: After restore, spoke drawnShares must decrease by restoredSharesAmount'; - - string constant HSPOST_HUB_ERC4626_RESTORE_C = - 'HSPOST_HUB_ERC4626_RESTORE_C: previewRestoredShares must be less than or equal to the restored shares after the action'; - - // GENERIC - string constant INV_HUB_ERC4626_A = 'INV_HUB_ERC4626_A: Spoke cannot have non-zero assets and zero shares in add side without any premium'; @@ -124,8 +90,6 @@ abstract contract HubInvariantsSpec { // ERC4626 ROUNDTRIP // /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice ROUNDTRIP - /// @dev ERC4626: redeem(deposit(a)) <= a string constant INV_HUB_ERC4626_RT_A = 'INV_HUB_ERC4626_RT_A: previewRemoveByShares(previewAddByAssets(a)) <= a'; @@ -159,7 +123,7 @@ abstract contract HubInvariantsSpec { 'INV_HUB_ERC4626_RT_H: previewAddByAssets(a) <= previewRemoveByAssets(a)'; /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB: AVAILABILITY // + // AVAILABILITY // /////////////////////////////////////////////////////////////////////////////////////////////// string constant INV_HUB_AVAILABILITY_A = 'INV_HUB_AVAILABILITY_A: getAddedAssets must not revert'; diff --git a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol index 4712906b1..f95e5d5c8 100644 --- a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol +++ b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol @@ -25,7 +25,7 @@ abstract contract HubPostconditionsSpec { /////////////////////////////////////////////////////////////////////////////////////////////*/ /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB // + // MONOTONICITY // /////////////////////////////////////////////////////////////////////////////////////////////// string constant GPOST_HUB_A = @@ -34,11 +34,25 @@ abstract contract HubPostconditionsSpec { string constant GPOST_HUB_B = "GPOST_HUB_B: Add exchange rate (total assets / total shares) cannot decrease (remains constant or increases). If no time passes, it stays constant. it increases due to interest accumulation, premium debt settlement and donations (from actions' rounding)."; + string constant GPOST_HUB_D = + 'GPOST_HUB_D: lastUpdateTimestamp must be <= block.timestamp after any action (timestamps cannot be in the future).'; + + string constant GPOST_HUB_G = + 'GPOST_HUB_G: lastUpdateTimestamp is monotonic non-decreasing across actions (time does not go backwards)'; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACCOUNTING // + /////////////////////////////////////////////////////////////////////////////////////////////// + string constant GPOST_HUB_C = 'GPOST_HUB_C: Borrow rate should always match the calculated amount right after any hub non-view operation in the same block.'; - string constant GPOST_HUB_D = - 'GPOST_HUB_D: lastUpdateTimestamp must be <= block.timestamp after any action (timestamps cannot be in the future).'; + string constant HSPOST_HUB_M = + 'HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution)'; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CAPS // + /////////////////////////////////////////////////////////////////////////////////////////////// string constant GPOST_HUB_E = 'GPOST_HUB_E: if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)'; @@ -46,9 +60,47 @@ abstract contract HubPostconditionsSpec { string constant GPOST_HUB_F = 'GPOST_HUB_F: if owed for a spoke & assetId increase, owed <= drawCap * precision (when cap != MAX)'; // TODO-ok take into account the deficit, use Owed instead of drawn -> review fix - string constant GPOST_HUB_G = - 'GPOST_HUB_G: lastUpdateTimestamp is monotonic non-decreasing across actions (time does not go backwards)'; + /////////////////////////////////////////////////////////////////////////////////////////////// + // ERC4626 // + /////////////////////////////////////////////////////////////////////////////////////////////// - string constant HSPOST_HUB_M = - 'HSPOST_HUB_M: refreshPremium cannot change total premium debt (only redistribution)'; + // Add + string constant HSPOST_HUB_ERC4626_ADD_A = + 'HSPOST_HUB_ERC4626_ADD_A: After add, spoke addedAssets must increase by at most addedAmount'; + + string constant HSPOST_HUB_ERC4626_ADD_B = + 'HSPOST_HUB_ERC4626_ADD_B: After add, spoke addedShares must increase by addedSharesAmount'; + + string constant HSPOST_HUB_ERC4626_ADD_C = + 'HSPOST_HUB_ERC4626_ADD_C: previewAddedShares must be less than or equal to the added shares after the action'; + + // Remove + string constant HSPOST_HUB_ERC4626_REMOVE_A = + 'HSPOST_HUB_ERC4626_REMOVE_A: After remove, spoke addedAssets must decrease by at least removedAmount'; + + string constant HSPOST_HUB_ERC4626_REMOVE_B = + 'HSPOST_HUB_ERC4626_REMOVE_B: After remove, spoke addedShares must decrease by removedSharesAmount'; + + string constant HSPOST_HUB_ERC4626_REMOVE_C = + 'HSPOST_HUB_ERC4626_REMOVE_C: previewRemovedShares must be greater than or equal to the removed shares after the action'; + + // Draw + string constant HSPOST_HUB_ERC4626_DRAW_A = + 'HSPOST_HUB_ERC4626_DRAW_A: After draw, spoke drawnAssets must increase by at least drawnAmount'; + + string constant HSPOST_HUB_ERC4626_DRAW_B = + 'HSPOST_HUB_ERC4626_DRAW_B: After draw, spoke drawnShares must increase by drawnSharesAmount'; + + string constant HSPOST_HUB_ERC4626_DRAW_C = + 'HSPOST_HUB_ERC4626_DRAW_C: previewDrawnShares must be greater than or equal to the drawn shares after the action'; + + // Restore + string constant HSPOST_HUB_ERC4626_RESTORE_A = + 'HSPOST_HUB_ERC4626_RESTORE_A: After restore, spoke drawnAssets must increase by at most drawnAmount'; + + string constant HSPOST_HUB_ERC4626_RESTORE_B = + 'HSPOST_HUB_ERC4626_RESTORE_B: After restore, spoke drawnShares must decrease by restoredSharesAmount'; + + string constant HSPOST_HUB_ERC4626_RESTORE_C = + 'HSPOST_HUB_ERC4626_RESTORE_C: previewRestoredShares must be less than or equal to the restored shares after the action'; } diff --git a/invariants/protocol-suite/Invariants.t.sol b/invariants/protocol-suite/Invariants.t.sol index 8b086ec2e..929e884e4 100644 --- a/invariants/protocol-suite/Invariants.t.sol +++ b/invariants/protocol-suite/Invariants.t.sol @@ -31,59 +31,70 @@ abstract contract Invariants is SpokeInvariants, HubInvariantAssertions { } /////////////////////////////////////////////////////////////////////////////////////////////// - // BASE INVARIANTS // + // HUB ACCOUNTING // /////////////////////////////////////////////////////////////////////////////////////////////// - function invariant_INV_HUB() public returns (bool) { - // Applied per hub + function invariant_INV_HUB_ACCOUNTING() public returns (bool) { for (uint256 i; i < hubs.length(); i++) { IHub hub = IHub(hubs.at(i)); - - // Applied per asset of the hub uint256 assetCount = hub.getAssetCount(); for (uint256 j; j < assetCount; j++) { assert_INV_HUB_A(hub, j); assert_INV_HUB_B(hub, j); assert_INV_HUB_C(hub, j); + assert_INV_HUB_GH(hub, j); + assert_INV_HUB_O(hub, j); + assert_INV_HUB_P(hub, j); + } + } + return true; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB SOLVENCY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function invariant_INV_HUB_SOLVENCY() public returns (bool) { + for (uint256 i; i < hubs.length(); i++) { + IHub hub = IHub(hubs.at(i)); + uint256 assetCount = hub.getAssetCount(); + for (uint256 j; j < assetCount; j++) { assert_INV_HUB_E(hub, j); assert_INV_HUB_F(hub, j); - assert_INV_HUB_GH(hub, j); assert_INV_HUB_I(hub, j); assert_INV_HUB_K(hub, j); - assert_INV_HUB_O(hub, j); - assert_INV_HUB_P(hub, j); + } + } + return true; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HUB MONOTONICITY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function invariant_INV_HUB_MONOTONICITY() public returns (bool) { + for (uint256 i; i < hubs.length(); i++) { + IHub hub = IHub(hubs.at(i)); + uint256 assetCount = hub.getAssetCount(); + for (uint256 j; j < assetCount; j++) { assert_INV_HUB_Q(hub, j); assert_INV_HUB_R(hub, j); } } - return true; } - function invariant_INV_SP() public returns (bool) { - // Applied per spoke + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE SYNC // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function invariant_INV_SP_SYNC() public returns (bool) { + // Applied per user-facing spoke for (uint256 i; i < spokes.length(); i++) { ISpoke spoke = ISpoke(spokes.at(i)); - - // Applied per actor on the spoke - for (uint256 j; j < actors.length(); j++) { - assert_INV_SP_D(spoke, actors.at(j)); - } - - // Applied per reserve of the spoke uint256 reserveCount = spoke.getReserveCount(); for (uint256 j; j < reserveCount; j++) { assert_INV_SP_A(spoke, j); - assert_INV_SP_C(spoke, j); - assert_INV_SP_E(spoke, j); - assert_INV_SP_F(spoke, j); - - // Applied per actor per reserve of the spoke - for (uint256 k; k < actors.length(); k++) { - assert_INV_SP_B(spoke, j, actors.at(k)); - assert_INV_SP_H(spoke, j, actors.at(k)); - assert_INV_SP_I(spoke, j, actors.at(k)); - } } } @@ -100,4 +111,65 @@ abstract contract Invariants is SpokeInvariants, HubInvariantAssertions { return true; } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE ACCOUNTING // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function invariant_INV_SP_ACCOUNTING() public returns (bool) { + for (uint256 i; i < spokes.length(); i++) { + ISpoke spoke = ISpoke(spokes.at(i)); + uint256 reserveCount = spoke.getReserveCount(); + for (uint256 j; j < reserveCount; j++) { + assert_INV_SP_C(spoke, j); + assert_INV_SP_E(spoke, j); + assert_INV_SP_F(spoke, j); + for (uint256 k; k < actors.length(); k++) { + assert_INV_SP_B(spoke, j, actors.at(k)); + } + } + } + return true; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE RISK // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function invariant_INV_SP_RISK() public returns (bool) { + for (uint256 i; i < spokes.length(); i++) { + ISpoke spoke = ISpoke(spokes.at(i)); + + // Applied per actor on the spoke + for (uint256 j; j < actors.length(); j++) { + assert_INV_SP_D(spoke, actors.at(j)); + } + + // Applied per reserve per actor of the spoke + uint256 reserveCount = spoke.getReserveCount(); + for (uint256 j; j < reserveCount; j++) { + for (uint256 k; k < actors.length(); k++) { + assert_INV_SP_H(spoke, j, actors.at(k)); + assert_INV_SP_I(spoke, j, actors.at(k)); + } + } + } + return true; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // REPLAY HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _checkAllHubInvariants() internal { + assertTrue(invariant_INV_HUB_ACCOUNTING()); + assertTrue(invariant_INV_HUB_SOLVENCY()); + assertTrue(invariant_INV_HUB_MONOTONICITY()); + } + + function _checkAllSpokeInvariants() internal { + assertTrue(invariant_INV_SP_SYNC()); + assertTrue(invariant_INV_SP_ACCOUNTING()); + assertTrue(invariant_INV_SP_RISK()); + } } diff --git a/invariants/protocol-suite/replays/ReplayTest_2.t.sol b/invariants/protocol-suite/replays/ReplayTest_2.t.sol index 92b0da3c9..eb082ba6e 100644 --- a/invariants/protocol-suite/replays/ReplayTest_2.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_2.t.sol @@ -48,7 +48,7 @@ contract ReplayTest2 is Invariants, Setup { Tester.updateUserRiskPremium(98); _delay(52383); Tester.updateUserRiskPremium(102); - invariant_INV_HUB(); + _checkAllHubInvariants(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/protocol-suite/replays/ReplayTest_3.t.sol b/invariants/protocol-suite/replays/ReplayTest_3.t.sol index f6a58d389..fde4b4236 100644 --- a/invariants/protocol-suite/replays/ReplayTest_3.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_3.t.sol @@ -44,7 +44,7 @@ contract ReplayTest3 is Invariants, Setup { _setUpActor(USER1); _delay(689004); Tester.setUsingAsCollateral(false, 15, 15); - invariant_INV_HUB(); + _checkAllHubInvariants(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/protocol-suite/replays/ReplayTest_6.t.sol b/invariants/protocol-suite/replays/ReplayTest_6.t.sol index 1f7eb94fc..17f41f5fc 100644 --- a/invariants/protocol-suite/replays/ReplayTest_6.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_6.t.sol @@ -43,7 +43,7 @@ contract ReplayTest6 is Invariants, Setup { _setUpActor(USER1); _delay(338347); Tester.freezeAllReserves(32); - invariant_INV_HUB(); + _checkAllHubInvariants(); } function test_replay_6_supply() public { @@ -54,7 +54,7 @@ contract ReplayTest6 is Invariants, Setup { _setUpActor(USER1); _delay(467); Tester.supply(1327428228, 3, 151, 99); - invariant_INV_SP(); + _checkAllSpokeInvariants(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/protocol-suite/specs/InvariantsSpec.t.sol b/invariants/protocol-suite/specs/InvariantsSpec.t.sol index 1737c9b98..602c9bf61 100644 --- a/invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -2,35 +2,9 @@ pragma solidity ^0.8.19; import {HubInvariantsSpec} from '../../hub-suite/specs/HubInvariantsSpec.t.sol'; +import {SpokeInvariantsSpec} from './SpokeInvariantsSpec.t.sol'; /// @title InvariantsSpec /// @notice Invariants specification for the protocol -/// @dev Contains spoke invariant strings. Hub invariant strings are inherited from HubInvariantsSpec. -abstract contract InvariantsSpec is HubInvariantsSpec { - /////////////////////////////////////////////////////////////////////////////////////////////// - // SPOKE // - /////////////////////////////////////////////////////////////////////////////////////////////// - - string constant INV_SP_A = - "INV_SP_A: Spoke reserve accounting should always match hub's spoke accounting on the corresponding registered asset."; - - string constant INV_SP_B = - 'INV_SP_B: User/Reserve cannot have non-zero assets and zero shares in supply or debt sides.'; - - string constant INV_SP_C = - 'INV_SP_C: Sum of spoke debts on a single asset must be greater or equal than the total debt of the reserve'; - - string constant INV_SP_D = 'INV_SP_D: Users without collateral also have no debt.'; - - string constant INV_SP_E = - 'INV_SP_E: Sum of user supplied shares on a spoke for a given asset == spoke supplied shares (hub spoke added shares)'; - - string constant INV_SP_F = - 'INV_SP_F: Sum of user supplied assets on a spoke for a given asset == spoke supplied assets (hub spoke added assets)'; - - string constant INV_SP_H = - 'INV_SP_H: User dynamicConfigKey must never exceed the reserve dynamicConfigKey (no future config reference)'; - - string constant INV_SP_I = - 'INV_SP_I: User cannot have premium shares/offset without drawn shares (premium debt is always repaid first, and can only be created when drawn shares exist)'; -} +/// @dev Aggregates hub and spoke invariant string. +abstract contract InvariantsSpec is HubInvariantsSpec, SpokeInvariantsSpec {} diff --git a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol index e94d56e6a..7b3df8759 100644 --- a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -8,12 +8,9 @@ import {HubPostconditionsSpec} from '../../hub-suite/specs/HubPostconditionsSpec /// @dev Contains spoke postcondition strings. Hub postcondition strings are inherited from HubPostconditionsSpec. abstract contract PostconditionsSpec is HubPostconditionsSpec { /////////////////////////////////////////////////////////////////////////////////////////////// - // SPOKE // + // SPOKE DEBT ORDERING // /////////////////////////////////////////////////////////////////////////////////////////////// - string constant GPOST_SP_A = - "GPOST_SP_A: Stored (user.premiumDrawnShares/user.baseDrawnShares) & calculated user risk premium (calculation based on user's position, via spoke.calculateUserAccountData) need to be the same right after an operation"; - string constant GPOST_SP_B = 'GPOST_SP_B: Premium debt of an individual user can only decrease by calling repay or liquidationCall when premium debt is not zero'; @@ -22,7 +19,12 @@ abstract contract PostconditionsSpec is HubPostconditionsSpec { string constant HSPOST_SP_C = 'HSPOST_SP_C: User liability should decrease after repayment'; - string constant HSPOST_SP_D = 'HSPOST_SP_D: Unhealthy users cannot borrow more'; + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE RISK // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant GPOST_SP_A = + "GPOST_SP_A: Stored (user.premiumDrawnShares/user.baseDrawnShares) & calculated user risk premium (calculation based on user's position, via spoke.calculateUserAccountData) need to be the same right after an operation"; string constant GPOST_SP_E = 'GPOST_SP_E: DynamicRiskConfiguration for a user position is updated to latest reserve state whenever an action can potentially make their position less healthy'; @@ -34,11 +36,17 @@ abstract contract PostconditionsSpec is HubPostconditionsSpec { string constant HSPOST_SP_F = 'HSPOST_SP_F: Total debt of a user should not change after updateUserRiskPremium'; + /////////////////////////////////////////////////////////////////////////////////////////////// + // SPOKE SOLVENCY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant HSPOST_SP_D = 'HSPOST_SP_D: Unhealthy users cannot borrow more'; + string constant GPOST_SP_H = 'GPOST_SP_H: if user totalDebt == 0 and withdraw is called, user can withdraw all supplied'; /////////////////////////////////////////////////////////////////////////////////////////////// - // SPOKE: LIQUIDATION // + // SPOKE LIQUIDATION // /////////////////////////////////////////////////////////////////////////////////////////////// string constant HSPOST_SP_LIQ_A = diff --git a/invariants/protocol-suite/specs/SpokeInvariantsSpec.t.sol b/invariants/protocol-suite/specs/SpokeInvariantsSpec.t.sol new file mode 100644 index 000000000..01d1a97a2 --- /dev/null +++ b/invariants/protocol-suite/specs/SpokeInvariantsSpec.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title SpokeInvariantsSpec +/// @notice Invariants specification for the spoke +/// @dev Contains pseudo code and description for the invariant properties in the spoke. +/// This is the canonical source for all spoke invariant strings. +abstract contract SpokeInvariantsSpec { + /////////////////////////////////////////////////////////////////////////////////////////////// + // SYNC // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant INV_SP_A = + "INV_SP_A: Spoke reserve accounting should always match hub's spoke accounting on the corresponding registered asset."; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACCOUNTING // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant INV_SP_B = + 'INV_SP_B: User/Reserve cannot have non-zero assets and zero shares in supply or debt sides.'; + + string constant INV_SP_C = + 'INV_SP_C: Sum of spoke debts on a single asset must be greater or equal than the total debt of the reserve'; + + string constant INV_SP_E = + 'INV_SP_E: Sum of user supplied shares on a spoke for a given asset == spoke supplied shares (hub spoke added shares)'; + + string constant INV_SP_F = + 'INV_SP_F: Sum of user supplied assets on a spoke for a given asset == spoke supplied assets (hub spoke added assets)'; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // RISK // + /////////////////////////////////////////////////////////////////////////////////////////////// + + string constant INV_SP_D = 'INV_SP_D: Users without collateral also have no debt.'; + + string constant INV_SP_H = + 'INV_SP_H: User dynamicConfigKey must never exceed the reserve dynamicConfigKey (no future config reference)'; + + string constant INV_SP_I = + 'INV_SP_I: User cannot have premium shares/offset without drawn shares (premium debt is always repaid first, and can only be created when drawn shares exist)'; +} From b620f5a63dc78b7e2703cc9dc38ab45370e1d8ee Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:55:03 +0530 Subject: [PATCH 073/106] chore: suppress compiler warning --- .../hub-suite/handlers/HubHandler.t.sol | 12 +++++----- .../protocol-suite/base/BaseHandler.t.sol | 3 ++- .../handlers/spoke/SpokeHandler.t.sol | 22 +++++++------------ 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index 17dc5d535..ba512c6c4 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -201,7 +201,7 @@ contract HubHandler is BaseHandler, IHubHandler { ); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( address(hub), abi.encodeCall(IHubBase.reportDeficit, (assetId, drawnAmount, premiumDelta)) ); @@ -219,7 +219,7 @@ contract HubHandler is BaseHandler, IHubHandler { address spoke = address(actors[USER3]); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( address(hub), abi.encodeCall(IHub.eliminateDeficit, (assetId, amount, spoke)) ); @@ -242,7 +242,7 @@ contract HubHandler is BaseHandler, IHubHandler { }); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( address(hub), abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta)) ); @@ -266,7 +266,7 @@ contract HubHandler is BaseHandler, IHubHandler { uint256 assetId = _getRandomBaseAssetId(i); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( address(hub), abi.encodeCall(IHubBase.refreshPremium, (assetId, premiumDelta)) ); @@ -289,7 +289,7 @@ contract HubHandler is BaseHandler, IHubHandler { uint256 assetId = _getRandomBaseAssetId(i); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( address(hub), abi.encodeCall(IHubBase.payFeeShares, (assetId, shares)) ); @@ -305,7 +305,7 @@ contract HubHandler is BaseHandler, IHubHandler { address toSpoke = _getRandomActor(j); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( address(hub), abi.encodeCall(IHub.transferShares, (assetId, shares, toSpoke)) ); diff --git a/invariants/protocol-suite/base/BaseHandler.t.sol b/invariants/protocol-suite/base/BaseHandler.t.sol index 6e7021426..3cbf80301 100644 --- a/invariants/protocol-suite/base/BaseHandler.t.sol +++ b/invariants/protocol-suite/base/BaseHandler.t.sol @@ -88,7 +88,8 @@ contract BaseHandler is HookAggregator { /// so even if minting the full amount overflows, the spoke will operate on /// whatever balance exists. function _tryMint(address token, address receiver, uint256 amount) internal { - token.call(abi.encodeCall(MockERC20.mint, (receiver, amount))); + (bool ok, ) = token.call(abi.encodeCall(MockERC20.mint, (receiver, amount))); + ok; // suppress compiler warning } /// @notice Best-effort mint + approve for spoke handlers diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index 74397cc8e..de4175e40 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -63,7 +63,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _tryMintAndApprove(underlying, address(actor), spoke, amount); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( spoke, abi.encodeCall(ISpokeBase.supply, (reserveId, amount, onBehalfOf)) ); @@ -90,7 +90,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, reserveId, onBehalfOf); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( spoke, abi.encodeCall(ISpokeBase.withdraw, (reserveId, amount, onBehalfOf)) ); @@ -130,7 +130,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { bool isHealthy = _isHealthy(spoke, onBehalfOf); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( spoke, abi.encodeCall(ISpokeBase.borrow, (reserveId, amount, onBehalfOf)) ); @@ -162,7 +162,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _tryMintAndApprove(underlying, address(actor), spoke, amount); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( spoke, abi.encodeCall(ISpokeBase.repay, (reserveId, amount, onBehalfOf)) ); @@ -254,7 +254,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( liquidationVars.spoke, abi.encodeCall( ISpokeBase.liquidationCall, @@ -362,7 +362,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, (usingAsCollateral ? reserveId : CHECK_ALL_RESERVES), onBehalfOf); _before(); - (bool ok, bytes memory ret) = actor.proxy( + (bool ok, ) = actor.proxy( spoke, abi.encodeCall(ISpoke.setUsingAsCollateral, (reserveId, usingAsCollateral, onBehalfOf)) ); @@ -383,10 +383,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); _before(); - (bool ok, bytes memory ret) = actor.proxy( - spoke, - abi.encodeCall(ISpoke.updateUserRiskPremium, (onBehalfOf)) - ); + (bool ok, ) = actor.proxy(spoke, abi.encodeCall(ISpoke.updateUserRiskPremium, (onBehalfOf))); if (ok) { _after(); @@ -411,10 +408,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); _before(); - (bool ok, bytes memory ret) = actor.proxy( - spoke, - abi.encodeCall(ISpoke.updateUserDynamicConfig, (onBehalfOf)) - ); + (bool ok, ) = actor.proxy(spoke, abi.encodeCall(ISpoke.updateUserDynamicConfig, (onBehalfOf))); if (ok) { _after(); From 83769cc0650a7bdfcb1f8963cfec9c4bb596204e Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:57:08 +0530 Subject: [PATCH 074/106] chore: naming consistency --- invariants/hub-suite/Invariants.t.sol | 12 ++++----- invariants/hub-suite/Setup.t.sol | 26 +++++++++---------- invariants/hub-suite/base/BaseStorage.t.sol | 4 +-- invariants/hub-suite/base/BaseTest.t.sol | 4 +-- .../hub-suite/handlers/HubHandler.t.sol | 2 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 2 +- .../hub-suite/hooks/HookAggregator.t.sol | 2 +- .../hub-suite/invariants/HubInvariants.t.sol | 2 +- .../hub-suite/replays/ReplayTest_1.t.sol | 10 +++---- .../hub-suite/replays/ReplayTest_2.t.sol | 10 +++---- .../hub-suite/replays/ReplayTest_3.t.sol | 10 +++---- .../hub-suite/replays/ReplayTest_4.t.sol | 10 +++---- .../hub-suite/replays/ReplayTest_5.t.sol | 10 +++---- .../hub-suite/replays/ReplayTest_6.t.sol | 10 +++---- .../hub-suite/replays/ReplayTest_7.t.sol | 10 +++---- .../hub-suite/replays/ReplayTest_8.t.sol | 10 +++---- .../hub-suite/replays/ReplayTest_9.t.sol | 10 +++---- 17 files changed, 72 insertions(+), 72 deletions(-) diff --git a/invariants/hub-suite/Invariants.t.sol b/invariants/hub-suite/Invariants.t.sol index 20786da3e..23f9d994c 100644 --- a/invariants/hub-suite/Invariants.t.sol +++ b/invariants/hub-suite/Invariants.t.sol @@ -59,8 +59,8 @@ abstract contract Invariants is HubInvariants { uint256 assetCount = hub.getAssetCount(); for (uint256 i; i < assetCount; i++) { for (uint256 j; j < NUMBER_OF_ACTORS; j++) { - assert_INV_HUB_ERC4626_A(i, actorAddresses[j]); - assert_INV_HUB_ERC4626_B(i, actorAddresses[j]); + assert_INV_HUB_ERC4626_A(i, actors[j]); + assert_INV_HUB_ERC4626_B(i, actors[j]); } assert_INV_HUB_ERC4626_C(i); assert_INV_HUB_ERC4626_D(i); @@ -81,10 +81,10 @@ abstract contract Invariants is HubInvariants { assert_INV_HUB_AVAILABILITY_D(i); assert_INV_HUB_AVAILABILITY_E(i); for (uint256 j; j < NUMBER_OF_ACTORS; j++) { - assert_INV_HUB_AVAILABILITY_F(i, actorAddresses[j]); - assert_INV_HUB_AVAILABILITY_G(i, actorAddresses[j]); - assert_INV_HUB_AVAILABILITY_H(i, actorAddresses[j]); - assert_INV_HUB_AVAILABILITY_I(i, actorAddresses[j]); + assert_INV_HUB_AVAILABILITY_F(i, actors[j]); + assert_INV_HUB_AVAILABILITY_G(i, actors[j]); + assert_INV_HUB_AVAILABILITY_H(i, actors[j]); + assert_INV_HUB_AVAILABILITY_I(i, actors[j]); } } return true; diff --git a/invariants/hub-suite/Setup.t.sol b/invariants/hub-suite/Setup.t.sol index 95ef51940..37fe58c93 100644 --- a/invariants/hub-suite/Setup.t.sol +++ b/invariants/hub-suite/Setup.t.sol @@ -178,7 +178,7 @@ contract Setup is BaseTest { // Add SPOKE 1 assets to hub hub.addSpoke( usdcAssetId, - address(actors[USER1]), + address(userToActor[USER1]), IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, @@ -189,7 +189,7 @@ contract Setup is BaseTest { ); hub.addSpoke( wethAssetId, - address(actors[USER1]), + address(userToActor[USER1]), IHub.SpokeConfig({ addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 3, @@ -200,7 +200,7 @@ contract Setup is BaseTest { ); hub.addSpoke( wbtcAssetId, - address(actors[USER1]), + address(userToActor[USER1]), IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, @@ -213,7 +213,7 @@ contract Setup is BaseTest { // Add SPOKE 2 assets to hub hub.addSpoke( wethAssetId, - address(actors[USER2]), + address(userToActor[USER2]), IHub.SpokeConfig({ addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, @@ -224,7 +224,7 @@ contract Setup is BaseTest { ); hub.addSpoke( wbtcAssetId, - address(actors[USER2]), + address(userToActor[USER2]), IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, @@ -237,7 +237,7 @@ contract Setup is BaseTest { // Add SPOKE 3 assets to hub hub.addSpoke( usdcAssetId, - address(actors[USER3]), + address(userToActor[USER3]), IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, @@ -248,7 +248,7 @@ contract Setup is BaseTest { ); hub.addSpoke( wethAssetId, - address(actors[USER3]), + address(userToActor[USER3]), IHub.SpokeConfig({ addCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, drawCap: (Constants.MAX_ALLOWED_SPOKE_CAP / 10) * 2, @@ -259,7 +259,7 @@ contract Setup is BaseTest { ); hub.addSpoke( wbtcAssetId, - address(actors[USER3]), + address(userToActor[USER3]), IHub.SpokeConfig({ addCap: Constants.MAX_ALLOWED_SPOKE_CAP, drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, @@ -273,7 +273,7 @@ contract Setup is BaseTest { weth.approve(address(hub), type(uint256).max); wbtc.approve(address(hub), type(uint256).max); - accessManager.grantRole(Roles.DEFICIT_ELIMINATOR_ROLE, address(actors[USER3]), 0); + accessManager.grantRole(Roles.DEFICIT_ELIMINATOR_ROLE, address(userToActor[USER3]), 0); { bytes4[] memory selectors = new bytes4[](1); selectors[0] = IHub.eliminateDeficit.selector; @@ -349,9 +349,9 @@ contract Setup is BaseTest { address[] memory contracts = new address[](1); contracts[0] = address(hub); - actorAddresses = ActorsUtils.setUpActors(addresses, tokens, contracts); - actors[USER1] = Actor(payable(actorAddresses[0])); - actors[USER2] = Actor(payable(actorAddresses[1])); - actors[USER3] = Actor(payable(actorAddresses[2])); + actors = ActorsUtils.setUpActors(addresses, tokens, contracts); + userToActor[USER1] = Actor(payable(actors[0])); + userToActor[USER2] = Actor(payable(actors[1])); + userToActor[USER3] = Actor(payable(actors[2])); } } diff --git a/invariants/hub-suite/base/BaseStorage.t.sol b/invariants/hub-suite/base/BaseStorage.t.sol index 6e21605a5..2c20a9c8d 100644 --- a/invariants/hub-suite/base/BaseStorage.t.sol +++ b/invariants/hub-suite/base/BaseStorage.t.sol @@ -39,10 +39,10 @@ abstract contract BaseStorage { Actor internal actor; /// @notice Mapping of fuzzer user addresses to actors - mapping(address => Actor) internal actors; + mapping(address => Actor) internal userToActor; /// @notice Array of all actor addresses (simulating spokes) - address[] internal actorAddresses; + address[] internal actors; /// @notice The signature of the action that is being executed bytes4 internal currentActionSignature; diff --git a/invariants/hub-suite/base/BaseTest.t.sol b/invariants/hub-suite/base/BaseTest.t.sol index ce3ba9db4..ff0521014 100644 --- a/invariants/hub-suite/base/BaseTest.t.sol +++ b/invariants/hub-suite/base/BaseTest.t.sol @@ -30,7 +30,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @dev Actor proxy mechanism modifier setup() virtual { - actor = actors[msg.sender]; + actor = userToActor[msg.sender]; _; delete actor; } @@ -64,7 +64,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU /// @notice Get a random actor proxy address function _getRandomActor(uint256 _i) internal view returns (address) { uint256 _actorIndex = _i % NUMBER_OF_ACTORS; - return actorAddresses[_actorIndex]; + return actors[_actorIndex]; } /// @notice Helper function to get a random base asset diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index ba512c6c4..f6f87c60d 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -216,7 +216,7 @@ contract HubHandler is BaseHandler, IHubHandler { function eliminateDeficit(uint256 amount, uint8 i) external setup { uint256 assetId = _getRandomBaseAssetId(i); // only spoke3 is given deficit eliminator role - address spoke = address(actors[USER3]); + address spoke = address(userToActor[USER3]); _before(); (bool ok, ) = actor.proxy( diff --git a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index 3768e377f..9a45446b5 100644 --- a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -116,7 +116,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { uint256 assetCount = hub.getAssetCount(); for (uint256 i; i < assetCount; ++i) { for (uint256 j; j < NUMBER_OF_ACTORS; ++j) { - address spoke = actorAddresses[j]; + address spoke = actors[j]; (uint256 drawn, uint256 premium) = hub.getSpokeOwed(i, spoke); vars.spokeDataVars[i][spoke] = SpokeDataVars({ spokeData: hub.getSpoke(i, spoke), diff --git a/invariants/hub-suite/hooks/HookAggregator.t.sol b/invariants/hub-suite/hooks/HookAggregator.t.sol index 6a824ffd7..d65fb3ba3 100644 --- a/invariants/hub-suite/hooks/HookAggregator.t.sol +++ b/invariants/hub-suite/hooks/HookAggregator.t.sol @@ -65,7 +65,7 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { assert_GPOST_HUB_G(i); for (uint256 j; j < NUMBER_OF_ACTORS; j++) { - address spoke = actorAddresses[j]; + address spoke = actors[j]; assert_GPOST_HUB_EF(i, spoke); } } diff --git a/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol index c4328444c..c8909560a 100644 --- a/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -26,7 +26,7 @@ abstract contract HubInvariants is HandlerAggregator, HubInvariantAssertions { uint256 count = NUMBER_OF_ACTORS; address[] memory spokes = new address[](count + 1); for (uint256 i; i < count; i++) { - spokes[i] = actorAddresses[i]; + spokes[i] = actors[i]; } spokes[count] = feeReceiver; return spokes; diff --git a/invariants/hub-suite/replays/ReplayTest_1.t.sol b/invariants/hub-suite/replays/ReplayTest_1.t.sol index 2ab57c299..6bec9439a 100644 --- a/invariants/hub-suite/replays/ReplayTest_1.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_1.t.sol @@ -27,7 +27,7 @@ contract ReplayTest1 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -57,25 +57,25 @@ contract ReplayTest1 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/hub-suite/replays/ReplayTest_2.t.sol b/invariants/hub-suite/replays/ReplayTest_2.t.sol index 98bb5fb4a..a123a45c1 100644 --- a/invariants/hub-suite/replays/ReplayTest_2.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_2.t.sol @@ -27,7 +27,7 @@ contract ReplayTest2Hub is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -99,25 +99,25 @@ contract ReplayTest2Hub is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/hub-suite/replays/ReplayTest_3.t.sol b/invariants/hub-suite/replays/ReplayTest_3.t.sol index b51c91017..8f9695c2f 100644 --- a/invariants/hub-suite/replays/ReplayTest_3.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_3.t.sol @@ -27,7 +27,7 @@ contract ReplayTest3Hub is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -58,25 +58,25 @@ contract ReplayTest3Hub is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/hub-suite/replays/ReplayTest_4.t.sol b/invariants/hub-suite/replays/ReplayTest_4.t.sol index 42378d9c1..19f22ecf6 100644 --- a/invariants/hub-suite/replays/ReplayTest_4.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_4.t.sol @@ -27,7 +27,7 @@ contract ReplayTest4 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -69,25 +69,25 @@ contract ReplayTest4 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/hub-suite/replays/ReplayTest_5.t.sol b/invariants/hub-suite/replays/ReplayTest_5.t.sol index 696ebc68e..b662d644c 100644 --- a/invariants/hub-suite/replays/ReplayTest_5.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_5.t.sol @@ -26,7 +26,7 @@ contract ReplayTest5Hub is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -56,25 +56,25 @@ contract ReplayTest5Hub is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/hub-suite/replays/ReplayTest_6.t.sol b/invariants/hub-suite/replays/ReplayTest_6.t.sol index 35b93dd3b..a94233116 100644 --- a/invariants/hub-suite/replays/ReplayTest_6.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_6.t.sol @@ -27,7 +27,7 @@ contract ReplayTest6 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -103,25 +103,25 @@ contract ReplayTest6 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/hub-suite/replays/ReplayTest_7.t.sol b/invariants/hub-suite/replays/ReplayTest_7.t.sol index 2a53db6eb..82bfdf388 100644 --- a/invariants/hub-suite/replays/ReplayTest_7.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_7.t.sol @@ -27,7 +27,7 @@ contract ReplayTest7 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -56,25 +56,25 @@ contract ReplayTest7 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/hub-suite/replays/ReplayTest_8.t.sol b/invariants/hub-suite/replays/ReplayTest_8.t.sol index a12c4d844..0fec3db4e 100644 --- a/invariants/hub-suite/replays/ReplayTest_8.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_8.t.sol @@ -27,7 +27,7 @@ contract ReplayTest8 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -66,25 +66,25 @@ contract ReplayTest8 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } diff --git a/invariants/hub-suite/replays/ReplayTest_9.t.sol b/invariants/hub-suite/replays/ReplayTest_9.t.sol index 8546899a4..92c36ecc3 100644 --- a/invariants/hub-suite/replays/ReplayTest_9.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_9.t.sol @@ -27,7 +27,7 @@ contract ReplayTest9 is Invariants, Setup { _setUp(); /// @dev fixes the actor to the first user - actor = actors[USER1]; + actor = userToActor[USER1]; vm.warp(101007); } @@ -53,25 +53,25 @@ contract ReplayTest9 is Invariants, Setup { /// @notice Set up an actor function _setUpActor(address _origin) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; } /// @notice Set up an actor and fast forward the time /// @dev Use for ECHIDNA call-traces function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { - actor = actors[_origin]; + actor = userToActor[_origin]; vm.warp(block.timestamp + _seconds); } /// @notice Set up a specific block and actor function _setUpBlockAndActor(uint256 _block, address _user) internal { vm.roll(_block); - actor = actors[_user]; + actor = userToActor[_user]; } /// @notice Set up a specific timestamp and actor function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { vm.warp(_timestamp); - actor = actors[_user]; + actor = userToActor[_user]; } } From 18cc79fc08a98c136e7c26cf6e89c3515289cd8d Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:57:41 +0530 Subject: [PATCH 075/106] chore: fill interfaces --- .../hub-suite/handlers/HubHandler.t.sol | 2 +- .../interfaces/IHubConfiguratorHandler.sol | 12 ++++++++++- .../handlers/interfaces/IHubHandler.sol | 20 +++++++++++++++++++ .../interfaces/IHubConfiguratorHandler.sol | 15 +++++++++++++- .../interfaces/ISpokeConfiguratorHandler.sol | 18 ++++++++++++++++- .../interfaces/ITreasurySpokeHandler.sol | 8 +++++++- 6 files changed, 70 insertions(+), 5 deletions(-) diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index f6f87c60d..e5e16100c 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -262,7 +262,7 @@ contract HubHandler is BaseHandler, IHubHandler { } // @dev broader `refreshPremium` to cover edge cases, above case exists for narrow happy path - function refreshPremium(IHubBase.PremiumDelta memory premiumDelta, uint8 i) external setup { + function refreshPremiumBroad(IHubBase.PremiumDelta memory premiumDelta, uint8 i) external setup { uint256 assetId = _getRandomBaseAssetId(i); _before(); diff --git a/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol index d12003b5e..14715b90f 100644 --- a/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol +++ b/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol @@ -3,4 +3,14 @@ pragma solidity ^0.8.19; /// @title IHubConfiguratorHandler /// @notice Interface for the HubConfiguratorHandler -interface IHubConfiguratorHandler {} +interface IHubConfiguratorHandler { + function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j) external; + + function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j) external; + + function updateSpokeRiskPremiumThreshold(uint256 riskPremiumThreshold, uint8 i, uint8 j) external; + + function updateSpokeHalted(bool halted, uint8 i, uint8 j) external; + + function updateLiquidityFee(uint256 liquidityFee, uint8 i) external; +} diff --git a/invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubHandler.sol index 6b9bacab5..0da8f98c3 100644 --- a/invariants/hub-suite/handlers/interfaces/IHubHandler.sol +++ b/invariants/hub-suite/handlers/interfaces/IHubHandler.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +import {IHubBase} from 'src/hub/interfaces/IHub.sol'; + /// @title IHubHandler /// @notice Interface for hub handler actions interface IHubHandler { @@ -28,6 +30,8 @@ interface IHubHandler { function refreshPremium(int256 sharesDelta, uint8 i) external; + function refreshPremiumBroad(IHubBase.PremiumDelta memory premiumDelta, uint8 i) external; + function payFeeShares(uint256 shares, uint8 i) external; function transferShares(uint256 shares, uint8 i, uint8 j) external; @@ -35,4 +39,20 @@ interface IHubHandler { function sweep(uint256 amount, uint8 i) external; function reclaim(uint256 amount, uint8 i) external; + + function roundtrip_ERC4626_RT_A(uint256 amount, uint8 i) external; + + function roundtrip_ERC4626_RT_B(uint256 amount, uint8 i) external; + + function roundtrip_ERC4626_RT_C(uint256 shares, uint8 i) external; + + function roundtrip_ERC4626_RT_D(uint256 shares, uint8 i) external; + + function roundtrip_ERC4626_RT_E(uint256 shares, uint8 i) external; + + function roundtrip_ERC4626_RT_F(uint256 shares, uint8 i) external; + + function roundtrip_ERC4626_RT_G(uint256 amount, uint8 i) external; + + function roundtrip_ERC4626_RT_H(uint256 amount, uint8 i) external; } diff --git a/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol index d12003b5e..b6818d4cc 100644 --- a/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol @@ -3,4 +3,17 @@ pragma solidity ^0.8.19; /// @title IHubConfiguratorHandler /// @notice Interface for the HubConfiguratorHandler -interface IHubConfiguratorHandler {} +interface IHubConfiguratorHandler { + function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j, uint8 k) external; + + function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j, uint8 k) external; + + function updateSpokeRiskPremiumThreshold( + uint256 riskPremiumThreshold, + uint8 i, + uint8 j, + uint8 k + ) external; + + function updateSpokeHalted(bool halted, uint8 i, uint8 j, uint8 k) external; +} diff --git a/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol b/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol index a5d1ce267..ccb456b56 100644 --- a/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol @@ -3,4 +3,20 @@ pragma solidity ^0.8.19; /// @title ISpokeConfiguratorHandler /// @notice Interface for the SpokeConfiguratorHandler -interface ISpokeConfiguratorHandler {} +interface ISpokeConfiguratorHandler { + function updateLiquidationTargetHealthFactor(uint256 targetHealthFactor, uint8 i) external; + + function updateHealthFactorForMaxBonus(uint256 healthFactorForMaxBonus, uint8 i) external; + + function updateLiquidationBonusFactor(uint256 liquidationBonusFactor, uint8 i) external; + + function updatePaused(bool halted, uint8 i, uint8 j) external; + + function updateFrozen(bool frozen, uint8 i, uint8 j) external; + + function updateBorrowable(bool borrowable, uint8 i, uint8 j) external; + + function pauseAllReserves(uint8 i) external; + + function freezeAllReserves(uint8 i) external; +} diff --git a/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol b/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol index 07108f442..a17a34ee6 100644 --- a/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol @@ -3,4 +3,10 @@ pragma solidity ^0.8.19; /// @title ITreasurySpokeHandler /// @notice Interface for the TreasurySpokeHandler -interface ITreasurySpokeHandler {} +interface ITreasurySpokeHandler { + function supply(uint256 amount, uint8 i, uint8 j) external; + + function withdraw(uint256 amount, uint8 i, uint8 j) external; + + function transfer(uint256 amount, uint8 i, uint8 j, uint8 k) external; +} From cdd364d3933c8cd91619ded169eab07204c67264 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:31:27 +0530 Subject: [PATCH 076/106] chore: fix configurator roles --- invariants/protocol-suite/Setup.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol index 91935e872..297367d45 100644 --- a/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -545,6 +545,8 @@ contract Setup is BaseTest { // Grant roles to configurators accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); accessManager.grantRole(Roles.SPOKE_ADMIN_ROLE, address(spokeConfigurator), 0); + accessManager.grantRole(Roles.HUB_CONFIGURATOR_ROLE, address(this), 0); + accessManager.grantRole(Roles.SPOKE_CONFIGURATOR_ROLE, address(this), 0); // Grant responsibilities to spokes { @@ -596,7 +598,7 @@ contract Setup is BaseTest { accessManager.setTargetFunctionRole( address(hubConfigurator), selectors, - type(uint64).max // !todo public role, fix this + Roles.HUB_CONFIGURATOR_ROLE ); } @@ -630,7 +632,7 @@ contract Setup is BaseTest { accessManager.setTargetFunctionRole( address(spokeConfigurator), selectors, - type(uint64).max // !todo public role, fix this + Roles.SPOKE_CONFIGURATOR_ROLE ); } } From 6427e1cee89c1ad571090c9aa632c40326ca3d46 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:42:50 +0530 Subject: [PATCH 077/106] fix: add new solvency invariants --- .../hub-suite/handlers/HubHandler.t.sol | 2 +- invariants/protocol-suite/base/BaseTest.t.sol | 6 +- .../handlers/spoke/SpokeHandler.t.sol | 89 +++++-------------- .../protocol-suite/hooks/HookAggregator.t.sol | 1 - .../specs/PostconditionsSpec.t.sol | 10 ++- 5 files changed, 32 insertions(+), 76 deletions(-) diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index e5e16100c..b452f0d70 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -39,7 +39,7 @@ contract HubHandler is BaseHandler, IHubHandler { if (ok) { _after(); - addedShares = uint256(abi.decode(ret, (uint256))); + addedShares = abi.decode(ret, (uint256)); assertGe( assetsBefore + amount, diff --git a/invariants/protocol-suite/base/BaseTest.t.sol b/invariants/protocol-suite/base/BaseTest.t.sol index a3b8be8e0..89ec121ec 100644 --- a/invariants/protocol-suite/base/BaseTest.t.sol +++ b/invariants/protocol-suite/base/BaseTest.t.sol @@ -26,7 +26,7 @@ import {BaseStorage} from './BaseStorage.t.sol'; abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdUtils { using EnumerableSet for EnumerableSet.AddressSet; - bool internal IS_TEST = true; // todo! make public + bool public IS_TEST = true; /////////////////////////////////////////////////////////////////////////////////////////////// // ACTOR PROXY MECHANISM // @@ -188,4 +188,8 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU uint256 totalShares = hub_.getAddedShares(assetId_); return totalAssets - hub_.previewRemoveByShares(assetId_, totalShares); } + + function _underlying(ISpoke spoke, uint256 reserveId) internal view returns (address) { + return spoke.getReserve(reserveId).underlying; + } } diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index de4175e40..8ba3cfb9f 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -48,19 +48,12 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { /////////////////////////////////////////////////////////////////////////////////////////////// function supply(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - // Get one of the three actors randomly address onBehalfOf = _getRandomActor(i); - address spoke = _getRandomSpoke(j); - - // Get one of the reserves IDs randomly uint256 reserveId = _getRandomReserveId(spoke, k); + _registerUserToCheck(spoke, reserveId, onBehalfOf); // register user to check post conditions - // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, onBehalfOf); - - address underlying = ISpoke(spoke).getReserve(reserveId).underlying; - _tryMintAndApprove(underlying, address(actor), spoke, amount); + _tryMintAndApprove(_underlying(spoke, reserveId), address(actor), spoke, amount); _before(); (bool ok, ) = actor.proxy( @@ -76,18 +69,11 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } function withdraw(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - // Get one of the three actors randomly address onBehalfOf = _getRandomActor(i); - address spoke = _getRandomSpoke(j); - - // Get one of the reserves IDs randomly uint256 reserveId = _getRandomReserveId(spoke, k); - - uint256 userAmount = ISpoke(spoke).getUserSuppliedAssets(reserveId, onBehalfOf); - - // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, onBehalfOf); + _registerUserToCheck(spoke, reserveId, onBehalfOf); // register user to check post conditions + bool healthyBefore = _isHealthy(spoke, onBehalfOf); _before(); (bool ok, ) = actor.proxy( @@ -95,39 +81,22 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { abi.encodeCall(ISpokeBase.withdraw, (reserveId, amount, onBehalfOf)) ); - // GPOST_SP_H: debt-free user with valid auth and unblocked reserve must be able to withdraw - // todo make this "position healthy" after withdraw, add similar check on borrow - if ( - _isAuthorized(spoke, onBehalfOf) && - !_isReserveActionBlocked(spoke, reserveId, false, false) && - _userVarsBefore(spoke, reserveId, onBehalfOf).debt.owed == 0 && - amount > 0 && - userAmount != 0 - ) { - assertTrue(ok, GPOST_SP_H); - } - if (ok) { _after(); + + assertTrue(healthyBefore, GPOST_SP_H); + assertTrue(_isHealthy(spoke, onBehalfOf), HSPOST_SP_I); } else { revert('SpokeHandler: withdraw failed'); } } function borrow(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - // Get one of the three actors randomly address onBehalfOf = _getRandomActor(i); - address spoke = _getRandomSpoke(j); - - // Get one of the reserves IDs randomly uint256 reserveId = _getRandomReserveId(spoke, k); - - // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, onBehalfOf); - - // Check if user is healthy - bool isHealthy = _isHealthy(spoke, onBehalfOf); + _registerUserToCheck(spoke, reserveId, onBehalfOf); // register user to check post conditions + bool healthyBefore = _isHealthy(spoke, onBehalfOf); _before(); (bool ok, ) = actor.proxy( @@ -138,28 +107,20 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (ok) { _after(); - ///// HSPOST ///// - - assertTrue(isHealthy, HSPOST_SP_D); + assertTrue(healthyBefore, HSPOST_SP_D); + assertTrue(_isHealthy(spoke, onBehalfOf), HSPOST_SP_I); } else { revert('SpokeHandler: borrow failed'); } } function repay(uint256 amount, uint8 i, uint8 j, uint8 k) external setup { - // Get one of the three actors randomly address onBehalfOf = _getRandomActor(i); - address spoke = _getRandomSpoke(j); - - // Get one of the reserves IDs randomly uint256 reserveId = _getRandomReserveId(spoke, k); + _registerUserToCheck(spoke, reserveId, onBehalfOf); // register user to check post conditions - // Register user to check postconditions - _registerUserToCheck(spoke, reserveId, onBehalfOf); - - address underlying = ISpoke(spoke).getReserve(reserveId).underlying; - _tryMintAndApprove(underlying, address(actor), spoke, amount); + _tryMintAndApprove(_underlying(spoke, reserveId), address(actor), spoke, amount); _before(); (bool ok, ) = actor.proxy( @@ -170,8 +131,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (ok) { _after(); - ///// HSPOST ///// - assertLe( _userVarsAfter(spoke, reserveId, onBehalfOf).debt.owed, _userVarsBefore(spoke, reserveId, onBehalfOf).debt.owed, @@ -191,15 +150,12 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { uint8 l ) external setup { LiquidationVars memory liquidationVars; - - // Get one of the three actors randomly liquidationVars.spoke = _getRandomSpoke(j); liquidationVars.debtToCover = debtToCover; liquidationVars.violator = _getRandomActor(i); liquidationVars.liquidator = address(actor); - // Get both reserves IDs randomly and the collateral underlying asset liquidationVars.collateralReserveId = _getRandomReserveId(liquidationVars.spoke, k); liquidationVars.debtReserveId = _getRandomReserveId(liquidationVars.spoke, l); liquidationVars.underlying = ISpoke(liquidationVars.spoke) @@ -224,7 +180,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { .balanceOf(address(actor)); } - // Register users to check postconditions: liquidated user and liquidator for both reserves + // register users to check post conditions: liquidated user and liquidator for both reserves _registerUserToCheck( liquidationVars.spoke, liquidationVars.debtReserveId, @@ -247,7 +203,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ); _tryMintAndApprove( - ISpoke(liquidationVars.spoke).getReserve(liquidationVars.debtReserveId).underlying, + _underlying(liquidationVars.spoke, liquidationVars.debtReserveId), address(actor), liquidationVars.spoke, debtToCover @@ -295,7 +251,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { .balanceOf(address(actor)); } - ///// HSPOST ///// + /// HSPOST /// assertLe(liquidationVars.debtLiquidated, violatorDebtVarsBefore.debt.owed, HSPOST_SP_LIQ_A); if ( @@ -342,19 +298,16 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external setup { address onBehalfOf = address(actor); - address spoke = _getRandomSpoke(i); - uint256 reserveId = _getRandomReserveId(spoke, j); (bool isUsingAsCollateral, ) = ISpoke(spoke).getUserReserveStatus(reserveId, onBehalfOf); - // !todo remove this require( usingAsCollateral != isUsingAsCollateral, 'SpokeHandler: usingAsCollateral already set' - ); + ); // usingAsCollateral - // Register user to check postconditions + // register user to check post conditions /// @dev setUsingAsCollateral(reserveId, FALSE) all reserves in user position should be refreshed, /// so we check all reserves in user position /// setUsingAsCollateral(reserveId, TRUE) only reserveId in user position should be refreshed, @@ -376,11 +329,8 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { function updateUserRiskPremium(uint8 i) external setup { address onBehalfOf = address(actor); - address spoke = _getRandomSpoke(i); - - // Register user to check postconditions - _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); + _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); // register user to check post conditions _before(); (bool ok, ) = actor.proxy(spoke, abi.encodeCall(ISpoke.updateUserRiskPremium, (onBehalfOf))); @@ -412,6 +362,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (ok) { _after(); + assertTrue(_isHealthy(spoke, onBehalfOf), HSPOST_SP_I); } else { revert('SpokeHandler: updateUserDynamicConfig failed'); } diff --git a/invariants/protocol-suite/hooks/HookAggregator.t.sol b/invariants/protocol-suite/hooks/HookAggregator.t.sol index b92db04bf..18fc75ccb 100644 --- a/invariants/protocol-suite/hooks/HookAggregator.t.sol +++ b/invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -109,7 +109,6 @@ abstract contract HookAggregator is DefaultBeforeAfterHooks { // Check properties for all reserves of the spoke, used after actions: updateUserRiskPremium, updateUserDynamicConfig if (reserveId == CHECK_ALL_RESERVES) { uint256 reserveCount = ISpoke(spoke).getReserveCount(); - // Iterate through all reserves of the spoke for (uint256 j; j < reserveCount; j++) { assert_GPOST_SP_A(spoke, j, user); assert_GPOST_SP_B(spoke, j, user); diff --git a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol index 7b3df8759..10bdedcbe 100644 --- a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -40,10 +40,12 @@ abstract contract PostconditionsSpec is HubPostconditionsSpec { // SPOKE SOLVENCY // /////////////////////////////////////////////////////////////////////////////////////////////// - string constant HSPOST_SP_D = 'HSPOST_SP_D: Unhealthy users cannot borrow more'; + string constant HSPOST_SP_D = 'HSPOST_SP_D: Unhealthy user cannot borrow more'; - string constant GPOST_SP_H = - 'GPOST_SP_H: if user totalDebt == 0 and withdraw is called, user can withdraw all supplied'; + string constant GPOST_SP_H = 'GPOST_SP_H: Unhealthy user cannot withdraw active collateral'; + + string constant HSPOST_SP_I = + 'HSPOST_SP_I: User is healthy after borrow/withdraw/updateUserDynamicConfig'; /////////////////////////////////////////////////////////////////////////////////////////////// // SPOKE LIQUIDATION // @@ -61,7 +63,7 @@ abstract contract PostconditionsSpec is HubPostconditionsSpec { string constant HSPOST_SP_LIQ_D = 'HSPOST_SP_LIQ_D: Liquidation cannot result in an amount of liquidated debt > debtToCover'; - string constant HSPOST_SP_LIQ_E = 'HSPOST_SP_LIQ_E: Only unhealthy users can be liquidated'; + string constant HSPOST_SP_LIQ_E = 'HSPOST_SP_LIQ_E: Only unhealthy user can be liquidated'; string constant HSPOST_SP_LIQ_G = 'HSPOST_SP_LIQ_G: After liquidation, if debt remains, health factor should improve toward target'; From 9d83eea0db17f20666e610e41fca3da49fb6d193 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:10:15 +0530 Subject: [PATCH 078/106] chore: fix compile --- invariants/protocol-suite/base/BaseTest.t.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invariants/protocol-suite/base/BaseTest.t.sol b/invariants/protocol-suite/base/BaseTest.t.sol index 89ec121ec..a6efd326f 100644 --- a/invariants/protocol-suite/base/BaseTest.t.sol +++ b/invariants/protocol-suite/base/BaseTest.t.sol @@ -192,4 +192,8 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU function _underlying(ISpoke spoke, uint256 reserveId) internal view returns (address) { return spoke.getReserve(reserveId).underlying; } + + function _underlying(address spoke, uint256 reserveId) internal view returns (address) { + return _underlying(ISpoke(spoke), reserveId); + } } From 5b33ae42b8e005ff9a8e7f25b44f50a914b1f401 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:10:52 +0530 Subject: [PATCH 079/106] feat: set user posm to handler --- .../handlers/interfaces/ISpokeHandler.sol | 1 + .../handlers/spoke/SpokeHandler.t.sol | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol b/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol index 839a07d0f..9309e5832 100644 --- a/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol @@ -19,4 +19,5 @@ interface ISpokeHandler { function setUsingAsCollateral(bool usingAsCollateral, uint8 i, uint8 j) external; function updateUserRiskPremium(uint8 i) external; function updateUserDynamicConfig(uint8 i) external; + function setUserPositionManager(bool approve, uint8 i, uint8 j) external; } diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index 8ba3cfb9f..e751bcf55 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -352,7 +352,6 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { function updateUserDynamicConfig(uint8 i) external setup { address onBehalfOf = address(actor); - address spoke = _getRandomSpoke(i); _registerUserToCheck(spoke, CHECK_ALL_RESERVES, onBehalfOf); @@ -368,7 +367,23 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { } } - // todo add updateUserPositionManager + function setUserPositionManager(bool approve, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + address positionManager = _getRandomActor(j); + + _before(); + (bool ok, ) = actor.proxy( + spoke, + abi.encodeCall(ISpoke.setUserPositionManager, (positionManager, approve)) + ); + + if (ok) { + _after(); + } else { + revert('SpokeHandler: setUserPositionManager failed'); + } + } + // todo check decoded ret /////////////////////////////////////////////////////////////////////////////////////////////// From 03c8bfc1c93fa4f56d3f3ec0527be49542b72bdb Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:37:37 +0530 Subject: [PATCH 080/106] chore: abstract duplicated helpers --- invariants/hub-suite/base/BaseHandler.t.sol | 64 +-------- .../hub-suite/utils/PropertiesConstants.sol | 29 ----- .../protocol-suite/base/BaseHandler.t.sol | 87 +------------ invariants/shared/utils/CommonHelpers.sol | 122 ++++++++++++++++++ invariants/shared/utils/DeployPermit2.sol | 4 +- invariants/shared/utils/ErrorHandlers.sol | 4 - invariants/shared/utils/PropertiesAsserts.sol | 2 - 7 files changed, 128 insertions(+), 184 deletions(-) delete mode 100644 invariants/hub-suite/utils/PropertiesConstants.sol create mode 100644 invariants/shared/utils/CommonHelpers.sol diff --git a/invariants/hub-suite/base/BaseHandler.t.sol b/invariants/hub-suite/base/BaseHandler.t.sol index c81f7d89d..f618cc9a0 100644 --- a/invariants/hub-suite/base/BaseHandler.t.sol +++ b/invariants/hub-suite/base/BaseHandler.t.sol @@ -1,20 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -// Interfaces -import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; - -// Libraries -import {MockERC20} from 'tests/mocks/MockERC20.sol'; - -// Contracts -import {Actor} from '../../shared/utils/Actor.sol'; +import {CommonHelpers} from '../../shared/utils/CommonHelpers.sol'; import {HookAggregator} from '../hooks/HookAggregator.t.sol'; /// @title BaseHandler /// @notice Contains common logic for all handlers /// @dev inherits all suite assertions since per action assertions are implmenteds in the handlers -contract BaseHandler is HookAggregator { +contract BaseHandler is HookAggregator, CommonHelpers { /////////////////////////////////////////////////////////////////////////////////////////////// // MODIFIERS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -26,57 +19,4 @@ contract BaseHandler is HookAggregator { /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Helper function to randomize a uint256 seed with a string salt - function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { - return uint256(keccak256(abi.encodePacked(seed, salt))); - } - - /// @notice Helper function to get a random value - function _getRandomValue(uint256 modulus) internal view returns (uint256) { - uint256 randomNumber = uint256( - keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender)) - ); - return randomNumber % modulus; // Adjust the modulus to the desired range - } - - /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor - function _approve(address token, Actor actor_, address spender, uint256 amount) internal { - (bool ok, bytes memory ret) = actor_.proxy( - token, - abi.encodeWithSelector(0x095ea7b3, spender, amount) - ); - require(ok, string(ret)); - } - - /// @notice Helper function to safely approve an amount of tokens to a spender - function _approve(address token, address owner, address spender, uint256 amount) internal { - vm.prank(owner); - _safeApprove(token, spender, 0); - vm.prank(owner); - _safeApprove(token, spender, amount); - } - - /// @notice Helper function to safely approve an amount of tokens to a spender - /// @dev This function is used to revert on failed approvals - function _safeApprove(address token, address spender, uint256 amount) internal { - (bool ok, bytes memory ret) = token.call(abi.encodeCall(IERC20.approve, (spender, amount))); - assert(ok); - if (ret.length > 0) assert(abi.decode(ret, (bool))); - } - - /// @notice Helper function to mint an amount of tokens to an address - function _mint(address token, address receiver, uint256 amount) internal { - MockERC20(token).mint(receiver, amount); - } - - /// @notice Helper function to mint an amount of tokens to an address and approve them to a spender - /// @param token Address of the token to mint - /// @param owner Address of the new owner of the tokens - /// @param spender Address of the spender to approve the tokens to - /// @param amount Amount of tokens to mint and approve - function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { - _mint(token, owner, amount); - _approve(token, owner, spender, amount); - } } diff --git a/invariants/hub-suite/utils/PropertiesConstants.sol b/invariants/hub-suite/utils/PropertiesConstants.sol deleted file mode 100644 index d7a07f005..000000000 --- a/invariants/hub-suite/utils/PropertiesConstants.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Vm} from 'forge-std/Base.sol'; - -/// @notice Testing constants and properties for hub-focused invariant suite -abstract contract PropertiesConstants { - /// @dev Cheat code address - address internal constant VM_ADDRESS = address(uint160(uint256(keccak256('hevm cheat code')))); - - /// @dev Virtual machine instance - Vm internal constant vm = Vm(VM_ADDRESS); - - /////////////////////////////////////////////////////////////////////////////////////////////// - // HUB PROPERTY STRINGS // - /////////////////////////////////////////////////////////////////////////////////////////////// - - // Hub Invariants - string internal constant HUB_INV_LIQUIDITY = 'HUB_INV: Liquidity accounting mismatch'; - string internal constant HUB_INV_SHARES = 'HUB_INV: Share calculation error'; - string internal constant HUB_INV_INTEREST = 'HUB_INV: Interest accrual mismatch'; - string internal constant HUB_INV_DEFICIT = 'HUB_INV: Deficit tracking error'; - - // Hub Postconditions - string internal constant HUB_POST_SUPPLY = 'HUB_POST: Supply state invalid'; - string internal constant HUB_POST_DRAW = 'HUB_POST: Draw state invalid'; - string internal constant HUB_POST_REPAY = 'HUB_POST: Repay state invalid'; - string internal constant HUB_POST_CAP = 'HUB_POST: Cap constraint violated'; -} diff --git a/invariants/protocol-suite/base/BaseHandler.t.sol b/invariants/protocol-suite/base/BaseHandler.t.sol index 3cbf80301..f618cc9a0 100644 --- a/invariants/protocol-suite/base/BaseHandler.t.sol +++ b/invariants/protocol-suite/base/BaseHandler.t.sol @@ -1,20 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -// Interfaces -import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; - -// Libraries -import {MockERC20} from 'tests/mocks/MockERC20.sol'; - -// Contracts -import {Actor} from '../../shared/utils/Actor.sol'; +import {CommonHelpers} from '../../shared/utils/CommonHelpers.sol'; import {HookAggregator} from '../hooks/HookAggregator.t.sol'; /// @title BaseHandler /// @notice Contains common logic for all handlers /// @dev inherits all suite assertions since per action assertions are implmenteds in the handlers -contract BaseHandler is HookAggregator { +contract BaseHandler is HookAggregator, CommonHelpers { /////////////////////////////////////////////////////////////////////////////////////////////// // MODIFIERS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -26,80 +19,4 @@ contract BaseHandler is HookAggregator { /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Helper function to randomize a uint256 seed with a string salt - function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { - return uint256(keccak256(abi.encodePacked(seed, salt))); - } - - /// @notice Helper function to get a random value - function _getRandomValue(uint256 modulus) internal view returns (uint256) { - uint256 randomNumber = uint256( - keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender)) - ); - return randomNumber % modulus; // Adjust the modulus to the desired range - } - - /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor - function _approve(address token, Actor actor_, address spender, uint256 amount) internal { - (bool ok, bytes memory ret) = actor_.proxy( - token, - abi.encodeCall(IERC20.approve, (spender, amount)) - ); - require(ok, string(ret)); - } - - /// @notice Helper function to safely approve an amount of tokens to a spender - function _approve(address token, address owner, address spender, uint256 amount) internal { - vm.prank(owner); - _safeApprove(token, spender, 0); - vm.prank(owner); - _safeApprove(token, spender, amount); - } - - /// @notice Helper function to safely approve an amount of tokens to a spender - /// @dev This function is used to revert on failed approvals - function _safeApprove(address token, address spender, uint256 amount) internal { - (bool ok, bytes memory ret) = token.call(abi.encodeCall(IERC20.approve, (spender, amount))); - assert(ok); - if (ret.length > 0) assert(abi.decode(ret, (bool))); - } - - /// @notice Helper function to mint an amount of tokens to an address - function _mint(address token, address receiver, uint256 amount) internal { - MockERC20(token).mint(receiver, amount); - } - - /// @notice Helper function to mint an amount of tokens to an address and approve them to a spender - /// @param token Address of the token to mint - /// @param owner Address of the new owner of the tokens - /// @param spender Address of the spender to approve the tokens to - /// @param amount Amount of tokens to mint and approve - function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { - _mint(token, owner, amount); - _approve(token, owner, spender, amount); - } - - /// @notice Best-effort mint — silently swallows overflow so the handler can proceed. - /// @dev Using a low-level call prevents the handler from reverting on mint failure - /// (e.g. totalSupply overflow), which would cause the fuzzer to discard the - /// entire call sequence. Spoke functions bound the passed amount to the user's - /// actual balance (e.g. repay(type(uint256).max) repays only the owed debt), - /// so even if minting the full amount overflows, the spoke will operate on - /// whatever balance exists. - function _tryMint(address token, address receiver, uint256 amount) internal { - (bool ok, ) = token.call(abi.encodeCall(MockERC20.mint, (receiver, amount))); - ok; // suppress compiler warning - } - - /// @notice Best-effort mint + approve for spoke handlers - function _tryMintAndApprove( - address token, - address owner, - address spender, - uint256 amount - ) internal { - _tryMint(token, owner, amount); - _approve(token, owner, spender, amount); - } } diff --git a/invariants/shared/utils/CommonHelpers.sol b/invariants/shared/utils/CommonHelpers.sol new file mode 100644 index 000000000..61bde7fbd --- /dev/null +++ b/invariants/shared/utils/CommonHelpers.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Vm} from 'forge-std/Vm.sol'; +import {MockERC20} from 'tests/mocks/MockERC20.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; +import {Actor} from './Actor.sol'; + +Vm constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); +uint256 constant UINT256_MAX = type(uint256).max; + +contract CommonHelpers { + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @notice Helper function to randomize a uint256 seed with a string salt + function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(seed, salt))); + } + + /// @notice Helper function to get a random value + function _getRandomValue(uint256 modulus) internal view returns (uint256) { + uint256 randomNumber = uint256( + keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender)) + ); + return randomNumber % modulus; // Adjust the modulus to the desired range + } + + /// @notice Helper function to bound a value between a min and max + /// @dev Taken from forge-std's stdUtils, renamed to avoid conflict with forge-std's import on foundry replays. + function _bound2(uint256 x, uint256 min, uint256 max) internal pure returns (uint256) { + assert(min <= max); + // If x is between min and max, return x directly. This is to ensure that dictionary values + // do not get shifted if the min is nonzero. More info: https://github.com/foundry-rs/forge-std/issues/188 + if (x >= min && x <= max) return x; + + uint256 size = max - min + 1; + + // If the value is 0, 1, 2, 3, wrap that to min, min+1, min+2, min+3. Similarly for the UINT256_MAX side. + // This helps ensure coverage of the min/max values. + if (x <= 3 && size > x) return min + x; + if (x >= UINT256_MAX - 3 && size > UINT256_MAX - x) return max - (UINT256_MAX - x); + + // Otherwise, wrap x into the range [min, max], i.e. the range is inclusive. + if (x > max) { + uint256 diff = x - max; + uint256 rem = diff % size; + if (rem == 0) return max; + return min + rem - 1; + } else if (x < min) { + uint256 diff = min - x; + uint256 rem = diff % size; + if (rem == 0) return min; + return max - rem + 1; + } + return x; // This should never be reached due to the first check, but is needed to silence compiler warnings. + } + + /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor + function _approve(address token, Actor actor, address spender, uint256 amount) internal { + (bool ok, bytes memory ret) = actor.proxy( + token, + abi.encodeCall(IERC20.approve, (spender, amount)) + ); + require(ok, string(ret)); + } + + /// @notice Helper function to safely approve an amount of tokens to a spender + function _approve(address token, address owner, address spender, uint256 amount) internal { + vm.prank(owner); + _safeApprove(token, spender, 0); + vm.prank(owner); + _safeApprove(token, spender, amount); + } + + /// @notice Helper function to safely approve an amount of tokens to a spender + /// @dev This function is used to revert on failed approvals + function _safeApprove(address token, address spender, uint256 amount) internal { + (bool ok, bytes memory ret) = token.call(abi.encodeCall(IERC20.approve, (spender, amount))); + assert(ok); + if (ret.length > 0) assert(abi.decode(ret, (bool))); + } + + /// @notice Helper function to mint an amount of tokens to an address + function _mint(address token, address receiver, uint256 amount) internal { + MockERC20(token).mint(receiver, amount); + } + + /// @notice Helper function to mint an amount of tokens to an address and approve them to a spender + /// @param token Address of the token to mint + /// @param owner Address of the new owner of the tokens + /// @param spender Address of the spender to approve the tokens to + /// @param amount Amount of tokens to mint and approve + function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { + _mint(token, owner, amount); + _approve(token, owner, spender, amount); + } + + /// @notice Best-effort mint — silently swallows overflow so the handler can proceed. + /// @dev Using a low-level call prevents the handler from reverting on mint failure + /// (e.g. totalSupply overflow), which would cause the fuzzer to discard the + /// entire call sequence. Spoke functions bound the passed amount to the user's + /// actual balance (e.g. repay(type(uint256).max) repays only the owed debt), + /// so even if minting the full amount overflows, the spoke will operate on + /// whatever balance exists. + function _tryMint(address token, address receiver, uint256 amount) internal { + (bool ok, ) = token.call(abi.encodeCall(MockERC20.mint, (receiver, amount))); + ok; // suppress compiler warning + } + + /// @notice Best-effort mint + approve for spoke handlers + function _tryMintAndApprove( + address token, + address owner, + address spender, + uint256 amount + ) internal { + _tryMint(token, owner, amount); + _approve(token, owner, spender, amount); + } +} diff --git a/invariants/shared/utils/DeployPermit2.sol b/invariants/shared/utils/DeployPermit2.sol index c1445f1b3..5bb619a16 100644 --- a/invariants/shared/utils/DeployPermit2.sol +++ b/invariants/shared/utils/DeployPermit2.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -/// @notice helper to deploy permit2 from precompiled bytecode -/// @dev useful if testing externally against permit2 and want to avoid +/// @notice Helper to deploy permit2 from precompiled bytecode +/// @dev Useful if testing externally against permit2 and want to avoid /// recompiling entirely and requiring viaIR compilation library DeployPermit2 { /// @notice deploy permit2 diff --git a/invariants/shared/utils/ErrorHandlers.sol b/invariants/shared/utils/ErrorHandlers.sol index 862fba2e4..6b648630d 100644 --- a/invariants/shared/utils/ErrorHandlers.sol +++ b/invariants/shared/utils/ErrorHandlers.sol @@ -12,10 +12,6 @@ library ErrorHandlers { event AssertFail(string); /// @notice Checks if a call failed due to an assertion error and propagates the error if found. - /////////////////////////////////////////////////////////////////////////////////////////////// - // ASSERTION ERRORS // - /////////////////////////////////////////////////////////////////////////////////////////////// - /// @param success Indicates whether the call was successful. /// @param returnData The data returned from the call. function handleAssertionError( diff --git a/invariants/shared/utils/PropertiesAsserts.sol b/invariants/shared/utils/PropertiesAsserts.sol index 003971c76..c82d3f6a8 100644 --- a/invariants/shared/utils/PropertiesAsserts.sol +++ b/invariants/shared/utils/PropertiesAsserts.sol @@ -461,7 +461,6 @@ abstract contract PropertiesAsserts { uint256 x, uint256 y ) internal pure returns (bool result) { - /// @solidity memory-safe-assembly assembly { let m := not(0) let mm1 := mulmod(a, b, m) @@ -490,7 +489,6 @@ library PropertiesLibString { } function toString(uint256 value) internal pure returns (string memory str) { - /// @solidity memory-safe-assembly assembly { // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the From 15f1c05128abe6700bb193966ab982ec43e1193f Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:38:52 +0530 Subject: [PATCH 081/106] fix: use premiumRay --- .../handlers/spoke/SpokeHandler.t.sol | 2 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 26 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index e751bcf55..2e75a0f0f 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -342,7 +342,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { for (uint256 j; j < reserveCount; j++) { UserVars memory varsBefore = _userVarsBefore(spoke, j, onBehalfOf); UserVars memory varsAfter = _userVarsAfter(spoke, j, onBehalfOf); - assertEq(varsBefore.debt.premium, varsAfter.debt.premium, HSPOST_HUB_M); // !todo add premium ray in those getters and check that here? + assertEq(varsBefore.debt.premiumRay, varsAfter.debt.premiumRay, HSPOST_HUB_M); assertEq(varsBefore.debt.owed, varsAfter.debt.owed, HSPOST_SP_F); } } else { diff --git a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index e9ea44149..a98704335 100644 --- a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -33,7 +33,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { struct Debt { uint256 drawn; - uint256 premium; + uint256 premiumRay; uint256 owed; } @@ -130,14 +130,15 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { IHub hub = IHub(hubs.at(i)); uint256 assetCount = hub.getAssetCount(); for (uint256 j; j < assetCount; j++) { - (uint256 drawn, uint256 premium) = hub.getAssetOwed(j); + (uint256 drawn, ) = hub.getAssetOwed(j); + uint256 premiumRay = hub.getAssetPremiumRay(j); defaultVars.assetVars[address(hub)][j] = AssetVars({ asset: hub.getAsset(j), drawnRate: hub.getAssetDrawnRate(j), drawnIndex: hub.getAssetDrawnIndex(j), totalAssets: hub.getAddedAssets(j), totalShares: hub.getAddedShares(j), - debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + debt: Debt({drawn: drawn, premiumRay: premiumRay, owed: drawn + premiumRay.fromRayUp()}) }); } } @@ -150,12 +151,13 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { for (uint256 j; j < assetCount; j++) { for (uint256 k; k < allSpokes.length; k++) { address spoke = allSpokes[k]; - (uint256 drawn, uint256 premium) = hub.getSpokeOwed(j, spoke); + (uint256 drawn, ) = hub.getSpokeOwed(j, spoke); + uint256 premiumRay = hub.getSpokePremiumRay(j, spoke); defaultVars.spokeVars[address(hub)][j][spoke] = SpokeVars({ spokeData: hub.getSpoke(j, spoke), addedAssets: hub.getSpokeAddedAssets(j, spoke), addedShares: hub.getSpokeAddedShares(j, spoke), - debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + debt: Debt({drawn: drawn, premiumRay: premiumRay, owed: drawn + premiumRay.fromRayUp()}) }); } } @@ -173,19 +175,21 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { if (userInfo.reserveId == CHECK_ALL_RESERVES) { uint256 reserveCount = spoke.getReserveCount(); for (uint256 j; j < reserveCount; ++j) { - (uint256 drawn, uint256 premium) = spoke.getUserDebt(j, userInfo.user); + (uint256 drawn, ) = spoke.getUserDebt(j, userInfo.user); + uint256 premiumRay = spoke.getUserPremiumDebtRay(j, userInfo.user); defaultVars.userVars[userInfo.spoke][j][userInfo.user] = UserVars({ position: spoke.getUserPosition(j, userInfo.user), - debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + debt: Debt({drawn: drawn, premiumRay: premiumRay, owed: drawn + premiumRay.fromRayUp()}) }); } } else { // Cache values for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral uint256 reserveId = userInfo.reserveId; - (uint256 drawn, uint256 premium) = spoke.getUserDebt(reserveId, userInfo.user); + (uint256 drawn, ) = spoke.getUserDebt(reserveId, userInfo.user); + uint256 premiumRay = spoke.getUserPremiumDebtRay(reserveId, userInfo.user); defaultVars.userVars[userInfo.spoke][reserveId][userInfo.user] = UserVars({ position: spoke.getUserPosition(reserveId, userInfo.user), - debt: Debt({drawn: drawn, premium: premium, owed: drawn + premium}) + debt: Debt({drawn: drawn, premiumRay: premiumRay, owed: drawn + premiumRay.fromRayUp()}) }); } } @@ -306,7 +310,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { UserVars memory userVarsBefore = _userVarsBefore(spoke, reserveId, user); UserVars memory userVarsAfter = _userVarsAfter(spoke, reserveId, user); - if (userVarsAfter.debt.premium < userVarsBefore.debt.premium) { + if (userVarsAfter.debt.premiumRay < userVarsBefore.debt.premiumRay) { assertTrue( currentActionSignature == ISpokeHandler.repay.selector || currentActionSignature == ISpokeHandler.liquidationCall.selector, @@ -320,7 +324,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { currentActionSignature == ISpokeHandler.liquidationCall.selector, GPOST_SP_B2 ); - assertEq(userVarsAfter.debt.premium, 0, GPOST_SP_B2); + assertEq(userVarsAfter.debt.premiumRay, 0, GPOST_SP_B2); } } From c3e8026881086434a446b37e37be0f94f0d35ee2 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:15:16 +0530 Subject: [PATCH 082/106] chore: cleanup --- .../protocol-suite/replays/ReplayTest_7.t.sol | 4 +- invariants/shared/utils/CommonHelpers.sol | 39 ------------------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/invariants/protocol-suite/replays/ReplayTest_7.t.sol b/invariants/protocol-suite/replays/ReplayTest_7.t.sol index 56c5b204f..774fb273e 100644 --- a/invariants/protocol-suite/replays/ReplayTest_7.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_7.t.sol @@ -54,8 +54,6 @@ contract ReplayTest7 is Invariants, Setup { _delay(62189); Tester.updateUserRiskPremium(101); _setUpActor(USER3); - _delay(590687); - Tester.setUsingAsCollateral(true, 111, 255); _delay(323286); Tester.donateUnderlyingToHub(194, 231, 231); _delay(505810); @@ -89,6 +87,8 @@ contract ReplayTest7 is Invariants, Setup { _setUpActor(USER3); _delay(582766); Tester.updateUserRiskPremium(25); + _checkAllHubInvariants(); + _checkAllSpokeInvariants(); } function test_replay_7_repay() public { diff --git a/invariants/shared/utils/CommonHelpers.sol b/invariants/shared/utils/CommonHelpers.sol index 61bde7fbd..65cd8cd5c 100644 --- a/invariants/shared/utils/CommonHelpers.sol +++ b/invariants/shared/utils/CommonHelpers.sol @@ -7,7 +7,6 @@ import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; import {Actor} from './Actor.sol'; Vm constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); -uint256 constant UINT256_MAX = type(uint256).max; contract CommonHelpers { /////////////////////////////////////////////////////////////////////////////////////////////// @@ -19,44 +18,6 @@ contract CommonHelpers { return uint256(keccak256(abi.encodePacked(seed, salt))); } - /// @notice Helper function to get a random value - function _getRandomValue(uint256 modulus) internal view returns (uint256) { - uint256 randomNumber = uint256( - keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender)) - ); - return randomNumber % modulus; // Adjust the modulus to the desired range - } - - /// @notice Helper function to bound a value between a min and max - /// @dev Taken from forge-std's stdUtils, renamed to avoid conflict with forge-std's import on foundry replays. - function _bound2(uint256 x, uint256 min, uint256 max) internal pure returns (uint256) { - assert(min <= max); - // If x is between min and max, return x directly. This is to ensure that dictionary values - // do not get shifted if the min is nonzero. More info: https://github.com/foundry-rs/forge-std/issues/188 - if (x >= min && x <= max) return x; - - uint256 size = max - min + 1; - - // If the value is 0, 1, 2, 3, wrap that to min, min+1, min+2, min+3. Similarly for the UINT256_MAX side. - // This helps ensure coverage of the min/max values. - if (x <= 3 && size > x) return min + x; - if (x >= UINT256_MAX - 3 && size > UINT256_MAX - x) return max - (UINT256_MAX - x); - - // Otherwise, wrap x into the range [min, max], i.e. the range is inclusive. - if (x > max) { - uint256 diff = x - max; - uint256 rem = diff % size; - if (rem == 0) return max; - return min + rem - 1; - } else if (x < min) { - uint256 diff = min - x; - uint256 rem = diff % size; - if (rem == 0) return min; - return max - rem + 1; - } - return x; // This should never be reached due to the first check, but is needed to silence compiler warnings. - } - /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor function _approve(address token, Actor actor, address spender, uint256 amount) internal { (bool ok, bytes memory ret) = actor.proxy( From 5a26b20f629ff0239d6cc3175b0802ee1047902b Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:15:45 +0530 Subject: [PATCH 083/106] feat: expand spoke configurator --- .../interfaces/ISpokeConfiguratorHandler.sol | 34 ++- .../spoke/SpokeConfiguratorHandler.t.sol | 211 +++++++++++++++++- .../shared/utils/PropertiesConstants.sol | 6 + 3 files changed, 235 insertions(+), 16 deletions(-) diff --git a/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol b/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol index ccb456b56..193a22c1d 100644 --- a/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol @@ -4,11 +4,8 @@ pragma solidity ^0.8.19; /// @title ISpokeConfiguratorHandler /// @notice Interface for the SpokeConfiguratorHandler interface ISpokeConfiguratorHandler { - function updateLiquidationTargetHealthFactor(uint256 targetHealthFactor, uint8 i) external; - - function updateHealthFactorForMaxBonus(uint256 healthFactorForMaxBonus, uint8 i) external; - - function updateLiquidationBonusFactor(uint256 liquidationBonusFactor, uint8 i) external; + // Reserve config + function updateCollateralRisk(uint256 collateralRisk, uint8 i, uint8 j) external; function updatePaused(bool halted, uint8 i, uint8 j) external; @@ -16,7 +13,34 @@ interface ISpokeConfiguratorHandler { function updateBorrowable(bool borrowable, uint8 i, uint8 j) external; + function updateReceiveSharesEnabled(bool receiveSharesEnabled, uint8 i, uint8 j) external; + function pauseAllReserves(uint8 i) external; function freezeAllReserves(uint8 i) external; + + // Liquidation config + function updateLiquidationTargetHealthFactor(uint256 targetHealthFactor, uint8 i) external; + + function updateHealthFactorForMaxBonus(uint256 healthFactorForMaxBonus, uint8 i) external; + + function updateLiquidationBonusFactor(uint256 liquidationBonusFactor, uint8 i) external; + + // Dynamic reserve config + function addCollateralFactor(uint256 collateralFactor, uint8 i, uint8 j) external; + + function updateCollateralFactor(uint256 collateralFactor, uint8 i, uint8 j, uint8 k) external; + + function addMaxLiquidationBonus(uint256 maxLiquidationBonus, uint8 i, uint8 j) external; + + function updateMaxLiquidationBonus( + uint256 maxLiquidationBonus, + uint8 i, + uint8 j, + uint8 k + ) external; + + function addLiquidationFee(uint256 liquidationFee, uint8 i, uint8 j) external; + + function updateLiquidationFee(uint256 liquidationFee, uint8 i, uint8 j, uint8 k) external; } diff --git a/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol index d7ec13187..4a939e42e 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol @@ -2,11 +2,12 @@ pragma solidity ^0.8.19; // Interfaces +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; import {ISpokeConfiguratorHandler} from '../interfaces/ISpokeConfiguratorHandler.sol'; // Libraries -import 'forge-std/console.sol'; +import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; // Test Contracts import {Actor} from '../../../shared/utils/Actor.sol'; @@ -14,7 +15,10 @@ import {BaseHandler} from '../../base/BaseHandler.t.sol'; /// @title SpokeConfiguratorHandler /// @notice Handler test contract for a set of actions +/// @dev Inputs are bounded to Spoke._validate* constraints on *admin* actions don't unnecessarily +/// discard fuzz inputs. contract SpokeConfiguratorHandler is BaseHandler, ISpokeConfiguratorHandler { + using PercentageMath for uint256; /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -25,50 +29,235 @@ contract SpokeConfiguratorHandler is BaseHandler, ISpokeConfiguratorHandler { // OWNER ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////// + // RESERVE CONFIG // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function updateCollateralRisk(uint256 collateralRisk, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + collateralRisk = _bound(collateralRisk, 0, MAX_ALLOWED_COLLATERAL_RISK); + spokeConfigurator.updateCollateralRisk(spoke, reserveId, collateralRisk); + } + + function updatePaused(bool halted, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updatePaused(spoke, reserveId, halted); + } + + function updateFrozen(bool frozen, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updateFrozen(spoke, reserveId, frozen); + } + + function updateBorrowable(bool borrowable, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updateBorrowable(spoke, reserveId, borrowable); + } + + function updateReceiveSharesEnabled(bool receiveSharesEnabled, uint8 i, uint8 j) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + spokeConfigurator.updateReceiveSharesEnabled(spoke, reserveId, receiveSharesEnabled); + } + + function pauseAllReserves(uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.pauseAllReserves(spoke); + } + + function freezeAllReserves(uint8 i) external setup { + address spoke = _getRandomSpoke(i); + spokeConfigurator.freezeAllReserves(spoke); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // LIQUIDATION CONFIG // + /////////////////////////////////////////////////////////////////////////////////////////////// + function updateLiquidationTargetHealthFactor(uint256 targetHealthFactor, uint8 i) external setup { address spoke = _getRandomSpoke(i); + targetHealthFactor = _bound( + targetHealthFactor, + HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + MAX_TARGET_HEALTH_FACTOR + ); spokeConfigurator.updateLiquidationTargetHealthFactor(spoke, targetHealthFactor); } function updateHealthFactorForMaxBonus(uint256 healthFactorForMaxBonus, uint8 i) external setup { address spoke = _getRandomSpoke(i); + healthFactorForMaxBonus = _bound( + healthFactorForMaxBonus, + 0, + HEALTH_FACTOR_LIQUIDATION_THRESHOLD - 1 + ); spokeConfigurator.updateHealthFactorForMaxBonus(spoke, healthFactorForMaxBonus); } function updateLiquidationBonusFactor(uint256 liquidationBonusFactor, uint8 i) external setup { address spoke = _getRandomSpoke(i); + liquidationBonusFactor = _bound(liquidationBonusFactor, 0, PERCENTAGE_FACTOR); spokeConfigurator.updateLiquidationBonusFactor(spoke, liquidationBonusFactor); } - function updatePaused(bool halted, uint8 i, uint8 j) external setup { + /////////////////////////////////////////////////////////////////////////////////////////////// + // DYNAMIC CONFIG // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function addCollateralFactor(uint256 collateralFactor, uint8 i, uint8 j) external setup { address spoke = _getRandomSpoke(i); uint256 reserveId = _getRandomReserveId(spoke, j); - spokeConfigurator.updatePaused(spoke, reserveId, halted); + + uint256 maxCf = _collateralFactorUpperBound(spoke, reserveId); + collateralFactor = _bound(collateralFactor, 1, maxCf); + spokeConfigurator.addCollateralFactor(spoke, reserveId, uint16(collateralFactor)); } - function updateFrozen(bool frozen, uint8 i, uint8 j) external setup { + function updateCollateralFactor( + uint256 collateralFactor, + uint8 i, + uint8 j, + uint8 k + ) external setup { address spoke = _getRandomSpoke(i); uint256 reserveId = _getRandomReserveId(spoke, j); - spokeConfigurator.updateFrozen(spoke, reserveId, frozen); + uint32 dynamicConfigKey = _getRandomDynamicConfigKey(spoke, reserveId, k); + + uint256 maxCf = _collateralFactorUpperBound(spoke, reserveId, dynamicConfigKey); + collateralFactor = _bound(collateralFactor, 1, maxCf); + spokeConfigurator.updateCollateralFactor( + spoke, + reserveId, + dynamicConfigKey, + uint16(collateralFactor) + ); } - function updateBorrowable(bool borrowable, uint8 i, uint8 j) external setup { + function addMaxLiquidationBonus(uint256 maxLiquidationBonus, uint8 i, uint8 j) external setup { address spoke = _getRandomSpoke(i); uint256 reserveId = _getRandomReserveId(spoke, j); - spokeConfigurator.updateBorrowable(spoke, reserveId, borrowable); + + uint256 maxMlb = _maxLiquidationBonusUpperBound(spoke, reserveId); + maxLiquidationBonus = _bound(maxLiquidationBonus, PERCENTAGE_FACTOR, maxMlb); + spokeConfigurator.addMaxLiquidationBonus(spoke, reserveId, maxLiquidationBonus); } - function pauseAllReserves(uint8 i) external setup { + function updateMaxLiquidationBonus( + uint256 maxLiquidationBonus, + uint8 i, + uint8 j, + uint8 k + ) external setup { address spoke = _getRandomSpoke(i); - spokeConfigurator.pauseAllReserves(spoke); + uint256 reserveId = _getRandomReserveId(spoke, j); + uint32 dynamicConfigKey = _getRandomDynamicConfigKey(spoke, reserveId, k); + + uint256 maxMlb = _maxLiquidationBonusUpperBound(spoke, reserveId, dynamicConfigKey); + maxLiquidationBonus = _bound(maxLiquidationBonus, PERCENTAGE_FACTOR, maxMlb); + spokeConfigurator.updateMaxLiquidationBonus( + spoke, + reserveId, + dynamicConfigKey, + maxLiquidationBonus + ); } - function freezeAllReserves(uint8 i) external setup { + function addLiquidationFee(uint256 liquidationFee, uint8 i, uint8 j) external setup { address spoke = _getRandomSpoke(i); - spokeConfigurator.freezeAllReserves(spoke); + uint256 reserveId = _getRandomReserveId(spoke, j); + liquidationFee = _bound(liquidationFee, 0, PERCENTAGE_FACTOR); + spokeConfigurator.addLiquidationFee(spoke, reserveId, liquidationFee); + } + + function updateLiquidationFee(uint256 liquidationFee, uint8 i, uint8 j, uint8 k) external setup { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId(spoke, j); + uint32 dynamicConfigKey = _getRandomDynamicConfigKey(spoke, reserveId, k); + liquidationFee = _bound(liquidationFee, 0, PERCENTAGE_FACTOR); + spokeConfigurator.updateLiquidationFee(spoke, reserveId, dynamicConfigKey, liquidationFee); } /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Returns the latest dynamic config for a reserve. + function _getLatestDynamicConfig( + address spoke, + uint256 reserveId + ) internal view returns (ISpoke.DynamicReserveConfig memory) { + uint32 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; + return ISpoke(spoke).getDynamicReserveConfig(reserveId, latestKey); + } + + /// @dev Returns a random dynamic config key in [0, reserve.dynamicConfigKey]. + function _getRandomDynamicConfigKey( + address spoke, + uint256 reserveId, + uint8 k + ) internal view returns (uint32) { + uint32 latestKey = ISpoke(spoke).getReserve(reserveId).dynamicConfigKey; + return uint32(_bound(k, 0, latestKey)); + } + + /// @dev Upper bound for collateralFactor derived from the reserve's latest maxLiquidationBonus. + function _collateralFactorUpperBound( + address spoke, + uint256 reserveId + ) internal view returns (uint256) { + uint256 maxLiquidationBonus = _getLatestDynamicConfig(spoke, reserveId).maxLiquidationBonus; + return _collateralFactorUpperBound(maxLiquidationBonus); + } + + /// @dev Upper bound for collateralFactor at a specific dynamic config key. + function _collateralFactorUpperBound( + address spoke, + uint256 reserveId, + uint32 dynamicConfigKey + ) internal view returns (uint256) { + uint256 maxLiquidationBonus = ISpoke(spoke) + .getDynamicReserveConfig(reserveId, dynamicConfigKey) + .maxLiquidationBonus; + return _collateralFactorUpperBound(maxLiquidationBonus); + } + + /// @dev Upper bound for maxLiquidationBonus derived from the reserve's latest collateralFactor. + function _maxLiquidationBonusUpperBound( + address spoke, + uint256 reserveId + ) internal view returns (uint256) { + uint256 collateralFactor = _getLatestDynamicConfig(spoke, reserveId).collateralFactor; + return _maxLiquidationBonusUpperBound(collateralFactor); + } + + /// @dev Upper bound for maxLiquidationBonus at a specific dynamic config key. + function _maxLiquidationBonusUpperBound( + address spoke, + uint256 reserveId, + uint32 dynamicConfigKey + ) internal view returns (uint256) { + uint256 collateralFactor = ISpoke(spoke) + .getDynamicReserveConfig(reserveId, dynamicConfigKey) + .collateralFactor; + return _maxLiquidationBonusUpperBound(collateralFactor); + } + + /// @dev Upper bound for maxLiquidationBonus for a given collateralFactor. + function _maxLiquidationBonusUpperBound( + uint256 collateralFactor + ) internal pure returns (uint256) { + if (collateralFactor == 0) return PERCENTAGE_FACTOR; + return (PercentageMath.PERCENTAGE_FACTOR - 1).percentDivDown(collateralFactor); + } + + /// @dev Upper bound for collateralFactor for a given maxLiquidationBonus. + function _collateralFactorUpperBound( + uint256 maxLiquidationBonus + ) internal pure returns (uint256) { + return (PercentageMath.PERCENTAGE_FACTOR - 1).percentDivDown(maxLiquidationBonus); + } } diff --git a/invariants/shared/utils/PropertiesConstants.sol b/invariants/shared/utils/PropertiesConstants.sol index c21a5d016..c607d45de 100644 --- a/invariants/shared/utils/PropertiesConstants.sol +++ b/invariants/shared/utils/PropertiesConstants.sol @@ -17,6 +17,12 @@ abstract contract PropertiesConstants { uint256 constant SPOKE_COUNT = 3; uint40 constant MAX_ALLOWED_SPOKE_CAP = type(uint40).max; + // Spoke validation constants + uint256 constant PERCENTAGE_FACTOR = 1e4; + uint256 constant MAX_ALLOWED_COLLATERAL_RISK = 1000_00; + uint64 constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; + uint256 constant MAX_TARGET_HEALTH_FACTOR = 2e18; + // Interest rate 1 data uint16 constant OPTIMAL_USAGE_RATIO_IR1 = 85_00; // 85.00% uint16 constant BASE_VARIABLE_BORROW_RATE_IR1 = 1_00; // 1.00% From 9a92a8654b6e4330281e616170c068360bb088a7 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:22:23 +0530 Subject: [PATCH 084/106] fix: include deficit in drawCap --- invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol | 2 +- invariants/hub-suite/specs/HubPostconditionsSpec.t.sol | 2 +- invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index 9a45446b5..a96d722d4 100644 --- a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -213,7 +213,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { if (spokeDataAfter.debt.owed > spokeDataBefore.debt.owed) { if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - spokeDataAfter.debt.owed, + spokeDataAfter.debt.owed + spokeDataAfter.spokeData.deficitRay.fromRayUp(), spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_F ); diff --git a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol index f95e5d5c8..11f62ffe1 100644 --- a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol +++ b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol @@ -58,7 +58,7 @@ abstract contract HubPostconditionsSpec { 'GPOST_HUB_E: if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)'; string constant GPOST_HUB_F = - 'GPOST_HUB_F: if owed for a spoke & assetId increase, owed <= drawCap * precision (when cap != MAX)'; // TODO-ok take into account the deficit, use Owed instead of drawn -> review fix + 'GPOST_HUB_F: if owed for a spoke & assetId increase, owed <= drawCap * precision (when cap != MAX)'; /////////////////////////////////////////////////////////////////////////////////////////////// // ERC4626 // diff --git a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index a98704335..76a74a734 100644 --- a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -278,7 +278,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { if (spokeDataAfter.debt.owed > spokeDataBefore.debt.owed) { if (spokeConfig.drawCap != MAX_ALLOWED_SPOKE_CAP) { assertLe( - spokeDataAfter.debt.owed, + spokeDataAfter.debt.owed + spokeDataAfter.spokeData.deficitRay.fromRayUp(), spokeConfig.drawCap * MathUtils.uncheckedExp(10, decimals), GPOST_HUB_F ); From 5400b4c2792c8b17797fff9c4de415b89663e6d9 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:36:33 +0530 Subject: [PATCH 085/106] feat: bound and improve fuzzer capacity on hub configurator handler as well --- .../hub-suite/handlers/HubConfiguratorHandler.t.sol | 13 +++++++++++-- .../handlers/hub/HubConfiguratorHandler.t.sol | 7 ++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol index f80ffb78d..bb0602f2c 100644 --- a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol +++ b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol @@ -14,6 +14,8 @@ import {BaseHandler} from '../base/BaseHandler.t.sol'; /// @title HubConfiguratorHandler /// @notice Handler test contract for a set of actions +/// @dev Inputs are bounded to Hub validation constraints so admin actions don't unnecessarily +/// discard fuzzer runs. contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // @@ -22,18 +24,20 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { // ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // + // SPOKE CONFIG // /////////////////////////////////////////////////////////////////////////////////////////////// function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j) external setup { uint256 assetId = _getRandomBaseAssetId(i); address spoke = _getRandomActor(j); + addCap = _bound(addCap, 0, MAX_ALLOWED_SPOKE_CAP); hubConfigurator.updateSpokeSupplyCap(address(hub), assetId, spoke, addCap); } function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j) external setup { uint256 assetId = _getRandomBaseAssetId(i); address spoke = _getRandomActor(j); + drawCap = _bound(drawCap, 0, MAX_ALLOWED_SPOKE_CAP); hubConfigurator.updateSpokeDrawCap(address(hub), assetId, spoke, drawCap); } @@ -44,6 +48,7 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { ) external setup { uint256 assetId = _getRandomBaseAssetId(i); address spoke = _getRandomActor(j); + riskPremiumThreshold = _bound(riskPremiumThreshold, 0, MAX_RISK_PREMIUM_THRESHOLD); hubConfigurator.updateSpokeRiskPremiumThreshold( address(hub), assetId, @@ -58,9 +63,13 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { hubConfigurator.updateSpokeHalted(address(hub), assetId, spoke, halted); } + /////////////////////////////////////////////////////////////////////////////////////////////// + // ASSET CONFIG // + /////////////////////////////////////////////////////////////////////////////////////////////// + function updateLiquidityFee(uint256 liquidityFee, uint8 i) external setup { uint256 assetId = _getRandomBaseAssetId(i); - liquidityFee = liquidityFee % PercentageMath.PERCENTAGE_FACTOR; + liquidityFee = _bound(liquidityFee, 0, PercentageMath.PERCENTAGE_FACTOR); hubConfigurator.updateLiquidityFee(address(hub), assetId, liquidityFee); } diff --git a/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol b/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol index 21c3dbafe..7be75842b 100644 --- a/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol +++ b/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol @@ -11,6 +11,8 @@ import {BaseHandler} from '../../base/BaseHandler.t.sol'; /// @title HubConfiguratorHandler /// @notice Handler test contract for a set of actions +/// @dev Inputs are bounded to Hub validation constraints so admin actions don't unnecessarily +/// discard fuzzer runs. contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { /////////////////////////////////////////////////////////////////////////////////////////////// // STATE VARIABLES // @@ -19,13 +21,14 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { // ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////// - // OWNER ACTIONS // + // SPOKE CONFIG // /////////////////////////////////////////////////////////////////////////////////////////////// function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j, uint8 k) external setup { address hub = _getRandomHub(i); uint256 assetId = _getRandomHubAssetId(hub, j); address spoke = _getRandomSpoke(k); + addCap = _bound(addCap, 0, MAX_ALLOWED_SPOKE_CAP); hubConfigurator.updateSpokeSupplyCap(hub, assetId, spoke, addCap); } @@ -33,6 +36,7 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { address hub = _getRandomHub(i); uint256 assetId = _getRandomHubAssetId(hub, j); address spoke = _getRandomSpoke(k); + drawCap = _bound(drawCap, 0, MAX_ALLOWED_SPOKE_CAP); hubConfigurator.updateSpokeDrawCap(hub, assetId, spoke, drawCap); } @@ -45,6 +49,7 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { address hub = _getRandomHub(i); uint256 assetId = _getRandomHubAssetId(hub, j); address spoke = _getRandomSpoke(k); + riskPremiumThreshold = _bound(riskPremiumThreshold, 0, MAX_RISK_PREMIUM_THRESHOLD); hubConfigurator.updateSpokeRiskPremiumThreshold(hub, assetId, spoke, riskPremiumThreshold); } From ecfaaa522480c2437f9b583faecdd90eab2d96b3 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:51:08 +0530 Subject: [PATCH 086/106] chore: fix compile --- invariants/shared/utils/PropertiesConstants.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/invariants/shared/utils/PropertiesConstants.sol b/invariants/shared/utils/PropertiesConstants.sol index c607d45de..957c73c5e 100644 --- a/invariants/shared/utils/PropertiesConstants.sol +++ b/invariants/shared/utils/PropertiesConstants.sol @@ -15,10 +15,9 @@ abstract contract PropertiesConstants { // Protocol constants uint256 constant SPOKE_COUNT = 3; - uint40 constant MAX_ALLOWED_SPOKE_CAP = type(uint40).max; - - // Spoke validation constants uint256 constant PERCENTAGE_FACTOR = 1e4; + uint40 constant MAX_ALLOWED_SPOKE_CAP = type(uint40).max; + uint24 constant MAX_RISK_PREMIUM_THRESHOLD = type(uint24).max; uint256 constant MAX_ALLOWED_COLLATERAL_RISK = 1000_00; uint64 constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; uint256 constant MAX_TARGET_HEALTH_FACTOR = 2e18; From fe5f6bb00556660108372dd6a5733fff1f271810 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 02:33:28 +0530 Subject: [PATCH 087/106] chore: cleanup --- invariants/hub-suite/specs/HubInvariantsSpec.t.sol | 2 +- invariants/hub-suite/specs/HubPostconditionsSpec.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol index f1a6106dc..2ef84652b 100644 --- a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -30,7 +30,7 @@ abstract contract HubInvariantsSpec { 'INV_HUB_C: Sum of [baseDrawnShares/premiumDrawnShares/premiumOffsetRay] of individual (spoke/user) should match the corresponding value of the asset on the Hub'; string constant INV_HUB_G = - 'INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT'; // TODO check if tolerance is correct + 'INV_HUB_G: totalAddedAssets = sum of addedAssets of all registered spokes (including present & past treasury spoke) with a tolerance of SPOKE_COUNT'; string constant INV_HUB_H = 'INV_HUB_H: totalAddedShares = sum of addedShares of all registered spokes'; diff --git a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol index 11f62ffe1..ba67cb6cc 100644 --- a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol +++ b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol @@ -58,7 +58,7 @@ abstract contract HubPostconditionsSpec { 'GPOST_HUB_E: if addedAssets for a spoke & assetId increase, addedAssets <= addCap * precision (when cap != MAX)'; string constant GPOST_HUB_F = - 'GPOST_HUB_F: if owed for a spoke & assetId increase, owed <= drawCap * precision (when cap != MAX)'; + 'GPOST_HUB_F: if owed for a spoke & assetId increase, owed + deficit <= drawCap * precision (when cap != MAX)'; /////////////////////////////////////////////////////////////////////////////////////////////// // ERC4626 // From 74069349339bc8fe376febeed26893e016460b35 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 02:46:23 +0530 Subject: [PATCH 088/106] ci: run echidna in exploration mode as well --- .github/workflows/invariants.yml | 6 +++--- .github/workflows/invariants_hub.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index ff7e9c6d3..4dec729fa 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: - mode: [property, assertion] + mode: [property, assertion, exploration] steps: - name: Checkout repository @@ -37,10 +37,10 @@ jobs: - name: Run Forge Build run: forge build --build-info - - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode + - name: Run Echidna ${{ matrix.mode }} Mode uses: crytic/echidna-action@v2 with: files: invariants/protocol-suite/Tester.t.sol contract: Tester config: invariants/protocol-suite/_config/echidna_config_ci.yaml - test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} + test-mode: ${{ matrix.mode }} diff --git a/.github/workflows/invariants_hub.yml b/.github/workflows/invariants_hub.yml index 5b6b669ac..30b9f0383 100644 --- a/.github/workflows/invariants_hub.yml +++ b/.github/workflows/invariants_hub.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: - mode: [property, assertion] + mode: [property, assertion, exploration] steps: - name: Checkout repository @@ -37,10 +37,10 @@ jobs: - name: Run Forge Build run: forge build --build-info - - name: Run Echidna ${{ matrix.mode == 'property' && 'Property' || 'Assertion' }} Mode + - name: Run Echidna ${{ matrix.mode }} Mode uses: crytic/echidna-action@v2 with: files: invariants/hub-suite/Tester.t.sol contract: Tester config: invariants/hub-suite/_config/echidna_config_ci.yaml - test-mode: ${{ matrix.mode == 'assertion' && 'assertion' || '' }} + test-mode: ${{ matrix.mode }} From f8325f2150dcc0b0520a646d5a38ec1de5541579 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 03:24:13 +0530 Subject: [PATCH 089/106] ci: add medusa --- .github/workflows/invariants.yml | 37 ++++++++- .github/workflows/invariants_hub.yml | 37 ++++++++- .../hub-suite/_config/medusa_config_ci.json | 80 ++++++++++++++++++ .../_config/medusa_config_ci.json | 83 +++++++++++++++++++ 4 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 invariants/hub-suite/_config/medusa_config_ci.json create mode 100644 invariants/protocol-suite/_config/medusa_config_ci.json diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index 4dec729fa..cd4933a20 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -1,4 +1,4 @@ -name: Echidna-Invariants +name: Invariants on: push: @@ -16,7 +16,7 @@ env: jobs: echidna: - name: Echidna-Invariants (${{ matrix.mode }}) + name: Echidna (${{ matrix.mode }}) runs-on: ubuntu-latest strategy: @@ -44,3 +44,36 @@ jobs: contract: Tester config: invariants/protocol-suite/_config/echidna_config_ci.yaml test-mode: ${{ matrix.mode }} + + medusa: + name: Medusa + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Run Foundry setup + uses: bgd-labs/github-workflows/.github/actions/foundry-setup@main + with: + FOUNDRY_VERSION: nightly + + - name: Run Forge Build + run: forge build --build-info + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: stable + cache: false + + - name: Install crytic-compile + run: pip install crytic-compile + + - name: Install Medusa + run: go install github.com/crytic/medusa@latest + + - name: Run Medusa + run: medusa fuzz --config invariants/protocol-suite/_config/medusa_config_ci.json diff --git a/.github/workflows/invariants_hub.yml b/.github/workflows/invariants_hub.yml index 30b9f0383..890de7551 100644 --- a/.github/workflows/invariants_hub.yml +++ b/.github/workflows/invariants_hub.yml @@ -1,4 +1,4 @@ -name: Echidna-Invariants-Hub +name: Invariants-Hub on: push: @@ -16,7 +16,7 @@ env: jobs: echidna: - name: Echidna-Invariants (${{ matrix.mode }}) + name: Echidna (${{ matrix.mode }}) runs-on: ubuntu-latest strategy: @@ -44,3 +44,36 @@ jobs: contract: Tester config: invariants/hub-suite/_config/echidna_config_ci.yaml test-mode: ${{ matrix.mode }} + + medusa: + name: Medusa + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Run Foundry setup + uses: bgd-labs/github-workflows/.github/actions/foundry-setup@main + with: + FOUNDRY_VERSION: nightly + + - name: Run Forge Build + run: forge build --build-info + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: stable + cache: false + + - name: Install crytic-compile + run: pip install crytic-compile + + - name: Install Medusa + run: go install github.com/crytic/medusa@latest + + - name: Run Medusa + run: medusa fuzz --config invariants/hub-suite/_config/medusa_config_ci.json diff --git a/invariants/hub-suite/_config/medusa_config_ci.json b/invariants/hub-suite/_config/medusa_config_ci.json new file mode 100644 index 000000000..6dc20b7b8 --- /dev/null +++ b/invariants/hub-suite/_config/medusa_config_ci.json @@ -0,0 +1,80 @@ +{ + "fuzzing": { + "workers": 2, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 100000, + "callSequenceLength": 100, + "corpusDirectory": "corpus", + "coverageEnabled": true, + "deploymentOrder": ["Tester"], + "targetContracts": ["Tester"], + "targetContractsBalances": [ + "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" + ], + "predeployedContracts": {}, + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": ["0x10000", "0x20000", "0x30000"], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 12500000000, + "transactionGasLimit": 1250000000, + "testing": { + "stopOnFailedTest": true, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "assertionModes": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": ["fuzz_", "invariant_"] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": ["optimize_"] + }, + "excludeFunctionSignatures": ["Tester.checkPostConditions()"] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": "invariants/hub-suite/Tester.t.sol", + "solcVersion": "", + "exportDirectory": "", + "args": [ + "--ignore-compile", + "--solc-remaps", + "forge-std/=../../../lib/forge-std/src/" + ] + } + }, + "logging": { + "level": "info", + "logDirectory": "" + } +} diff --git a/invariants/protocol-suite/_config/medusa_config_ci.json b/invariants/protocol-suite/_config/medusa_config_ci.json new file mode 100644 index 000000000..07c2247e5 --- /dev/null +++ b/invariants/protocol-suite/_config/medusa_config_ci.json @@ -0,0 +1,83 @@ +{ + "fuzzing": { + "workers": 2, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 100000, + "callSequenceLength": 100, + "corpusDirectory": "corpus", + "coverageEnabled": true, + "deploymentOrder": ["Tester"], + "targetContracts": ["Tester"], + "targetContractsBalances": [ + "0xffffffffffffffffffffffffffffffffffffffffffffffffffff" + ], + "predeployedContracts": { + "LiquidationLogic": "0xf01" + }, + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": ["0x10000", "0x20000", "0x30000"], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 12500000000, + "transactionGasLimit": 1250000000, + "testing": { + "stopOnFailedTest": true, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "assertionModes": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": ["fuzz_", "invariant_"] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": ["optimize_"] + }, + "excludeFunctionSignatures": ["Tester.checkPostConditions()"] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": true + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": "invariants/protocol-suite/Tester.t.sol", + "solcVersion": "", + "exportDirectory": "", + "args": [ + "--ignore-compile", + "--solc-remaps", + "forge-std/=../../../lib/forge-std/src/", + "--compile-libraries=(LiquidationLogic,0xf01)" + ] + } + }, + "logging": { + "level": "info", + "logDirectory": "" + } +} From 4ac3dbaad7912cf858647fe67a843ad85d10fe17 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 03:32:10 +0530 Subject: [PATCH 090/106] ci: install slither --- .github/workflows/invariants.yml | 7 +++++-- .github/workflows/invariants_hub.yml | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index cd4933a20..12afd0b61 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -9,6 +9,9 @@ on: env: FOUNDRY_PROFILE: invariant +permissions: + contents: read + # ! todo uncomment before merging # concurrency: # group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -69,8 +72,8 @@ jobs: go-version: stable cache: false - - name: Install crytic-compile - run: pip install crytic-compile + - name: Install slither-analyzer + run: pip install slither-analyzer - name: Install Medusa run: go install github.com/crytic/medusa@latest diff --git a/.github/workflows/invariants_hub.yml b/.github/workflows/invariants_hub.yml index 890de7551..774704212 100644 --- a/.github/workflows/invariants_hub.yml +++ b/.github/workflows/invariants_hub.yml @@ -9,6 +9,9 @@ on: env: FOUNDRY_PROFILE: invariant +permissions: + contents: read + # ! todo uncomment before merging # concurrency: # group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -69,8 +72,8 @@ jobs: go-version: stable cache: false - - name: Install crytic-compile - run: pip install crytic-compile + - name: Install slither-analyzer + run: pip install slither-analyzer - name: Install Medusa run: go install github.com/crytic/medusa@latest From 4b2fb0da3d84bb8f63e5caf974d8ac6ff73307a9 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 03:55:31 +0530 Subject: [PATCH 091/106] fix: failing post hook --- .../hub-suite/_config/medusa_config_ci.json | 2 +- .../_config/medusa_config_ci.json | 2 +- .../hooks/DefaultBeforeAfterHooks.t.sol | 41 ++++++++++++------- .../protocol-suite/replays/ReplayTest_8.t.sol | 26 ++++++++++++ 4 files changed, 55 insertions(+), 16 deletions(-) diff --git a/invariants/hub-suite/_config/medusa_config_ci.json b/invariants/hub-suite/_config/medusa_config_ci.json index 6dc20b7b8..05da8f4b7 100644 --- a/invariants/hub-suite/_config/medusa_config_ci.json +++ b/invariants/hub-suite/_config/medusa_config_ci.json @@ -1,6 +1,6 @@ { "fuzzing": { - "workers": 2, + "workers": 10, "workerResetLimit": 50, "timeout": 0, "testLimit": 100000, diff --git a/invariants/protocol-suite/_config/medusa_config_ci.json b/invariants/protocol-suite/_config/medusa_config_ci.json index 07c2247e5..43bcbef93 100644 --- a/invariants/protocol-suite/_config/medusa_config_ci.json +++ b/invariants/protocol-suite/_config/medusa_config_ci.json @@ -1,6 +1,6 @@ { "fuzzing": { - "workers": 2, + "workers": 10, "workerResetLimit": 50, "timeout": 0, "testLimit": 100000, diff --git a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index 76a74a734..9d01689ef 100644 --- a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -49,6 +49,8 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { struct UserVars { ISpoke.UserPosition position; Debt debt; + bool collateral; + bool borrowing; } struct UserAccountDataVars { @@ -168,6 +170,7 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { for (uint256 i; i < usersToCheck.length; ++i) { UserInfo memory userInfo = usersToCheck[i]; ISpoke spoke = ISpoke(userInfo.spoke); + address user = userInfo.spoke; defaultVars.userAccountDataVars[userInfo.spoke][userInfo.user].data = spoke .getUserAccountData(userInfo.user); @@ -175,26 +178,32 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { if (userInfo.reserveId == CHECK_ALL_RESERVES) { uint256 reserveCount = spoke.getReserveCount(); for (uint256 j; j < reserveCount; ++j) { - (uint256 drawn, ) = spoke.getUserDebt(j, userInfo.user); - uint256 premiumRay = spoke.getUserPremiumDebtRay(j, userInfo.user); - defaultVars.userVars[userInfo.spoke][j][userInfo.user] = UserVars({ - position: spoke.getUserPosition(j, userInfo.user), - debt: Debt({drawn: drawn, premiumRay: premiumRay, owed: drawn + premiumRay.fromRayUp()}) - }); + _setUserVars(defaultVars, spoke, j, user); } } else { // Cache values for a specific reserve of the spoke, used after actions: supply, withdraw, borrow, repay, setUsingAsCollateral - uint256 reserveId = userInfo.reserveId; - (uint256 drawn, ) = spoke.getUserDebt(reserveId, userInfo.user); - uint256 premiumRay = spoke.getUserPremiumDebtRay(reserveId, userInfo.user); - defaultVars.userVars[userInfo.spoke][reserveId][userInfo.user] = UserVars({ - position: spoke.getUserPosition(reserveId, userInfo.user), - debt: Debt({drawn: drawn, premiumRay: premiumRay, owed: drawn + premiumRay.fromRayUp()}) - }); + _setUserVars(defaultVars, spoke, userInfo.reserveId, user); } } } + function _setUserVars( + DefaultVars storage defaultVars, + ISpoke spoke, + uint256 reserveId, + address user + ) internal { + (uint256 drawn, ) = spoke.getUserDebt(reserveId, user); + uint256 premiumRay = spoke.getUserPremiumDebtRay(reserveId, user); + (bool collateral, bool borrowing) = spoke.getUserReserveStatus(reserveId, user); + defaultVars.userVars[address(spoke)][reserveId][user] = UserVars({ + position: spoke.getUserPosition(reserveId, user), + debt: Debt({drawn: drawn, premiumRay: premiumRay, owed: drawn + premiumRay.fromRayUp()}), + collateral: collateral, + borrowing: borrowing + }); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // POST CONDITIONS: HUB // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -339,7 +348,11 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { signature == ISpokeHandler.setUsingAsCollateral.selector || signature == ISpokeHandler.updateUserDynamicConfig.selector ) { - assertEq(latestKey, userKey, GPOST_SP_E); + // Dynamic config keys are only refreshed for collateral positions + // (see _processUserAccountData — refresh happens inside `if (collateral)` only). + if (_userVarsBefore(spoke, reserveId, user).collateral) { + assertEq(latestKey, userKey, GPOST_SP_E); + } } } diff --git a/invariants/protocol-suite/replays/ReplayTest_8.t.sol b/invariants/protocol-suite/replays/ReplayTest_8.t.sol index df32f8d6b..3255485be 100644 --- a/invariants/protocol-suite/replays/ReplayTest_8.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_8.t.sol @@ -226,6 +226,32 @@ contract ReplayTest7 is Invariants, Setup { ); } + function test_replay_setUsingAsCollateral_1() public { + _setUpActor(USER3); + _delay(48); + Tester.setUsingAsCollateral(true, 0, 42); + _setUpActor(USER1); + Tester.addLiquidationFee( + 13323504814495136016677116177882803507625235598720014455673974911556623963286, + 124, + 0 + ); + _setUpActor(USER3); + Tester.setUsingAsCollateral(false, 200, 146); + } + + function test_updateUserDynamicConfig_1() public { + _setUpActor(USER3); + _delay(56); + Tester.addMaxLiquidationBonus( + 172001014342743600581657909606579704666279210377953032615107995338039342389, + 14, + 0 + ); + _setUpActor(USER1); + Tester.updateUserDynamicConfig(0); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// From 98a3122e041b4e9fa33e8615847d2ca4f513785d Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 04:03:07 +0530 Subject: [PATCH 092/106] ci: inc medusa runs --- invariants/hub-suite/_config/medusa_config_ci.json | 4 ++-- invariants/protocol-suite/_config/medusa_config_ci.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invariants/hub-suite/_config/medusa_config_ci.json b/invariants/hub-suite/_config/medusa_config_ci.json index 05da8f4b7..ad7209670 100644 --- a/invariants/hub-suite/_config/medusa_config_ci.json +++ b/invariants/hub-suite/_config/medusa_config_ci.json @@ -3,8 +3,8 @@ "workers": 10, "workerResetLimit": 50, "timeout": 0, - "testLimit": 100000, - "callSequenceLength": 100, + "testLimit": 200000, + "callSequenceLength": 300, "corpusDirectory": "corpus", "coverageEnabled": true, "deploymentOrder": ["Tester"], diff --git a/invariants/protocol-suite/_config/medusa_config_ci.json b/invariants/protocol-suite/_config/medusa_config_ci.json index 43bcbef93..d65fd8b5e 100644 --- a/invariants/protocol-suite/_config/medusa_config_ci.json +++ b/invariants/protocol-suite/_config/medusa_config_ci.json @@ -3,8 +3,8 @@ "workers": 10, "workerResetLimit": 50, "timeout": 0, - "testLimit": 100000, - "callSequenceLength": 100, + "testLimit": 200000, + "callSequenceLength": 300, "corpusDirectory": "corpus", "coverageEnabled": true, "deploymentOrder": ["Tester"], From 897005910fb112a7f3ae021201edd4cdb8dfee72 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 04:13:17 +0530 Subject: [PATCH 093/106] ci: run on 5h timeout --- invariants/hub-suite/_config/echidna_config_ci.yaml | 7 +++++-- invariants/hub-suite/_config/medusa_config_ci.json | 4 ++-- invariants/protocol-suite/_config/echidna_config_ci.yaml | 7 +++++-- invariants/protocol-suite/_config/medusa_config_ci.json | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/invariants/hub-suite/_config/echidna_config_ci.yaml b/invariants/hub-suite/_config/echidna_config_ci.yaml index 964e975fa..f838c077d 100644 --- a/invariants/hub-suite/_config/echidna_config_ci.yaml +++ b/invariants/hub-suite/_config/echidna_config_ci.yaml @@ -10,8 +10,11 @@ balanceAddr: 0x1000000000000000000000000 #balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) balanceContract: 0x1000000000000000000000000000000000000000000000000 -#testLimit is the number of test sequences to run -testLimit: 200000 +#testLimit is the number of test sequences to run (0 = unlimited, rely on timeout) +testLimit: 0 + +#timeout in seconds (5 hours) +timeout: 18000 #seqLen defines how many transactions are in a test sequence seqLen: 300 diff --git a/invariants/hub-suite/_config/medusa_config_ci.json b/invariants/hub-suite/_config/medusa_config_ci.json index ad7209670..aa71b1f03 100644 --- a/invariants/hub-suite/_config/medusa_config_ci.json +++ b/invariants/hub-suite/_config/medusa_config_ci.json @@ -2,8 +2,8 @@ "fuzzing": { "workers": 10, "workerResetLimit": 50, - "timeout": 0, - "testLimit": 200000, + "timeout": 18000, + "testLimit": 0, "callSequenceLength": 300, "corpusDirectory": "corpus", "coverageEnabled": true, diff --git a/invariants/protocol-suite/_config/echidna_config_ci.yaml b/invariants/protocol-suite/_config/echidna_config_ci.yaml index 964e975fa..f838c077d 100644 --- a/invariants/protocol-suite/_config/echidna_config_ci.yaml +++ b/invariants/protocol-suite/_config/echidna_config_ci.yaml @@ -10,8 +10,11 @@ balanceAddr: 0x1000000000000000000000000 #balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) balanceContract: 0x1000000000000000000000000000000000000000000000000 -#testLimit is the number of test sequences to run -testLimit: 200000 +#testLimit is the number of test sequences to run (0 = unlimited, rely on timeout) +testLimit: 0 + +#timeout in seconds (5 hours) +timeout: 18000 #seqLen defines how many transactions are in a test sequence seqLen: 300 diff --git a/invariants/protocol-suite/_config/medusa_config_ci.json b/invariants/protocol-suite/_config/medusa_config_ci.json index d65fd8b5e..c4103a7de 100644 --- a/invariants/protocol-suite/_config/medusa_config_ci.json +++ b/invariants/protocol-suite/_config/medusa_config_ci.json @@ -2,8 +2,8 @@ "fuzzing": { "workers": 10, "workerResetLimit": 50, - "timeout": 0, - "testLimit": 200000, + "timeout": 18000, + "testLimit": 0, "callSequenceLength": 300, "corpusDirectory": "corpus", "coverageEnabled": true, From add9c16c936847969306b6b039f0aa05e3bd3a5e Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 04:21:05 +0530 Subject: [PATCH 094/106] chore: cleanup --- .../protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol | 2 -- invariants/protocol-suite/specs/PostconditionsSpec.t.sol | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index 9d01689ef..599abf75e 100644 --- a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -348,8 +348,6 @@ abstract contract DefaultBeforeAfterHooks is BaseHooks { signature == ISpokeHandler.setUsingAsCollateral.selector || signature == ISpokeHandler.updateUserDynamicConfig.selector ) { - // Dynamic config keys are only refreshed for collateral positions - // (see _processUserAccountData — refresh happens inside `if (collateral)` only). if (_userVarsBefore(spoke, reserveId, user).collateral) { assertEq(latestKey, userKey, GPOST_SP_E); } diff --git a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol index 10bdedcbe..0e2c3e338 100644 --- a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -24,14 +24,13 @@ abstract contract PostconditionsSpec is HubPostconditionsSpec { /////////////////////////////////////////////////////////////////////////////////////////////// string constant GPOST_SP_A = - "GPOST_SP_A: Stored (user.premiumDrawnShares/user.baseDrawnShares) & calculated user risk premium (calculation based on user's position, via spoke.calculateUserAccountData) need to be the same right after an operation"; + "GPOST_SP_A: Stored (user.premiumDrawnShares/user.baseDrawnShares) & calculated user risk premium (calculation based on user's position, via spoke.calculateUserAccountData) are the same right after an operation"; string constant GPOST_SP_E = 'GPOST_SP_E: DynamicRiskConfiguration for a user position is updated to latest reserve state whenever an action can potentially make their position less healthy'; - // - Updates on: borrow, withdraw, disableAsCollateral. + // - Updates on: borrow, withdraw, disableAsCollateral, updateUserDynamicConfig. // - Unchanged on: supply, repay, liquidate, updateUserRiskPremium, setUserPositionManager. // - Enabling collateral updates only the relevant reserve's dynamic config. - // - Exception: updateUserDynamicConfig explicitly refreshes the user's dynamic config. string constant HSPOST_SP_F = 'HSPOST_SP_F: Total debt of a user should not change after updateUserRiskPremium'; From a8f06ce8f608eb4be5f74bebf4d7bc3f23fcf881 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 04:22:07 +0530 Subject: [PATCH 095/106] ci: fix echidna run --- invariants/hub-suite/_config/echidna_config_ci.yaml | 8 ++++---- invariants/protocol-suite/_config/echidna_config_ci.yaml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/invariants/hub-suite/_config/echidna_config_ci.yaml b/invariants/hub-suite/_config/echidna_config_ci.yaml index f838c077d..79a8eb1fb 100644 --- a/invariants/hub-suite/_config/echidna_config_ci.yaml +++ b/invariants/hub-suite/_config/echidna_config_ci.yaml @@ -10,11 +10,11 @@ balanceAddr: 0x1000000000000000000000000 #balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) balanceContract: 0x1000000000000000000000000000000000000000000000000 -#testLimit is the number of test sequences to run (0 = unlimited, rely on timeout) -testLimit: 0 +#testLimit is the number of test sequences to run +testLimit: 10000000 -#timeout in seconds (5 hours) -timeout: 18000 +#timeout in seconds +timeout: 18000 # 5 hours #seqLen defines how many transactions are in a test sequence seqLen: 300 diff --git a/invariants/protocol-suite/_config/echidna_config_ci.yaml b/invariants/protocol-suite/_config/echidna_config_ci.yaml index f838c077d..79a8eb1fb 100644 --- a/invariants/protocol-suite/_config/echidna_config_ci.yaml +++ b/invariants/protocol-suite/_config/echidna_config_ci.yaml @@ -10,11 +10,11 @@ balanceAddr: 0x1000000000000000000000000 #balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) balanceContract: 0x1000000000000000000000000000000000000000000000000 -#testLimit is the number of test sequences to run (0 = unlimited, rely on timeout) -testLimit: 0 +#testLimit is the number of test sequences to run +testLimit: 10000000 -#timeout in seconds (5 hours) -timeout: 18000 +#timeout in seconds +timeout: 18000 # 5 hours #seqLen defines how many transactions are in a test sequence seqLen: 300 From f2bfede14e7e628ce0ad439c6093a12e12d794fe Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:06:51 +0530 Subject: [PATCH 096/106] chore: license cleanup --- invariants/hub-suite/HandlerAggregator.t.sol | 5 +++-- invariants/hub-suite/Invariants.t.sol | 5 +++-- invariants/hub-suite/Setup.t.sol | 5 +++-- invariants/hub-suite/SpecAggregator.t.sol | 5 +++-- invariants/hub-suite/Tester.t.sol | 5 +++-- invariants/hub-suite/base/BaseHandler.t.sol | 5 +++-- invariants/hub-suite/base/BaseHooks.t.sol | 5 +++-- invariants/hub-suite/base/BaseStorage.t.sol | 5 +++-- invariants/hub-suite/base/BaseTest.t.sol | 5 +++-- invariants/hub-suite/base/ProtocolAssertions.t.sol | 5 +++-- .../hub-suite/handlers/HubConfiguratorHandler.t.sol | 5 +++-- invariants/hub-suite/handlers/HubHandler.t.sol | 5 +++-- .../handlers/interfaces/IHubConfiguratorHandler.sol | 5 +++-- invariants/hub-suite/handlers/interfaces/IHubHandler.sol | 5 +++-- .../handlers/simulators/DonationAttackHandler.t.sol | 5 +++-- invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol | 5 +++-- invariants/hub-suite/hooks/HookAggregator.t.sol | 5 +++-- .../hub-suite/invariants/HubInvariantAssertions.t.sol | 5 +++-- invariants/hub-suite/invariants/HubInvariants.t.sol | 5 +++-- invariants/hub-suite/specs/HubInvariantsSpec.t.sol | 5 +++-- invariants/hub-suite/specs/HubPostconditionsSpec.t.sol | 5 +++-- invariants/hub-suite/utils/StdAsserts.sol | 5 +++-- invariants/protocol-suite/HandlerAggregator.t.sol | 5 +++-- invariants/protocol-suite/Invariants.t.sol | 5 +++-- invariants/protocol-suite/Setup.t.sol | 5 +++-- invariants/protocol-suite/SpecAggregator.t.sol | 5 +++-- invariants/protocol-suite/Tester.t.sol | 5 +++-- invariants/protocol-suite/TesterFoundry.t.sol | 5 +++-- invariants/protocol-suite/base/BaseHandler.t.sol | 5 +++-- invariants/protocol-suite/base/BaseHooks.t.sol | 5 +++-- invariants/protocol-suite/base/BaseStorage.t.sol | 5 +++-- invariants/protocol-suite/base/BaseTest.t.sol | 5 +++-- invariants/protocol-suite/base/ProtocolAssertions.t.sol | 5 +++-- .../handlers/hub/HubConfiguratorHandler.t.sol | 5 +++-- .../handlers/interfaces/IHubConfiguratorHandler.sol | 5 +++-- .../protocol-suite/handlers/interfaces/IHubHandler.sol | 5 +++-- .../handlers/interfaces/ISpokeConfiguratorHandler.sol | 5 +++-- .../protocol-suite/handlers/interfaces/ISpokeHandler.sol | 5 +++-- .../handlers/interfaces/ITreasurySpokeHandler.sol | 5 +++-- .../handlers/simulators/DonationAttackHandler.t.sol | 5 +++-- .../handlers/simulators/PriceFeedSimulatorHandler.t.sol | 5 +++-- .../handlers/spoke/SpokeConfiguratorHandler.t.sol | 5 +++-- .../protocol-suite/handlers/spoke/SpokeHandler.t.sol | 5 +++-- .../handlers/spoke/TreasurySpokeHandler.t.sol | 8 +++----- .../protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol | 5 +++-- invariants/protocol-suite/hooks/HookAggregator.t.sol | 5 +++-- .../protocol-suite/invariants/SpokeInvariants.t.sol | 5 +++-- invariants/protocol-suite/specs/InvariantsSpec.t.sol | 5 +++-- invariants/protocol-suite/specs/PostconditionsSpec.t.sol | 5 +++-- invariants/protocol-suite/specs/SpokeInvariantsSpec.t.sol | 5 +++-- invariants/shared/utils/Actor.sol | 5 +++-- invariants/shared/utils/ActorsUtils.sol | 5 +++-- invariants/shared/utils/CommonHelpers.sol | 5 +++-- invariants/shared/utils/ErrorHandlers.sol | 5 +++-- invariants/shared/utils/PropertiesConstants.sol | 5 +++-- 55 files changed, 165 insertions(+), 113 deletions(-) diff --git a/invariants/hub-suite/HandlerAggregator.t.sol b/invariants/hub-suite/HandlerAggregator.t.sol index 7fd465e18..0f04cbd63 100644 --- a/invariants/hub-suite/HandlerAggregator.t.sol +++ b/invariants/hub-suite/HandlerAggregator.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Handler contracts import {HubHandler} from './handlers/HubHandler.t.sol'; diff --git a/invariants/hub-suite/Invariants.t.sol b/invariants/hub-suite/Invariants.t.sol index 23f9d994c..2671c952a 100644 --- a/invariants/hub-suite/Invariants.t.sol +++ b/invariants/hub-suite/Invariants.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {HubInvariants} from './invariants/HubInvariants.t.sol'; diff --git a/invariants/hub-suite/Setup.t.sol b/invariants/hub-suite/Setup.t.sol index 37fe58c93..744ad46ab 100644 --- a/invariants/hub-suite/Setup.t.sol +++ b/invariants/hub-suite/Setup.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Libraries import {ActorsUtils} from '../shared/utils/ActorsUtils.sol'; diff --git a/invariants/hub-suite/SpecAggregator.t.sol b/invariants/hub-suite/SpecAggregator.t.sol index dd160ffd3..721a3744e 100644 --- a/invariants/hub-suite/SpecAggregator.t.sol +++ b/invariants/hub-suite/SpecAggregator.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Test Contracts import {HubInvariantsSpec} from './specs/HubInvariantsSpec.t.sol'; diff --git a/invariants/hub-suite/Tester.t.sol b/invariants/hub-suite/Tester.t.sol index ce2a71ffa..2a79bd796 100644 --- a/invariants/hub-suite/Tester.t.sol +++ b/invariants/hub-suite/Tester.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {Invariants} from './Invariants.t.sol'; import {Setup} from './Setup.t.sol'; diff --git a/invariants/hub-suite/base/BaseHandler.t.sol b/invariants/hub-suite/base/BaseHandler.t.sol index f618cc9a0..c8c5dad60 100644 --- a/invariants/hub-suite/base/BaseHandler.t.sol +++ b/invariants/hub-suite/base/BaseHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {CommonHelpers} from '../../shared/utils/CommonHelpers.sol'; import {HookAggregator} from '../hooks/HookAggregator.t.sol'; diff --git a/invariants/hub-suite/base/BaseHooks.t.sol b/invariants/hub-suite/base/BaseHooks.t.sol index e5015ea22..af6c30598 100644 --- a/invariants/hub-suite/base/BaseHooks.t.sol +++ b/invariants/hub-suite/base/BaseHooks.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Contracts import {ProtocolAssertions} from './ProtocolAssertions.t.sol'; diff --git a/invariants/hub-suite/base/BaseStorage.t.sol b/invariants/hub-suite/base/BaseStorage.t.sol index 2c20a9c8d..7aacb734c 100644 --- a/invariants/hub-suite/base/BaseStorage.t.sol +++ b/invariants/hub-suite/base/BaseStorage.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Contracts import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; diff --git a/invariants/hub-suite/base/BaseTest.t.sol b/invariants/hub-suite/base/BaseTest.t.sol index ff0521014..46850898c 100644 --- a/invariants/hub-suite/base/BaseTest.t.sol +++ b/invariants/hub-suite/base/BaseTest.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Libraries import {Vm} from 'forge-std/Base.sol'; diff --git a/invariants/hub-suite/base/ProtocolAssertions.t.sol b/invariants/hub-suite/base/ProtocolAssertions.t.sol index 30b41eb54..0b6b9bd1a 100644 --- a/invariants/hub-suite/base/ProtocolAssertions.t.sol +++ b/invariants/hub-suite/base/ProtocolAssertions.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Base import {BaseTest} from './BaseTest.t.sol'; diff --git a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol index bb0602f2c..d4e6b2bbf 100644 --- a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol +++ b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Libraries import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index b452f0d70..952e14858 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Interfaces import {IHub, IHubBase} from 'src/hub/interfaces/IHub.sol'; diff --git a/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol index 14715b90f..e5c4d4402 100644 --- a/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol +++ b/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title IHubConfiguratorHandler /// @notice Interface for the HubConfiguratorHandler diff --git a/invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubHandler.sol index 0da8f98c3..3eab4adee 100644 --- a/invariants/hub-suite/handlers/interfaces/IHubHandler.sol +++ b/invariants/hub-suite/handlers/interfaces/IHubHandler.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {IHubBase} from 'src/hub/interfaces/IHub.sol'; diff --git a/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol b/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol index ad927e37b..4e35e3f0b 100644 --- a/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol +++ b/invariants/hub-suite/handlers/simulators/DonationAttackHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Test Contracts import {BaseHandler} from '../../base/BaseHandler.t.sol'; diff --git a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol index a96d722d4..20e86562d 100644 --- a/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/hub-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Libraries import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; diff --git a/invariants/hub-suite/hooks/HookAggregator.t.sol b/invariants/hub-suite/hooks/HookAggregator.t.sol index d65fb3ba3..e088a13b7 100644 --- a/invariants/hub-suite/hooks/HookAggregator.t.sol +++ b/invariants/hub-suite/hooks/HookAggregator.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Hook Contracts import {DefaultBeforeAfterHooks} from './DefaultBeforeAfterHooks.t.sol'; diff --git a/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol b/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol index fbb5775c3..bbbf94a1e 100644 --- a/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol +++ b/invariants/hub-suite/invariants/HubInvariantAssertions.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Libraries import {Premium} from 'src/hub/libraries/Premium.sol'; diff --git a/invariants/hub-suite/invariants/HubInvariants.t.sol b/invariants/hub-suite/invariants/HubInvariants.t.sol index c8909560a..3ff901fbb 100644 --- a/invariants/hub-suite/invariants/HubInvariants.t.sol +++ b/invariants/hub-suite/invariants/HubInvariants.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Interfaces import {IHub} from 'src/hub/interfaces/IHub.sol'; diff --git a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol index 2ef84652b..8176af86b 100644 --- a/invariants/hub-suite/specs/HubInvariantsSpec.t.sol +++ b/invariants/hub-suite/specs/HubInvariantsSpec.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title HubInvariantsSpec /// @notice Invariants specification for the hub diff --git a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol index ba67cb6cc..c5aff7698 100644 --- a/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol +++ b/invariants/hub-suite/specs/HubPostconditionsSpec.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title HubPostconditionsSpec /// @notice Postconditions specification for the hub diff --git a/invariants/hub-suite/utils/StdAsserts.sol b/invariants/hub-suite/utils/StdAsserts.sol index a9720f170..0ac78b864 100644 --- a/invariants/hub-suite/utils/StdAsserts.sol +++ b/invariants/hub-suite/utils/StdAsserts.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @notice Standard assertions for the test suite abstract contract StdAsserts { diff --git a/invariants/protocol-suite/HandlerAggregator.t.sol b/invariants/protocol-suite/HandlerAggregator.t.sol index 563e9172f..bef08f0b6 100644 --- a/invariants/protocol-suite/HandlerAggregator.t.sol +++ b/invariants/protocol-suite/HandlerAggregator.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Handler contracts import {SpokeHandler} from './handlers/spoke/SpokeHandler.t.sol'; diff --git a/invariants/protocol-suite/Invariants.t.sol b/invariants/protocol-suite/Invariants.t.sol index 929e884e4..501b4f440 100644 --- a/invariants/protocol-suite/Invariants.t.sol +++ b/invariants/protocol-suite/Invariants.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; diff --git a/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol index 297367d45..e09a7e8e8 100644 --- a/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Libraries import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; diff --git a/invariants/protocol-suite/SpecAggregator.t.sol b/invariants/protocol-suite/SpecAggregator.t.sol index 14259ce14..27fcf0241 100644 --- a/invariants/protocol-suite/SpecAggregator.t.sol +++ b/invariants/protocol-suite/SpecAggregator.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Test Contracts import {InvariantsSpec} from './specs/InvariantsSpec.t.sol'; diff --git a/invariants/protocol-suite/Tester.t.sol b/invariants/protocol-suite/Tester.t.sol index f8edf57bf..735a17d43 100644 --- a/invariants/protocol-suite/Tester.t.sol +++ b/invariants/protocol-suite/Tester.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {Invariants} from './Invariants.t.sol'; import {Setup} from './Setup.t.sol'; diff --git a/invariants/protocol-suite/TesterFoundry.t.sol b/invariants/protocol-suite/TesterFoundry.t.sol index c0c5ddeb4..6b12a543d 100644 --- a/invariants/protocol-suite/TesterFoundry.t.sol +++ b/invariants/protocol-suite/TesterFoundry.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Contracts import {StdInvariant} from 'forge-std/StdInvariant.sol'; diff --git a/invariants/protocol-suite/base/BaseHandler.t.sol b/invariants/protocol-suite/base/BaseHandler.t.sol index f618cc9a0..c8c5dad60 100644 --- a/invariants/protocol-suite/base/BaseHandler.t.sol +++ b/invariants/protocol-suite/base/BaseHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {CommonHelpers} from '../../shared/utils/CommonHelpers.sol'; import {HookAggregator} from '../hooks/HookAggregator.t.sol'; diff --git a/invariants/protocol-suite/base/BaseHooks.t.sol b/invariants/protocol-suite/base/BaseHooks.t.sol index e5015ea22..af6c30598 100644 --- a/invariants/protocol-suite/base/BaseHooks.t.sol +++ b/invariants/protocol-suite/base/BaseHooks.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Contracts import {ProtocolAssertions} from './ProtocolAssertions.t.sol'; diff --git a/invariants/protocol-suite/base/BaseStorage.t.sol b/invariants/protocol-suite/base/BaseStorage.t.sol index ce29ac03f..bec6faa54 100644 --- a/invariants/protocol-suite/base/BaseStorage.t.sol +++ b/invariants/protocol-suite/base/BaseStorage.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; diff --git a/invariants/protocol-suite/base/BaseTest.t.sol b/invariants/protocol-suite/base/BaseTest.t.sol index a6efd326f..d7b961aff 100644 --- a/invariants/protocol-suite/base/BaseTest.t.sol +++ b/invariants/protocol-suite/base/BaseTest.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Libraries import {Vm} from 'forge-std/Base.sol'; diff --git a/invariants/protocol-suite/base/ProtocolAssertions.t.sol b/invariants/protocol-suite/base/ProtocolAssertions.t.sol index 30b41eb54..0b6b9bd1a 100644 --- a/invariants/protocol-suite/base/ProtocolAssertions.t.sol +++ b/invariants/protocol-suite/base/ProtocolAssertions.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Base import {BaseTest} from './BaseTest.t.sol'; diff --git a/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol b/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol index 7be75842b..9ddc7a139 100644 --- a/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol +++ b/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Interfaces import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; diff --git a/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol index b6818d4cc..70423181b 100644 --- a/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title IHubConfiguratorHandler /// @notice Interface for the HubConfiguratorHandler diff --git a/invariants/protocol-suite/handlers/interfaces/IHubHandler.sol b/invariants/protocol-suite/handlers/interfaces/IHubHandler.sol index c86de9be2..007eb885d 100644 --- a/invariants/protocol-suite/handlers/interfaces/IHubHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/IHubHandler.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title IHubHandler /// @notice Interface for the HubHandler diff --git a/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol b/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol index 193a22c1d..3b2015a75 100644 --- a/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/ISpokeConfiguratorHandler.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title ISpokeConfiguratorHandler /// @notice Interface for the SpokeConfiguratorHandler diff --git a/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol b/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol index 9309e5832..2b16c0767 100644 --- a/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/ISpokeHandler.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title ISpokeHandler /// @notice Interface for the SpokeHandler diff --git a/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol b/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol index a17a34ee6..64f6e9454 100644 --- a/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/ITreasurySpokeHandler.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title ITreasurySpokeHandler /// @notice Interface for the TreasurySpokeHandler diff --git a/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol b/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol index 7eb58d02e..e3d98a5b1 100644 --- a/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol +++ b/invariants/protocol-suite/handlers/simulators/DonationAttackHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; diff --git a/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol b/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol index bd932bd1f..892feed73 100644 --- a/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol +++ b/invariants/protocol-suite/handlers/simulators/PriceFeedSimulatorHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Test Contracts import {BaseHandler} from '../../base/BaseHandler.t.sol'; diff --git a/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol index 4a939e42e..a25246ce5 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeConfiguratorHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Interfaces import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index 2e75a0f0f..6324286cb 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Interfaces import {ISpoke, ISpokeBase} from 'src/spoke/interfaces/ISpoke.sol'; diff --git a/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol index c10f58948..05fbad10e 100644 --- a/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol @@ -1,13 +1,11 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Interfaces import {ITreasurySpokeHandler} from '../interfaces/ITreasurySpokeHandler.sol'; import {ITreasurySpoke} from 'src/spoke/interfaces/ITreasurySpoke.sol'; -// Libraries -import 'forge-std/console.sol'; - // Test Contracts import {BaseHandler} from '../../base/BaseHandler.t.sol'; diff --git a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol index 599abf75e..fd0c34a84 100644 --- a/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol +++ b/invariants/protocol-suite/hooks/DefaultBeforeAfterHooks.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Libraries import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; diff --git a/invariants/protocol-suite/hooks/HookAggregator.t.sol b/invariants/protocol-suite/hooks/HookAggregator.t.sol index 18fc75ccb..f145e0b74 100644 --- a/invariants/protocol-suite/hooks/HookAggregator.t.sol +++ b/invariants/protocol-suite/hooks/HookAggregator.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {DefaultBeforeAfterHooks} from './DefaultBeforeAfterHooks.t.sol'; diff --git a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol index 362cbd09a..d7ce938c6 100644 --- a/invariants/protocol-suite/invariants/SpokeInvariants.t.sol +++ b/invariants/protocol-suite/invariants/SpokeInvariants.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; diff --git a/invariants/protocol-suite/specs/InvariantsSpec.t.sol b/invariants/protocol-suite/specs/InvariantsSpec.t.sol index 602c9bf61..6a77926d3 100644 --- a/invariants/protocol-suite/specs/InvariantsSpec.t.sol +++ b/invariants/protocol-suite/specs/InvariantsSpec.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {HubInvariantsSpec} from '../../hub-suite/specs/HubInvariantsSpec.t.sol'; import {SpokeInvariantsSpec} from './SpokeInvariantsSpec.t.sol'; diff --git a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol index 0e2c3e338..d6c54f360 100644 --- a/invariants/protocol-suite/specs/PostconditionsSpec.t.sol +++ b/invariants/protocol-suite/specs/PostconditionsSpec.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {HubPostconditionsSpec} from '../../hub-suite/specs/HubPostconditionsSpec.t.sol'; diff --git a/invariants/protocol-suite/specs/SpokeInvariantsSpec.t.sol b/invariants/protocol-suite/specs/SpokeInvariantsSpec.t.sol index 01d1a97a2..24ab757f4 100644 --- a/invariants/protocol-suite/specs/SpokeInvariantsSpec.t.sol +++ b/invariants/protocol-suite/specs/SpokeInvariantsSpec.t.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title SpokeInvariantsSpec /// @notice Invariants specification for the spoke diff --git a/invariants/shared/utils/Actor.sol b/invariants/shared/utils/Actor.sol index 180d3c195..0877ddea0 100644 --- a/invariants/shared/utils/Actor.sol +++ b/invariants/shared/utils/Actor.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Interfaces import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol'; diff --git a/invariants/shared/utils/ActorsUtils.sol b/invariants/shared/utils/ActorsUtils.sol index 9620af9d4..230c308a2 100644 --- a/invariants/shared/utils/ActorsUtils.sol +++ b/invariants/shared/utils/ActorsUtils.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; // Test Contracts import {Actor} from './Actor.sol'; diff --git a/invariants/shared/utils/CommonHelpers.sol b/invariants/shared/utils/CommonHelpers.sol index 65cd8cd5c..a3d7890d2 100644 --- a/invariants/shared/utils/CommonHelpers.sol +++ b/invariants/shared/utils/CommonHelpers.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; import {Vm} from 'forge-std/Vm.sol'; import {MockERC20} from 'tests/mocks/MockERC20.sol'; diff --git a/invariants/shared/utils/ErrorHandlers.sol b/invariants/shared/utils/ErrorHandlers.sol index 6b648630d..165e28744 100644 --- a/invariants/shared/utils/ErrorHandlers.sol +++ b/invariants/shared/utils/ErrorHandlers.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; /// @title ErrorHandlers /// @notice Library for handling errors in the test suite diff --git a/invariants/shared/utils/PropertiesConstants.sol b/invariants/shared/utils/PropertiesConstants.sol index 957c73c5e..15bd32b67 100644 --- a/invariants/shared/utils/PropertiesConstants.sol +++ b/invariants/shared/utils/PropertiesConstants.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; abstract contract PropertiesConstants { // Echidna constants From 5daa74dc654091e11b82e1a5c374596a2fbec383 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:00:48 +0530 Subject: [PATCH 097/106] fix: mint fee shares handler --- invariants/hub-suite/Setup.t.sol | 7 +++++-- invariants/hub-suite/handlers/HubHandler.t.sol | 10 ++++++++++ .../hub-suite/handlers/interfaces/IHubHandler.sol | 2 ++ medusa.hub.json | 4 ++-- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/invariants/hub-suite/Setup.t.sol b/invariants/hub-suite/Setup.t.sol index 744ad46ab..988ce49c5 100644 --- a/invariants/hub-suite/Setup.t.sol +++ b/invariants/hub-suite/Setup.t.sol @@ -287,12 +287,15 @@ contract Setup is BaseTest { // Grant roles to configurators accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(this), 0); + accessManager.grantRole(Roles.HUB_CONFIGURATOR_ROLE, address(this), 0); + // Grant responsibilities on hubs { - bytes4[] memory selectors = new bytes4[](3); + bytes4[] memory selectors = new bytes4[](4); selectors[0] = IHub.updateSpokeConfig.selector; selectors[1] = IHub.setInterestRateData.selector; selectors[2] = IHub.updateAssetConfig.selector; + selectors[3] = IHub.mintFeeShares.selector; accessManager.setTargetFunctionRole(address(hub), selectors, Roles.HUB_ADMIN_ROLE); } @@ -324,7 +327,7 @@ contract Setup is BaseTest { accessManager.setTargetFunctionRole( address(hubConfigurator), selectors, - accessManager.PUBLIC_ROLE() + Roles.HUB_CONFIGURATOR_ROLE ); } } diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index 952e14858..b5fd75f05 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -345,6 +345,16 @@ contract HubHandler is BaseHandler, IHubHandler { } } + function mintFeeShares(uint8 i) external { + uint256 assetId = _getRandomBaseAssetId(i); + _before(); + try hub.mintFeeShares(assetId) { + _after(); + } catch { + revert('HubHandler: mintFeeShares failed'); + } + } + /////////////////////////////////////////////////////////////////////////////////////////////// // ERC4626 ROUNDTRIP (STATELESS) // /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/invariants/hub-suite/handlers/interfaces/IHubHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubHandler.sol index 3eab4adee..098b72b27 100644 --- a/invariants/hub-suite/handlers/interfaces/IHubHandler.sol +++ b/invariants/hub-suite/handlers/interfaces/IHubHandler.sol @@ -41,6 +41,8 @@ interface IHubHandler { function reclaim(uint256 amount, uint8 i) external; + function mintFeeShares(uint8 i) external; + function roundtrip_ERC4626_RT_A(uint256 amount, uint8 i) external; function roundtrip_ERC4626_RT_B(uint256 amount, uint8 i) external; diff --git a/medusa.hub.json b/medusa.hub.json index 2364bcf54..3b3b49ecd 100644 --- a/medusa.hub.json +++ b/medusa.hub.json @@ -1,8 +1,8 @@ { "fuzzing": { - "workers": 10, + "workers": 15, "workerResetLimit": 50, - "timeout": 0, + "timeout": 25000, "testLimit": 0, "callSequenceLength": 100, "corpusDirectory": "invariants/hub-suite/_corpus/medusa", From 30ce5d5076782b4ac7c06af7717e85fad740226f Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:01:23 +0530 Subject: [PATCH 098/106] fix: treasury spoke handler --- .../handlers/spoke/TreasurySpokeHandler.t.sol | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol index 05fbad10e..b3c8268a3 100644 --- a/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.0; // Interfaces import {ITreasurySpokeHandler} from '../interfaces/ITreasurySpokeHandler.sol'; import {ITreasurySpoke} from 'src/spoke/interfaces/ITreasurySpoke.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; // Test Contracts import {BaseHandler} from '../../base/BaseHandler.t.sol'; @@ -16,13 +17,6 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// - /* - - E.g. num of active pools - uint256 public activePools; - - */ - /////////////////////////////////////////////////////////////////////////////////////////////// // ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -32,16 +26,13 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { /////////////////////////////////////////////////////////////////////////////////////////////// function supply(uint256 amount, uint8 i, uint8 j) external { - // TODO fix coverage issues - // Get one of the hub addresses randomly - address hubAddress = _getRandomHub(i); - address treasurySpoke = hubInfo[hubAddress].treasurySpoke; - - // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(treasurySpoke, j); + address hub = _getRandomHub(i); + address spoke = hubInfo[hub].treasurySpoke; + uint256 reserveId = _getRandomReserveId(spoke, j); + _tryMintAndApprove(_underlying(spoke, reserveId), address(this), spoke, amount); _before(); - try ITreasurySpoke(treasurySpoke).supply(reserveId, amount, msg.sender) { + try ISpoke(spoke).supply(reserveId, amount, msg.sender) { _after(); } catch { revert('TreasurySpokeHandler: supply failed'); @@ -49,15 +40,12 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { } function withdraw(uint256 amount, uint8 i, uint8 j) external { - // Get one of the hub addresses randomly - address hubAddress = _getRandomHub(i); - address treasurySpoke = hubInfo[hubAddress].treasurySpoke; - - // Get one of the reserves IDs randomly - uint256 reserveId = _getRandomReserveId(treasurySpoke, j); + address hub = _getRandomHub(i); + address spoke = hubInfo[hub].treasurySpoke; + uint256 reserveId = _getRandomReserveId(spoke, j); _before(); - try ITreasurySpoke(treasurySpoke).withdraw(reserveId, amount, msg.sender) { + try ISpoke(spoke).withdraw(reserveId, amount, msg.sender) { _after(); } catch { revert('TreasurySpokeHandler: withdraw failed'); @@ -65,17 +53,12 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { } function transfer(uint256 amount, uint8 i, uint8 j, uint8 k) external { - // Get one of the hub addresses randomly - address hubAddress = _getRandomHub(i); - - // Get one of the assets IDs randomly + address hub = _getRandomHub(i); address asset = _getRandomBaseAsset(j); - - // Get one of the actors randomly address to = _getRandomActor(k); _before(); - try ITreasurySpoke(hubInfo[hubAddress].treasurySpoke).transfer(asset, to, amount) { + try ITreasurySpoke(hubInfo[hub].treasurySpoke).transfer(asset, to, amount) { _after(); } catch { revert('TreasurySpokeHandler: transfer failed'); From 83023afe92608a73029e04e6d7a86c09ddaefccb Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:05:00 +0530 Subject: [PATCH 099/106] feat: hub admin handler --- invariants/hub-suite/HandlerAggregator.t.sol | 8 +- .../hub-suite/handlers/HubAdminHandler.t.sol | 100 ++++++++++++++++++ .../handlers/interfaces/IHubAdminHandler.sol | 15 +++ .../protocol-suite/HandlerAggregator.t.sol | 4 +- invariants/protocol-suite/Setup.t.sol | 11 +- .../handlers/hub/HubAdminHandler.t.sol | 48 +++++++++ 6 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 invariants/hub-suite/handlers/HubAdminHandler.t.sol create mode 100644 invariants/hub-suite/handlers/interfaces/IHubAdminHandler.sol create mode 100644 invariants/protocol-suite/handlers/hub/HubAdminHandler.t.sol diff --git a/invariants/hub-suite/HandlerAggregator.t.sol b/invariants/hub-suite/HandlerAggregator.t.sol index 0f04cbd63..c4d1f9a4f 100644 --- a/invariants/hub-suite/HandlerAggregator.t.sol +++ b/invariants/hub-suite/HandlerAggregator.t.sol @@ -5,9 +5,15 @@ pragma solidity ^0.8.0; // Handler contracts import {HubHandler} from './handlers/HubHandler.t.sol'; import {HubConfiguratorHandler} from './handlers/HubConfiguratorHandler.t.sol'; +import {HubAdminHandler} from './handlers/HubAdminHandler.t.sol'; import {DonationAttackHandler} from './handlers/simulators/DonationAttackHandler.t.sol'; /// @notice Helper contract to aggregate all handler contracts for hub suite -abstract contract HandlerAggregator is HubHandler, HubConfiguratorHandler, DonationAttackHandler { +abstract contract HandlerAggregator is + HubHandler, + HubConfiguratorHandler, + HubAdminHandler, + DonationAttackHandler +{ function _setUpHandlers() internal {} } diff --git a/invariants/hub-suite/handlers/HubAdminHandler.t.sol b/invariants/hub-suite/handlers/HubAdminHandler.t.sol new file mode 100644 index 000000000..92445931f --- /dev/null +++ b/invariants/hub-suite/handlers/HubAdminHandler.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +// Interfaces +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {IHubAdminHandler} from './interfaces/IHubAdminHandler.sol'; + +// Test Contracts +import {CommonHelpers} from '../../shared/utils/CommonHelpers.sol'; +import {BaseHandler} from '../base/BaseHandler.t.sol'; + +/// @title HubAdminHandlerBase +/// @notice Handler for hub-level admin operations that are not configurator-restricted: +/// liquidity reinvestment (sweep/reclaim) and fee share minting (mintFeeShares). +/// @dev These target the Hub's reinvestment controller interface and access-managed fee paths. +/// The handler itself is the caller (not an actor proxy), so it must be configured as the +/// reinvestmentController for sweep/reclaim and granted permissions for mintFeeShares. +abstract contract HubAdminHandlerBase is CommonHelpers, IHubAdminHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function sweep(uint256 amount, uint8 i, uint8 j) external { + uint256 assetId = _getRandomAssetId(i); + IHub hub = _getRandomHub(j); + + _beforeHook(); + try hub.sweep(assetId, amount) { + _afterHook(); + } catch { + revert('HubHandler: sweep failed'); + } + } + + function reclaim(uint256 amount, uint8 i, uint8 j) external { + uint256 assetId = _getRandomAssetId(i); + IHub hub = _getRandomHub(j); + + _tryMint(hub.getAsset(assetId).underlying, address(hub), amount); + + _beforeHook(); + try hub.reclaim(assetId, amount) { + _afterHook(); + } catch { + revert('HubHandler: reclaim failed'); + } + } + + function mintFeeShares(uint8 i, uint8 j) external { + uint256 assetId = _getRandomAssetId(i); + IHub hub = _getRandomHub(j); + + _beforeHook(); + try hub.mintFeeShares(assetId) { + _afterHook(); + } catch { + revert('HubHandler: mintFeeShares failed'); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// @dev Hook invoked before each handler action. Concrete handlers wire this to the suite's _before() snapshot. + function _beforeHook() internal virtual; + /// @dev Hook invoked after each handler action. Concrete handlers wire this to the suite's _after() snapshot + postcondition checks. + function _afterHook() internal virtual; + + /// @dev Returns the hub to target for the current action. + function _getRandomHub(uint8 i) internal view virtual returns (IHub); + + /// @dev Returns the hub-level assetId to target for the current action. + function _getRandomAssetId(uint8 i) internal view virtual returns (uint256); +} + +/// @title HubAdminHandler +/// @notice Hub-suite concrete handler — single hub, selects a random asset id per call. +contract HubAdminHandler is HubAdminHandlerBase, BaseHandler { + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _beforeHook() internal override { + _before(); + } + + function _afterHook() internal override { + _after(); + } + + function _getRandomHub(uint8) internal view override returns (IHub) { + return hub; + } + + function _getRandomAssetId(uint8 i) internal view override returns (uint256) { + return _getRandomBaseAssetId(i); + } +} diff --git a/invariants/hub-suite/handlers/interfaces/IHubAdminHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubAdminHandler.sol new file mode 100644 index 000000000..948cac037 --- /dev/null +++ b/invariants/hub-suite/handlers/interfaces/IHubAdminHandler.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +/// @title IHubAdminHandler +/// @notice Interface for the HubAdminHandler — targets hub-level admin operations +/// that are not spoke-restricted: liquidity reinvestment (sweep/reclaim) +/// and fee share minting (mintFeeShares). +interface IHubAdminHandler { + function sweep(uint256 amount, uint8 i, uint8 j) external; + + function reclaim(uint256 amount, uint8 i, uint8 j) external; + + function mintFeeShares(uint8 i, uint8 j) external; +} diff --git a/invariants/protocol-suite/HandlerAggregator.t.sol b/invariants/protocol-suite/HandlerAggregator.t.sol index bef08f0b6..bc2d40bf0 100644 --- a/invariants/protocol-suite/HandlerAggregator.t.sol +++ b/invariants/protocol-suite/HandlerAggregator.t.sol @@ -5,8 +5,9 @@ pragma solidity ^0.8.0; // Handler contracts import {SpokeHandler} from './handlers/spoke/SpokeHandler.t.sol'; import {TreasurySpokeHandler} from './handlers/spoke/TreasurySpokeHandler.t.sol'; -import {HubConfiguratorHandler} from './handlers/hub/HubConfiguratorHandler.t.sol'; import {SpokeConfiguratorHandler} from './handlers/spoke/SpokeConfiguratorHandler.t.sol'; +import {HubConfiguratorHandler} from './handlers/hub/HubConfiguratorHandler.t.sol'; +import {HubAdminHandler} from './handlers/hub/HubAdminHandler.t.sol'; // Simulator contracts import {PriceFeedSimulatorHandler} from './handlers/simulators/PriceFeedSimulatorHandler.t.sol'; @@ -18,6 +19,7 @@ abstract contract HandlerAggregator is TreasurySpokeHandler, HubConfiguratorHandler, // Configurators SpokeConfiguratorHandler, + HubAdminHandler, PriceFeedSimulatorHandler, // Simulators DonationAttackHandler { diff --git a/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol index e09a7e8e8..46eb2448d 100644 --- a/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -204,7 +204,7 @@ contract Setup is BaseTest { liquidityFee: 5_00, feeReceiver: address(treasurySpoke1), irStrategy: address(irStrategy1), - reinvestmentController: address(0) + reinvestmentController: address(this) }), new bytes(0) ); @@ -224,7 +224,7 @@ contract Setup is BaseTest { liquidityFee: 10_00, feeReceiver: address(treasurySpoke1), irStrategy: address(irStrategy1), - reinvestmentController: address(0) + reinvestmentController: address(this) }), new bytes(0) ); @@ -254,7 +254,7 @@ contract Setup is BaseTest { liquidityFee: 10_00, feeReceiver: address(treasurySpoke2), irStrategy: address(irStrategy2), - reinvestmentController: address(0) + reinvestmentController: address(this) }), new bytes(0) ); @@ -274,7 +274,7 @@ contract Setup is BaseTest { liquidityFee: 5_00, feeReceiver: address(treasurySpoke2), irStrategy: address(irStrategy2), - reinvestmentController: address(0) + reinvestmentController: address(this) }), new bytes(0) ); @@ -564,9 +564,10 @@ contract Setup is BaseTest { // Grant responsibilities to hubs { - bytes4[] memory selectors = new bytes4[](2); + bytes4[] memory selectors = new bytes4[](3); selectors[0] = IHub.updateSpokeConfig.selector; selectors[1] = IHub.setInterestRateData.selector; + selectors[2] = IHub.mintFeeShares.selector; // enables HubAdminHandler to materialize fee shares accessManager.setTargetFunctionRole(address(hub1), selectors, Roles.HUB_ADMIN_ROLE); accessManager.setTargetFunctionRole(address(hub2), selectors, Roles.HUB_ADMIN_ROLE); } diff --git a/invariants/protocol-suite/handlers/hub/HubAdminHandler.t.sol b/invariants/protocol-suite/handlers/hub/HubAdminHandler.t.sol new file mode 100644 index 000000000..5f9360425 --- /dev/null +++ b/invariants/protocol-suite/handlers/hub/HubAdminHandler.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +// Interfaces +import {IHub} from 'src/hub/interfaces/IHub.sol'; + +// Test Contracts +import {HubAdminHandlerBase} from '../../../hub-suite/handlers/HubAdminHandler.t.sol'; +import {BaseHandler} from '../../base/BaseHandler.t.sol'; + +/// @title HubAdminHandler +/// @notice Protocol-suite concrete handler — multi-hub variant that selects hubs indirectly +/// through spoke → reserveId → hub mappings, covering all hub/asset combinations +/// reachable from the deployed spokes. +contract HubAdminHandler is BaseHandler, HubAdminHandlerBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _beforeHook() internal override { + _before(); + } + + function _afterHook() internal override { + _after(); + } + + /// @dev Picks a random spoke, derives a random reserveId, then resolves the backing hub. + function _getRandomHub(uint8 i) internal view override returns (IHub) { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId( + spoke, + _bound(_randomize(i, vm.toString(spoke)), 0, type(uint8).max) + ); + return IHub(_getHubAddress(spoke, reserveId)); + } + + /// @dev Picks a random spoke, derives a random reserveId, then resolves the hub-level assetId. + function _getRandomAssetId(uint8 i) internal view override returns (uint256) { + address spoke = _getRandomSpoke(i); + uint256 reserveId = _getRandomReserveId( + spoke, + _bound(_randomize(i, vm.toString(spoke)), 0, type(uint8).max) + ); + return _getAssetId(spoke, reserveId); + } +} From 513579495cc55283d27c02a8522da78cac73a611 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:55:13 +0530 Subject: [PATCH 100/106] chore: adapt to latest --- invariants/hub-suite/Setup.t.sol | 2 +- invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol | 4 ++-- .../hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol | 2 +- invariants/hub-suite/replays/ReplayTest_4.t.sol | 4 ++-- invariants/protocol-suite/Setup.t.sol | 2 +- .../protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol | 4 ++-- .../handlers/interfaces/IHubConfiguratorHandler.sol | 2 +- invariants/protocol-suite/replays/ReplayTest_7.t.sol | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/invariants/hub-suite/Setup.t.sol b/invariants/hub-suite/Setup.t.sol index 988ce49c5..8c45bf273 100644 --- a/invariants/hub-suite/Setup.t.sol +++ b/invariants/hub-suite/Setup.t.sol @@ -313,7 +313,7 @@ contract Setup is BaseTest { selectors[9] = IHubConfigurator.addSpokeToAssets.selector; selectors[10] = IHubConfigurator.updateSpokeActive.selector; selectors[11] = IHubConfigurator.updateSpokeHalted.selector; - selectors[12] = IHubConfigurator.updateSpokeSupplyCap.selector; + selectors[12] = IHubConfigurator.updateSpokeAddCap.selector; selectors[13] = IHubConfigurator.updateSpokeDrawCap.selector; selectors[14] = IHubConfigurator.updateSpokeRiskPremiumThreshold.selector; selectors[15] = IHubConfigurator.updateSpokeCaps.selector; diff --git a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol index d4e6b2bbf..a002755f9 100644 --- a/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol +++ b/invariants/hub-suite/handlers/HubConfiguratorHandler.t.sol @@ -28,11 +28,11 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { // SPOKE CONFIG // /////////////////////////////////////////////////////////////////////////////////////////////// - function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j) external setup { + function updateSpokeAddCap(uint256 addCap, uint8 i, uint8 j) external setup { uint256 assetId = _getRandomBaseAssetId(i); address spoke = _getRandomActor(j); addCap = _bound(addCap, 0, MAX_ALLOWED_SPOKE_CAP); - hubConfigurator.updateSpokeSupplyCap(address(hub), assetId, spoke, addCap); + hubConfigurator.updateSpokeAddCap(address(hub), assetId, spoke, addCap); } function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j) external setup { diff --git a/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol index e5c4d4402..e2a8a0e7e 100644 --- a/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol +++ b/invariants/hub-suite/handlers/interfaces/IHubConfiguratorHandler.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; /// @title IHubConfiguratorHandler /// @notice Interface for the HubConfiguratorHandler interface IHubConfiguratorHandler { - function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j) external; + function updateSpokeAddCap(uint256 addCap, uint8 i, uint8 j) external; function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j) external; diff --git a/invariants/hub-suite/replays/ReplayTest_4.t.sol b/invariants/hub-suite/replays/ReplayTest_4.t.sol index 19f22ecf6..857bf901a 100644 --- a/invariants/hub-suite/replays/ReplayTest_4.t.sol +++ b/invariants/hub-suite/replays/ReplayTest_4.t.sol @@ -43,7 +43,7 @@ contract ReplayTest4 is Invariants, Setup { _setUpActor(USER1); Tester.draw(1, 0); _delay(150); - Tester.updateSpokeSupplyCap(0, 9, 50); + Tester.updateSpokeAddCap(0, 9, 50); Tester.add(9, 0); } @@ -51,7 +51,7 @@ contract ReplayTest4 is Invariants, Setup { function test_replay_4_draw() public { _setUpActor(USER1); Tester.add(926436254396264375725066, 1); - Tester.updateSpokeSupplyCap(0, 1, 0); + Tester.updateSpokeAddCap(0, 1, 0); Tester.draw(68, 1); _delay(659719); Tester.draw(403197591059359279380077, 1); diff --git a/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol index 46eb2448d..3cb376500 100644 --- a/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -586,7 +586,7 @@ contract Setup is BaseTest { selectors[9] = IHubConfigurator.addSpokeToAssets.selector; selectors[10] = IHubConfigurator.updateSpokeActive.selector; selectors[11] = IHubConfigurator.updateSpokeHalted.selector; - selectors[12] = IHubConfigurator.updateSpokeSupplyCap.selector; + selectors[12] = IHubConfigurator.updateSpokeAddCap.selector; selectors[13] = IHubConfigurator.updateSpokeDrawCap.selector; selectors[14] = IHubConfigurator.updateSpokeRiskPremiumThreshold.selector; selectors[15] = IHubConfigurator.updateSpokeCaps.selector; diff --git a/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol b/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol index 9ddc7a139..32e8da423 100644 --- a/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol +++ b/invariants/protocol-suite/handlers/hub/HubConfiguratorHandler.t.sol @@ -25,12 +25,12 @@ contract HubConfiguratorHandler is BaseHandler, IHubConfiguratorHandler { // SPOKE CONFIG // /////////////////////////////////////////////////////////////////////////////////////////////// - function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j, uint8 k) external setup { + function updateSpokeAddCap(uint256 addCap, uint8 i, uint8 j, uint8 k) external setup { address hub = _getRandomHub(i); uint256 assetId = _getRandomHubAssetId(hub, j); address spoke = _getRandomSpoke(k); addCap = _bound(addCap, 0, MAX_ALLOWED_SPOKE_CAP); - hubConfigurator.updateSpokeSupplyCap(hub, assetId, spoke, addCap); + hubConfigurator.updateSpokeAddCap(hub, assetId, spoke, addCap); } function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j, uint8 k) external setup { diff --git a/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol b/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol index 70423181b..adbdbbeaf 100644 --- a/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol +++ b/invariants/protocol-suite/handlers/interfaces/IHubConfiguratorHandler.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; /// @title IHubConfiguratorHandler /// @notice Interface for the HubConfiguratorHandler interface IHubConfiguratorHandler { - function updateSpokeSupplyCap(uint256 addCap, uint8 i, uint8 j, uint8 k) external; + function updateSpokeAddCap(uint256 addCap, uint8 i, uint8 j, uint8 k) external; function updateSpokeDrawCap(uint256 drawCap, uint8 i, uint8 j, uint8 k) external; diff --git a/invariants/protocol-suite/replays/ReplayTest_7.t.sol b/invariants/protocol-suite/replays/ReplayTest_7.t.sol index 774fb273e..8d1e441d1 100644 --- a/invariants/protocol-suite/replays/ReplayTest_7.t.sol +++ b/invariants/protocol-suite/replays/ReplayTest_7.t.sol @@ -100,7 +100,7 @@ contract ReplayTest7 is Invariants, Setup { _delay(20833); Tester.borrow(2973933138, 65, 107, 12); _setUpActor(USER1); - Tester.updateSpokeSupplyCap(172, 180, 253, 253); + Tester.updateSpokeAddCap(172, 180, 253, 253); _setUpActor(USER3); _delay(997); Tester.repay( From ff8da5e2638d8fbfe0c1a01951b47e070449c360 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:16:38 +0530 Subject: [PATCH 101/106] chore: upload artifacts --- .github/workflows/invariants.yml | 14 ++++++++++++++ .github/workflows/invariants_hub.yml | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index 12afd0b61..2399d5578 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -48,6 +48,13 @@ jobs: config: invariants/protocol-suite/_config/echidna_config_ci.yaml test-mode: ${{ matrix.mode }} + - name: Upload Echidna corpus + uses: actions/upload-artifact@v4 + if: always() + with: + name: echidna-corpus-protocol-${{ matrix.mode }} + path: corpus/ + medusa: name: Medusa runs-on: ubuntu-latest @@ -80,3 +87,10 @@ jobs: - name: Run Medusa run: medusa fuzz --config invariants/protocol-suite/_config/medusa_config_ci.json + + - name: Upload Medusa corpus + uses: actions/upload-artifact@v4 + if: always() + with: + name: medusa-corpus-protocol + path: corpus/ diff --git a/.github/workflows/invariants_hub.yml b/.github/workflows/invariants_hub.yml index 774704212..8e6103a60 100644 --- a/.github/workflows/invariants_hub.yml +++ b/.github/workflows/invariants_hub.yml @@ -48,6 +48,13 @@ jobs: config: invariants/hub-suite/_config/echidna_config_ci.yaml test-mode: ${{ matrix.mode }} + - name: Upload Echidna corpus + uses: actions/upload-artifact@v4 + if: always() + with: + name: echidna-corpus-hub-${{ matrix.mode }} + path: corpus/ + medusa: name: Medusa runs-on: ubuntu-latest @@ -80,3 +87,10 @@ jobs: - name: Run Medusa run: medusa fuzz --config invariants/hub-suite/_config/medusa_config_ci.json + + - name: Upload Medusa corpus + uses: actions/upload-artifact@v4 + if: always() + with: + name: medusa-corpus-hub + path: corpus/ From 6e9535897806c6194fb6d263908b9e724e67f185 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:23:22 +0530 Subject: [PATCH 102/106] fix: setUserPositionManager handler --- invariants/protocol-suite/Setup.t.sol | 6 ++++++ invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/invariants/protocol-suite/Setup.t.sol b/invariants/protocol-suite/Setup.t.sol index 3cb376500..5288ef571 100644 --- a/invariants/protocol-suite/Setup.t.sol +++ b/invariants/protocol-suite/Setup.t.sol @@ -546,6 +546,8 @@ contract Setup is BaseTest { // Grant roles to configurators accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); accessManager.grantRole(Roles.SPOKE_ADMIN_ROLE, address(spokeConfigurator), 0); + accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(this), 0); + accessManager.grantRole(Roles.SPOKE_ADMIN_ROLE, address(this), 0); accessManager.grantRole(Roles.HUB_CONFIGURATOR_ROLE, address(this), 0); accessManager.grantRole(Roles.SPOKE_CONFIGURATOR_ROLE, address(this), 0); @@ -665,6 +667,10 @@ contract Setup is BaseTest { for (uint256 i; i < actorAddresses.length; ++i) { userToActor[users[i]] = Actor(payable(actorAddresses[i])); actors.add(actorAddresses[i]); + // set all actors are valid position managers such that they can perform actions + // onBehalfOf after approval using setUserPositionManager handler + spoke1.updatePositionManager(actorAddresses[i], true); + spoke2.updatePositionManager(actorAddresses[i], true); } } } diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index 6324286cb..c79e7fd61 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -306,7 +306,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { require( usingAsCollateral != isUsingAsCollateral, 'SpokeHandler: usingAsCollateral already set' - ); // usingAsCollateral + ); // usingAsCollateral is a noop // register user to check post conditions /// @dev setUsingAsCollateral(reserveId, FALSE) all reserves in user position should be refreshed, @@ -338,7 +338,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (ok) { _after(); - ///// HSPOST ///// + /// HSPOST /// uint256 reserveCount = ISpoke(spoke).getReserveCount(); for (uint256 j; j < reserveCount; j++) { UserVars memory varsBefore = _userVarsBefore(spoke, j, onBehalfOf); From 6e3b20062c71b110284b01c6cf6bc4c7273226d0 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:34:53 +0530 Subject: [PATCH 103/106] tmp: run ci for 1 hour --- invariants/hub-suite/_config/echidna_config_ci.yaml | 2 +- invariants/hub-suite/_config/medusa_config_ci.json | 2 +- invariants/protocol-suite/_config/echidna_config_ci.yaml | 2 +- invariants/protocol-suite/_config/medusa_config_ci.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invariants/hub-suite/_config/echidna_config_ci.yaml b/invariants/hub-suite/_config/echidna_config_ci.yaml index 79a8eb1fb..35d23ee45 100644 --- a/invariants/hub-suite/_config/echidna_config_ci.yaml +++ b/invariants/hub-suite/_config/echidna_config_ci.yaml @@ -14,7 +14,7 @@ balanceContract: 0x1000000000000000000000000000000000000000000000000 testLimit: 10000000 #timeout in seconds -timeout: 18000 # 5 hours +timeout: 3600 # 1 hour #seqLen defines how many transactions are in a test sequence seqLen: 300 diff --git a/invariants/hub-suite/_config/medusa_config_ci.json b/invariants/hub-suite/_config/medusa_config_ci.json index aa71b1f03..cac15ff5a 100644 --- a/invariants/hub-suite/_config/medusa_config_ci.json +++ b/invariants/hub-suite/_config/medusa_config_ci.json @@ -2,7 +2,7 @@ "fuzzing": { "workers": 10, "workerResetLimit": 50, - "timeout": 18000, + "timeout": 3600, "testLimit": 0, "callSequenceLength": 300, "corpusDirectory": "corpus", diff --git a/invariants/protocol-suite/_config/echidna_config_ci.yaml b/invariants/protocol-suite/_config/echidna_config_ci.yaml index 79a8eb1fb..35d23ee45 100644 --- a/invariants/protocol-suite/_config/echidna_config_ci.yaml +++ b/invariants/protocol-suite/_config/echidna_config_ci.yaml @@ -14,7 +14,7 @@ balanceContract: 0x1000000000000000000000000000000000000000000000000 testLimit: 10000000 #timeout in seconds -timeout: 18000 # 5 hours +timeout: 3600 # 1 hour #seqLen defines how many transactions are in a test sequence seqLen: 300 diff --git a/invariants/protocol-suite/_config/medusa_config_ci.json b/invariants/protocol-suite/_config/medusa_config_ci.json index c4103a7de..5a5d9a629 100644 --- a/invariants/protocol-suite/_config/medusa_config_ci.json +++ b/invariants/protocol-suite/_config/medusa_config_ci.json @@ -2,7 +2,7 @@ "fuzzing": { "workers": 10, "workerResetLimit": 50, - "timeout": 18000, + "timeout": 3600, "testLimit": 0, "callSequenceLength": 300, "corpusDirectory": "corpus", From 849af5741b73ba3f6d296e80eaa6cf6d8cd2ccbd Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:37:44 +0530 Subject: [PATCH 104/106] chore: whitespace --- invariants/hub-suite/handlers/HubAdminHandler.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/invariants/hub-suite/handlers/HubAdminHandler.t.sol b/invariants/hub-suite/handlers/HubAdminHandler.t.sol index 92445931f..680d7668e 100644 --- a/invariants/hub-suite/handlers/HubAdminHandler.t.sol +++ b/invariants/hub-suite/handlers/HubAdminHandler.t.sol @@ -65,6 +65,7 @@ abstract contract HubAdminHandlerBase is CommonHelpers, IHubAdminHandler { /// @dev Hook invoked before each handler action. Concrete handlers wire this to the suite's _before() snapshot. function _beforeHook() internal virtual; + /// @dev Hook invoked after each handler action. Concrete handlers wire this to the suite's _after() snapshot + postcondition checks. function _afterHook() internal virtual; From 5fa65fe543e2b88084ac67bda15a46d517b3e2c3 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:43:48 +0530 Subject: [PATCH 105/106] feat: run invariants with foundry --- .github/workflows/invariants.yml | 29 +++++++++++++++++-- foundry.toml | 18 ++++++++++++ .../hub-suite/handlers/HubHandler.t.sol | 26 ++++++++--------- invariants/protocol-suite/TesterFoundry.t.sol | 14 +-------- .../handlers/spoke/SpokeHandler.t.sol | 23 +++++++-------- .../handlers/spoke/TreasurySpokeHandler.t.sol | 6 ++-- 6 files changed, 72 insertions(+), 44 deletions(-) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index 2399d5578..4115f03f3 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -50,7 +50,6 @@ jobs: - name: Upload Echidna corpus uses: actions/upload-artifact@v4 - if: always() with: name: echidna-corpus-protocol-${{ matrix.mode }} path: corpus/ @@ -90,7 +89,33 @@ jobs: - name: Upload Medusa corpus uses: actions/upload-artifact@v4 - if: always() with: name: medusa-corpus-protocol path: corpus/ + + foundry: + name: Foundry + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Run Foundry setup + uses: bgd-labs/github-workflows/.github/actions/foundry-setup@main + with: + FOUNDRY_VERSION: nightly + + - name: Run Forge Build + run: forge build --build-info + + - name: Run Forge Test + run: forge test --mc TesterFoundry -vvv --show-progress + + - name: Upload Foundry corpus + uses: actions/upload-artifact@v4 + with: + name: foundry-corpus-protocol + path: invariants/protocol-suite/_corpus/foundry diff --git a/foundry.toml b/foundry.toml index 687de909f..bdc020bba 100644 --- a/foundry.toml +++ b/foundry.toml @@ -64,6 +64,24 @@ test = 'invariants/' additional_compiler_profiles = [] compilation_restrictions = [] +[profile.invariant.invariant] +fail_on_revert = false +runs = 1000000 +depth = 300 +corpus_dir = "invariants/protocol-suite/_corpus/foundry" +show_solidity = true +show_metrics = true +show_edge_coverage = true + +[profile.invariant.fuzz] +seed = '0x1' +include_storage = true +include_push_bytes = true +call_override = false +dictionary_weight = 80 +shrink_sequence = true + + [rpc_endpoints] mainnet = "${RPC_MAINNET}" optimism = "${RPC_OPTIMISM}" diff --git a/invariants/hub-suite/handlers/HubHandler.t.sol b/invariants/hub-suite/handlers/HubHandler.t.sol index b5fd75f05..1d378d1c9 100644 --- a/invariants/hub-suite/handlers/HubHandler.t.sol +++ b/invariants/hub-suite/handlers/HubHandler.t.sol @@ -55,7 +55,7 @@ contract HubHandler is BaseHandler, IHubHandler { assertLe(previewAddedShares, addedShares, HSPOST_HUB_ERC4626_ADD_C); } else { - revert('HubHandler: add failed'); + vm.assume(false); } } @@ -91,7 +91,7 @@ contract HubHandler is BaseHandler, IHubHandler { assertGe(previewRemovedShares, removedShares, HSPOST_HUB_ERC4626_REMOVE_C); } else { - revert('HubHandler: remove failed'); + vm.assume(false); } } @@ -125,7 +125,7 @@ contract HubHandler is BaseHandler, IHubHandler { assertGe(previewDrawnShares, drawnShares, HSPOST_HUB_ERC4626_DRAW_C); } else { - revert('HubHandler: draw failed'); + vm.assume(false); } } @@ -183,7 +183,7 @@ contract HubHandler is BaseHandler, IHubHandler { assertLe(previewRestoredShares, restoredDrawnShares, HSPOST_HUB_ERC4626_RESTORE_C); } else { - revert('HubHandler: restore failed'); + vm.assume(false); } } @@ -210,7 +210,7 @@ contract HubHandler is BaseHandler, IHubHandler { if (ok) { _after(); } else { - revert('HubHandler: reportDeficit failed'); + vm.assume(false); } } @@ -228,7 +228,7 @@ contract HubHandler is BaseHandler, IHubHandler { if (ok) { _after(); } else { - revert('HubHandler: eliminateDeficit failed'); + vm.assume(false); } } @@ -258,7 +258,7 @@ contract HubHandler is BaseHandler, IHubHandler { HSPOST_HUB_M ); } else { - revert('HubHandler: refreshPremium failed'); + vm.assume(false); } } @@ -282,7 +282,7 @@ contract HubHandler is BaseHandler, IHubHandler { HSPOST_HUB_M ); } else { - revert('HubHandler: refreshPremium failed'); + vm.assume(false); } } @@ -297,7 +297,7 @@ contract HubHandler is BaseHandler, IHubHandler { if (ok) { _after(); } else { - revert('HubHandler: payFeeShares failed'); + vm.assume(false); } } @@ -314,7 +314,7 @@ contract HubHandler is BaseHandler, IHubHandler { if (ok) { _after(); } else { - revert('HubHandler: transferShares failed'); + vm.assume(false); } } @@ -326,7 +326,7 @@ contract HubHandler is BaseHandler, IHubHandler { try hub.sweep(assetId, amount) { _after(); } catch { - revert('HubHandler: sweep failed'); + vm.assume(false); } } @@ -341,7 +341,7 @@ contract HubHandler is BaseHandler, IHubHandler { try hub.reclaim(assetId, amount) { _after(); } catch { - revert('HubHandler: reclaim failed'); + vm.assume(false); } } @@ -351,7 +351,7 @@ contract HubHandler is BaseHandler, IHubHandler { try hub.mintFeeShares(assetId) { _after(); } catch { - revert('HubHandler: mintFeeShares failed'); + vm.assume(false); } } diff --git a/invariants/protocol-suite/TesterFoundry.t.sol b/invariants/protocol-suite/TesterFoundry.t.sol index 6b12a543d..85c2bd82e 100644 --- a/invariants/protocol-suite/TesterFoundry.t.sol +++ b/invariants/protocol-suite/TesterFoundry.t.sol @@ -11,18 +11,6 @@ import {Setup} from './Setup.t.sol'; /// @notice Entry point for invariant testing, inherits all contracts, invariants & handler /// @dev Mono contract that contains all the testing logic contract TesterFoundry is Invariants, Setup, StdInvariant { - /// forge-config: default.invariant.fail-on-revert = false - /// forge-config: default.invariant.runs = 1000 - /// forge-config: default.invariant.depth = 100 - /// forge-config: default.invariant.corpus-dir = "invariants/_corpus/foundry" - /// forge-config: default.invariant.show-solidity = true - /// forge-config: default.invariant.show-metrics = true - /// forge-config: default.fuzz.seed = '0x1' - /// forge-config: default.fuzz.include-storage = true - /// forge-config: default.fuzz.include-push-bytes = true - /// forge-config: default.fuzz.call-override = false - /// forge-config: default.fuzz.dictionary-weight = 80 - /// forge-config: default.fuzz.shrink-sequence = true /// @dev Foundry compatibility faster setup debugging function setUp() public { // Deploy protocol contracts and protocol actors @@ -33,7 +21,7 @@ contract TesterFoundry is Invariants, Setup, StdInvariant { // Exclude target selectors bytes4[] memory selectors = new bytes4[](1); - selectors[0] = bytes4(keccak256('checkPostConditions()')); + selectors[0] = this.checkPostConditions.selector; excludeSelector(FuzzSelector({addr: address(this), selectors: selectors})); diff --git a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol index c79e7fd61..3b58b52b7 100644 --- a/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/SpokeHandler.t.sol @@ -65,7 +65,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (ok) { _after(); } else { - revert('SpokeHandler: supply failed'); + vm.assume(false); } } @@ -88,7 +88,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertTrue(healthyBefore, GPOST_SP_H); assertTrue(_isHealthy(spoke, onBehalfOf), HSPOST_SP_I); } else { - revert('SpokeHandler: withdraw failed'); + vm.assume(false); } } @@ -111,7 +111,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertTrue(healthyBefore, HSPOST_SP_D); assertTrue(_isHealthy(spoke, onBehalfOf), HSPOST_SP_I); } else { - revert('SpokeHandler: borrow failed'); + vm.assume(false); } } @@ -138,7 +138,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { HSPOST_SP_C ); } else { - revert('SpokeHandler: repay failed'); + vm.assume(false); } } @@ -293,7 +293,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { ); } } else { - revert('SpokeHandler: liquidationCall failed'); + vm.assume(false); } } @@ -303,10 +303,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { uint256 reserveId = _getRandomReserveId(spoke, j); (bool isUsingAsCollateral, ) = ISpoke(spoke).getUserReserveStatus(reserveId, onBehalfOf); - require( - usingAsCollateral != isUsingAsCollateral, - 'SpokeHandler: usingAsCollateral already set' - ); // usingAsCollateral is a noop + vm.assume(usingAsCollateral != isUsingAsCollateral); // usingAsCollateral is a noop // register user to check post conditions /// @dev setUsingAsCollateral(reserveId, FALSE) all reserves in user position should be refreshed, @@ -324,7 +321,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (ok) { _after(); } else { - revert('SpokeHandler: setUsingAsCollateral failed'); + vm.assume(false); } } @@ -347,7 +344,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { assertEq(varsBefore.debt.owed, varsAfter.debt.owed, HSPOST_SP_F); } } else { - revert('SpokeHandler: updateUserRiskPremium failed'); + vm.assume(false); } } @@ -364,7 +361,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { _after(); assertTrue(_isHealthy(spoke, onBehalfOf), HSPOST_SP_I); } else { - revert('SpokeHandler: updateUserDynamicConfig failed'); + vm.assume(false); } } @@ -381,7 +378,7 @@ contract SpokeHandler is BaseHandler, ISpokeHandler { if (ok) { _after(); } else { - revert('SpokeHandler: setUserPositionManager failed'); + vm.assume(false); } } diff --git a/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol b/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol index b3c8268a3..3e0bba863 100644 --- a/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol +++ b/invariants/protocol-suite/handlers/spoke/TreasurySpokeHandler.t.sol @@ -35,7 +35,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { try ISpoke(spoke).supply(reserveId, amount, msg.sender) { _after(); } catch { - revert('TreasurySpokeHandler: supply failed'); + vm.assume(false); } } @@ -48,7 +48,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { try ISpoke(spoke).withdraw(reserveId, amount, msg.sender) { _after(); } catch { - revert('TreasurySpokeHandler: withdraw failed'); + vm.assume(false); } } @@ -61,7 +61,7 @@ contract TreasurySpokeHandler is BaseHandler, ITreasurySpokeHandler { try ITreasurySpoke(hubInfo[hub].treasurySpoke).transfer(asset, to, amount) { _after(); } catch { - revert('TreasurySpokeHandler: transfer failed'); + vm.assume(false); } } From f80ad64dd769887affe20eb2c4cc2813840bba96 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:16:31 +0530 Subject: [PATCH 106/106] chore: reduce foundry runs to be under 6h run --- foundry.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/foundry.toml b/foundry.toml index bdc020bba..c0e117e37 100644 --- a/foundry.toml +++ b/foundry.toml @@ -65,9 +65,9 @@ additional_compiler_profiles = [] compilation_restrictions = [] [profile.invariant.invariant] -fail_on_revert = false -runs = 1000000 -depth = 300 +fail_on_revert = true +runs = 10000 +depth = 1000 corpus_dir = "invariants/protocol-suite/_corpus/foundry" show_solidity = true show_metrics = true