From 39fed4b12506d621fd01d5ae3139aaf466404c8e Mon Sep 17 00:00:00 2001 From: Dima Lekhovitsky Date: Sat, 30 Mar 2024 13:58:18 +0200 Subject: [PATCH] test: minimal invariant tests --- .github/workflows/pr.yml | 3 + contracts/test/invariant/Deployer.sol | 409 ++++++++++++++++++ contracts/test/invariant/Invariants.sol | 266 ++++++++++++ .../test/invariant/handlers/CreditHandler.sol | 263 +++++++++++ .../test/invariant/handlers/HandlerBase.sol | 36 ++ .../test/invariant/handlers/PoolHandler.sol | 146 +++++++ .../test/invariant/handlers/VotingHandler.sol | 179 ++++++++ .../invariant/tests/GaugeVoting.inv.t.sol | 45 ++ .../test/invariant/tests/Global.inv.t.sol | 100 +++++ .../invariant/tests/InvariantTestBase.sol | 33 ++ .../invariant/tests/IsolatedCredit.inv.t.sol | 57 +++ .../invariant/tests/IsolatedPool.inv.t.sol | 54 +++ .../test/invariant/tests/MockVoting.inv.t.sol | 44 ++ contracts/test/invaritants/Deployer.sol | 32 -- contracts/test/invaritants/Handler.sol | 65 --- .../test/invaritants/OpenInvariants.t.sol | 24 - contracts/test/invaritants/TargetAttacker.sol | 116 ----- .../mocks/governance/VotingContractMock.sol | 18 + .../test/suites/CreditManagerFactory.sol | 8 +- foundry.toml | 5 +- 20 files changed, 1658 insertions(+), 245 deletions(-) create mode 100644 contracts/test/invariant/Deployer.sol create mode 100644 contracts/test/invariant/Invariants.sol create mode 100644 contracts/test/invariant/handlers/CreditHandler.sol create mode 100644 contracts/test/invariant/handlers/HandlerBase.sol create mode 100644 contracts/test/invariant/handlers/PoolHandler.sol create mode 100644 contracts/test/invariant/handlers/VotingHandler.sol create mode 100644 contracts/test/invariant/tests/GaugeVoting.inv.t.sol create mode 100644 contracts/test/invariant/tests/Global.inv.t.sol create mode 100644 contracts/test/invariant/tests/InvariantTestBase.sol create mode 100644 contracts/test/invariant/tests/IsolatedCredit.inv.t.sol create mode 100644 contracts/test/invariant/tests/IsolatedPool.inv.t.sol create mode 100644 contracts/test/invariant/tests/MockVoting.inv.t.sol delete mode 100644 contracts/test/invaritants/Deployer.sol delete mode 100644 contracts/test/invaritants/Handler.sol delete mode 100644 contracts/test/invaritants/OpenInvariants.t.sol delete mode 100644 contracts/test/invaritants/TargetAttacker.sol create mode 100644 contracts/test/mocks/governance/VotingContractMock.sol diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 78e4069a..9f085d8b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -44,6 +44,9 @@ jobs: - name: Run gas tests run: forge test --mt test_G -vv + - name: Run invariant tests + run: forge test --mt invariant + - name: Perform checks run: | yarn prettier:ci diff --git a/contracts/test/invariant/Deployer.sol b/contracts/test/invariant/Deployer.sol new file mode 100644 index 00000000..874f1f54 --- /dev/null +++ b/contracts/test/invariant/Deployer.sol @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {Test} from "forge-std/Test.sol"; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; +import {ContractsRegister} from "@gearbox-protocol/core-v2/contracts/core/ContractsRegister.sol"; + +import {AccountFactoryV3} from "../../core/AccountFactoryV3.sol"; +import {AddressProviderV3} from "../../core/AddressProviderV3.sol"; +import {BotListV3} from "../../core/BotListV3.sol"; +import {PriceOracleV3} from "../../core/PriceOracleV3.sol"; + +import {CreditConfiguratorV3, CreditManagerOpts} from "../../credit/CreditConfiguratorV3.sol"; +import {CreditManagerV3} from "../../credit/CreditManagerV3.sol"; + +import {GaugeV3} from "../../governance/GaugeV3.sol"; +import {EPOCH_LENGTH, GearStakingV3, VotingContractStatus} from "../../governance/GearStakingV3.sol"; + +import {LinearInterestRateModelV3} from "../../pool/LinearInterestRateModelV3.sol"; +import {PoolQuotaKeeperV3} from "../../pool/PoolQuotaKeeperV3.sol"; +import {PoolV3} from "../../pool/PoolV3.sol"; + +import {PriceFeedMock} from "../mocks/oracles/PriceFeedMock.sol"; +import {ERC20Mock} from "../mocks/token/ERC20Mock.sol"; +import {WETHMock} from "../mocks/token/WETHMock.sol"; + +import {CreditManagerFactory} from "../suites/CreditManagerFactory.sol"; + +contract Deployer is Test { + address configurator; + address controller; + address treasury; + address weth; + address gear; + + ACL acl; + AccountFactoryV3 accountFactory; + AddressProviderV3 addressProvider; + BotListV3 botList; + ContractsRegister contractsRegister; + GearStakingV3 gearStaking; + PriceOracleV3 priceOracle; + + address[] tokensList; + mapping(string => ERC20) tokens; + + mapping(string => PoolV3) pools; + mapping(string => CreditManagerV3) creditManagers; + + function setUp() public virtual { + _deployCore(); + _deployTokensAndPriceFeeds(); + _deployPool("DAI"); + _deployPool("USDC"); + _deployCreditManager("DAI"); + _deployCreditManager("USDC"); + } + + // ------- // + // GETTERS // + // ------- // + + function _getToken(string memory symbol) internal view returns (ERC20 token) { + token = tokens[symbol]; + require(address(token) != address(0), string.concat("Deployer: Token ", symbol, " not deployed yet")); + } + + function _getPool(string memory name) internal view returns (PoolV3 pool) { + pool = pools[name]; + require(address(pool) != address(0), string.concat("Deployer: Pool ", name, " not deployed yet")); + } + + function _getQuotaKeeper(string memory poolName) internal view returns (PoolQuotaKeeperV3) { + return PoolQuotaKeeperV3(_getPool(poolName).poolQuotaKeeper()); + } + + function _getGauge(string memory poolName) internal view returns (GaugeV3) { + return GaugeV3(_getQuotaKeeper(poolName).gauge()); + } + + function _getQuotedTokens(string memory poolName) internal view returns (address[] memory quotedTokens) { + uint256 numTokens = tokensList.length; + quotedTokens = new address[](numTokens); + + uint256 numQuotedTokens; + GaugeV3 gauge = _getGauge(poolName); + for (uint256 i; i < numTokens; ++i) { + if (gauge.isTokenAdded(tokensList[i])) quotedTokens[numQuotedTokens++] = tokensList[i]; + } + assembly { + mstore(quotedTokens, numQuotedTokens) + } + } + + function _getCreditManager(string memory name) internal view returns (CreditManagerV3 creditManager) { + creditManager = creditManagers[name]; + require( + address(creditManager) != address(0), string.concat("Deployer: Credit manager ", name, " not deployed yet") + ); + } + + // ---- // + // CORE // + // ---- // + + function _deployCore() internal { + configurator = makeAddr("CONFIGURATOR"); + controller = makeAddr("CONTROLLER"); + treasury = makeAddr("TREASURY"); + + weth = address(new WETHMock()); + gear = address(new ERC20Mock("Gearbox", "GEAR", 18)); + + vm.startPrank(configurator); + acl = new ACL(); + addressProvider = new AddressProviderV3(address(acl)); + addressProvider.setAddress("TREASURY", treasury, false); + addressProvider.setAddress("GEAR_TOKEN", gear, false); + addressProvider.setAddress("WETH_TOKEN", weth, false); + + contractsRegister = new ContractsRegister(address(addressProvider)); + addressProvider.setAddress("CONTRACTS_REGISTER", address(contractsRegister), false); + + accountFactory = new AccountFactoryV3(address(addressProvider)); + addressProvider.setAddress("ACCOUNT_FACTORY", address(accountFactory), false); + + botList = new BotListV3(address(addressProvider)); + addressProvider.setAddress("BOT_LIST", address(botList), true); + + gearStaking = new GearStakingV3(address(addressProvider), block.timestamp); + addressProvider.setAddress("GEAR_STAKING", address(gearStaking), true); + + priceOracle = new PriceOracleV3(address(addressProvider)); + addressProvider.setAddress("PRICE_ORACLE", address(priceOracle), true); + priceOracle.setController(controller); + vm.stopPrank(); + } + + // ------ // + // TOKENS // + // ------ // + + struct TokenConfig { + string name; + string symbol; + uint8 decimals; + int256 price; + uint32 stalenessPeriod; + bool trusted; + } + + function _defaultTokenConfigs() internal pure returns (TokenConfig[] memory configs) { + configs = new TokenConfig[](5); + configs[0] = TokenConfig("Wrapped Ether", "WETH", 18, 2_500e8, 1 days, true); + configs[1] = TokenConfig("Wrapped Bitcoin", "WBTC", 8, 50_000e8, 1 days, true); + configs[2] = TokenConfig("USD Coin", "USDC", 6, 1e8, 1 days, true); + configs[3] = TokenConfig("Dai Stablecoin", "DAI", 18, 1e8, 1 days, true); + configs[4] = TokenConfig("ChainLink Token", "LINK", 18, 15e8, 1 days, true); + } + + function _deployTokensAndPriceFeeds() internal { + _deployTokensAndPriceFeeds(_defaultTokenConfigs()); + } + + function _deployTokensAndPriceFeeds(TokenConfig[] memory configs) internal { + require(address(priceOracle) != address(0), "Deployer: Core not deployed yet"); + + vm.startPrank(configurator); + for (uint256 i; i < configs.length; ++i) { + TokenConfig memory config = configs[i]; + _addToken( + config.symbol, + Strings.equal(config.symbol, "WETH") + ? ERC20(weth) + : new ERC20Mock(config.name, config.symbol, config.decimals) + ); + address priceFeed = address(new PriceFeedMock(config.price, 8)); + priceOracle.setPriceFeed(address(tokens[config.symbol]), priceFeed, config.stalenessPeriod, config.trusted); + } + vm.stopPrank(); + } + + function _addToken(string memory symbol, ERC20 token) internal { + require(address(tokens[symbol]) == address(0), string.concat("Deployer: Token ", symbol, " already deployed")); + tokens[symbol] = token; + tokensList.push(address(token)); + } + + // ----- // + // POOLS // + // ----- // + + struct PoolConfig { + string underlyingSymbol; + string name; + string symbol; + uint256 totalDebtLimit; + uint256 deadShares; + IRMParams irmParams; + QuotaConfig[] quotas; + } + + struct IRMParams { + uint16 U_1; + uint16 U_2; + uint16 R_base; + uint16 R_slope1; + uint16 R_slope2; + uint16 R_slope3; + bool isBorrowingMoreU2Forbidden; + } + + struct QuotaConfig { + string symbol; + uint96 limit; + uint16 minRate; + uint16 maxRate; + uint16 increaseFee; + } + + function _defaultPoolConfig(string memory underlying) internal pure returns (PoolConfig memory) { + require( + Strings.equal(underlying, "DAI") || Strings.equal(underlying, "USDC"), + string.concat("Deployer: No default pool configuration for ", underlying, " underlying") + ); + + bool isDai = Strings.equal(underlying, "DAI"); + + QuotaConfig[] memory quotas = new QuotaConfig[](3); + quotas[0] = QuotaConfig("WETH", isDai ? 25_000_000e18 : 25_000_000e6, 1, 500, 0); + quotas[1] = QuotaConfig("WBTC", isDai ? 25_000_000e18 : 25_000_000e6, 1, 500, 0); + quotas[2] = QuotaConfig("LINK", isDai ? 5_000_000e18 : 5_000_000e6, 100, 1000, 10); + + return PoolConfig({ + underlyingSymbol: underlying, + name: string.concat("Diesel ", underlying, " v3"), + symbol: string.concat("d", underlying, "v3"), + totalDebtLimit: isDai ? 50_000_000e18 : 50_000_000e6, + deadShares: 1e5, + irmParams: _defaultIRMParams(), + quotas: quotas + }); + } + + function _defaultIRMParams() internal pure returns (IRMParams memory) { + return IRMParams({ + U_1: 80_00, + U_2: 90_00, + R_base: 0, + R_slope1: 5, + R_slope2: 20, + R_slope3: 100_00, + isBorrowingMoreU2Forbidden: true + }); + } + + function _deployPool(string memory underlying) internal returns (PoolV3 pool) { + pool = _deployPool(_defaultPoolConfig(underlying)); + } + + function _deployPool(PoolConfig memory config) internal returns (PoolV3 pool) { + ERC20 underlying = _getToken(config.underlyingSymbol); + require( + address(pools[config.name]) == address(0), + string.concat("Deployer: Pool ", config.name, " already deployed") + ); + + vm.startPrank(configurator); + LinearInterestRateModelV3 irm = new LinearInterestRateModelV3( + config.irmParams.U_1, + config.irmParams.U_2, + config.irmParams.R_base, + config.irmParams.R_slope1, + config.irmParams.R_slope2, + config.irmParams.R_slope3, + config.irmParams.isBorrowingMoreU2Forbidden + ); + + pool = new PoolV3( + address(addressProvider), + address(underlying), + address(irm), + config.totalDebtLimit, + config.name, + config.symbol + ); + deal(address(underlying), configurator, config.deadShares); + underlying.approve(address(pool), config.deadShares); + pool.deposit(config.deadShares, address(0xdead)); + pool.setController(controller); + + GaugeV3 gauge = new GaugeV3(address(pool), address(gearStaking)); + gauge.setController(controller); + gearStaking.setVotingContractStatus(address(gauge), VotingContractStatus.ALLOWED); + + PoolQuotaKeeperV3 quotaKeeper = new PoolQuotaKeeperV3(address(pool)); + quotaKeeper.setController(controller); + quotaKeeper.setGauge(address(gauge)); + pool.setPoolQuotaKeeper(address(quotaKeeper)); + + pools[config.name] = pool; + contractsRegister.addPool(address(pool)); + _addToken(config.symbol, pool); + + for (uint256 i; i < config.quotas.length; ++i) { + QuotaConfig memory quota = config.quotas[i]; + address token = address(_getToken(quota.symbol)); + gauge.addQuotaToken(token, quota.minRate, quota.maxRate); + quotaKeeper.setTokenLimit(token, quota.limit); + quotaKeeper.setTokenQuotaIncreaseFee(token, quota.increaseFee); + } + gauge.setFrozenEpoch(false); + vm.stopPrank(); + } + + // --------------- // + // CREDIT MANAGERS // + // --------------- // + + struct CreditManagerConfig { + string poolName; + string name; + uint256 debtLimit; + uint128 minDebt; + uint128 maxDebt; + bool expirable; + CollateralConfig[] collaterals; + } + + struct CollateralConfig { + string symbol; + uint16 liquidationThreshold; + } + + function _defaultCreditManagerConfig(string memory underlying) internal pure returns (CreditManagerConfig memory) { + require( + Strings.equal(underlying, "DAI") || Strings.equal(underlying, "USDC"), + string.concat("Deployer: No default credit manager configuration for ", underlying, " underlying") + ); + + bool isDai = Strings.equal(underlying, "DAI"); + + CollateralConfig[] memory collaterals = new CollateralConfig[](4); + collaterals[0] = CollateralConfig(isDai ? "USDC" : "DAI", 90_00); + collaterals[1] = CollateralConfig("WETH", 80_00); + collaterals[2] = CollateralConfig("WBTC", 80_00); + collaterals[3] = CollateralConfig("LINK", 75_00); + + return CreditManagerConfig({ + poolName: string.concat("Diesel ", underlying, " v3"), + name: string.concat(underlying, " v3"), + debtLimit: isDai ? 25_000_000e18 : 25_000_000e6, + minDebt: isDai ? 10_000e18 : 10_000e6, + maxDebt: isDai ? 200_000e18 : 200_000e6, + expirable: false, + collaterals: collaterals + }); + } + + function _deployCreditManager(string memory underlying) internal returns (CreditManagerV3 creditManager) { + creditManager = _deployCreditManager(_defaultCreditManagerConfig(underlying)); + } + + function _deployCreditManager(CreditManagerConfig memory config) internal returns (CreditManagerV3 creditManager) { + PoolV3 pool = _getPool(config.poolName); + require( + address(creditManagers[config.name]) == address(0), + string.concat("Deployer: Credit manager ", config.name, " already deployed") + ); + + vm.startPrank(configurator); + CreditManagerFactory cmf = new CreditManagerFactory( + address(addressProvider), + address(pool), + CreditManagerOpts({ + minDebt: config.minDebt, + maxDebt: config.maxDebt, + degenNFT: address(0), + expirable: config.expirable, + name: config.name + }), + 0 + ); + + CreditConfiguratorV3 creditConfigurator = cmf.creditConfigurator(); + creditConfigurator.setController(controller); + + creditManager = cmf.creditManager(); + creditManagers[config.name] = creditManager; + contractsRegister.addCreditManager(address(creditManager)); + PoolV3(pool).setCreditManagerDebtLimit(address(creditManager), config.debtLimit); + accountFactory.addCreditManager(address(creditManager)); + botList.setCreditManagerApprovedStatus(address(creditManager), true); + PoolQuotaKeeperV3(PoolV3(pool).poolQuotaKeeper()).addCreditManager(address(creditManager)); + + for (uint256 i; i < config.collaterals.length; ++i) { + CollateralConfig memory collateral = config.collaterals[i]; + address token = address(_getToken(collateral.symbol)); + creditConfigurator.addCollateralToken(token, collateral.liquidationThreshold); + } + vm.stopPrank(); + } +} diff --git a/contracts/test/invariant/Invariants.sol b/contracts/test/invariant/Invariants.sol new file mode 100644 index 00000000..f805c5db --- /dev/null +++ b/contracts/test/invariant/Invariants.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {Test} from "forge-std/Test.sol"; + +import {CreditManagerV3} from "../../credit/CreditManagerV3.sol"; +import {GaugeV3} from "../../governance/GaugeV3.sol"; +import {GearStakingV3} from "../../governance/GearStakingV3.sol"; +import {CollateralCalcTask, CollateralDebtData} from "../../interfaces/ICreditManagerV3.sol"; +import {PoolV3} from "../../pool/PoolV3.sol"; + +import {CreditHandler} from "./handlers/CreditHandler.sol"; +import {PoolHandler} from "./handlers/PoolHandler.sol"; +import {VotingHandler} from "./handlers/VotingHandler.sol"; + +contract Invariants is Test { + // ------ // + // VOTING // + // ------ // + + /// @dev INV:[V-1]: All users' total stake is equal to staking contract's GEAR balance, + /// assuming no direct transfers + function _assert_voting_invariant_01(VotingHandler votingHandler) internal { + GearStakingV3 gearStaking = votingHandler.gearStaking(); + address[] memory stakers = votingHandler.getStakers(); + uint256 totalStaked; + for (uint256 i; i < stakers.length; ++i) { + totalStaked += gearStaking.balanceOf(stakers[i]); + } + assertEq( + totalStaked, + votingHandler.gear().balanceOf(address(gearStaking)), + "INV:[V-1]: Total stake does not equal GEAR balance" + ); + } + + /// @dev INV:[V-2]: Each user's total stake is consistently split between available stake, + /// withdrawable amounts and casted votes + function _assert_voting_invariant_02(VotingHandler votingHandler) internal { + GearStakingV3 gearStaking = votingHandler.gearStaking(); + address[] memory stakers = votingHandler.getStakers(); + for (uint256 i; i < stakers.length; ++i) { + uint256 available = gearStaking.availableBalance(stakers[i]); + + (uint256 withdrawable, uint256[4] memory scheduled) = gearStaking.getWithdrawableAmounts(stakers[i]); + withdrawable += scheduled[0] + scheduled[1] + scheduled[2] + scheduled[3]; + + uint256 casted = votingHandler.getVotesCastedBy(stakers[i]); + + assertEq( + gearStaking.balanceOf(stakers[i]), + available + withdrawable + casted, + "INV:[V-2]: Inconsistent user's total stake" + ); + } + } + + /// @dev INV:[V-3]: Votes casted for each gauge are consistently split between all tokens + function _assert_voting_invariant_03(VotingHandler votingHandler) internal { + address[] memory gauges = votingHandler.getVotingContracts(); + for (uint256 i; i < gauges.length; ++i) { + uint256 totalVotes; + address[] memory tokens = votingHandler.getGaugeTokens(gauges[i]); + for (uint256 j; j < tokens.length; ++j) { + (,, uint256 totalVotesLpSide, uint256 totalVotesCaSide) = GaugeV3(gauges[i]).quotaRateParams(tokens[j]); + totalVotes += totalVotesCaSide + totalVotesLpSide; + } + assertEq(totalVotes, votingHandler.getVotesCastedFor(gauges[i]), "INV:[V-3]: Inconsistent votes for gauge"); + } + } + + /// @dev INV:[V-4]: Total and per-user votes for all tokens are consistent for each gauge + function _assert_voting_invariant_04(VotingHandler votingHandler) internal { + address[] memory gauges = votingHandler.getVotingContracts(); + for (uint256 i; i < gauges.length; ++i) { + address[] memory tokens = votingHandler.getGaugeTokens(gauges[i]); + for (uint256 j; j < tokens.length; ++j) { + uint256 totalVotesLP; + uint256 totalVotesCA; + address[] memory stakers = votingHandler.getStakers(); + for (uint256 k; k < stakers.length; ++k) { + (uint256 votesLP, uint256 votesCA) = GaugeV3(gauges[i]).userTokenVotes(stakers[k], tokens[j]); + totalVotesLP += votesLP; + totalVotesCA += votesCA; + } + (,, uint96 totalVotesLpSide, uint96 totalVotesCaSide) = GaugeV3(gauges[i]).quotaRateParams(tokens[j]); + assertEq(totalVotesLpSide, totalVotesLP, "INV:[V-4]: Inconsistent LP side votes for token"); + assertEq(totalVotesCaSide, totalVotesCA, "INV:[V-4]: Inconsistent CA side votes for token"); + } + } + } + + // ---- // + // POOL // + // ---- // + + /// @dev INV:[P-1]: Total and per-manager debt limits are respected, assuming no configuration changes + function _assert_pool_invariant_01(PoolHandler poolHandler) internal { + PoolV3 pool = poolHandler.pool(); + assertLe(pool.totalBorrowed(), pool.totalDebtLimit(), "INV:[P-1]: Total debt exceeds limit"); + address[] memory creditManagers = pool.creditManagers(); + for (uint256 i; i < creditManagers.length; ++i) { + assertLe( + pool.creditManagerBorrowed(creditManagers[i]), + pool.creditManagerDebtLimit(creditManagers[i]), + "INV:[P-1]: Credit manager debt exceeds limit" + ); + } + } + + /// @dev INV:[P-2]: Total and per-manager debt amounts are consistent + function _assert_pool_invariant_02(PoolHandler poolHandler) internal { + PoolV3 pool = poolHandler.pool(); + uint256 totalBorrowed; + address[] memory creditManagers = pool.creditManagers(); + for (uint256 j; j < creditManagers.length; ++j) { + totalBorrowed += pool.creditManagerBorrowed(creditManagers[j]); + } + assertEq( + pool.totalBorrowed(), + totalBorrowed, + "INV:[P-2]: Total debt is inconsistent with per-manager borrowed amounts" + ); + } + + /// @dev INV:[P-3]: Pool is solvent when there's no outstanding debt + function _assert_pool_invariant_03(PoolHandler poolHandler) internal { + PoolV3 pool = poolHandler.pool(); + uint256 borrowed = pool.totalBorrowed(); + if (borrowed == 0) { + assertGe(pool.availableLiquidity(), pool.expectedLiquidity(), "INV:[P-3]: Pool is insolvent"); + } + } + + /// @dev INV:[P-4]: Pool expects profit when there is outstanding debt + function _assert_pool_invariant_04(PoolHandler poolHandler) internal { + PoolV3 pool = poolHandler.pool(); + uint256 borrowed = pool.totalBorrowed(); + if (borrowed != 0) { + // TODO: might need to add some buffer on the left since managers transfer a bit more than needed + assertGe(pool.expectedLiquidity(), pool.availableLiquidity() + borrowed, "INV:[P-4]: Pool is unprofitable"); + } + } + + /// @dev INV:[P-5]: Exchange rate growth in time is bounded + function _assert_pool_invariant_05(PoolHandler poolHandler) internal { + assertLe( + poolHandler.exchangeRate(), + 1e18 + 1e18 * (block.timestamp - poolHandler.initialTimestamp()) / 365 days, + "INV:[P-5]: Inadequate exchange rate growth" + ); + } + + // ------ // + // CREDIT // + // ------ // + + /// @dev INV:[C-1]: Credit manager's debt in the pool must be consistently split across all credit accounts + function _assert_credit_invariant_01(CreditHandler creditHandler) internal { + CreditManagerV3 creditManager = creditHandler.creditManager(); + uint256 creditManagerDebt; + address[] memory creditAccounts = creditManager.creditAccounts(); + for (uint256 i; i < creditAccounts.length; ++i) { + creditManagerDebt += creditHandler.getDebt(creditAccounts[i]); + } + assertEq( + creditManagerDebt, + PoolV3(creditManager.pool()).creditManagerBorrowed(address(creditManager)), + "INV:[C-1]: Inconsistent debt between pool and credit manager" + ); + } + + /// @dev INV:[C-2]: Credit accounts with zero debt principal have no accrued interest, fees or enabled quoted tokens + function _assert_credit_invariant_02(CreditHandler creditHandler) internal { + CreditManagerV3 creditManager = creditHandler.creditManager(); + address[] memory creditAccounts = creditManager.creditAccounts(); + for (uint256 i; i < creditAccounts.length; ++i) { + CollateralDebtData memory cdd = + creditManager.calcDebtAndCollateral(creditAccounts[i], CollateralCalcTask.DEBT_ONLY); + if (cdd.debt == 0) { + assertEq(cdd.totalDebtUSD, 0, "INV:[C-2]: Non-zero accrued interest or fees"); + assertEq(cdd.enabledTokensMask & cdd.quotedTokensMask, 0, "INV:[C-2]: Enabled quoted tokens"); + } + } + } + + /// @dev INV:[C-3]: Credit accounts with non-zero debt principal have the latter within allowed limits, + /// assuming no configuration changes + function _assert_credit_invariant_03(CreditHandler creditHandler) internal { + CreditManagerV3 creditManager = creditHandler.creditManager(); + address[] memory creditAccounts = creditManager.creditAccounts(); + for (uint256 i; i < creditAccounts.length; ++i) { + uint256 debt = creditHandler.getDebt(creditAccounts[i]); + if (debt != 0) { + assertGe(debt, creditHandler.minDebt(), "INV:[C-3]: Debt principal below limit"); + assertLe(debt, creditHandler.maxDebt(), "INV:[C-3]: Debt principal above limit"); + } + } + } + + /// @dev INV:[C-4]: Credit account has quoted token enabled if and only if the quota is greater than 0 + function _assert_credit_invariant_04(CreditHandler creditHandler) internal { + CreditManagerV3 creditManager = creditHandler.creditManager(); + address[] memory creditAccounts = creditManager.creditAccounts(); + uint256 quotedTokensMask = creditManager.quotedTokensMask(); + while (quotedTokensMask != 0) { + uint256 tokenMask = quotedTokensMask & uint256(-int256(quotedTokensMask)); + address token = creditManager.getTokenByMask(tokenMask); + for (uint256 i; i < creditAccounts.length; ++i) { + uint256 enabledTokensMask = creditManager.enabledTokensMaskOf(creditAccounts[i]); + (uint256 quota,) = creditHandler.poolQuotaKeeper().getQuota(creditAccounts[i], token); + if (quota == 0) { + assertEq(enabledTokensMask & tokenMask, 0, "INV:[C-4]: Enabled quoted token with zero quota"); + } else { + assertGt(enabledTokensMask & tokenMask, 0, "INV:[C-4]: Disabled quoted token with non-zero quota"); + } + } + quotedTokensMask ^= tokenMask; + } + } + + /// @dev INV:[C-5]: Number of enabled tokens on the credit account doesn't exceed the maximum allowed, + /// assuming no configuration changes + function _assert_credit_invariant_05(CreditHandler creditHandler) internal { + CreditManagerV3 creditManager = creditHandler.creditManager(); + address[] memory creditAccounts = creditManager.creditAccounts(); + for (uint256 i; i < creditAccounts.length; ++i) { + uint256 numEnabledTokens; + // NOTE: exclude underlying token + uint256 enabledTokensMask = creditManager.enabledTokensMaskOf(creditAccounts[i]) & ~uint256(1); + while (enabledTokensMask > 0) { + enabledTokensMask &= enabledTokensMask - 1; + ++numEnabledTokens; + } + assertLe(numEnabledTokens, creditManager.maxEnabledTokens(), "INV:[C-5]: More enabled tokens than allowed"); + } + } + + // ------ // + // GLOBAL // + // ------ // + + /// @dev INV:[G-1]: Interest accrued by all credit accounts approximately equals pool's expected revenue + function _assert_global_invariant_01(PoolHandler poolHandler, CreditHandler creditHandler) internal { + PoolV3 pool = poolHandler.pool(); + CreditManagerV3 creditManager = creditHandler.creditManager(); + require(creditManager.pool() == address(pool), "Invariants: Credit manager connected to wrong pool"); + + uint256 accruedInterest; + address[] memory creditAccounts = creditManager.creditAccounts(); + for (uint256 j; j < creditAccounts.length; ++j) { + CollateralDebtData memory cdd = + creditManager.calcDebtAndCollateral(creditAccounts[j], CollateralCalcTask.DEBT_ONLY); + accruedInterest += cdd.accruedInterest; + } + + assertApproxEqRel( + pool.expectedLiquidity(), + pool.availableLiquidity() + pool.totalBorrowed() + accruedInterest, + 1e14, // 0.01% + "INV:[G-1]: Accrued interest is inconsistent with expected revenue" + ); + } +} diff --git a/contracts/test/invariant/handlers/CreditHandler.sol b/contracts/test/invariant/handlers/CreditHandler.sol new file mode 100644 index 00000000..a9a8a7d0 --- /dev/null +++ b/contracts/test/invariant/handlers/CreditHandler.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {MultiCall} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol"; +import {CreditConfiguratorV3} from "../../../credit/CreditConfiguratorV3.sol"; +import {CreditFacadeV3} from "../../../credit/CreditFacadeV3.sol"; +import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; +import {ICreditFacadeV3Multicall} from "../../../interfaces/ICreditFacadeV3Multicall.sol"; +import {PoolQuotaKeeperV3} from "../../../pool/PoolQuotaKeeperV3.sol"; +import {HandlerBase} from "./HandlerBase.sol"; + +contract CreditHandler is HandlerBase { + CreditManagerV3 public creditManager; + CreditFacadeV3 public creditFacade; + CreditConfiguratorV3 public creditConfigurator; + ERC20 public underlying; + + address _creditAccount; + address _owner; + + modifier withCreditAccount(uint256 idx) { + _creditAccount = _get(creditManager.creditAccounts(), idx); + _owner = creditManager.getBorrowerOrRevert(_creditAccount); + vm.startPrank(_owner); + _; + vm.stopPrank(); + } + + constructor(CreditManagerV3 creditManager_, uint256 maxTimeDelta) HandlerBase(maxTimeDelta) { + creditManager = creditManager_; + creditFacade = CreditFacadeV3(creditManager_.creditFacade()); + creditConfigurator = CreditConfiguratorV3(creditManager_.creditConfigurator()); + underlying = ERC20(creditManager_.underlying()); + } + + // ------- // + // GETTERS // + // ------- // + + function minDebt() public view returns (uint256 min) { + (min,) = creditFacade.debtLimits(); + } + + function maxDebt() public view returns (uint256 max) { + (, max) = creditFacade.debtLimits(); + } + + function getDebt(address creditAccount) public view returns (uint256 debt) { + (debt,,,,,,,) = creditManager.creditAccountInfo(creditAccount); + } + + function poolQuotaKeeper() public view returns (PoolQuotaKeeperV3) { + return PoolQuotaKeeperV3(creditManager.poolQuotaKeeper()); + } + + // -------------- // + // FUZZ FUNCTIONS // + // -------------- // + + function addCollateral(Ctx memory ctx, uint256 creditAccountIdx, uint256 tokenIdx, uint256 amount) + external + applyContext(ctx) + withCreditAccount(creditAccountIdx) + { + ERC20 token = _getToken(tokenIdx); + amount = bound(amount, 0, token.balanceOf(_owner)); + if (amount == 0) return; + + token.approve(address(creditManager), amount); + _multicall( + address(creditFacade), abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (address(token), amount)) + ); + } + + function withdrawCollateral(Ctx memory ctx, uint256 creditAccountIdx, uint256 tokenIdx, uint256 amount) + external + applyContext(ctx) + withCreditAccount(creditAccountIdx) + { + ERC20 token = _getToken(tokenIdx); + amount = bound(amount, 0, token.balanceOf(_creditAccount)); + if (amount == 0) return; + + _multicall( + address(creditFacade), + abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (address(token), amount, _owner)) + ); + } + + function increaseDebt(Ctx memory ctx, uint256 creditAccountIdx, uint256 amount) + external + applyContext(ctx) + withCreditAccount(creditAccountIdx) + { + amount = bound(amount, 0, maxDebt() - getDebt(_creditAccount)); + if (amount == 0) return; + + vm.roll(block.number + 1); + _multicall(address(creditFacade), abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (amount))); + } + + function decreaseDebt(Ctx memory ctx, uint256 creditAccountIdx, uint256 amount) + external + applyContext(ctx) + withCreditAccount(creditAccountIdx) + { + amount = bound(amount, 0, underlying.balanceOf(_creditAccount)); + + vm.roll(block.number + 1); + _multicall(address(creditFacade), abi.encodeCall(ICreditFacadeV3Multicall.decreaseDebt, (amount))); + } + + function borrowAndWithdraw(Ctx memory ctx, uint256 creditAccountIdx, uint256 amount) + external + applyContext(ctx) + withCreditAccount(creditAccountIdx) + { + amount = bound(amount, 0, maxDebt() - getDebt(_creditAccount)); + if (amount == 0) return; + + vm.roll(block.number + 1); + _multicall( + address(creditFacade), + abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (amount)), + address(creditFacade), + abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (address(underlying), amount, _owner)) + ); + } + + function addAndRepay(Ctx memory ctx, uint256 creditAccountIdx, uint256 amount) + external + applyContext(ctx) + withCreditAccount(creditAccountIdx) + { + amount = bound(amount, 0, underlying.balanceOf(_owner)); + if (amount == 0) return; + + vm.roll(block.number + 1); + underlying.approve(address(creditManager), amount); + _multicall( + address(creditFacade), + abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (address(underlying), amount)), + address(creditFacade), + abi.encodeCall(ICreditFacadeV3Multicall.decreaseDebt, (amount)) + ); + } + + function increaseQuota(Ctx memory ctx, uint256 creditAccountIdx, uint256 tokenIdx, uint256 amount) + external + applyContext(ctx) + withCreditAccount(creditAccountIdx) + { + ERC20 token = _getToken(tokenIdx); + if (creditManager.quotedTokensMask() & creditManager.getTokenMaskOrRevert(address(token)) == 0) return; + (uint256 quota,) = poolQuotaKeeper().getQuotaAndOutstandingInterest(_creditAccount, address(token)); + amount = bound(amount, 0, 2 * maxDebt() - quota); + if (amount == 0) return; + + _multicall( + address(creditFacade), + abi.encodeCall(ICreditFacadeV3Multicall.updateQuota, (address(token), int96(uint96(amount)), 0)) + ); + } + + function decreaseQuota(Ctx memory ctx, uint256 creditAccountIdx, uint256 tokenIdx, uint256 amount) + external + applyContext(ctx) + withCreditAccount(creditAccountIdx) + { + ERC20 token = _getToken(tokenIdx); + if (creditManager.quotedTokensMask() & creditManager.getTokenMaskOrRevert(address(token)) == 0) return; + (uint256 quota,) = poolQuotaKeeper().getQuotaAndOutstandingInterest(_creditAccount, address(token)); + amount = bound(amount, 0, quota); + if (amount == 0) return; + + _multicall( + address(creditFacade), + abi.encodeCall(ICreditFacadeV3Multicall.updateQuota, (address(token), -int96(uint96(amount)), 0)) + ); + } + + function swapCollateral( + Ctx memory ctx, + uint256 creditAccountIdx, + uint256 token1Idx, + uint256 amount1, + uint256 token2Idx, + uint256 amount2 + ) external applyContext(ctx) withCreditAccount(creditAccountIdx) { + ERC20 token1 = _getToken(token1Idx); + ERC20 token2 = _getToken(token2Idx); + if (address(token1) == address(token2)) return; + amount1 = bound(amount1, 0, token1.balanceOf(_creditAccount)); + amount2 = bound(amount2, 0, token2.balanceOf(_owner)); + if (amount1 == 0 || amount2 == 0) return; + + token2.approve(address(creditManager), amount2); + _multicall( + address(creditFacade), + abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (address(token1), amount1, _owner)), + address(creditFacade), + abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (address(token2), amount2)) + ); + } + + // --------- // + // INTERNALS // + // --------- // + + function _getToken(uint256 idx) internal view returns (ERC20) { + idx = bound(idx, 0, creditManager.collateralTokensCount() - 1); + return ERC20(creditManager.getTokenByMask(1 << idx)); + } + + function _multicall(address target0, bytes memory data0) internal { + MultiCall[] memory calls = new MultiCall[](1); + calls[0] = MultiCall(target0, data0); + creditFacade.multicall(_creditAccount, calls); + } + + function _multicall(address target0, bytes memory data0, address target1, bytes memory data1) internal { + MultiCall[] memory calls = new MultiCall[](2); + calls[0] = MultiCall(target0, data0); + calls[1] = MultiCall(target1, data1); + creditFacade.multicall(_creditAccount, calls); + } + + function _multicall( + address target0, + bytes memory data0, + address target1, + bytes memory data1, + address target2, + bytes memory data2 + ) internal { + MultiCall[] memory calls = new MultiCall[](3); + calls[0] = MultiCall(target0, data0); + calls[1] = MultiCall(target1, data1); + calls[2] = MultiCall(target2, data2); + creditFacade.multicall(_creditAccount, calls); + } + + function _multicall( + address target0, + bytes memory data0, + address target1, + bytes memory data1, + address target2, + bytes memory data2, + address target3, + bytes memory data3 + ) internal { + MultiCall[] memory calls = new MultiCall[](4); + calls[0] = MultiCall(target0, data0); + calls[1] = MultiCall(target1, data1); + calls[2] = MultiCall(target2, data2); + calls[3] = MultiCall(target3, data3); + creditFacade.multicall(_creditAccount, calls); + } +} diff --git a/contracts/test/invariant/handlers/HandlerBase.sol b/contracts/test/invariant/handlers/HandlerBase.sol new file mode 100644 index 00000000..83028fbf --- /dev/null +++ b/contracts/test/invariant/handlers/HandlerBase.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {Test} from "forge-std/Test.sol"; + +abstract contract HandlerBase is Test { + struct Ctx { + bool skipTime; + uint256 timeDelta; + } + + uint256 public immutable initialTimestamp; + uint256 _maxTimeDelta; + + modifier applyContext(Ctx memory ctx) { + if (ctx.skipTime) { + ctx.timeDelta = bound(ctx.timeDelta, 0, _maxTimeDelta); + vm.warp(block.timestamp + ctx.timeDelta); + } + _; + } + + constructor(uint256 maxTimeDelta) { + initialTimestamp = block.timestamp; + _maxTimeDelta = maxTimeDelta; + } + + function _get(address[] memory array, uint256 index) internal view returns (address) { + uint256 num = array.length; + require(num != 0, "HandlerBase: Empty array"); + index = bound(index, 0, num - 1); + return array[index]; + } +} diff --git a/contracts/test/invariant/handlers/PoolHandler.sol b/contracts/test/invariant/handlers/PoolHandler.sol new file mode 100644 index 00000000..5087ebc2 --- /dev/null +++ b/contracts/test/invariant/handlers/PoolHandler.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {PoolV3} from "../../../pool/PoolV3.sol"; +import {HandlerBase} from "./HandlerBase.sol"; + +contract PoolHandler is HandlerBase { + PoolV3 public pool; + ERC20 public underlying; + + address[] _depositors; + address _depositor; + address _manager; + + modifier withDepositor(uint256 idx) { + _depositor = _get(_depositors, idx); + vm.startPrank(_depositor); + _; + vm.stopPrank(); + } + + modifier withManager(uint256 idx) { + _manager = _get(pool.creditManagers(), idx); + vm.startPrank(_manager); + _; + vm.stopPrank(); + } + + constructor(PoolV3 pool_, uint256 maxTimeDelta) HandlerBase(maxTimeDelta) { + pool = pool_; + underlying = ERC20(pool_.underlyingToken()); + } + + function addDepositor(address depositor) external { + _depositors.push(depositor); + } + + function getDepositors() external view returns (address[] memory) { + return _depositors; + } + + function exchangeRate() external view returns (uint256) { + uint256 assets = 10 ** underlying.decimals(); + uint256 shares = pool.convertToShares(assets); + return assets * 1e18 / shares; + } + + // -------- // + // ERC-4626 // + // -------- // + + function deposit(Ctx memory ctx, uint256 depositorIdx, uint256 assets, uint256 receiverIdx) + external + applyContext(ctx) + withDepositor(depositorIdx) + { + assets = bound(assets, 0, underlying.balanceOf(_depositor)); + deal(address(underlying), _depositor, assets); + + underlying.approve(address(pool), assets); + pool.deposit(assets, _get(_depositors, receiverIdx)); + } + + function mint(Ctx memory ctx, uint256 depositorIdx, uint256 shares, uint256 receiverIdx) + external + applyContext(ctx) + withDepositor(depositorIdx) + { + shares = bound(shares, 0, pool.previewDeposit(underlying.balanceOf(_depositor))); + uint256 assets = pool.previewMint(shares); + deal(address(underlying), _depositor, assets); + + underlying.approve(address(pool), assets); + assets = pool.mint(shares, _get(_depositors, receiverIdx)); + } + + function withdraw(Ctx memory ctx, uint256 depositorIdx, uint256 assets, uint256 receiverIdx) + external + applyContext(ctx) + withDepositor(depositorIdx) + { + assets = bound(assets, 0, pool.maxWithdraw(_depositor)); + + pool.withdraw(assets, _get(_depositors, receiverIdx), _depositor); + } + + function redeem(Ctx memory ctx, uint256 depositorIdx, uint256 shares, uint256 receiverIdx) + external + applyContext(ctx) + withDepositor(depositorIdx) + { + shares = bound(shares, 0, pool.maxRedeem(_depositor)); + + pool.redeem(shares, _get(_depositors, receiverIdx), _depositor); + } + + // ----------- // + // MOCK CREDIT // + // ----------- // + + function borrow(Ctx memory ctx, uint256 managerIdx, uint256 amount) + external + applyContext(ctx) + withManager(managerIdx) + { + uint256 borrowable = pool.creditManagerBorrowable(_manager); + if (borrowable == 0) return; + amount = bound(amount, 1, borrowable); + + pool.lendCreditAccount(amount, _manager); + } + + function repayWithProfit(Ctx memory ctx, uint256 managerIdx, uint256 amount, uint256 profit) + external + applyContext(ctx) + withManager(managerIdx) + { + uint256 borrowed = pool.creditManagerBorrowed(_manager); + if (borrowed == 0) return; + amount = bound(amount, 0, borrowed); + + // NOTE: profit is bounded to credit manager's "free" balance + profit = bound(profit, 0, underlying.balanceOf(_manager) - borrowed); + + underlying.transfer(address(pool), amount + profit); + pool.repayCreditAccount(amount, profit, 0); + } + + function repayWithLoss(Ctx memory ctx, uint256 managerIdx, uint256 loss) + external + applyContext(ctx) + withManager(managerIdx) + { + uint256 amount = pool.creditManagerBorrowed(_manager); + if (amount == 0) return; + + // NOTE: with no base or quota interest, loss is bounded to the borrowed amount + loss = bound(loss, 0, amount); + + underlying.transfer(address(pool), amount - loss); + pool.repayCreditAccount(amount, 0, loss); + } +} diff --git a/contracts/test/invariant/handlers/VotingHandler.sol b/contracts/test/invariant/handlers/VotingHandler.sol new file mode 100644 index 00000000..2bf41c7a --- /dev/null +++ b/contracts/test/invariant/handlers/VotingHandler.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {GearStakingV3, MultiVote} from "../../../governance/GearStakingV3.sol"; +import {IGaugeV3, UserVotes} from "../../../interfaces/IGaugeV3.sol"; +import {VotingContractMock} from "../../mocks/governance/VotingContractMock.sol"; +import {HandlerBase} from "./HandlerBase.sol"; + +contract VotingHandler is HandlerBase { + GearStakingV3 public gearStaking; + ERC20 public gear; + + address[] _stakers; + address _staker; + + address[] _votingContracts; + mapping(address => address[]) _gaugeTokens; + + mapping(address => mapping(address => uint256)) _votesCasted; + + modifier withStaker(uint256 idx) { + _staker = _get(_stakers, idx); + vm.startPrank(_staker); + _; + vm.stopPrank(); + } + + constructor(GearStakingV3 gearStaking_, uint256 maxTimeDelta) HandlerBase(maxTimeDelta) { + gearStaking = gearStaking_; + gear = ERC20(gearStaking_.gear()); + } + + function addStaker(address staker) external { + _stakers.push(staker); + } + + function getStakers() external view returns (address[] memory) { + return _stakers; + } + + function addVotingContract(address votingContract) external { + _votingContracts.push(votingContract); + } + + function getVotingContracts() external view returns (address[] memory) { + return _votingContracts; + } + + function setGaugeTokens(address gauge, address[] memory tokens) external { + _gaugeTokens[gauge] = tokens; + } + + function getGaugeTokens(address gauge) external view returns (address[] memory) { + return _gaugeTokens[gauge]; + } + + function getVotesCastedBy(address staker) external view returns (uint256 votesCasted) { + for (uint256 i; i < _votingContracts.length; ++i) { + votesCasted += _votesCasted[staker][_votingContracts[i]]; + } + } + + function getVotesCastedFor(address votingContract) external view returns (uint256 votesCasted) { + for (uint256 i; i < _stakers.length; ++i) { + votesCasted += _votesCasted[_stakers[i]][votingContract]; + } + } + + // ------- // + // STAKING // + // ------- // + + function deposit(Ctx memory ctx, uint256 stakerIdx, uint96 amount) + external + applyContext(ctx) + withStaker(stakerIdx) + { + amount = uint96(bound(amount, 0, gear.balanceOf(_staker))); + gear.approve(address(gearStaking), amount); + gearStaking.deposit(amount, new MultiVote[](0)); + } + + function withdraw(Ctx memory ctx, uint256 stakerIdx, uint96 amount, uint256 toIdx) + external + applyContext(ctx) + withStaker(stakerIdx) + { + amount = uint96(bound(amount, 0, gearStaking.availableBalance(_staker))); + gearStaking.withdraw(amount, _get(_stakers, toIdx), new MultiVote[](0)); + } + + function claimWithdrawals(Ctx memory ctx, uint256 stakerIdx, uint256 toIdx) + external + applyContext(ctx) + withStaker(stakerIdx) + { + gearStaking.claimWithdrawals(_get(_stakers, toIdx)); + } + + // ----------- // + // MOCK VOTING // + // ----------- // + + function vote(Ctx memory ctx, uint256 stakerIdx, uint96 amount, uint256 votingContractIdx) + external + applyContext(ctx) + withStaker(stakerIdx) + { + address votingContract = _get(_votingContracts, votingContractIdx); + amount = uint96(bound(amount, 0, gearStaking.availableBalance(_staker))); + + MultiVote[] memory votes = new MultiVote[](1); + votes[0] = MultiVote(votingContract, amount, true, ""); + gearStaking.multivote(votes); + + _votesCasted[_staker][votingContract] += amount; + } + + function unvote(Ctx memory ctx, uint256 stakerIdx, uint96 amount, uint256 votingContractIdx) + external + applyContext(ctx) + withStaker(stakerIdx) + { + address votingContract = _get(_votingContracts, votingContractIdx); + amount = uint96(bound(amount, 0, _votesCasted[_staker][votingContract])); + + MultiVote[] memory votes = new MultiVote[](1); + votes[0] = MultiVote(votingContract, amount, false, ""); + gearStaking.multivote(votes); + + _votesCasted[_staker][votingContract] -= amount; + } + + // ------------ // + // GAUGE VOTING // + // ------------ // + + function voteGauge( + Ctx memory ctx, + uint256 stakerIdx, + uint96 amount, + uint256 gaugeIdx, + uint256 tokenIdx, + bool lpSide + ) external applyContext(ctx) withStaker(stakerIdx) { + address gauge = _get(_votingContracts, gaugeIdx); + address token = _get(_gaugeTokens[gauge], tokenIdx); + amount = uint96(bound(amount, 0, gearStaking.availableBalance(_staker))); + + MultiVote[] memory votes = new MultiVote[](1); + votes[0] = MultiVote(gauge, amount, true, abi.encode(token, lpSide)); + gearStaking.multivote(votes); + + _votesCasted[_staker][gauge] += amount; + } + + function unvoteGauge( + Ctx memory ctx, + uint256 stakerIdx, + uint96 amount, + uint256 gaugeIdx, + uint256 tokenIdx, + bool lpSide + ) external applyContext(ctx) withStaker(stakerIdx) { + address gauge = _get(_votingContracts, gaugeIdx); + address token = _get(_gaugeTokens[gauge], tokenIdx); + (uint96 votesLP, uint96 votesCA) = IGaugeV3(gauge).userTokenVotes(_staker, token); + amount = uint96(bound(amount, 0, lpSide ? votesLP : votesCA)); + + MultiVote[] memory votes = new MultiVote[](1); + votes[0] = MultiVote(gauge, amount, false, abi.encode(token, lpSide)); + gearStaking.multivote(votes); + + _votesCasted[_staker][gauge] -= amount; + } +} diff --git a/contracts/test/invariant/tests/GaugeVoting.inv.t.sol b/contracts/test/invariant/tests/GaugeVoting.inv.t.sol new file mode 100644 index 00000000..81352ae7 --- /dev/null +++ b/contracts/test/invariant/tests/GaugeVoting.inv.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {VotingHandler} from "../handlers/VotingHandler.sol"; +import {InvariantTestBase} from "./InvariantTestBase.sol"; + +contract GaugeVotingInvariantTest is InvariantTestBase { + VotingHandler votingHandler; + + function setUp() public override { + _deployCore(); + _deployTokensAndPriceFeeds(); + _deployPool("DAI"); + _deployPool("USDC"); + + votingHandler = new VotingHandler(gearStaking, 30 days); + address[] memory stakers = _generateAddrs("Staker", 5); + for (uint256 i; i < stakers.length; ++i) { + votingHandler.addStaker(stakers[i]); + deal(gear, stakers[i], 10_000_000e18); + } + + votingHandler.addVotingContract(address(_getGauge("Diesel DAI v3"))); + votingHandler.addVotingContract(address(_getGauge("Diesel USDC v3"))); + votingHandler.setGaugeTokens(address(_getGauge("Diesel DAI v3")), _getQuotedTokens("Diesel DAI v3")); + votingHandler.setGaugeTokens(address(_getGauge("Diesel USDC v3")), _getQuotedTokens("Diesel USDC v3")); + + Selector[] memory selectors = new Selector[](5); + selectors[0] = Selector(votingHandler.deposit.selector, 2); + selectors[1] = Selector(votingHandler.withdraw.selector, 1); + selectors[2] = Selector(votingHandler.claimWithdrawals.selector, 1); + selectors[3] = Selector(votingHandler.voteGauge.selector, 3); + selectors[4] = Selector(votingHandler.unvoteGauge.selector, 3); + _addFuzzingTarget(address(votingHandler), selectors); + } + + function invariant_gauge_voting() public { + _assert_voting_invariant_01(votingHandler); + _assert_voting_invariant_02(votingHandler); + _assert_voting_invariant_03(votingHandler); + _assert_voting_invariant_04(votingHandler); + } +} diff --git a/contracts/test/invariant/tests/Global.inv.t.sol b/contracts/test/invariant/tests/Global.inv.t.sol new file mode 100644 index 00000000..0d0a2b43 --- /dev/null +++ b/contracts/test/invariant/tests/Global.inv.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {MultiCall} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol"; + +import {CreditHandler} from "../handlers/CreditHandler.sol"; +import {PoolHandler} from "../handlers/PoolHandler.sol"; +import {VotingHandler} from "../handlers/VotingHandler.sol"; + +import {InvariantTestBase} from "./InvariantTestBase.sol"; + +contract GlobalInvariantTest is InvariantTestBase { + VotingHandler votingHandler; + PoolHandler poolHandler; + CreditHandler creditHandler; + + function setUp() public override { + _deployCore(); + _deployTokensAndPriceFeeds(); + _deployPool("DAI"); + _deployCreditManager("DAI"); + + votingHandler = new VotingHandler(gearStaking, 30 days); + address[] memory stakers = _generateAddrs("Staker", 5); + for (uint256 i; i < stakers.length; ++i) { + votingHandler.addStaker(stakers[i]); + deal(gear, stakers[i], 10_000_000e18); + } + votingHandler.addVotingContract(address(_getGauge("Diesel DAI v3"))); + votingHandler.setGaugeTokens(address(_getGauge("Diesel DAI v3")), _getQuotedTokens("Diesel DAI v3")); + + poolHandler = new PoolHandler(_getPool("Diesel DAI v3"), 30 days); + address[] memory depositors = _generateAddrs("Depositor", 5); + for (uint256 i; i < depositors.length; ++i) { + poolHandler.addDepositor(depositors[i]); + deal(address(tokens["DAI"]), depositors[i], 10_000_000e18); + } + + creditHandler = new CreditHandler(_getCreditManager("DAI v3"), 30 days); + address[] memory owners = _generateAddrs("Owner", 5); + for (uint256 i; i < owners.length; ++i) { + vm.prank(owners[i]); + creditHandler.creditFacade().openCreditAccount(owners[i], new MultiCall[](0), 0); + deal(address(tokens["DAI"]), owners[i], 2_500_000e18); + deal(address(tokens["WETH"]), owners[i], 1_000e18); + deal(address(tokens["WBTC"]), owners[i], 50e8); + deal(address(tokens["LINK"]), owners[i], 100_000e18); + } + + Selector[] memory votingSelectors = new Selector[](5); + votingSelectors[0] = Selector(votingHandler.deposit.selector, 2); + votingSelectors[1] = Selector(votingHandler.withdraw.selector, 1); + votingSelectors[2] = Selector(votingHandler.claimWithdrawals.selector, 1); + votingSelectors[3] = Selector(votingHandler.voteGauge.selector, 3); + votingSelectors[4] = Selector(votingHandler.unvoteGauge.selector, 3); + _addFuzzingTarget(address(votingHandler), votingSelectors); + + Selector[] memory poolSelectors = new Selector[](4); + poolSelectors[0] = Selector(poolHandler.deposit.selector, 4); + poolSelectors[1] = Selector(poolHandler.mint.selector, 1); + poolSelectors[2] = Selector(poolHandler.withdraw.selector, 1); + poolSelectors[3] = Selector(poolHandler.redeem.selector, 4); + _addFuzzingTarget(address(poolHandler), poolSelectors); + + Selector[] memory creditSelectors = new Selector[](9); + creditSelectors[0] = Selector(creditHandler.addCollateral.selector, 3); + creditSelectors[1] = Selector(creditHandler.withdrawCollateral.selector, 3); + creditSelectors[2] = Selector(creditHandler.increaseDebt.selector, 3); + creditSelectors[3] = Selector(creditHandler.decreaseDebt.selector, 3); + creditSelectors[4] = Selector(creditHandler.addAndRepay.selector, 3); + creditSelectors[5] = Selector(creditHandler.borrowAndWithdraw.selector, 3); + creditSelectors[6] = Selector(creditHandler.increaseQuota.selector, 4); + creditSelectors[7] = Selector(creditHandler.decreaseQuota.selector, 4); + creditSelectors[6] = Selector(creditHandler.swapCollateral.selector, 4); + _addFuzzingTarget(address(creditHandler), creditSelectors); + } + + function invariant_global() public { + _assert_voting_invariant_01(votingHandler); + _assert_voting_invariant_02(votingHandler); + _assert_voting_invariant_03(votingHandler); + _assert_voting_invariant_04(votingHandler); + + _assert_pool_invariant_01(poolHandler); + _assert_pool_invariant_02(poolHandler); + _assert_pool_invariant_03(poolHandler); + _assert_pool_invariant_04(poolHandler); + _assert_pool_invariant_05(poolHandler); + + _assert_credit_invariant_01(creditHandler); + _assert_credit_invariant_02(creditHandler); + _assert_credit_invariant_03(creditHandler); + _assert_credit_invariant_04(creditHandler); + _assert_credit_invariant_05(creditHandler); + + _assert_global_invariant_01(poolHandler, creditHandler); + } +} diff --git a/contracts/test/invariant/tests/InvariantTestBase.sol b/contracts/test/invariant/tests/InvariantTestBase.sol new file mode 100644 index 00000000..e379cd43 --- /dev/null +++ b/contracts/test/invariant/tests/InvariantTestBase.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {Deployer} from "../Deployer.sol"; +import {Invariants} from "../Invariants.sol"; + +contract InvariantTestBase is Deployer, Invariants { + function _generateAddrs(string memory label, uint256 num) internal returns (address[] memory addrs) { + addrs = new address[](num); + for (uint256 i; i < num; ++i) { + addrs[i] = makeAddr(string.concat(label, " ", vm.toString(i))); + } + } + + struct Selector { + bytes4 selector; + uint256 times; + } + + function _addFuzzingTarget(address target, Selector[] memory selectors) internal { + targetContract(target); + for (uint256 i; i < selectors.length; ++i) { + if (selectors[i].times == 0) continue; + FuzzSelector memory fuzzSelector = FuzzSelector(target, new bytes4[](selectors[i].times)); + for (uint256 j; j < selectors[i].times; ++j) { + fuzzSelector.selectors[j] = selectors[i].selector; + } + targetSelector(fuzzSelector); + } + } +} diff --git a/contracts/test/invariant/tests/IsolatedCredit.inv.t.sol b/contracts/test/invariant/tests/IsolatedCredit.inv.t.sol new file mode 100644 index 00000000..c4bfcd5d --- /dev/null +++ b/contracts/test/invariant/tests/IsolatedCredit.inv.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {MultiCall} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol"; + +import {CreditHandler} from "../handlers/CreditHandler.sol"; +import {InvariantTestBase} from "./InvariantTestBase.sol"; + +contract IsolatedCreditInvariantTest is InvariantTestBase { + CreditHandler creditHandler; + + function setUp() public override { + _deployCore(); + _deployTokensAndPriceFeeds(); + _deployPool("DAI"); + _deployCreditManager("DAI"); + + // NOTE: seed pool with liquidity + deal(address(tokens["DAI"]), address(this), 30_000_000e18); + tokens["DAI"].approve(address(_getPool("Diesel DAI v3")), 30_000_000e18); + _getPool("Diesel DAI v3").deposit(30_000_000e18, address(this)); + + // NOTE: testing in a single point in time + creditHandler = new CreditHandler(_getCreditManager("DAI v3"), 0 days); + address[] memory owners = _generateAddrs("Owner", 5); + for (uint256 i; i < owners.length; ++i) { + vm.prank(owners[i]); + creditHandler.creditFacade().openCreditAccount(owners[i], new MultiCall[](0), 0); + deal(address(tokens["DAI"]), owners[i], 2_500_000e18); + deal(address(tokens["WETH"]), owners[i], 1_000e18); + deal(address(tokens["WBTC"]), owners[i], 50e8); + deal(address(tokens["LINK"]), owners[i], 100_000e18); + } + + Selector[] memory selectors = new Selector[](9); + selectors[0] = Selector(creditHandler.addCollateral.selector, 3); + selectors[1] = Selector(creditHandler.withdrawCollateral.selector, 3); + selectors[2] = Selector(creditHandler.increaseDebt.selector, 3); + selectors[3] = Selector(creditHandler.decreaseDebt.selector, 3); + selectors[4] = Selector(creditHandler.addAndRepay.selector, 3); + selectors[5] = Selector(creditHandler.borrowAndWithdraw.selector, 3); + selectors[6] = Selector(creditHandler.increaseQuota.selector, 4); + selectors[7] = Selector(creditHandler.decreaseQuota.selector, 4); + selectors[8] = Selector(creditHandler.swapCollateral.selector, 4); + _addFuzzingTarget(address(creditHandler), selectors); + } + + function invariant_isolated_credit() public { + _assert_credit_invariant_01(creditHandler); + _assert_credit_invariant_02(creditHandler); + _assert_credit_invariant_03(creditHandler); + _assert_credit_invariant_04(creditHandler); + _assert_credit_invariant_05(creditHandler); + } +} diff --git a/contracts/test/invariant/tests/IsolatedPool.inv.t.sol b/contracts/test/invariant/tests/IsolatedPool.inv.t.sol new file mode 100644 index 00000000..7b93a801 --- /dev/null +++ b/contracts/test/invariant/tests/IsolatedPool.inv.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {PoolHandler} from "../handlers/PoolHandler.sol"; +import {InvariantTestBase} from "./InvariantTestBase.sol"; + +contract IsolatedPoolInvariantTest is InvariantTestBase { + PoolHandler poolHandler; + + function setUp() public override { + _deployCore(); + _deployTokensAndPriceFeeds(); + _deployPool("DAI"); + + // NOTE: testing in a single point in time + poolHandler = new PoolHandler(_getPool("Diesel DAI v3"), 0 days); + address[] memory depositors = _generateAddrs("Depositor", 5); + for (uint256 i; i < depositors.length; ++i) { + poolHandler.addDepositor(depositors[i]); + deal(address(tokens["DAI"]), depositors[i], 10_000_000e18); + } + + // NOTE: add dummy credit managers + address[] memory creditManagers = _generateAddrs("Credit manager", 2); + vm.startPrank(configurator); + for (uint256 i; i < 2; ++i) { + vm.mockCall(creditManagers[i], abi.encodeWithSignature("pool()"), abi.encode(address(poolHandler.pool()))); + contractsRegister.addCreditManager(creditManagers[i]); + poolHandler.pool().setCreditManagerDebtLimit(creditManagers[i], 30_000_000e18); + deal(address(tokens["DAI"]), creditManagers[i], 5_000_000e18); + } + vm.stopPrank(); + + Selector[] memory selectors = new Selector[](7); + selectors[0] = Selector(poolHandler.deposit.selector, 4); + selectors[1] = Selector(poolHandler.mint.selector, 1); + selectors[2] = Selector(poolHandler.withdraw.selector, 1); + selectors[3] = Selector(poolHandler.redeem.selector, 4); + selectors[4] = Selector(poolHandler.borrow.selector, 5); + selectors[5] = Selector(poolHandler.repayWithProfit.selector, 3); + selectors[6] = Selector(poolHandler.repayWithLoss.selector, 2); + _addFuzzingTarget(address(poolHandler), selectors); + } + + function invariant_isolated_pool() public { + _assert_pool_invariant_01(poolHandler); + _assert_pool_invariant_02(poolHandler); + _assert_pool_invariant_03(poolHandler); + _assert_pool_invariant_04(poolHandler); + _assert_pool_invariant_05(poolHandler); + } +} diff --git a/contracts/test/invariant/tests/MockVoting.inv.t.sol b/contracts/test/invariant/tests/MockVoting.inv.t.sol new file mode 100644 index 00000000..e3c2bbba --- /dev/null +++ b/contracts/test/invariant/tests/MockVoting.inv.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {VotingContractStatus} from "../../../interfaces/IGearStakingV3.sol"; +import {VotingContractMock} from "../../mocks/governance/VotingContractMock.sol"; +import {VotingHandler} from "../handlers/VotingHandler.sol"; +import {InvariantTestBase} from "./InvariantTestBase.sol"; + +contract MockVotingInvariantTest is InvariantTestBase { + VotingHandler votingHandler; + + function setUp() public override { + _deployCore(); + + votingHandler = new VotingHandler(gearStaking, 30 days); + address[] memory stakers = _generateAddrs("Staker", 5); + for (uint256 i; i < stakers.length; ++i) { + votingHandler.addStaker(stakers[i]); + deal(gear, stakers[i], 10_000_000e18); + } + + for (uint256 i; i < 2; ++i) { + address votingContractMock = address(new VotingContractMock()); + vm.prank(configurator); + gearStaking.setVotingContractStatus(votingContractMock, VotingContractStatus.ALLOWED); + votingHandler.addVotingContract(votingContractMock); + } + + Selector[] memory selectors = new Selector[](5); + selectors[0] = Selector(votingHandler.deposit.selector, 2); + selectors[1] = Selector(votingHandler.withdraw.selector, 1); + selectors[2] = Selector(votingHandler.claimWithdrawals.selector, 1); + selectors[3] = Selector(votingHandler.vote.selector, 3); + selectors[4] = Selector(votingHandler.unvote.selector, 3); + _addFuzzingTarget(address(votingHandler), selectors); + } + + function invariant_mock_voting() public { + _assert_voting_invariant_01(votingHandler); + _assert_voting_invariant_02(votingHandler); + } +} diff --git a/contracts/test/invaritants/Deployer.sol b/contracts/test/invaritants/Deployer.sol deleted file mode 100644 index b5964c7b..00000000 --- a/contracts/test/invaritants/Deployer.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import {IntegrationTestHelper} from "../helpers/IntegrationTestHelper.sol"; -import {AdapterMock} from "../mocks/core/AdapterMock.sol"; -import {TargetContractMock} from "../mocks/core/TargetContractMock.sol"; -import "../lib/constants.sol"; -import "forge-std/Vm.sol"; - -contract GearboxInstance is IntegrationTestHelper { - function _setUp() public { - _setupCore(); - - _deployMockCreditAndPool(); - - targetMock = new TargetContractMock(); - adapterMock = new AdapterMock(address(creditManager), address(targetMock)); - - vm.prank(CONFIGURATOR); - creditConfigurator.allowAdapter(address(adapterMock)); - } - - function mf() external { - vm.roll(block.number + 1); - } - - function getVm() external pure returns (Vm) { - return vm; - } -} diff --git a/contracts/test/invaritants/Handler.sol b/contracts/test/invaritants/Handler.sol deleted file mode 100644 index 2c8c8dcd..00000000 --- a/contracts/test/invaritants/Handler.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import {GearboxInstance} from "./Deployer.sol"; - -import {ICreditFacadeV3Multicall} from "../../interfaces/ICreditFacadeV3.sol"; -import {MultiCall} from "../../interfaces/ICreditFacadeV3.sol"; -import {MultiCallBuilder} from "../lib/MultiCallBuilder.sol"; -import "forge-std/Test.sol"; -import "../lib/constants.sol"; -import "forge-std/console.sol"; -import "forge-std/Vm.sol"; - -contract Handler { - Vm internal vm; - GearboxInstance gi; - - uint256 b; - uint256 counter; - - constructor(GearboxInstance _gi) { - gi = _gi; - vm = gi.getVm(); - b = block.timestamp; - } - - function openCA(uint256 _debt) public { - vm.roll(++b); - console.log(++counter); - (uint256 minDebt, uint256 maxDebt) = gi.creditFacade().debtLimits(); - - uint256 debt = minDebt + (_debt % (maxDebt - minDebt)); - - if (gi.pool().availableLiquidity() < 2 * debt) { - gi.tokenTestSuite().mint(gi.underlyingT(), INITIAL_LP, 3 * debt); - gi.tokenTestSuite().approve(gi.underlyingT(), INITIAL_LP, address(gi.pool())); - - vm.startPrank(INITIAL_LP); - gi.pool().deposit(3 * debt, INITIAL_LP); - vm.stopPrank(); - } - - if (gi.pool().creditManagerBorrowable(address(gi.creditManager())) > debt) { - gi.tokenTestSuite().mint(gi.underlyingT(), address(this), debt); - gi.tokenTestSuite().approve(gi.underlyingT(), address(this), address(gi.creditManager())); - - gi.creditFacade().openCreditAccount( - address(this), - MultiCallBuilder.build( - MultiCall({ - target: address(gi.creditFacade()), - callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (debt)) - }), - MultiCall({ - target: address(gi.creditFacade()), - callData: abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (gi.underlying(), debt)) - }) - ), - 0 - ); - } - } -} diff --git a/contracts/test/invaritants/OpenInvariants.t.sol b/contracts/test/invaritants/OpenInvariants.t.sol deleted file mode 100644 index fb365bca..00000000 --- a/contracts/test/invaritants/OpenInvariants.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import {GearboxInstance} from "./Deployer.sol"; -import {Handler} from "./Handler.sol"; - -import "forge-std/Test.sol"; -import "../lib/constants.sol"; - -contract InvariantGearboxTest is Test { - GearboxInstance gi; - Handler handler; - - function setUp() public { - gi = new GearboxInstance(); - gi._setUp(); - handler = new Handler(gi); - targetContract(address(handler)); - } - - // function invariant_example() external {} -} diff --git a/contracts/test/invaritants/TargetAttacker.sol b/contracts/test/invaritants/TargetAttacker.sol deleted file mode 100644 index 6f567db2..00000000 --- a/contracts/test/invaritants/TargetAttacker.sol +++ /dev/null @@ -1,116 +0,0 @@ -// // SPDX-License-Identifier: UNLICENSED -// // Gearbox Protocol. Generalized leverage for DeFi protocols -// // (c) Gearbox Foundation, 2023. -// pragma solidity ^0.8.17; - -// import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -// import {ICreditManagerV3, CollateralCalcTask, CollateralDebtData} from "../../interfaces/ICreditManagerV3.sol"; -// import {IPriceOracleV3} from "../../interfaces/IPriceOracleV3.sol"; -// import {ITokenTestSuite} from "../interfaces/ITokenTestSuite.sol"; -// import {PriceFeedMock} from "../mocks/oracles/PriceFeedMock.sol"; -// import {Random} from "./Random.sol"; - -// /// @title Target Hacker -// /// This contract simulates different technics to hack the system by provided seed - -// contract TargetAttacker is Random { -// using Math for uint256; - -// ICreditManagerV3 creditManager; -// IPriceOracleV3 priceOracle; -// ITokenTestSuite tokenTestSuite; -// address creditAccount; - -// constructor(address _creditManager, address _priceOracle, address _tokenTestSuite) { -// creditManager = ICreditManagerV3(_creditManager); -// priceOracle = IPriceOracleV3(_priceOracle); -// tokenTestSuite = ITokenTestSuite(_tokenTestSuite); -// } - -// // Act function tests different scenarios related to any action -// // which could potential attacker use. Calling internal contracts -// // depositing funds into pools, withdrawing, liquidating, etc. - -// // it also could update prices for updatable price oracles - -// function act(uint256 _seed) external { -// setSeed(_seed); -// creditAccount = msg.sender; - -// function ()[3] memory fnActions = [_stealTokens, _changeTokenPrice, _swapTokens]; - -// fnActions[getRandomInRange(fnActions.length)](); -// } - -// function _changeTokenPrice() internal { -// uint256 cTokensQty = creditManager.collateralTokensCount(); -// uint256 mask = 1 << getRandomInRange(cTokensQty); -// (address token,) = creditManager.collateralTokenByMask(mask); - -// address priceFeed = IPriceOracleV3(priceOracle).priceFeeds(token); - -// (, int256 price,,,) = PriceFeedMock(priceFeed).latestRoundData(); - -// uint256 sign = getRandomInRange(2); -// uint256 deltaPct = getRandomInRange(500); - -// int256 newPrice = -// sign == 1 ? price * (10000 + int256(deltaPct)) / 10000 : price * (10000 - int256(deltaPct)) / 10000; - -// PriceFeedMock(priceFeed).setPrice(newPrice); -// } - -// function _swapTokens() internal { -// uint256 cTokensQty = creditManager.collateralTokensCount(); -// uint256 mask0 = 1 << getRandomInRange(cTokensQty); -// uint256 mask1 = 1 << getRandomInRange(cTokensQty); - -// (address tokenIn,) = creditManager.collateralTokenByMask(mask0); -// (address tokenOut,) = creditManager.collateralTokenByMask(mask1); - -// uint256 balance = IERC20(tokenIn).balanceOf(creditAccount); - -// uint256 tokenInAmount = getRandomInRange(balance); - -// uint256 tokenInEq = priceOracle.convert(tokenInAmount, tokenIn, tokenOut); - -// IERC20(tokenIn).transferFrom(creditAccount, address(this), tokenInAmount); -// tokenTestSuite.mint(tokenOut, creditAccount, getRandomInRange(tokenInEq)); -// } - -// function _stealTokens() internal { -// uint256 cTokensQty = creditManager.collateralTokensCount(); -// uint256 mask = 1 << getRandomInRange(cTokensQty); -// (tokenIn,) = creditManager.collateralTokenByMask(mask); -// uint256 balance = IERC20(tokenIn).balanceOf(creditAccount); -// IERC20(tokenIn).transferFrom(creditAccount, address(this), getRandomInRange(balance)); -// } - -// /// Swaps token with some deviation from oracle price - -// function _swap() internal { -// uint256 cTokensQty = creditManager.collateralTokensCount(); - -// (tokenIn,) = creditManager.collateralTokenByMask(1 << getRandomInRange(cTokensQty)); -// uint256 balance = IERC20(tokenIn).balanceOf(creditAccount); -// uint256 amount = getRandomInRange(balance); -// IERC20(tokenIn).transferFrom(creditAccount, address(this), amount); - -// (tokenOut,) = creditManager.collateralTokenByMask(1 << getRandomInRange(cTokensQty)); - -// uint256 amountOut = (priceOracle.convert(amount, tokenIn, tokenOut) * (120 - getRandomInRange(40))) / 100; -// amountOut = Math.min(amountOut, IERC20(tokenOut).balanceOf(address(this))); -// IERC20(tokenOut).transfer(creditAccount, amountOut); -// } - -// function _deposit() internal { -// uint256 amount = getRandomInRange95(pool.availableLiquidity()); -// pool.deposit(amount, address(this)); -// } - -// function _withdraw() internal { -// uint256 amount = getRandomInRange95(pool.balanceOf(address(this))); -// pool.withdraw(amount, address(this), address(this)); -// } -// } diff --git a/contracts/test/mocks/governance/VotingContractMock.sol b/contracts/test/mocks/governance/VotingContractMock.sol new file mode 100644 index 00000000..d33e34fa --- /dev/null +++ b/contracts/test/mocks/governance/VotingContractMock.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {IVotingContractV3} from "../../../interfaces/IVotingContractV3.sol"; + +contract VotingContractMock is IVotingContractV3 { + mapping(address => uint96) public userVotes; + + function vote(address user, uint96 votes, bytes calldata) external { + userVotes[user] += votes; + } + + function unvote(address user, uint96 votes, bytes calldata) external { + userVotes[user] -= votes; + } +} diff --git a/contracts/test/suites/CreditManagerFactory.sol b/contracts/test/suites/CreditManagerFactory.sol index 0a01faf0..b706d8ff 100644 --- a/contracts/test/suites/CreditManagerFactory.sol +++ b/contracts/test/suites/CreditManagerFactory.sol @@ -3,7 +3,7 @@ // (c) Gearbox Foundation, 2023. pragma solidity ^0.8.17; -import "@openzeppelin/contracts/utils/Create2.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; import {CreditManagerV3} from "../../credit/CreditManagerV3.sol"; import {CreditFacadeV3} from "../../credit/CreditFacadeV3.sol"; @@ -19,11 +19,7 @@ contract CreditManagerFactory { constructor(address _ap, address _pool, CreditManagerOpts memory opts, bytes32 salt) { creditManager = new CreditManagerV3(_ap, _pool, opts.name); - creditFacade = new CreditFacadeV3( - address(creditManager), - opts.degenNFT, - opts.expirable - ); + creditFacade = new CreditFacadeV3(address(creditManager), opts.degenNFT, opts.expirable); bytes memory configuratorByteCode = abi.encodePacked(type(CreditConfiguratorV3).creationCode, abi.encode(creditManager, creditFacade, opts)); diff --git a/foundry.toml b/foundry.toml index 5a977850..2b4768db 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -libs = ['lib'] +libs = ['lib', 'node_modules'] out = 'forge-out' solc_version = '0.8.17' evm_version = 'shanghai' @@ -17,6 +17,7 @@ fs_permissions = [{ access = "read-write", path = "./"}] max_test_rejects = 200000 [invariant] -fail_on_revert = true +fail_on_revert = false +preserve_state = true runs = 200 depth = 10