diff --git a/README.md b/README.md index 9d187f62..f7361230 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,23 @@ forge test ./test/scripts/coverage.sh ``` -### invariants tests (`/tests/invariants`) +## Invariant Testing Suite + +This project has been set up with a suite of tests that check for specific invariants and properties for the evk, implemented by [vnmrtz.eth](https://twitter.com/vn_martinez_) from [Enigma Dark](https://www.enigmadark.com/). These tests are located in the `test/invariants` directory. They are written in Solidity and are designed to be run with the [echidna](https://github.com/crytic/echidna) fuzzing tool. + +Installation and usage of these tools is outside the scope of this README, but you can find more information in the respective repositories: +- [Echidna Installation](https://github.com/crytic/echidna) + +To run invariant tests with Echidna: + +```sh +./test/scripts/echidna.sh +``` + +To run assert tests with Echidna: + ```sh -./test/scripts/echidna.sh # property mode -./test/scripts/echidna-assert.sh # assertion mode -./test/scripts/medusa.sh +./test/scripts/echidna-assert.sh ``` ## Safety diff --git a/test/invariants/CryticToFoundry.t.sol b/test/invariants/CryticToFoundry.t.sol index d7b0dc5a..0a2c682c 100644 --- a/test/invariants/CryticToFoundry.t.sol +++ b/test/invariants/CryticToFoundry.t.sol @@ -16,6 +16,7 @@ import {Setup} from "./Setup.t.sol"; contract CryticToFoundry is Invariants, Setup { modifier setup() override { _; + violatorTemp = address(0); } /// @dev Foundry compatibility faster setup debugging @@ -108,7 +109,7 @@ contract CryticToFoundry is Invariants, Setup { this.enableController(468322383632155574862945881956174631649161871295786712111360326257); this.setPrice(726828870758264026864714326152620643619927705875320304690180955674, 11); this.enableCollateral(15111); - this.setLTV(3456147621700665956033923462455625826034483547574136595412029999975872, 1, 1, 0); + ////this.setLTV(1, 1, 0); this.depositToActor(1, 0); this.borrowTo(1, 304818507942225219676445155333052560942359548832832651640621508); echidna_BM_INVARIANT(); @@ -116,7 +117,7 @@ contract CryticToFoundry is Invariants, Setup { function test_echidna_VM_INVARIANT_C1() public { vm.skip(true); - this.setLTV(161537350060562470698068789285938700031433026666990925968846691117425, 1, 1, 0); + //this.setLTV(1, 1, 0); this.mintToActor(2, 0); this.setPrice(15141093523755052381928072114906306924899029026721034293540167406168436, 12); this.enableController(0); @@ -157,64 +158,375 @@ contract CryticToFoundry is Invariants, Setup { console.log("----------"); - /* this.loop(2,0); + assert_VM_INVARIANT_C(); + } - console.log("----------"); + function test_VM_INVARIANT5() public { + //this.setLTV(1, 1, 0); + this.mintToActor(1, 0); + this.enableCollateral(0); + this.setPrice(167287376704962748125159831258059871163051958738722404000304447051, 11); + this.enableController(0); + this.borrowTo(1, 0); + this.repayTo(1, 0); + } - console.log("balanceOf: ", eTST.balanceOf(address(actor))); - console.log("debtOf: ", eTST.debtOf(address(actor))); + function test_borrowing_coverage() public { + this.depositToActor(100000000000, 0); + this.depositCollateralToActor(10000000000000000000, 0); + //this.setLTV(1e4, 1e4, 0); + this.setPrice(0, 1e18); + this.setPrice(2, 1e18); + this.enableController(0); + this.enableCollateral(0); + this.borrowTo(1, 318379198685755841947315679159524764739517957049356450836); + } - console.log("TotalSupply: ", eTST.totalSupply()); - console.log("TotalAssets: ", eTST.totalAssets()); + function test_repay_coverage() public { + _setUpActorAndDelay(USER1, 80932); + this.withdraw(1524785991, address(789465)); + _setUpActorAndDelay(USER2, 112444); + this.borrowTo(4369999, 13609915390318032700896377232220083079395853842395211195870658482809261409341); + _setUpActorAndDelay(USER1, 582191); + this.repayTo( + 53800299281813669058744869349379135374526250922535558898616928332200178883310, + 62919552230079478689518140392915001440407013600808919191104461459407257746645 + ); + _setUpActorAndDelay(USER2, 440097); + this.repayTo(4370001, 148); + } - console.log("----------"); + /////////////////////////////////////////////////////////////////////////////////////////////// + // FIX REVISION // + /////////////////////////////////////////////////////////////////////////////////////////////// - this.repayWithShares(3,0); + function test_borrowCoverage() public { + this.depositToActor(1, 243882164617235048764904608); + this.enableCollateral(0); + this.mintCollateralToActor(2, 0); + this.enableController(1800258655224746867118946383650397); + this.borrowTo(1, 26044929278355311110009317891546932514); + } - console.log("----------"); + function test_repayCoverage() public { + this.depositToActor(1, 20688509469227803629548472237356323996913977); + this.enableController(3554832964617168045885851522075051375906075737831533802085648538); + this.enableCollateral(7486334844696997538320940916429211443676470145365163389407051361); + this.mintCollateralToActor(2, 1044); + this.borrowTo(1, 6265957024289727264541187492529679182159257861190152393); + this.repayTo(1, 0); + } - console.log("balanceOf: ", eTST.balanceOf(address(actor))); - console.log("debtOf: ", eTST.debtOf(address(actor))); + function test_repayWithShareCoverage() public { + // 1 + _setUpActorAndDelay(USER2, 361136); + this.depositCollateralToActor( + 115792089237316195423570985008687907853269984665640564039457584007913129639935, 4370000 + ); - console.log("TotalSupply: ", eTST.totalSupply()); - console.log("TotalAssets: ", eTST.totalAssets()); + // 2 + _setUpActorAndDelay(USER2, 172101); + this.mintToActor(4370000, 13093023029431517515116297493464108021640503049347176947550935870228090307603); - console.log("----------"); */ + // 3 + _setUpActorAndDelay(USER3, 463587); + this.enableCollateral(4370000); - assert_VM_INVARIANT_C(); + // 4 + _setUpActorAndDelay(USER3, 390247); + this.enableController(7571938424744497050025392125255968711315919643451955475188); + + // 5 + _setUpActorAndDelay(USER3, 198598); + this.borrowTo(621, 106369723326817504322082057395634583314815541356617033724546638068337675944543); + + // 6 + _setUpActorAndDelay(USER2, 49735); + this.mintToActor(1524785993, 4294362097683428896960963005118562901165815780824328019769912464120687011232); + + // 7 + _setUpActorAndDelay(USER1, 526194); + this.repayWithShares(1524785993, 101789372662970476840627742190011536897514615145941489024870010488433281218895); + } + + function test_pullDebtCoverage() public { + // 1 + _setUpActorAndDelay(USER2, 361136); + this.depositCollateralToActor( + 115792089237316195423570985008687907853269984665640564039457584007913129639935, 4370000 + ); + + // 2 + _setUpActorAndDelay(USER3, 463587); + this.enableCollateral(4370000); + + // 3 + _setUpActorAndDelay(USER1, 112444); + this.enableController(89542571243649197051430772920307087535249499496671593682963902732427039403647); + + // 4 + _setUpActorAndDelay(USER1, 525476); + this.enableCollateral(115792089237316195423570985008687907853269984665640564039457584007913129639932); + + // 5 + _setUpActorAndDelay(USER2, 400981); + this.depositToActor(1524785992, 70660979405370551306431037636906474910548328261468048786572907510610053763936); + + // 6 + _setUpActorAndDelay(USER3, 390247); + this.enableController(7571938424744497050025392125255968711315919643451955475188); + + // 7 + _setUpActorAndDelay(USER3, 4177); + this.setPrice(115518271729930361139946212800867286559642462937563235984193410469727763769722, 4370001); + + // 8 + _setUpActorAndDelay(USER1, 136392); + this.mintCollateralToActor(1524785991, 4370001); + + // 9 + _setUpActorAndDelay(USER1, 32767); + this.borrowTo( + 115792089237316195423570985008687907853269984665640564039457584007913129639935, + 115792089237316195423570985008687907853269984665640564039457584007913129639935 + ); + + // 10 + _setUpActorAndDelay(USER3, 136394); + this.pullDebt(4370001, 4370000); } - function test_liquidate_bug() public { - _setUpActorAndDelay(USER3, 297507); - this.setLTV(115792089237316195423570985008687907853269984665640564039457584007913129639935, 433, 433, 0); - _setUpActor(USER1); - this.enableController(1524785991); + function test_withdrawAssertion1() public { + // 1 + _setUpActorAndDelay(USER2, 414736); + this.depositToActor(1524785992, 70660979405370551306431037636906474910548328261468048786572907510610053763936); + + // 2 + _setUpActorAndDelay(USER1, 490448); + this.mintCollateralToActor(4369999, 1524785993); + + // 3 + _setUpActorAndDelay(USER1, 525476); + this.enableCollateral(115792089237316195423570985008687907853269984665640564039457584007913129639932); + + // 4 + _setUpActorAndDelay(USER3, 521319); + this.enableCollateral(69322857472681895304938038838156628612468271802824680897342979361702488172834); + + // 5 + _setUpActorAndDelay(USER3, 439556); + this.enableController(7571938424744497050025392125255968711315919643451955475188); + + // 5 _setUpActorAndDelay(USER1, 439556); - this.enableCollateral(217905055956562793374063556811130300111285293815122069343455239377127312); - _setUpActorAndDelay(USER3, 566039); - this.enableCollateral(29); - _setUpActorAndDelay(USER3, 209930); - this.enableController(1524785993); - _delay(271957); - this.liquidate(2848675, 0, 512882652); + this.enableController(7571938424744497050025392125255968711315919643451955475186); + + // 6 + _setUpActorAndDelay(USER3, 4177); + this.setPrice(115518271729930361139946212800867286559642462937563235984193410469727763769722, 4370001); + + // 7 + _setUpActorAndDelay(USER2, 16802); + this.depositToActor(1524785991, 97); + + //8 + _setUpActorAndDelay(USER3, 94693); + this.borrowTo(1524785993, 969); + + // 9 + _setUpActorAndDelay(USER2, 400981); + this.setPrice( + 2918028944904530115677089601774046851350051826784276012161292107035031693773, + 442886312423865560653921465497495729504510949 + ); + + // 10 + _setUpActorAndDelay(USER1, 45142); + this.liquidate(0, 4369999, 4370000); } - function test_VM_INVARIANT5() public { - this.setLTV(22366818273602115439851901107761977982005180121616743889078085180117, 1, 1, 0); - this.mintToActor(1, 0); - this.enableCollateral(0); - this.setPrice(167287376704962748125159831258059871163051958738722404000304447051, 11); - this.enableController(0); - this.borrowTo(1, 0); - this.repayTo(1, 0); + function test_liquidationCoverage2() public { + // 1 + _setUpActorAndDelay(USER3, 463588); + this.enableCollateral(4370000); + + // 2 + _setUpActorAndDelay(USER2, 361136); + this.depositCollateralToActor( + 115792089237316195423570985008687907853269984665640564039457584007913129639935, 4370000 + ); + + // 3 + _setUpActorAndDelay(USER2, 4177); + this.enableController(43984744813353288885881838083846214633156180114953346776955246789109343466400); + + // 4 + _setUpActorAndDelay(USER1, 447588); + this.depositToActor(1741670863, 66705428148130105166133477109592350496349947889048187540830930814966397951229); + + // 5 + _setUpActorAndDelay(USER2, 318197); + this.enableCollateral(4369999); + + // 6 + _setUpActorAndDelay(USER3, 390247); + this.enableController(7571938424744497050025392125255968711315919643451955475188); + + // 7 + _setUpActorAndDelay(USER3, 198598); + this.borrowTo(621, 106369723326817504322082057395634583314815541356617033724546638068337675944543); + + // 8 + _setUpActorAndDelay(USER1, 38350); + this.setPrice(4072285750, 3476355193255497328741503339101032877); + + // 9 + _setUpActorAndDelay(USER2, 344203); + this.liquidate( + 115792089237316195423570985008687907853269984665640564039457584007913129639935, + 1000000000000000000000, + 71058326314738440509394117295321094000732326230855579883183929776326632007787 + ); } - function test_borrowing_coverage() public { - this.enableController(0); - this.borrowTo( + function test_liquidationAssertion() public { + // 1 + _setUpActorAndDelay(USER2, 361136); + this.depositCollateralToActor( + 115792089237316195423570985008687907853269984665640564039457584007913129639935, 4370000 + ); + + // 2 + _setUpActorAndDelay(USER3, 401699); + this.enableCollateral(1524785993); + + // 3 + _setUpActorAndDelay(USER2, 4177); + this.enableController(43984744813353288885881838083846214633156180114953346776955246789109343466400); + + // 4 + _setUpActorAndDelay(USER1, 447588); + this.depositToActor(1741670863, 66705428148130105166133477109592350496349947889048187540830930814966397951229); + + // 5 + _setUpActorAndDelay(USER3, 390247); + this.enableController(7571938424744497050025392125255968711315919643451955475188); + + // 6 + _setUpActorAndDelay(USER3, 198598); + this.borrowTo(621, 106369723326817504322082057395634583314815541356617033724546638068337675944543); + + // 7 + _setUpActorAndDelay(USER2, 344203); + this.liquidate( 115792089237316195423570985008687907853269984665640564039457584007913129639935, - 1210346675714198101847835018885699222114751859615 + 43076252055564096230512111437641469522776970924014296726523799212235104557082, + 71058326314738440509394117295321094000732326230855579883183929776326632007787 + ); + } + + function test_failedAssertion3() public { + // 1 + _setUpActorAndDelay(USER3, 136392); + this.donate(4370000, 678); + + // 2 + _setUpActorAndDelay(USER2, 361136); + this.depositCollateralToActor( + 115792089237316195423570985008687907853269984665640564039457584007913129639935, 4370000 + ); + + // 3 + _setUpActorAndDelay(USER3, 521319); + this.enableCollateral(69322857472681895304938038838156628612468271802824680897342979361702488172834); + + // 4 + _setUpActorAndDelay(USER3, 390247); + this.enableController(7571938424744497050025392125255968711315919643451955475188); + + // 5 + _setUpActorAndDelay(USER1, 444463); + this.skim(4370000, 1524785992); + + // 6 + _setUpActorAndDelay(USER3, 198598); + this.borrowTo(621, 106369723326817504322082057395634583314815541356617033724546638068337675944543); + + // 7 + _setUpActorAndDelay(USER2, 135921); + this.enableController(47910244435710312607121944724351163516662496867297591373521936988402150727512); + + // 8 + _setUpActorAndDelay(USER1, 478623); + this.setPrice(4072285750, 34763553996521932554973228741503339150003228757); + console.log("Cash.before: ", eTST.cash()); + // 9 + _setUpActorAndDelay(USER2, 344203); + this.liquidate( + 115792089237316195423570985008687907853269984665640564039457584007913129639935, + 43076252055564096230512111437641469522776970924014296726523799212235104557082, + 71058326314738440509394117295321094000732326230855579883183929776326632007787 + ); + + // 10 + _setUpActorAndDelay(USER2, 490448); + console.log("Totalassets.after: ", eTST.totalAssets()); + this.assert_BM_INVARIANT_G(); + } + + function test_echidna_BM_INVARIANT() public { + // 1 + _setUpActorAndDelay(USER2, 361136); + this.depositCollateralToActor( + 115792089237316195423570985008687907853269984665640564039457584007913129639935, 4370000 + ); + + // 2 + _setUpActorAndDelay(USER2, 172101); + this.mintToActor(4370000, 13093023029431517515116297493464108021640503049347176947550935870228090307603); + + // 3 + _setUpActorAndDelay(USER3, 463587); + this.enableCollateral(4370000); + + // 4 + _setUpActorAndDelay(USER3, 390247); + this.enableController(7571938424744497050025392125255968711315919643451955475188); + + // 5 + _setUpActorAndDelay(USER3, 198598); + this.borrowTo(621, 106369723326817504322082057395634583314815541356617033724546638068337675944543); + + // 6 + _setUpActorAndDelay(USER1, 511822); + this.mintToActor(1524785991, 1524785991); + + // 7 + _setUpActorAndDelay(USER1, 361136); + this.repayWithShares(611, 1524785993); + + // 8 + _setUpActorAndDelay(USER3, 31594); + this.depositCollateralToActor( + 4370000, 82219537351169213569972228309424431267756425113427529629224679938600261088229 ); + + // 9 + _setUpActorAndDelay(USER3, 545945); + this.repayTo( + 30414654813179944674153456741833515114307152122709807632681140684837095933833, + 115792089237316195423570985008687907853269984665640564039457584007913129639935 + ); + + console.log(eTST.totalBorrows()); + + // 10 + _setUpActorAndDelay(USER3, 434894); + this.assert_BM_INVARIANT_G(); + + console.log(eTST.totalBorrows()); + + echidna_BM_INVARIANT(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/test/invariants/Invariants.t.sol b/test/invariants/Invariants.t.sol index d82b8447..947eaa20 100644 --- a/test/invariants/Invariants.t.sol +++ b/test/invariants/Invariants.t.sol @@ -53,7 +53,7 @@ abstract contract Invariants is function echidna_VM_INVARIANT() public monotonicTimestamp returns (bool) { assert_VM_INVARIANT_A(); - //assert_VM_INVARIANT_C(); + assert_VM_INVARIANT_C(); return true; } diff --git a/test/invariants/InvariantsSpec.t.sol b/test/invariants/InvariantsSpec.t.sol index 60b264dc..0b3a5bd3 100644 --- a/test/invariants/InvariantsSpec.t.sol +++ b/test/invariants/InvariantsSpec.t.sol @@ -6,6 +6,31 @@ pragma solidity ^0.8.19; /// @dev Contains pseudo code and description for the invariants in the protocol /// @dev Invariants for Token, Vault, Borrowing, Liquidations mechanics abstract contract InvariantsSpec { + /*///////////////////////////////////////////////////////////////////////////////////////////// + // PROPERTY TYPES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// On this invariant testing framework there exists two types of Properties: + + - INVARIANTS (INV): + - These are properties that should always hold true in the system. + - They are implemented under /invariants folder. + + - POSTCONDITIONS: + - These are properties that should hold true after an action is executed. + - They are implemented under /hooks and /handlers. + + - There exists two types of POSTCONDITIONS: + - GLOBAL POSTCONDITIONS (GPOST): + - These are properties that should always hold true after an action is executed. + - They are checked in `_checkPostConditions` function in the HookAggregator contract. + + - HANDLER SPECIFIC POSTCONDITIONS (HSPOST): + // - These are properties that should hold true after an specific action is executed in a specific context. + - They are implemented on each handler function under HANDLER SPECIFIC POSTCONDITIONS comment. + + /////////////////////////////////////////////////////////////////////////////////////////////*/ + /////////////////////////////////////////////////////////////////////////////////////////////// // BASE // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -45,7 +70,7 @@ abstract contract InvariantsSpec { string constant VM_INVARIANT_B = "VM_INVARIANT_B: If totalSupply increases new totalSupply must be less than or equal to supply cap"; - string constant VM_INVARIANT_C = "VM_INVARIANT_C: If totalAssets == 0 <=> totalSupply == 0"; + string constant VM_INVARIANT_C = "VM_INVARIANT_C: If totalAssets == 0 => totalSupply == 0"; /////////////////////////////////////////////////////////////////////////////////////////////// // VAULT MODULE: ERC4626 INVARIANTS // @@ -173,10 +198,20 @@ abstract contract InvariantsSpec { string constant LM_INVARIANT_A = "LM_INVARIANT_A: Liquidation can only succed if violator is unhealthy"; - string constant LM_INVARIANT_B = "LM_INVARIANT_B: debtSocialization == 0 => exchangeRate <= exchangeRate' "; + string constant LM_INVARIANT_B = "LM_INVARIANT_B: debtSocialization == 0 => exchangeRate <= exchangeRate'"; - string constant LM_INVARIANT_C = "LM_INVARIANT_C: Only a liquidation can leave a healthy account unhealthy"; + string constant LM_INVARIANT_C = + "LM_INVARIANT_C: Only a deposit, mintToActor, skim, repayTo, convertFees & liquidate can leave an account in an unhealthy state"; string constant LM_INVARIANT_D = "LM_INVARIANT_D: Only liquidations can deteriorate health score of an already unhealthy account"; + + string constant LM_INVARIANT_E = + "LM_INVARIANT_E: After a successful liquidation, repayAssets amount of debt should be transferred to sender"; + + string constant LM_INVARIANT_F = + "LM_INVARIANT_E: After a successful liquidation, minYieldBalance amount of collateral should be transferred to sender"; + + string constant LM_INVARIANT_G = + "LM_INVARIANT_E: After a successful liquidation, if debtSocialization is enabled, violator debt should be 0"; } diff --git a/test/invariants/Setup.t.sol b/test/invariants/Setup.t.sol index 69bfed5d..c06ec57e 100644 --- a/test/invariants/Setup.t.sol +++ b/test/invariants/Setup.t.sol @@ -17,21 +17,13 @@ import {SequenceRegistry} from "../../src/SequenceRegistry/SequenceRegistry.sol" // Modules import { BalanceForwarderExtended, - BalanceForwarder, BorrowingExtended, - Borrowing, GovernanceExtended, - Governance, InitializeExtended, - Initialize, LiquidationExtended, - Liquidation, RiskManagerExtended, - RiskManager, TokenExtended, - Token, - VaultExtended, - Vault + VaultExtended } from "test/invariants/helpers/extended/ModulesExtended.sol"; // Test Contracts @@ -41,6 +33,10 @@ import {MockPriceOracle} from "../mocks/MockPriceOracle.sol"; import {Actor} from "./utils/Actor.sol"; import {BaseTest} from "./base/BaseTest.t.sol"; import {EVaultExtended} from "./helpers/extended/EVaultExtended.sol"; +import {ILiquidationModuleHandler} from "./handlers/interfaces/ILiquidationModuleHandler.sol"; +import {IVaultModuleHandler} from "./handlers/interfaces/IVaultModuleHandler.sol"; +import {IGovernanceModuleHandler} from "./handlers/interfaces/IGovernanceModuleHandler.sol"; +import {IBorrowingModuleHandler} from "./handlers/interfaces/IBorrowingModuleHandler.sol"; /// @title Setup /// @notice Setup contract for the invariant test Suite, inherited by Tester @@ -61,19 +57,25 @@ contract Setup is BaseTest { feeReceiver = _makeAddr("feeReceiver"); protocolConfig = new ProtocolConfig(address(this), feeReceiver); - // Deploy the oracle and integrations - balanceTracker = address(new MockBalanceTracker()); - oracle = new MockPriceOracle(); - sequenceRegistry = address(new SequenceRegistry()); - // Deploy the mock assets assetTST = new TestERC20(); assetTST2 = new TestERC20(); baseAssets.push(address(assetTST)); baseAssets.push(address(assetTST2)); + // Deploy the oracle and integrations + balanceTracker = address(new MockBalanceTracker()); + oracle = new MockPriceOracle(); unitOfAccount = address(1); + + // Set initial prices for the simulation tokens + oracle.setPrice(address(assetTST), unitOfAccount, 1e18); + oracle.setPrice(address(assetTST2), unitOfAccount, 1e18); + + sequenceRegistry = address(new SequenceRegistry()); permit2 = DeployPermit2.deployPermit2(); + + _setUpOps(); } function _deployVaults() internal { @@ -82,14 +84,14 @@ contract Setup is BaseTest { Base.Integrations(address(evc), address(protocolConfig), sequenceRegistry, balanceTracker, permit2); Dispatch.DeployedModules memory modules = Dispatch.DeployedModules({ - initialize: address(new Initialize(integrations)), - token: address(new Token(integrations)), - vault: address(new Vault(integrations)), - borrowing: address(new Borrowing(integrations)), - liquidation: address(new Liquidation(integrations)), - riskManager: address(new RiskManager(integrations)), - balanceForwarder: address(new BalanceForwarder(integrations)), - governance: address(new Governance(integrations)) + initialize: address(new InitializeExtended(integrations)), + token: address(new TokenExtended(integrations)), + vault: address(new VaultExtended(integrations)), + borrowing: address(new BorrowingExtended(integrations)), + liquidation: address(new LiquidationExtended(integrations)), + riskManager: address(new RiskManagerExtended(integrations)), + balanceForwarder: address(new BalanceForwarderExtended(integrations)), + governance: address(new GovernanceExtended(integrations)) }); // Deploy the vault implementation @@ -111,6 +113,9 @@ contract Setup is BaseTest { ); eTST2.setInterestRateModel(address(new IRMTestDefault())); vaults.push(address(eTST2)); + + // Set default LTV + eTST.setLTV(address(eTST2), 1e4, 1e4, 0); } function _setUpActors() internal { @@ -147,4 +152,18 @@ contract Setup is BaseTest { assert(success); actorAddress = address(_actor); } + + /// @notice Set up the operations that can leave an account unhealthy without reverting + function _setUpOps() internal { + // Vault + uncheckedHealthOperations[IVaultModuleHandler.depositToActor.selector] = true; + uncheckedHealthOperations[IVaultModuleHandler.mintToActor.selector] = true; + uncheckedHealthOperations[IVaultModuleHandler.skim.selector] = true; + // Borrowing + uncheckedHealthOperations[IBorrowingModuleHandler.repayTo.selector] = true; + // Governance + uncheckedHealthOperations[IGovernanceModuleHandler.convertFees.selector] = true; + // Liquidation + uncheckedHealthOperations[ILiquidationModuleHandler.liquidate.selector] = true; + } } diff --git a/test/invariants/base/BaseHandler.t.sol b/test/invariants/base/BaseHandler.t.sol index fdd62783..5a4ad93c 100644 --- a/test/invariants/base/BaseHandler.t.sol +++ b/test/invariants/base/BaseHandler.t.sol @@ -62,6 +62,11 @@ contract BaseHandler is HookAggregator { return baseAssets[randomValue % baseAssets.length]; } + function _getRandomVault(uint256 i) internal view returns (address) { + uint256 randomValue = _randomize(i, "randomVault"); + return vaults[randomValue % vaults.length]; + } + /// @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))); diff --git a/test/invariants/base/BaseHooks.t.sol b/test/invariants/base/BaseHooks.t.sol index 7aad5850..decbbc4e 100644 --- a/test/invariants/base/BaseHooks.t.sol +++ b/test/invariants/base/BaseHooks.t.sol @@ -24,4 +24,9 @@ contract BaseHooks is ProtocolAssertions, InvariantsSpec { function _getHealthScore(uint256 liabilityValue, uint256 collateralValue) internal pure returns (uint256) { return liabilityValue == 0 ? 1e18 : collateralValue * 1e18 / liabilityValue; } + + /// @notice Returns the actor or violator address if it has been set in the handler + function _getActorOrViolator() internal view returns (address) { + return (violatorTemp != address(0)) ? violatorTemp : address(actor); + } } diff --git a/test/invariants/base/BaseStorage.t.sol b/test/invariants/base/BaseStorage.t.sol index 53a998e0..39b0089c 100644 --- a/test/invariants/base/BaseStorage.t.sol +++ b/test/invariants/base/BaseStorage.t.sol @@ -101,4 +101,8 @@ abstract contract BaseStorage { /////////////////////////////////////////////////////////////////////////////////////////////// address[] internal baseAssets; + + address violatorTemp; + + mapping(bytes4 => bool) internal uncheckedHealthOperations; } diff --git a/test/invariants/base/BaseTest.t.sol b/test/invariants/base/BaseTest.t.sol index ae25e418..bf298db2 100644 --- a/test/invariants/base/BaseTest.t.sol +++ b/test/invariants/base/BaseTest.t.sol @@ -4,6 +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"; // Utils import {Actor} from "../utils/Actor.sol"; @@ -17,7 +18,7 @@ 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 public IS_TEST = true; + bool internal IS_TEST = true; /////////////////////////////////////////////////////////////////////////////////////////////// // ACTOR PROXY MECHANISM // @@ -28,6 +29,7 @@ abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdU actor = actors[msg.sender]; _; actor = Actor(payable(address(0))); + violatorTemp = address(0); } /// @dev Solves medusa backward time warp issue diff --git a/test/invariants/base/ProtocolAssertions.t.sol b/test/invariants/base/ProtocolAssertions.t.sol index 30dbeb79..e27127c6 100644 --- a/test/invariants/base/ProtocolAssertions.t.sol +++ b/test/invariants/base/ProtocolAssertions.t.sol @@ -10,7 +10,10 @@ import {StdAsserts} from "../utils/StdAsserts.sol"; abstract contract ProtocolAssertions is StdAsserts, BaseTest { /// @notice Returns true if an account is healthy (liability <= collateral) function isAccountHealthy(uint256 _liability, uint256 _collateral) internal pure returns (bool) { - return _liability <= _collateral; + if (_collateral == 0 && _liability == 0) { + return true; + } + return _liability < _collateral; } /// @notice Checks whether the account is healthy from a BORROWING perspective diff --git a/test/invariants/handlers/external/EVCHandler.t.sol b/test/invariants/handlers/external/EVCHandler.t.sol index 8a11bde3..080653dc 100644 --- a/test/invariants/handlers/external/EVCHandler.t.sol +++ b/test/invariants/handlers/external/EVCHandler.t.sol @@ -52,7 +52,7 @@ contract EVCHandler is BaseHandler { // Get one of the three actors randomly address account = _getRandomActor(i); - address vaultAddress = address(assetTST); + address vaultAddress = address(eTST2); (success, returnData) = actor.proxy( address(evc), @@ -71,7 +71,7 @@ contract EVCHandler is BaseHandler { // Get one of the three actors randomly address account = _getRandomActor(i); - address vaultAddress = address(assetTST); + address vaultAddress = address(eTST2); (success, returnData) = actor.proxy( address(evc), diff --git a/test/invariants/handlers/interfaces/IBorrowingModuleHandler.sol b/test/invariants/handlers/interfaces/IBorrowingModuleHandler.sol new file mode 100644 index 00000000..a0984eb6 --- /dev/null +++ b/test/invariants/handlers/interfaces/IBorrowingModuleHandler.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IBorrowingModuleHandler { + function repayTo(uint256 assets, uint256 i) external; +} diff --git a/test/invariants/handlers/interfaces/IGovernanceModuleHandler.sol b/test/invariants/handlers/interfaces/IGovernanceModuleHandler.sol new file mode 100644 index 00000000..2be36171 --- /dev/null +++ b/test/invariants/handlers/interfaces/IGovernanceModuleHandler.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IGovernanceModuleHandler { + function convertFees() external; +} diff --git a/test/invariants/handlers/interfaces/IVaultModuleHandler.sol b/test/invariants/handlers/interfaces/IVaultModuleHandler.sol new file mode 100644 index 00000000..44eab2a3 --- /dev/null +++ b/test/invariants/handlers/interfaces/IVaultModuleHandler.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IVaultModuleHandler { + function depositToActor(uint256 assets, uint256 i) external; + function mintToActor(uint256 shares, uint256 i) external; + function skim(uint256 assets, uint256 i) external; +} diff --git a/test/invariants/handlers/modules/BorrowingModuleHandler.t.sol b/test/invariants/handlers/modules/BorrowingModuleHandler.t.sol index 4b2bb888..d3200099 100644 --- a/test/invariants/handlers/modules/BorrowingModuleHandler.t.sol +++ b/test/invariants/handlers/modules/BorrowingModuleHandler.t.sol @@ -41,9 +41,7 @@ contract BorrowingModuleHandler is BaseHandler { (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IBorrowing.borrow.selector, assets, receiver)); - (uint256 shares) = abi.decode(returnData, (uint256)); - - if (!isAccountHealthyBefore && (assets != 0 && shares != 0)) { + if (!isAccountHealthyBefore && (assets != 0)) { /// @dev BM_INVARIANT_E assertFalse(success, BM_INVARIANT_E); } else { @@ -64,6 +62,10 @@ contract BorrowingModuleHandler is BaseHandler { (, uint256 liabilityValueBefore) = _getAccountLiquidity(receiver, false); + uint256 totalOwed = eTST.debtOf(receiver); + + assets = clampLe(assets, totalOwed); + _before(); (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IBorrowing.repay.selector, assets, receiver)); @@ -91,7 +93,10 @@ contract BorrowingModuleHandler is BaseHandler { actor.proxy(target, abi.encodeWithSelector(IBorrowing.repayWithShares.selector, amount, receiver)); if (success) { + _after(); + uint256 shares = abi.decode(returnData, (uint256)); + _decreaseGhostShares(shares, address(actor)); } } @@ -134,12 +139,14 @@ contract BorrowingModuleHandler is BaseHandler { if (eTST.totalBorrows() == 0) { uint256 balanceBefore = eTST.balanceOf(address(actor)); + (success, returnData) = actor.proxy( address(eTST), abi.encodeWithSelector(IERC4626.redeem.selector, balanceBefore, address(actor), address(actor)) ); - _decreaseGhostShares(balanceBefore, address(actor)); - assertTrue(success, BM_INVARIANT_G); + + if (success) _decreaseGhostShares(balanceBefore, address(actor)); + //assertTrue(success, BM_INVARIANT_G); TODO remove comment } } diff --git a/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol b/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol index d0921a5e..e27bd2a3 100644 --- a/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol +++ b/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol @@ -36,16 +36,16 @@ contract GovernanceModuleHandler is BaseHandler { } } - function setLTV(uint256 i, uint16 borrowLTV, uint16 liquidationLTV, uint24 rampDuration) external { - address collateral = _getRandomBaseAsset(i); + function setLTV(uint16 borrowLTV, uint16 liquidationLTV, uint24 rampDuration) external { + address collateral = address(eTST2); eTST.setLTV(collateral, borrowLTV, liquidationLTV, rampDuration); assert(true); } - function clearLTV(uint256 i) external { - address collateral = _getRandomBaseAsset(i); + function clearLTV() external { + address collateral = address(eTST2); eTST.clearLTV(collateral); diff --git a/test/invariants/handlers/modules/LiquidationModuleHandler.t.sol b/test/invariants/handlers/modules/LiquidationModuleHandler.t.sol index bfd67241..22e51636 100644 --- a/test/invariants/handlers/modules/LiquidationModuleHandler.t.sol +++ b/test/invariants/handlers/modules/LiquidationModuleHandler.t.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +// Libraries +import "../../../../src/EVault/shared/Constants.sol"; + // Test Contracts import {Actor} from "../../utils/Actor.sol"; import {BaseHandler} from "../../base/BaseHandler.t.sol"; @@ -8,6 +11,8 @@ import {BaseHandler} from "../../base/BaseHandler.t.sol"; // Interfaces import {ILiquidation} from "../../../../src/EVault/IEVault.sol"; +import "forge-std/console.sol"; + /// @title LiquidationModuleHandler /// @notice Handler test contract for the VaultRegularBorrowable actions contract LiquidationModuleHandler is BaseHandler { @@ -15,6 +20,15 @@ contract LiquidationModuleHandler is BaseHandler { // STATE VARIABLES // /////////////////////////////////////////////////////////////////////////////////////////////// + struct LiquidationData { + bool violatorStatus; + bool senderStatus; + uint256 debtViolator; + uint256 debtSender; + uint256 collateralBalanceViolator; + uint256 collateralBalanceSender; + } + /////////////////////////////////////////////////////////////////////////////////////////////// // GHOST VARAIBLES // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -27,44 +41,72 @@ contract LiquidationModuleHandler is BaseHandler { bool success; bytes memory returnData; + LiquidationData memory liquidationData; + address target = address(eTST); + // Get one of the three actors randomly address violator = _getRandomActor(i); - bool violatorStatus = isAccountHealthyLiquidation(violator); + // Get violator info + liquidationData.violatorStatus = isAccountHealthyLiquidation(violator); + liquidationData.debtViolator = eTST.debtOf(violator); + + // Get sender info + liquidationData.senderStatus = isAccountHealthyLiquidation(address(actor)); + liquidationData.debtSender = eTST.debtOf(address(actor)); + + liquidationData.collateralBalanceViolator = eTST2.balanceOf(violator); + liquidationData.collateralBalanceSender = eTST2.balanceOf(address(actor)); + + (uint256 maxRepay, uint256 maxYield) = eTST.checkLiquidation(address(actor), violator, address(eTST2)); { - address collateral = _getRandomAccountCollateral(i, address(actor)); + { + (, uint256 liabilityValue) = eTST.accountLiquidity(violator, true); + require(liabilityValue > 0, "LiquidationModuleHandler: debtViolator is 0"); + + minYieldBalance = clampLe(minYieldBalance, maxYield); + } + + _beforeLiquidation(violator); - _before(); (success, returnData) = actor.proxy( target, abi.encodeWithSelector( - ILiquidation.liquidate.selector, violator, collateral, repayAssets, minYieldBalance + ILiquidation.liquidate.selector, violator, address(eTST2), repayAssets, minYieldBalance ) ); } - if (success) { + + if (success && (maxRepay != 0 || minYieldBalance != 0)) { _after(); - /// @dev LM_INVARIANT_A - if (repayAssets != 0) { - assertFalse(violatorStatus, LM_INVARIANT_A); + // Violator should be unhealthy before liquidation + assertFalse(liquidationData.violatorStatus, LM_INVARIANT_A); + + if (repayAssets == type(uint256).max) repayAssets = maxRepay; + + // Debt accounting + if (!eTST.isFlagSet(CFG_DONT_SOCIALIZE_DEBT) && eTST2.balanceOf(violator) == 0) { + assertEq(eTST.debtOf(violator), 0, LM_INVARIANT_G); + } else { + assertEq(eTST.debtOf(violator), liquidationData.debtViolator - repayAssets, LM_INVARIANT_E); } + assertEq(eTST.debtOf(address(actor)), liquidationData.debtSender + repayAssets, LM_INVARIANT_E); + + // Collateral accounting + assertEq(eTST2.balanceOf(violator), liquidationData.collateralBalanceViolator - maxYield, LM_INVARIANT_F); + assertEq( + eTST2.balanceOf(address(actor)), liquidationData.collateralBalanceSender + maxYield, LM_INVARIANT_F + ); + + // Sender should stay healthy + assertTrue(isAccountHealthy(address(actor)), LM_INVARIANT_C); } } /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // /////////////////////////////////////////////////////////////////////////////////////////////// - - function _getActorWithDebt() internal view returns (address) { - address _actor = address(actor); - for (uint256 k; k < NUMBER_OF_ACTORS; k++) { - if (_actor != actorAddresses[k] && eTST.debtOf(address(actorAddresses[k])) > 0) { - return address(actorAddresses[k]); - } - } - return address(0); - } } diff --git a/test/invariants/handlers/modules/TokenModuleHandler.t.sol b/test/invariants/handlers/modules/TokenModuleHandler.t.sol index 02e3a9e5..87a85c9d 100644 --- a/test/invariants/handlers/modules/TokenModuleHandler.t.sol +++ b/test/invariants/handlers/modules/TokenModuleHandler.t.sol @@ -23,14 +23,14 @@ contract TokenModuleHandler is BaseHandler { // ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - function approveTo(uint256 i, uint256 amount) external setup { + function approveTo(uint256 i, uint8 j, uint256 amount) external setup { bool success; bytes memory returnData; // Get one of the three actors randomly address spender = _getRandomActor(i); - address target = address(eTST); + address target = _getRandomVault(j); (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); @@ -39,56 +39,26 @@ contract TokenModuleHandler is BaseHandler { } } - /* function transfer(address to, uint256 amount) external setup { - bool success; - bytes memory returnData; - - address target = address(eTST); - - (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC20.transfer.selector, to, amount)); - - if (success) { - ghost_sumSharesBalancesPerUser[address(actor)] -= amount; - ghost_sumSharesBalancesPerUser[to] += amount; - } - } */ - - function transferTo(uint256 i, uint256 amount) external setup { + function transferTo(uint256 i, uint8 j, uint256 amount) external setup { bool success; bytes memory returnData; // Get one of the three actors randomly address to = _getRandomActor(i); - address target = address(eTST); + address target = _getRandomVault(j); (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC20.transfer.selector, to, amount)); if (success) { - ghost_sumSharesBalancesPerUser[address(actor)] -= amount; - ghost_sumSharesBalancesPerUser[to] += amount; + if (target == address(eTST)) { + ghost_sumSharesBalancesPerUser[address(actor)] -= amount; + ghost_sumSharesBalancesPerUser[to] += amount; + } } } - /* function transferFrom(uint256 i, address to, uint256 amount) external setup { - bool success; - bytes memory returnData; - - // Get one of the three actors randomly - address from = _getRandomActor(i); - - address target = address(eTST); - - (success, returnData) = - actor.proxy(target, abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount)); - - if (success) { - ghost_sumSharesBalancesPerUser[from] -= amount; - ghost_sumSharesBalancesPerUser[to] += amount; - } - } */ - - function transferFromTo(uint256 i, uint256 u, uint256 amount) external setup { + function transferFromTo(uint256 i, uint8 j, uint256 u, uint256 amount) external setup { bool success; bytes memory returnData; @@ -97,14 +67,16 @@ contract TokenModuleHandler is BaseHandler { // Get one of the three actors randomly address to = _getRandomActor(u); - address target = address(eTST); + address target = _getRandomVault(j); (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount)); if (success) { - ghost_sumSharesBalancesPerUser[from] -= amount; - ghost_sumSharesBalancesPerUser[to] += amount; + if (target == address(eTST)) { + ghost_sumSharesBalancesPerUser[from] -= amount; + ghost_sumSharesBalancesPerUser[to] += amount; + } } } diff --git a/test/invariants/handlers/modules/VaultModuleHandler.t.sol b/test/invariants/handlers/modules/VaultModuleHandler.t.sol index 50432bf6..a7f9b023 100644 --- a/test/invariants/handlers/modules/VaultModuleHandler.t.sol +++ b/test/invariants/handlers/modules/VaultModuleHandler.t.sol @@ -9,7 +9,7 @@ import {Actor} from "../../utils/Actor.sol"; import {BaseHandler} from "../../base/BaseHandler.t.sol"; // Interfaces -import {IERC4626} from "../../../../src/EVault/IEVault.sol"; +import {IERC4626, IVault} from "../../../../src/EVault/IEVault.sol"; /// @title VaultModuleHandler /// @notice Handler test contract for the generic ERC4626 vault actions @@ -19,22 +19,22 @@ contract VaultModuleHandler is BaseHandler { /////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////// - // ACTIONS // + // DEPOSITOR ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - /* function deposit(uint256 assets, address receiver) external setup { + function depositToActor(uint256 assets, uint256 i) external setup { bool success; bytes memory returnData; + // Get one of the three actors randomly + address receiver = _getRandomActor(i); + address target = address(eTST); uint256 previewedShares = eTST.previewDeposit(assets); - _approve(address(eTST.asset()), actor, target, assets); - _before(); - (success, returnData) = - actor.proxy(target, abi.encodeWithSelector(IERC4626.deposit.selector, assets, receiver)); + (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC4626.deposit.selector, assets, receiver)); if (success) { _after(); @@ -47,9 +47,9 @@ contract VaultModuleHandler is BaseHandler { /// @dev ERC4626_DEPOSIT_INVARIANT_B assertLe(previewedShares, shares, ERC4626_DEPOSIT_INVARIANT_B); } - } */ + } - function depositToActor(uint256 assets, uint256 i) external setup { + function mintToActor(uint256 shares, uint256 i) external setup { bool success; bytes memory returnData; @@ -58,123 +58,157 @@ contract VaultModuleHandler is BaseHandler { address target = address(eTST); - uint256 previewedShares = eTST.previewDeposit(assets); + uint256 previewedAssets = eTST.previewMint(shares); _before(); - (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC4626.deposit.selector, assets, receiver)); + (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC4626.mint.selector, shares, receiver)); if (success) { _after(); - uint256 shares = abi.decode(returnData, (uint256)); + uint256 assets = abi.decode(returnData, (uint256)); _increaseGhostAssets(assets, address(receiver)); _increaseGhostShares(shares, address(receiver)); - /// @dev ERC4626_DEPOSIT_INVARIANT_B - assertLe(previewedShares, shares, ERC4626_DEPOSIT_INVARIANT_B); + /// @dev ERC4626_MINT_INVARIANT_B + assertGe(previewedAssets, assets, ERC4626_MINT_INVARIANT_B); } } - /* function mint(uint256 shares, address receiver) external setup { + function withdraw(uint256 assets, address receiver) external setup { bool success; bytes memory returnData; address target = address(eTST); - uint256 previewedAssets = eTST.previewMint(shares); + uint256 previewedShares = eTST.previewWithdraw(assets); _before(); (success, returnData) = - actor.proxy(target, abi.encodeWithSelector(IERC4626.mint.selector, shares, receiver)); + actor.proxy(target, abi.encodeWithSelector(IERC4626.withdraw.selector, assets, receiver, address(actor))); if (success) { _after(); - uint256 assets = abi.decode(returnData, (uint256)); + uint256 shares = abi.decode(returnData, (uint256)); - _increaseGhostAssets(assets, address(receiver)); - _increaseGhostShares(shares, address(receiver)); + _decreaseGhostAssets(assets, address(actor)); + _decreaseGhostShares(shares, address(actor)); - /// @dev ERC4626_MINT_INVARIANT_B - assertGe(previewedAssets, assets, ERC4626_MINT_INVARIANT_B); + /// @dev ERC4626_WITHDRAW_INVARIANT_B + assertGe(previewedShares, shares, ERC4626_WITHDRAW_INVARIANT_B); } - } */ + } - function mintToActor(uint256 shares, uint256 i) external setup { + function redeem(uint256 shares, address receiver) external setup { bool success; bytes memory returnData; - // Get one of the three actors randomly - address receiver = _getRandomActor(i); - address target = address(eTST); - uint256 previewedAssets = eTST.previewMint(shares); + uint256 previewedAssets = eTST.previewRedeem(shares); _before(); - (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC4626.mint.selector, shares, receiver)); + (success, returnData) = + actor.proxy(target, abi.encodeWithSelector(IERC4626.redeem.selector, shares, receiver, address(actor))); if (success) { _after(); uint256 assets = abi.decode(returnData, (uint256)); - _increaseGhostAssets(assets, address(receiver)); - _increaseGhostShares(shares, address(receiver)); + _decreaseGhostAssets(assets, address(actor)); + _decreaseGhostShares(shares, address(actor)); - /// @dev ERC4626_MINT_INVARIANT_B - assertGe(previewedAssets, assets, ERC4626_MINT_INVARIANT_B); + /// @dev ERC4626_REDEEM_INVARIANT_B + assertLe(previewedAssets, assets, ERC4626_REDEEM_INVARIANT_B); } } - function withdraw(uint256 assets, address receiver) external setup { + function skim(uint256 assets, uint256 i) external setup { bool success; bytes memory returnData; address target = address(eTST); - uint256 previewedShares = eTST.previewWithdraw(assets); + // Get one of the three actors randomly + address receiver = _getRandomActor(i); _before(); - (success, returnData) = - actor.proxy(target, abi.encodeWithSelector(IERC4626.withdraw.selector, assets, receiver, address(actor))); + (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IVault.skim.selector, assets, receiver)); if (success) { _after(); uint256 shares = abi.decode(returnData, (uint256)); - _decreaseGhostAssets(assets, address(actor)); - _decreaseGhostShares(shares, address(actor)); + _increaseGhostAssets(assets, address(receiver)); + _increaseGhostShares(shares, address(receiver)); + } + } - /// @dev ERC4626_WITHDRAW_INVARIANT_B - assertGe(previewedShares, shares, ERC4626_WITHDRAW_INVARIANT_B); + /////////////////////////////////////////////////////////////////////////////////////////////// + // COLLATERAL ACTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function depositCollateralToActor(uint256 assets, uint256 i) external setup { + bool success; + bytes memory returnData; + + // Get one of the three actors randomly + address receiver = _getRandomActor(i); + + address target = address(eTST2); + + (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC4626.deposit.selector, assets, receiver)); + + if (success) { + assert(true); } } - function redeem(uint256 shares, address receiver) external setup { + function mintCollateralToActor(uint256 shares, uint256 i) external setup { bool success; bytes memory returnData; - address target = address(eTST); + // Get one of the three actors randomly + address receiver = _getRandomActor(i); - uint256 previewedAssets = eTST.previewRedeem(shares); + address target = address(eTST2); + + (success, returnData) = actor.proxy(target, abi.encodeWithSelector(IERC4626.mint.selector, shares, receiver)); + + if (success) { + assert(true); + } + } + + function withdrawCollateral(uint256 assets, address receiver) external setup { + bool success; + bytes memory returnData; + + address target = address(eTST2); - _before(); (success, returnData) = - actor.proxy(target, abi.encodeWithSelector(IERC4626.redeem.selector, shares, receiver, address(actor))); + actor.proxy(target, abi.encodeWithSelector(IERC4626.withdraw.selector, assets, receiver, address(actor))); if (success) { - _after(); + assert(true); + } + } - uint256 assets = abi.decode(returnData, (uint256)); + function redeemCollateral(uint256 shares, address receiver) external setup { + bool success; + bytes memory returnData; - _decreaseGhostAssets(assets, address(actor)); - _decreaseGhostShares(shares, address(actor)); + address target = address(eTST2); - /// @dev ERC4626_REDEEM_INVARIANT_B - assertLe(previewedAssets, assets, ERC4626_REDEEM_INVARIANT_B); + (success, returnData) = + actor.proxy(target, abi.encodeWithSelector(IERC4626.redeem.selector, shares, receiver, address(actor))); + + if (success) { + assert(true); } } diff --git a/test/invariants/handlers/simulators/PriceOracleHandler.t.sol b/test/invariants/handlers/simulators/PriceOracleHandler.t.sol index 04b7a7e9..f8a089cc 100644 --- a/test/invariants/handlers/simulators/PriceOracleHandler.t.sol +++ b/test/invariants/handlers/simulators/PriceOracleHandler.t.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.19; import {BaseHandler} from "../../base/BaseHandler.t.sol"; +import "forge-std/console.sol"; + /// @title PriceOracleHandler /// @notice Handler test contract for the PriceOracle actions contract PriceOracleHandler is BaseHandler { @@ -18,20 +20,29 @@ contract PriceOracleHandler is BaseHandler { // ACTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice This function simulates changes in the interest rate model + /// @notice This function simulates changes in the price of an asset function setPrice(uint256 i, uint256 price) external { address baseAsset = _getRandomBaseAsset(i); oracle.setPrice(baseAsset, unitOfAccount, price); } - /* - /// @notice This function simulates changes in the interest rate model - function setResolvedAsset(uint256 i) external { - address vaultAddress = address(eTST); + /// @notice This function simulates smaller changes in the price of an asset + function changePrice(uint256 i, uint16 deltaPercentage, bool up) external { + address baseAsset = _getRandomBaseAsset(i); + + deltaPercentage = uint16(clampLe(deltaPercentage, 1e4)); + + uint256 price = oracle.getQuote(1e18, baseAsset, unitOfAccount); - oracle.setResolvedAsset(vaultAddress); - } */ + if (up) { + price = price + (price * deltaPercentage) / 1e4; + } else { + price = price - (price * deltaPercentage) / 1e4; + } + + oracle.setPrice(baseAsset, unitOfAccount, price); + } /////////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // diff --git a/test/invariants/helpers/extended/EVaultExtended.sol b/test/invariants/helpers/extended/EVaultExtended.sol index 2a3a6dcb..0a6f86e2 100644 --- a/test/invariants/helpers/extended/EVaultExtended.sol +++ b/test/invariants/helpers/extended/EVaultExtended.sol @@ -13,7 +13,7 @@ import {EVault} from "../../../../src/EVault/EVault.sol"; // Types import "../../../../src/EVault/shared/types/Types.sol"; -contract EVaultExtended is EVault { +contract EVaultExtended is EVault, FunctionOverrides { constructor(Integrations memory integrations, DeployedModules memory modules) EVault(integrations, modules) {} function getReentrancyLock() external view returns (bool) { @@ -35,7 +35,7 @@ contract EVaultExtended is EVault { function isFlagSet(uint32 bitMask) external view returns (bool) { return vaultStorage.configFlags.isSet(bitMask); } - /* + function initOperation(uint32 operation, address accountToCheck) internal override (Base, FunctionOverrides) @@ -91,5 +91,5 @@ contract EVaultExtended is EVault { override (BorrowUtils, FunctionOverrides) { FunctionOverrides.transferBorrow(vaultCache, from, to, assets); - }*/ + } } diff --git a/test/invariants/helpers/extended/FunctionOverrides.sol b/test/invariants/helpers/extended/FunctionOverrides.sol index 6c8d54d8..e3b186e4 100644 --- a/test/invariants/helpers/extended/FunctionOverrides.sol +++ b/test/invariants/helpers/extended/FunctionOverrides.sol @@ -13,12 +13,12 @@ import "../../utils/StdAsserts.sol"; /// @notice Abstract contract to override functions and check internal invariants. abstract contract FunctionOverrides is BalanceUtils, BorrowUtils, StdAsserts, InvariantsSpec { - uint32 internal constant INIT_OPERATION_FLAG = 1 << 31; + bool public initOperationFlag; /// @notice Internal invariants for low level operations /// @dev Similar to Postconditions but checked internally within the transaction function checkInvariants(address checkedAccount, address controllerEnabled) internal { - assertTrue(Flags.unwrap(vaultStorage.hookedOps) & INIT_OPERATION_FLAG != 0, INTERNAL_INVARIANT_A); + assertTrue(initOperationFlag, INTERNAL_INVARIANT_A); assertTrue(evc.isVaultStatusCheckDeferred(address(this)), INTERNAL_INVARIANT_B); assertTrue( checkedAccount == address(0) || evc.isAccountStatusCheckDeferred(checkedAccount), INTERNAL_INVARIANT_C @@ -36,7 +36,7 @@ abstract contract FunctionOverrides is BalanceUtils, BorrowUtils, StdAsserts, In returns (VaultCache memory vaultCache, address account) { (vaultCache, account) = super.initOperation(operation, accountToCheck); - vaultStorage.hookedOps = Flags.wrap(Flags.unwrap(vaultStorage.hookedOps) | INIT_OPERATION_FLAG); + initOperationFlag = true; } function increaseBalance( diff --git a/test/invariants/helpers/extended/ModulesExtended.sol b/test/invariants/helpers/extended/ModulesExtended.sol index 317eaf00..c35bdd4c 100644 --- a/test/invariants/helpers/extended/ModulesExtended.sol +++ b/test/invariants/helpers/extended/ModulesExtended.sol @@ -96,6 +96,10 @@ contract BorrowingExtended is Borrowing, FunctionOverrides { contract GovernanceExtended is Governance, FunctionOverrides { constructor(Integrations memory integrations) Governance(integrations) {} + function resetInitOperationFlag() public governorOnly { + initOperationFlag = false; + } + function initOperation(uint32 operation, address accountToCheck) internal override (Base, FunctionOverrides) diff --git a/test/invariants/hooks/BorrowingBeforeAfterHooks.t.sol b/test/invariants/hooks/BorrowingBeforeAfterHooks.t.sol index b12bcab7..532f36e5 100644 --- a/test/invariants/hooks/BorrowingBeforeAfterHooks.t.sol +++ b/test/invariants/hooks/BorrowingBeforeAfterHooks.t.sol @@ -12,6 +12,8 @@ import {Pretty, Strings} from "../utils/Pretty.sol"; import {BaseHooks} from "../base/BaseHooks.t.sol"; import {ILiquidationModuleHandler} from "../handlers/interfaces/ILiquidationModuleHandler.sol"; +import "forge-std/console.sol"; + /// @title Borrowing Before After Hooks /// @notice Helper contract for before and after hooks /// @dev This contract is inherited by handlers @@ -66,7 +68,7 @@ abstract contract BorrowingBeforeAfterHooks is BaseHooks { borrowingVars.controllerEnabledBefore = evc.isControllerEnabled(address(actor), address(eTST)); // Liquidity (borrowingVars.collateralValueBefore, borrowingVars.liabilityValueBefore) = - _getAccountLiquidity(address(actor), false); + _getAccountLiquidity(_getActorOrViolator(), false); // Caps (, uint16 _borrowCap) = eTST.caps(); borrowingVars.borrowCapBefore = AmountCap.wrap(_borrowCap).resolve(); @@ -86,7 +88,7 @@ abstract contract BorrowingBeforeAfterHooks is BaseHooks { borrowingVars.controllerEnabledAfter = evc.isControllerEnabled(address(actor), address(eTST)); // Liquidity (borrowingVars.collateralValueAfter, borrowingVars.liabilityValueAfter) = - _getAccountLiquidity(address(actor), false); + _getAccountLiquidity(_getActorOrViolator(), false); // Caps (, uint16 _borrowCap) = eTST.caps(); borrowingVars.borrowCapAfter = AmountCap.wrap(_borrowCap).resolve(); @@ -116,10 +118,8 @@ abstract contract BorrowingBeforeAfterHooks is BaseHooks { } function assert_LM_INVARIANT_C() internal { - if (isAccountHealthy(borrowingVars.liabilityValueBefore, borrowingVars.collateralValueBefore)) { - if (!isAccountHealthy(borrowingVars.liabilityValueAfter, borrowingVars.collateralValueAfter)) { - assertEq(bytes32(msg.sig), bytes32(ILiquidationModuleHandler.liquidate.selector), LM_INVARIANT_C); - } + if (!isAccountHealthy(borrowingVars.liabilityValueAfter, borrowingVars.collateralValueAfter)) { + assertTrue(uncheckedHealthOperations[msg.sig], LM_INVARIANT_C); } } diff --git a/test/invariants/hooks/HookAggregator.t.sol b/test/invariants/hooks/HookAggregator.t.sol index 17b523b5..1f90276f 100644 --- a/test/invariants/hooks/HookAggregator.t.sol +++ b/test/invariants/hooks/HookAggregator.t.sol @@ -14,6 +14,11 @@ abstract contract HookAggregator is VaultBeforeAfterHooks, BorrowingBeforeAfterH _borrowingHooksBefore(); } + function _beforeLiquidation(address violator) internal { + violatorTemp = violator; + _before(); + } + /// @notice Modular hook selector, per module function _after() internal { _vaultHooksAfter(); @@ -21,6 +26,9 @@ abstract contract HookAggregator is VaultBeforeAfterHooks, BorrowingBeforeAfterH // Postconditions _checkPostConditions(); + + // Reset + violatorTemp = address(0); } /// @notice Postconditions for the handlers diff --git a/test/invariants/hooks/VaultBeforeAfterHooks.t.sol b/test/invariants/hooks/VaultBeforeAfterHooks.t.sol index f5ea0115..b862fad5 100644 --- a/test/invariants/hooks/VaultBeforeAfterHooks.t.sol +++ b/test/invariants/hooks/VaultBeforeAfterHooks.t.sol @@ -13,6 +13,10 @@ import {BaseHooks} from "../base/BaseHooks.t.sol"; import "forge-std/console.sol"; +interface IGovernanceOverride { + function resetInitOperationFlag() external; +} + /// @title Vault Before After Hooks /// @notice Helper contract for before and after hooks /// @dev This contract is inherited by handlers @@ -47,6 +51,8 @@ abstract contract VaultBeforeAfterHooks is BaseHooks { VaultVars vaultVars; function _vaultHooksBefore() internal { + // this will fail if there's no overrides on + try IGovernanceOverride(address(eTST)).resetInitOperationFlag() {} catch {} // Exchange Rate vaultVars.exchangeRateBefore = _calculateExchangeRate(); // ERC4626 diff --git a/test/invariants/invariants/BorrowingModuleInvariants.t.sol b/test/invariants/invariants/BorrowingModuleInvariants.t.sol index 6f6f2267..fc4e8ffe 100644 --- a/test/invariants/invariants/BorrowingModuleInvariants.t.sol +++ b/test/invariants/invariants/BorrowingModuleInvariants.t.sol @@ -16,7 +16,9 @@ import {HandlerAggregator} from "../HandlerAggregator.t.sol"; /// @dev Inherits HandlerAggregator for checking actions in assertion testing mode abstract contract BorrowingModuleInvariants is HandlerAggregator { function assert_BM_INVARIANT_A(address _borrower) internal { - assertGe(eTST.totalBorrows(), eTST.debtOf(_borrower), BM_INVARIANT_A); + if (eTST.debtOf(_borrower) > NUMBER_OF_ACTORS) { + assertGe(eTST.totalBorrows(), eTST.debtOf(_borrower) - NUMBER_OF_ACTORS, BM_INVARIANT_A); + } } function assert_BM_INVARIANT_B() internal { diff --git a/test/invariants/invariants/VaultModuleInvariants.t.sol b/test/invariants/invariants/VaultModuleInvariants.t.sol index 07322eb1..bb26d54f 100644 --- a/test/invariants/invariants/VaultModuleInvariants.t.sol +++ b/test/invariants/invariants/VaultModuleInvariants.t.sol @@ -23,9 +23,6 @@ abstract contract VaultModuleInvariants is HandlerAggregator { if (eTST.totalAssets() == 0) { assertEq(eTST.totalSupply(), 0, VM_INVARIANT_C); } - if (eTST.totalSupply() == 0) { - assertEq(eTST.totalAssets(), 0, VM_INVARIANT_C); - } } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/test/invariants/utils/Actor.sol b/test/invariants/utils/Actor.sol index f0dea928..fff975c1 100644 --- a/test/invariants/utils/Actor.sol +++ b/test/invariants/utils/Actor.sol @@ -17,13 +17,17 @@ contract Actor { tokens = _tokens; callers = _callers; for (uint256 i = 0; i < tokens.length; i++) { - IERC20(tokens[i]).approve(callers[i], type(uint256).max); + for (uint256 j = 0; j < callers.length; j++) { + IERC20(tokens[i]).approve(callers[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 @@ -32,6 +36,26 @@ contract Actor { 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 {}