From 251667de2ea030fb1d95e50d72da6c1167eb943b Mon Sep 17 00:00:00 2001 From: Van0k Date: Tue, 4 Feb 2025 14:25:07 +0400 Subject: [PATCH 1/7] feat: first test batch --- .../configuration/ConfigurationTestHelper.sol | 203 ++++++++ .../CreditSuiteConfiguration.unit.t.sol | 442 ++++++++++++++++++ .../PoolConfiguration.unit.t.sol | 152 ++++++ .../PriceOracleConfiguration.unit.t.sol | 118 +++++ .../RateKeeperConfiguration.unit.t.sol | 61 +++ contracts/test/helpers/BCRHelpers.sol | 13 +- contracts/test/helpers/CCGHelper.sol | 47 +- .../test/helpers/InstanceManagerHelper.sol | 10 +- .../mocks/MockCreditConfiguratorPatch.sol | 12 + 9 files changed, 1035 insertions(+), 23 deletions(-) create mode 100644 contracts/test/configuration/ConfigurationTestHelper.sol create mode 100644 contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol create mode 100644 contracts/test/configuration/PoolConfiguration.unit.t.sol create mode 100644 contracts/test/configuration/PriceOracleConfiguration.unit.t.sol create mode 100644 contracts/test/configuration/RateKeeperConfiguration.unit.t.sol create mode 100644 contracts/test/mocks/MockCreditConfiguratorPatch.sol diff --git a/contracts/test/configuration/ConfigurationTestHelper.sol b/contracts/test/configuration/ConfigurationTestHelper.sol new file mode 100644 index 0000000..1c45716 --- /dev/null +++ b/contracts/test/configuration/ConfigurationTestHelper.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; + +import {CrossChainMultisig, CrossChainCall} from "../../global/CrossChainMultisig.sol"; +import {InstanceManager} from "../../instance/InstanceManager.sol"; +import {PriceFeedStore} from "../../instance/PriceFeedStore.sol"; +import {IBytecodeRepository} from "../../interfaces/IBytecodeRepository.sol"; +import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; +import {IInstanceManager} from "../../interfaces/IInstanceManager.sol"; +import {ICreditConfigureActions} from "../../factories/CreditFactory.sol"; + +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {ERC20Mock} from "@gearbox-protocol/core-v3/contracts/test/mocks/token/ERC20Mock.sol"; +import {MockPriceFeed} from "../mocks/MockPriceFeed.sol"; + +import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; +import {ICreditManagerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol"; +import {ICreditFacadeV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3.sol"; +import {ICreditConfiguratorV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditConfiguratorV3.sol"; + +import { + AP_PRICE_FEED_STORE, + AP_INSTANCE_MANAGER_PROXY, + AP_INTEREST_RATE_MODEL_FACTORY, + AP_CREDIT_FACTORY, + AP_POOL_FACTORY, + AP_PRICE_ORACLE_FACTORY, + AP_RATE_KEEPER_FACTORY, + AP_MARKET_CONFIGURATOR_FACTORY, + AP_LOSS_POLICY_FACTORY, + AP_GOVERNOR, + AP_POOL, + AP_POOL_QUOTA_KEEPER, + AP_PRICE_ORACLE, + AP_MARKET_CONFIGURATOR, + AP_ACL, + AP_CONTRACTS_REGISTER, + AP_INTEREST_RATE_MODEL_LINEAR, + AP_RATE_KEEPER_TUMBLER, + AP_RATE_KEEPER_GAUGE, + AP_LOSS_POLICY_DEFAULT, + AP_CREDIT_MANAGER, + AP_CREDIT_FACADE, + AP_CREDIT_CONFIGURATOR, + NO_VERSION_CONTROL +} from "../../libraries/ContractLiterals.sol"; + +import {DeployParams} from "../../interfaces/Types.sol"; +import {CreditFacadeParams, CreditManagerParams} from "../../factories/CreditFactory.sol"; + +import {GlobalSetup} from "../helpers/GlobalSetup.sol"; +import {MarketConfigurator} from "../../market/MarketConfigurator.sol"; +import {MarketConfiguratorFactory} from "../../instance/MarketConfiguratorFactory.sol"; + +contract ConfigurationTestHelper is Test, GlobalSetup { + address public admin; + address public emergencyAdmin; + + address public WETH; + address public USDC; + address public GEAR; + address public CHAINLINK_ETH_USD; + address public CHAINLINK_USDC_USD; + + string constant name = "Test Market ETH"; + string constant symbol = "dETH"; + + MarketConfigurator public marketConfigurator; + address public addressProvider; + + IPoolV3 public pool; + ICreditManagerV3 public creditManager; + ICreditFacadeV3 public creditFacade; + ICreditConfiguratorV3 public creditConfigurator; + + function setUp() public virtual { + vm.chainId(1); + + _setUpGlobalContracts(); + _deployMockTokens(); + + CrossChainCall[] memory calls = new CrossChainCall[](1); + calls[0] = _generateActivateCall(1, instanceOwner, makeAddr("TREASURY"), WETH, GEAR); + _submitProposalAndSign("Activate instance", calls); + + _setupPriceFeedStore(); + + admin = makeAddr("admin"); + emergencyAdmin = makeAddr("emergencyAdmin"); + + addressProvider = instanceManager.addressProvider(); + + address mcf = + IAddressProvider(addressProvider).getAddressOrRevert(AP_MARKET_CONFIGURATOR_FACTORY, NO_VERSION_CONTROL); + vm.prank(admin); + marketConfigurator = MarketConfigurator( + MarketConfiguratorFactory(mcf).createMarketConfigurator(emergencyAdmin, address(0), "Test Curator", false) + ); + + pool = IPoolV3(_deployTestPool()); + creditManager = ICreditManagerV3(_deployTestCreditSuite()); + creditFacade = ICreditFacadeV3(creditManager.creditFacade()); + creditConfigurator = ICreditConfiguratorV3(creditManager.creditConfigurator()); + } + + function _isTestMode() internal pure virtual override returns (bool) { + return true; + } + + function _deployMockTokens() internal { + // Deploy mock tokens + WETH = address(new ERC20Mock("Mock WETH", "WETH", 18)); + USDC = address(new ERC20Mock("Mock USDC", "USDC", 6)); + GEAR = address(new ERC20Mock("Mock GEAR", "GEAR", 18)); + + // Mint initial supply + ERC20Mock(WETH).mint(address(this), 1000000 ether); + ERC20Mock(USDC).mint(address(this), 1000000000 * 10 ** 6); + ERC20Mock(GEAR).mint(address(this), 1000000 ether); + + // Deploy mock price feeds + CHAINLINK_ETH_USD = address(new MockPriceFeed()); + CHAINLINK_USDC_USD = address(new MockPriceFeed()); + + // Set initial prices + MockPriceFeed(CHAINLINK_ETH_USD).setPrice(2000 * 10 ** 8); // $2000 + MockPriceFeed(CHAINLINK_USDC_USD).setPrice(1 * 10 ** 8); // $1 + } + + function _setupPriceFeedStore() internal { + _addPriceFeed(CHAINLINK_ETH_USD, 1 days, "ETH/USD"); + _addPriceFeed(CHAINLINK_USDC_USD, 1 days, "USDC/USD"); + + _allowPriceFeed(WETH, CHAINLINK_ETH_USD); + _allowPriceFeed(USDC, CHAINLINK_USDC_USD); + } + + function _deployTestPool() internal returns (address) { + address poolFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_POOL_FACTORY, 3_10); + + IERC20(WETH).transfer(poolFactory, 1e18); + + address _pool = MarketConfigurator(marketConfigurator).previewCreateMarket(3_10, WETH, name, symbol); + + DeployParams memory interestRateModelParams = DeployParams({ + postfix: "LINEAR", + salt: 0, + constructorParams: abi.encode(100, 200, 100, 100, 200, 300, false) + }); + DeployParams memory rateKeeperParams = + DeployParams({postfix: "TUMBLER", salt: 0, constructorParams: abi.encode(_pool, 7 days)}); + DeployParams memory lossPolicyParams = + DeployParams({postfix: "DEFAULT", salt: 0, constructorParams: abi.encode(_pool, addressProvider)}); + + vm.prank(admin); + _pool = MarketConfigurator(marketConfigurator).createMarket({ + minorVersion: 3_10, + underlying: WETH, + name: name, + symbol: symbol, + interestRateModelParams: interestRateModelParams, + rateKeeperParams: rateKeeperParams, + lossPolicyParams: lossPolicyParams, + underlyingPriceFeed: CHAINLINK_ETH_USD + }); + + return _pool; + } + + function _deployTestCreditSuite() internal returns (address) { + DeployParams memory accountFactoryParams = + DeployParams({postfix: "DEFAULT", salt: 0, constructorParams: abi.encode(addressProvider)}); + + CreditManagerParams memory creditManagerParams = CreditManagerParams({ + maxEnabledTokens: 4, + feeInterest: 10_00, + feeLiquidation: 1_50, + liquidationPremium: 1_50, + feeLiquidationExpired: 1_50, + liquidationPremiumExpired: 1_50, + minDebt: 1e18, + maxDebt: 20e18, + name: "Credit Manager ETH", + accountFactoryParams: accountFactoryParams + }); + + CreditFacadeParams memory facadeParams = + CreditFacadeParams({degenNFT: address(0), expirable: false, migrateBotList: false}); + + bytes memory creditSuiteParams = abi.encode(creditManagerParams, facadeParams); + + vm.prank(admin); + return MarketConfigurator(marketConfigurator).createCreditSuite(3_10, address(pool), creditSuiteParams); + } + + function _addUSDC() internal { + vm.prank(admin); + marketConfigurator.addToken(address(pool), USDC, CHAINLINK_USDC_USD); + } +} diff --git a/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol b/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol new file mode 100644 index 0000000..c363038 --- /dev/null +++ b/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; + +import {MockCreditConfiguratorPatch} from "../mocks/MockCreditConfiguratorPatch.sol"; +import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {CreditFactory} from "../../factories/CreditFactory.sol"; +import {ICreditConfigureActions} from "../../interfaces/factories/ICreditConfigureActions.sol"; +import {ICreditEmergencyConfigureActions} from "../../interfaces/factories/ICreditEmergencyConfigureActions.sol"; +import {IPriceOracleConfigureActions} from "../../interfaces/factories/IPriceOracleConfigureActions.sol"; +import {ICreditConfiguratorV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditConfiguratorV3.sol"; +import {ICreditFacadeV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3.sol"; +import {ICreditManagerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol"; +import {DeployParams} from "../../interfaces/Types.sol"; +import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; +import {IContractsRegister} from "../../interfaces/IContractsRegister.sol"; +import {IBytecodeRepository} from "../../interfaces/IBytecodeRepository.sol"; +import { + NO_VERSION_CONTROL, + AP_BYTECODE_REPOSITORY, + AP_CREDIT_FACTORY, + AP_WETH_TOKEN, + AP_CREDIT_CONFIGURATOR +} from "../../libraries/ContractLiterals.sol"; +import {CreditFacadeParams} from "../../factories/CreditFactory.sol"; +import {CrossChainCall} from "../helpers/GlobalSetup.sol"; + +import {GeneralMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/GeneralMock.sol"; +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v3/contracts/libraries/Constants.sol"; +import {UploadableContract} from "../helpers/GlobalSetup.sol"; + +import { + IUniswapV3Adapter, + UniswapV3PoolStatus +} from "@gearbox-protocol/integrations-v3/contracts/interfaces/uniswap/IUniswapV3Adapter.sol"; + +contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { + address target; + + function setUp() public override { + super.setUp(); + + target = address(new GeneralMock()); + } + + function _uploadCreditConfiguratorPatch() internal { + CrossChainCall[] memory calls = new CrossChainCall[](1); + + bytes32 bytecodeHash = + _uploadByteCodeAndSign(type(MockCreditConfiguratorPatch).creationCode, AP_CREDIT_CONFIGURATOR, 3_11); + + calls[0] = _generateAllowSystemContractCall(bytecodeHash); + + _submitProposalAndSign("Allow system contracts", calls); + } + + /// REGULAR CONFIGURATION TESTS /// + + function test_CS_01_allowAdapter() public { + DeployParams memory params = DeployParams({ + postfix: "BALANCER_VAULT", + salt: 0, + constructorParams: abi.encode(address(creditManager), target) + }); + + address bytecodeRepository = + IAddressProvider(addressProvider).getAddressOrRevert(AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL); + address creditFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_CREDIT_FACTORY, 3_10); + + address expectedAdapter = IBytecodeRepository(bytecodeRepository).computeAddress( + "ADAPTER::BALANCER_VAULT", + 3_10, + params.constructorParams, + keccak256(abi.encode(0, address(marketConfigurator))), + creditFactory + ); + + vm.expectCall( + address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.allowAdapter, (expectedAdapter)) + ); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.allowAdapter, (params)) + ); + + assertEq(ICreditManagerV3(creditManager).adapterToContract(expectedAdapter), target, "Adapter must be allowed"); + } + + function test_CS_02_forbidAdapter() public { + DeployParams memory params = DeployParams({ + postfix: "BALANCER_VAULT", + salt: 0, + constructorParams: abi.encode(address(creditManager), target) + }); + + vm.startPrank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.allowAdapter, (params)) + ); + + address adapter = ICreditManagerV3(creditManager).contractToAdapter(target); + + vm.expectCall(address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.forbidAdapter, (adapter))); + + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.forbidAdapter, (adapter)) + ); + vm.stopPrank(); + + assertEq(ICreditManagerV3(creditManager).contractToAdapter(target), address(0), "Adapter must not be allowed"); + } + + function test_CS_03_setFees() public { + uint16 feeLiquidation = 100; + uint16 liquidationPremium = 200; + uint16 feeLiquidationExpired = 100; + uint16 liquidationPremiumExpired = 200; + + vm.expectCall( + address(creditConfigurator), + abi.encodeCall( + ICreditConfiguratorV3.setFees, + (feeLiquidation, liquidationPremium, feeLiquidationExpired, liquidationPremiumExpired) + ) + ); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), + abi.encodeCall( + ICreditConfigureActions.setFees, + (feeLiquidation, liquidationPremium, feeLiquidationExpired, liquidationPremiumExpired) + ) + ); + + (, uint16 fl, uint16 lp, uint16 fle, uint16 lpe) = ICreditManagerV3(creditManager).fees(); + assertEq(fl, feeLiquidation, "Incorrect feeLiquidation"); + assertEq(lp, PERCENTAGE_FACTOR - liquidationPremium, "Incorrect liquidationPremium"); + assertEq(fle, feeLiquidationExpired, "Incorrect feeLiquidationExpired"); + assertEq(lpe, PERCENTAGE_FACTOR - liquidationPremiumExpired, "Incorrect liquidationPremiumExpired"); + } + + function test_CS_04_addCollateralToken() public { + _addUSDC(); + + address token = USDC; + uint16 liquidationThreshold = 8000; + + vm.expectCall( + address(creditConfigurator), + abi.encodeCall(ICreditConfiguratorV3.addCollateralToken, (token, liquidationThreshold)) + ); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), + abi.encodeCall(ICreditConfigureActions.addCollateralToken, (token, liquidationThreshold)) + ); + + ICreditManagerV3(creditManager).getTokenMaskOrRevert(token); + assertEq( + ICreditManagerV3(creditManager).liquidationThresholds(token), + liquidationThreshold, + "Incorrect liquidation threshold" + ); + } + + function test_CS_05_forbidToken() public { + _addUSDC(); + + address token = USDC; + uint16 liquidationThreshold = 8000; + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), + abi.encodeCall(ICreditConfigureActions.addCollateralToken, (token, liquidationThreshold)) + ); + + vm.expectCall(address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.forbidToken, (token))); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.forbidToken, (token)) + ); + + assertTrue( + ICreditFacadeV3(creditFacade).forbiddenTokenMask() + & ICreditManagerV3(creditManager).getTokenMaskOrRevert(token) != 0, + "Token must be forbidden" + ); + } + + function test_CS_06_allowToken() public { + _addUSDC(); + + address token = USDC; + uint16 liquidationThreshold = 8000; + + vm.startPrank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), + abi.encodeCall(ICreditConfigureActions.addCollateralToken, (token, liquidationThreshold)) + ); + + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.forbidToken, (token)) + ); + + vm.expectCall(address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.allowToken, (token))); + + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.allowToken, (token)) + ); + vm.stopPrank(); + + assertTrue( + ICreditFacadeV3(creditFacade).forbiddenTokenMask() + & ICreditManagerV3(creditManager).getTokenMaskOrRevert(token) == 0, + "Token must be allowed" + ); + } + + function test_CS_07_upgradeCreditConfigurator() public { + _uploadCreditConfiguratorPatch(); + + address bytecodeRepository = + IAddressProvider(addressProvider).getAddressOrRevert(AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL); + address creditFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_CREDIT_FACTORY, 3_10); + + // Compute expected new configurator address + address expectedNewConfigurator = IBytecodeRepository(bytecodeRepository).computeAddress( + "CREDIT_CONFIGURATOR", + 3_11, + abi.encode(marketConfigurator.acl(), address(creditManager)), + bytes32(bytes20(address(marketConfigurator))), + creditFactory + ); + + // Expect call to current configurator to upgrade + vm.expectCall( + address(creditConfigurator), + abi.encodeCall(ICreditConfiguratorV3.upgradeCreditConfigurator, (expectedNewConfigurator)) + ); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.upgradeCreditConfigurator, ()) + ); + + // Verify configurator was upgraded + assertEq( + ICreditManagerV3(creditManager).creditConfigurator(), + expectedNewConfigurator, + "Credit configurator must be upgraded" + ); + } + + function test_CS_08_upgradeCreditFacade() public { + address bytecodeRepository = + IAddressProvider(addressProvider).getAddressOrRevert(AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL); + address creditFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_CREDIT_FACTORY, 3_10); + + CreditFacadeParams memory params = + CreditFacadeParams({degenNFT: address(0), expirable: true, migrateBotList: true}); + + address contractsRegister = marketConfigurator.contractsRegister(); + address lossPolicy = IContractsRegister(contractsRegister).getLossPolicy(address(pool)); + + // Compute expected new facade address + address expectedNewFacade = IBytecodeRepository(bytecodeRepository).computeAddress( + "CREDIT_FACADE", + 3_10, + abi.encode( + marketConfigurator.acl(), + address(creditManager), + lossPolicy, + ICreditFacadeV3(creditFacade).botList(), + WETH, + params.degenNFT, + params.expirable + ), + bytes32(bytes20(address(marketConfigurator))), + creditFactory + ); + + // Expect call to configurator to set new facade + vm.expectCall( + address(creditConfigurator), + abi.encodeCall(ICreditConfiguratorV3.setCreditFacade, (expectedNewFacade, true)) + ); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.upgradeCreditFacade, (params)) + ); + + // Verify facade was upgraded + assertEq(ICreditManagerV3(creditManager).creditFacade(), expectedNewFacade, "Credit facade must be upgraded"); + } + + function test_CS_09_configureAdapter() public { + _addUSDC(); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.addCollateralToken, (USDC, 8000)) + ); + + // First deploy Uniswap V3 adapter + DeployParams memory params = DeployParams({ + postfix: "UNISWAP_V3_ROUTER", + salt: 0, + constructorParams: abi.encode(address(creditManager), target) + }); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.allowAdapter, (params)) + ); + + address adapter = ICreditManagerV3(creditManager).contractToAdapter(target); + + // Configure pool status + UniswapV3PoolStatus[] memory pools = new UniswapV3PoolStatus[](1); + pools[0] = UniswapV3PoolStatus({token0: WETH, token1: USDC, fee: 500, allowed: true}); + + vm.expectCall(adapter, abi.encodeCall(IUniswapV3Adapter.setPoolStatusBatch, (pools))); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), + abi.encodeCall( + ICreditConfigureActions.configureAdapterFor, + (target, abi.encodeCall(IUniswapV3Adapter.setPoolStatusBatch, (pools))) + ) + ); + + // Verify pool was allowed + assertTrue(IUniswapV3Adapter(adapter).isPoolAllowed(WETH, USDC, 500), "Pool must be allowed"); + } + + function test_CS_10_pause_unpause() public { + vm.startPrank(admin); + + // Expect call to creditFacade.pause + vm.expectCall(address(creditFacade), abi.encodeCall(ICreditFacadeV3.pause, ())); + + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.pause, ()) + ); + + assertTrue(Pausable(address(creditFacade)).paused(), "Credit facade must be paused"); + + vm.expectCall(address(creditFacade), abi.encodeCall(ICreditFacadeV3.unpause, ())); + + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.unpause, ()) + ); + vm.stopPrank(); + + assertFalse(Pausable(address(creditFacade)).paused(), "Credit facade must be unpaused"); + } + + /// EMERGENCY CONFIGURATION TESTS /// + + function test_CS_11_emergency_forbidAdapter() public { + DeployParams memory params = DeployParams({ + postfix: "BALANCER_VAULT", + salt: 0, + constructorParams: abi.encode(address(creditManager), target) + }); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.allowAdapter, (params)) + ); + + address adapter = ICreditManagerV3(creditManager).contractToAdapter(target); + + vm.expectCall(address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.forbidAdapter, (adapter))); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigureCreditSuite( + address(creditManager), abi.encodeCall(ICreditEmergencyConfigureActions.forbidAdapter, (adapter)) + ); + + assertEq(ICreditManagerV3(creditManager).contractToAdapter(target), address(0), "Adapter must be forbidden"); + } + + function test_CS_12_emergency_forbidToken() public { + _addUSDC(); + + address token = USDC; + uint16 liquidationThreshold = 8000; + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), + abi.encodeCall(ICreditConfigureActions.addCollateralToken, (token, liquidationThreshold)) + ); + + vm.expectCall(address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.forbidToken, (token))); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigureCreditSuite( + address(creditManager), abi.encodeCall(ICreditEmergencyConfigureActions.forbidToken, (token)) + ); + + assertTrue( + ICreditFacadeV3(creditFacade).forbiddenTokenMask() + & ICreditManagerV3(creditManager).getTokenMaskOrRevert(token) != 0, + "Token must be forbidden" + ); + } + + function test_CS_13_emergency_forbidBorrowing() public { + vm.expectCall(address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.forbidBorrowing, ())); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigureCreditSuite( + address(creditManager), abi.encodeCall(ICreditEmergencyConfigureActions.forbidBorrowing, ()) + ); + + assertEq(ICreditFacadeV3(creditFacade).maxDebtPerBlockMultiplier(), 0, "Borrowing must be forbidden"); + } + + function test_CS_14_emergency_pause() public { + vm.expectCall(address(creditFacade), abi.encodeCall(ICreditFacadeV3.pause, ())); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigureCreditSuite( + address(creditManager), abi.encodeCall(ICreditEmergencyConfigureActions.pause, ()) + ); + + assertTrue(Pausable(address(creditFacade)).paused(), "Credit facade must be paused"); + } +} diff --git a/contracts/test/configuration/PoolConfiguration.unit.t.sol b/contracts/test/configuration/PoolConfiguration.unit.t.sol new file mode 100644 index 0000000..cba069c --- /dev/null +++ b/contracts/test/configuration/PoolConfiguration.unit.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; + +import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {IPoolConfigureActions} from "../../interfaces/factories/IPoolConfigureActions.sol"; +import {IPoolEmergencyConfigureActions} from "../../interfaces/factories/IPoolEmergencyConfigureActions.sol"; +import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; +import {IPoolQuotaKeeperV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolQuotaKeeperV3.sol"; +import {IPriceOracleV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol"; +import {GeneralMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/GeneralMock.sol"; +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v3/contracts/libraries/Constants.sol"; + +contract PoolConfigurationUnitTest is ConfigurationTestHelper { + address private _target; + address private _quotaKeeper; + + function setUp() public override { + super.setUp(); + + _target = address(new GeneralMock()); + _quotaKeeper = IPoolV3(pool).poolQuotaKeeper(); + } + + /// REGULAR CONFIGURATION TESTS /// + + function test_P_01_setTotalDebtLimit() public { + uint256 limit = 1_000_000; + + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.setTotalDebtLimit, (limit))); + + vm.prank(admin); + marketConfigurator.configurePool( + address(pool), abi.encodeCall(IPoolConfigureActions.setTotalDebtLimit, (limit)) + ); + + assertEq(IPoolV3(pool).totalDebtLimit(), limit, "Incorrect total debt limit"); + } + + function test_P_02_setCreditManagerDebtLimit() public { + uint256 limit = 500_000; + + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.setCreditManagerDebtLimit, (address(creditManager), limit))); + + vm.prank(admin); + marketConfigurator.configurePool( + address(pool), + abi.encodeCall(IPoolConfigureActions.setCreditManagerDebtLimit, (address(creditManager), limit)) + ); + + assertEq( + IPoolV3(pool).creditManagerDebtLimit(address(creditManager)), limit, "Incorrect credit manager debt limit" + ); + } + + function test_P_03_setTokenLimit() public { + _addUSDC(); + + address token = USDC; + uint96 limit = 100_000; + + vm.expectCall(_quotaKeeper, abi.encodeCall(IPoolQuotaKeeperV3.setTokenLimit, (token, limit))); + + vm.prank(admin); + marketConfigurator.configurePool( + address(pool), abi.encodeCall(IPoolConfigureActions.setTokenLimit, (token, limit)) + ); + + (,,,, uint96 tokenLimit,) = IPoolQuotaKeeperV3(_quotaKeeper).getTokenQuotaParams(token); + assertEq(tokenLimit, limit, "Incorrect token limit"); + } + + function test_P_04_setTokenQuotaIncreaseFee() public { + _addUSDC(); + + address token = USDC; + uint16 fee = 100; // 1% + + vm.expectCall(_quotaKeeper, abi.encodeCall(IPoolQuotaKeeperV3.setTokenQuotaIncreaseFee, (token, fee))); + + vm.prank(admin); + marketConfigurator.configurePool( + address(pool), abi.encodeCall(IPoolConfigureActions.setTokenQuotaIncreaseFee, (token, fee)) + ); + + (,, uint16 quotaIncreaseFee,,,) = IPoolQuotaKeeperV3(_quotaKeeper).getTokenQuotaParams(token); + assertEq(quotaIncreaseFee, fee, "Incorrect quota increase fee"); + } + + function test_P_05_pause_unpause() public { + vm.startPrank(admin); + + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.pause, ())); + + marketConfigurator.configurePool(address(pool), abi.encodeCall(IPoolConfigureActions.pause, ())); + + assertTrue(Pausable(address(pool)).paused(), "Pool must be paused"); + + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.unpause, ())); + + marketConfigurator.configurePool(address(pool), abi.encodeCall(IPoolConfigureActions.unpause, ())); + vm.stopPrank(); + + assertFalse(Pausable(address(pool)).paused(), "Pool must be unpaused"); + } + + /// EMERGENCY CONFIGURATION TESTS /// + + function test_P_06_emergency_setCreditManagerDebtLimitToZero() public { + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.setCreditManagerDebtLimit, (address(creditManager), 0))); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigurePool( + address(pool), + abi.encodeCall(IPoolEmergencyConfigureActions.setCreditManagerDebtLimitToZero, (address(creditManager))) + ); + + assertEq( + IPoolV3(pool).creditManagerDebtLimit(address(creditManager)), 0, "Credit manager debt limit must be zero" + ); + } + + function test_P_07_emergency_setTokenLimitToZero() public { + _addUSDC(); + + address token = USDC; + + vm.expectCall(_quotaKeeper, abi.encodeCall(IPoolQuotaKeeperV3.setTokenLimit, (token, 0))); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigurePool( + address(pool), abi.encodeCall(IPoolEmergencyConfigureActions.setTokenLimitToZero, (token)) + ); + + (,,,, uint96 tokenLimit,) = IPoolQuotaKeeperV3(_quotaKeeper).getTokenQuotaParams(token); + assertEq(tokenLimit, 0, "Token limit must be zero"); + } + + function test_P_08_emergency_pause() public { + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.pause, ())); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigurePool( + address(pool), abi.encodeCall(IPoolEmergencyConfigureActions.pause, ()) + ); + + assertTrue(Pausable(address(pool)).paused(), "Pool must be paused"); + } +} diff --git a/contracts/test/configuration/PriceOracleConfiguration.unit.t.sol b/contracts/test/configuration/PriceOracleConfiguration.unit.t.sol new file mode 100644 index 0000000..fd5654a --- /dev/null +++ b/contracts/test/configuration/PriceOracleConfiguration.unit.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {IPriceOracleConfigureActions} from "../../interfaces/factories/IPriceOracleConfigureActions.sol"; +import {IPriceOracleEmergencyConfigureActions} from + "../../interfaces/factories/IPriceOracleEmergencyConfigureActions.sol"; +import {IPriceOracleV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol"; +import {IPriceFeedStore} from "../../interfaces/IPriceFeedStore.sol"; +import {MockPriceFeed} from "../mocks/MockPriceFeed.sol"; +import {IContractsRegister} from "../../interfaces/IContractsRegister.sol"; +import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; +import {AP_PRICE_FEED_STORE, NO_VERSION_CONTROL} from "../../libraries/ContractLiterals.sol"; + +contract PriceOracleConfigurationUnitTest is ConfigurationTestHelper { + address private _target; + address private _priceFeedStore; + address private _priceOracle; + + function setUp() public override { + super.setUp(); + + _target = address(new MockPriceFeed()); + _priceOracle = IContractsRegister(marketConfigurator.contractsRegister()).getPriceOracle(address(pool)); + _priceFeedStore = IAddressProvider(addressProvider).getAddressOrRevert(AP_PRICE_FEED_STORE, NO_VERSION_CONTROL); + + _addUSDC(); + } + + /// REGULAR CONFIGURATION TESTS /// + + function test_PO_01_setPriceFeed() public { + address token = USDC; + address priceFeed = address(new MockPriceFeed()); + uint32 stalenessPeriod = 3600; + + vm.mockCall( + _priceFeedStore, abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (token, priceFeed)), abi.encode(true) + ); + + vm.mockCall( + _priceFeedStore, + abi.encodeCall(IPriceFeedStore.getStalenessPeriod, (priceFeed)), + abi.encode(stalenessPeriod) + ); + + vm.expectCall(_priceOracle, abi.encodeCall(IPriceOracleV3.setPriceFeed, (token, priceFeed, stalenessPeriod))); + + vm.prank(admin); + marketConfigurator.configurePriceOracle( + address(pool), abi.encodeCall(IPriceOracleConfigureActions.setPriceFeed, (token, priceFeed)) + ); + + assertEq(IPriceOracleV3(_priceOracle).priceFeeds(token), priceFeed, "Incorrect price feed"); + } + + function test_PO_02_setReservePriceFeed() public { + address token = USDC; + address priceFeed = address(new MockPriceFeed()); + uint32 stalenessPeriod = 3600; + + vm.mockCall( + _priceFeedStore, abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (token, priceFeed)), abi.encode(true) + ); + + vm.mockCall( + _priceFeedStore, + abi.encodeCall(IPriceFeedStore.getStalenessPeriod, (priceFeed)), + abi.encode(stalenessPeriod) + ); + + vm.expectCall( + _priceOracle, abi.encodeCall(IPriceOracleV3.setReservePriceFeed, (token, priceFeed, stalenessPeriod)) + ); + + vm.prank(admin); + marketConfigurator.configurePriceOracle( + address(pool), abi.encodeCall(IPriceOracleConfigureActions.setReservePriceFeed, (token, priceFeed)) + ); + + assertEq(IPriceOracleV3(_priceOracle).reservePriceFeeds(token), priceFeed, "Incorrect reserve price feed"); + } + + /// EMERGENCY CONFIGURATION TESTS /// + + function test_PO_03_emergency_setPriceFeed() public { + address token = USDC; + address priceFeed = address(new MockPriceFeed()); + uint32 stalenessPeriod = 3600; + + vm.mockCall( + _priceFeedStore, abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (token, priceFeed)), abi.encode(true) + ); + + vm.mockCall( + _priceFeedStore, + abi.encodeCall(IPriceFeedStore.getStalenessPeriod, (priceFeed)), + abi.encode(stalenessPeriod) + ); + + vm.mockCall( + _priceFeedStore, + abi.encodeCall(IPriceFeedStore.getAllowanceTimestamp, (token, priceFeed)), + abi.encode(block.timestamp - 1 days - 1) + ); + + vm.expectCall(_priceOracle, abi.encodeCall(IPriceOracleV3.setPriceFeed, (token, priceFeed, stalenessPeriod))); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigurePriceOracle( + address(pool), abi.encodeCall(IPriceOracleEmergencyConfigureActions.setPriceFeed, (token, priceFeed)) + ); + + assertEq(IPriceOracleV3(_priceOracle).priceFeeds(token), priceFeed, "Incorrect price feed"); + } +} diff --git a/contracts/test/configuration/RateKeeperConfiguration.unit.t.sol b/contracts/test/configuration/RateKeeperConfiguration.unit.t.sol new file mode 100644 index 0000000..993792b --- /dev/null +++ b/contracts/test/configuration/RateKeeperConfiguration.unit.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {TumblerV3} from "@gearbox-protocol/core-v3/contracts/pool/TumblerV3.sol"; +import {ITumblerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ITumblerV3.sol"; +import {IPoolQuotaKeeperV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolQuotaKeeperV3.sol"; +import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; +import {IContractsRegister} from "../../interfaces/IContractsRegister.sol"; + +contract RateKeeperConfigurationUnitTest is ConfigurationTestHelper { + address private _rateKeeper; + address private _quotaKeeper; + + function setUp() public override { + super.setUp(); + + _quotaKeeper = IPoolV3(pool).poolQuotaKeeper(); + _rateKeeper = IPoolQuotaKeeperV3(_quotaKeeper).gauge(); + + _addUSDC(); + } + + /// REGULAR CONFIGURATION TESTS /// + + function test_RK_01_setRate() public { + address token = USDC; + uint16 rate = 100; // 1% + + vm.expectCall(_rateKeeper, abi.encodeCall(ITumblerV3.setRate, (token, rate))); + + vm.prank(admin); + marketConfigurator.configureRateKeeper(address(pool), abi.encodeCall(ITumblerV3.setRate, (token, rate))); + + address[] memory tokens = new address[](1); + tokens[0] = token; + + uint16[] memory rates = ITumblerV3(_rateKeeper).getRates(tokens); + assertEq(rates[0], rate, "Incorrect rate"); + } + + function test_RK_02_updateRates() public { + vm.warp(block.timestamp + ITumblerV3(_rateKeeper).epochLength()); + + vm.expectCall(_rateKeeper, abi.encodeCall(ITumblerV3.updateRates, ())); + + vm.prank(admin); + marketConfigurator.configureRateKeeper(address(pool), abi.encodeCall(ITumblerV3.updateRates, ())); + } + + function test_RK_03_addToken_reverts() public { + address token = USDC; + + vm.expectRevert(); + + vm.prank(admin); + marketConfigurator.configureRateKeeper(address(pool), abi.encodeCall(TumblerV3.addToken, (token))); + } +} diff --git a/contracts/test/helpers/BCRHelpers.sol b/contracts/test/helpers/BCRHelpers.sol index cf75f43..7387c11 100644 --- a/contracts/test/helpers/BCRHelpers.sol +++ b/contracts/test/helpers/BCRHelpers.sol @@ -27,9 +27,16 @@ contract BCRHelpers is SignatureHelper { authorKey = _generatePrivateKey("AUTHOR"); author = vm.rememberKey(authorKey); // Print debug info - console.log("BCR setup:"); - console.log("Auditor:", auditor, "Key:", auditorKey.toHexString()); - console.log("Author:", author, "Key:", authorKey.toHexString()); + + if (!_isTestMode()) { + console.log("BCR setup:"); + console.log("Auditor:", auditor, "Key:", auditorKey.toHexString()); + console.log("Author:", author, "Key:", authorKey.toHexString()); + } + } + + function _isTestMode() internal pure virtual returns (bool) { + return false; } function _uploadByteCode(bytes memory _initCode, bytes32 _contractType, uint256 _version) diff --git a/contracts/test/helpers/CCGHelper.sol b/contracts/test/helpers/CCGHelper.sol index 8bc6dd8..139ca87 100644 --- a/contracts/test/helpers/CCGHelper.sol +++ b/contracts/test/helpers/CCGHelper.sol @@ -40,11 +40,17 @@ contract CCGHelper is SignatureHelper { signer2 = vm.rememberKey(signer2Key); dao = vm.rememberKey(_generatePrivateKey("DAO")); - // Print debug info - console.log("Cross chain multisig setup:"); - console.log("Signer 1:", signer1, "Key:", signer1Key.toHexString()); - console.log("Signer 2:", signer2, "Key:", signer2Key.toHexString()); - console.log("DAO:", dao); + if (!_isTestMode()) { + // Print debug info + console.log("Cross chain multisig setup:"); + console.log("Signer 1:", signer1, "Key:", signer1Key.toHexString()); + console.log("Signer 2:", signer2, "Key:", signer2Key.toHexString()); + console.log("DAO:", dao); + } + } + + function _isTestMode() internal pure virtual returns (bool) { + return false; } function _setUpCCG() internal { @@ -107,27 +113,32 @@ contract CCGHelper is SignatureHelper { ) ); - console.log("tt"); - console.logBytes32(structHash); + if (!_isTestMode()) { + console.log("tt"); + console.logBytes32(structHash); + } bytes memory signature1 = _sign(signer1Key, ECDSA.toTypedDataHash(_ccmDomainSeparator(), structHash)); multisig.signProposal(proposalHash, signature1); - - console.log("== SIGNER 1 =="); - console.log("name", currentProposal.name); - console.log("proposalHash"); - console.logBytes32(proposalHash); - console.log("prevHash"); - console.logBytes32(currentProposal.prevHash); - console.log(signature1.toHexString()); + if (!_isTestMode()) { + console.log("== SIGNER 1 =="); + console.log("name", currentProposal.name); + console.log("proposalHash"); + console.logBytes32(proposalHash); + console.log("prevHash"); + console.logBytes32(currentProposal.prevHash); + console.log(signature1.toHexString()); + } bytes memory signature2 = _sign(signer2Key, ECDSA.toTypedDataHash(_ccmDomainSeparator(), structHash)); multisig.signProposal(proposalHash, signature2); - console.log("== SIGNER 2=="); - console.log("name", currentProposal.name); - console.log(signature2.toHexString()); + if (!_isTestMode()) { + console.log("== SIGNER 2=="); + console.log("name", currentProposal.name); + console.log(signature2.toHexString()); + } prevProposalHash = proposalHash; } diff --git a/contracts/test/helpers/InstanceManagerHelper.sol b/contracts/test/helpers/InstanceManagerHelper.sol index e69fedf..2796500 100644 --- a/contracts/test/helpers/InstanceManagerHelper.sol +++ b/contracts/test/helpers/InstanceManagerHelper.sol @@ -35,8 +35,14 @@ contract InstanceManagerHelper is BCRHelpers, CCGHelper { instanceOwnerKey = _generatePrivateKey("INSTANCE_OWNER"); instanceOwner = vm.rememberKey(instanceOwnerKey); - console.log("Instance owner setup:"); - console.log("Instance owner:", instanceOwner, "Key:", instanceOwnerKey.toHexString()); + if (!_isTestMode()) { + console.log("Instance owner setup:"); + console.log("Instance owner:", instanceOwner, "Key:", instanceOwnerKey.toHexString()); + } + } + + function _isTestMode() internal pure virtual override(CCGHelper, BCRHelpers) returns (bool) { + return false; } function _setUpInstanceManager() internal { diff --git a/contracts/test/mocks/MockCreditConfiguratorPatch.sol b/contracts/test/mocks/MockCreditConfiguratorPatch.sol new file mode 100644 index 0000000..89f9ae2 --- /dev/null +++ b/contracts/test/mocks/MockCreditConfiguratorPatch.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {CreditConfiguratorV3} from "@gearbox-protocol/core-v3/contracts/credit/CreditConfiguratorV3.sol"; +import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; + +contract MockCreditConfiguratorPatch is CreditConfiguratorV3, Test { + constructor(address _acl, address _creditManager) CreditConfiguratorV3(_acl, _creditManager) { + vm.mockCall(address(this), abi.encodeCall(IVersion.version, ()), abi.encode(311)); + } +} From 8d4105dee74062a0758b3a42a41e7ec9568c0c27 Mon Sep 17 00:00:00 2001 From: Van0k Date: Thu, 6 Feb 2025 13:13:22 +0400 Subject: [PATCH 2/7] feat: rate keeper / irm tests + credit suite test improvements --- contracts/helpers/DefaultLossPolicy.sol | 2 +- .../legacy/MarketConfiguratorLegacy.sol | 2 +- .../configuration/ConfigurationTestHelper.sol | 6 +- .../CreditSuiteConfiguration.unit.t.sol | 107 ++++++++++++++++-- .../InterestRateModelConfiguration.unit.t.sol | 41 +++++++ .../LossPolicyConfiguration.unit.t.sol | 40 +++++++ contracts/test/mocks/MockIRM.sol | 25 ++++ 7 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 contracts/test/configuration/InterestRateModelConfiguration.unit.t.sol create mode 100644 contracts/test/configuration/LossPolicyConfiguration.unit.t.sol create mode 100644 contracts/test/mocks/MockIRM.sol diff --git a/contracts/helpers/DefaultLossPolicy.sol b/contracts/helpers/DefaultLossPolicy.sol index f6144df..c9138cd 100644 --- a/contracts/helpers/DefaultLossPolicy.sol +++ b/contracts/helpers/DefaultLossPolicy.sol @@ -12,7 +12,7 @@ contract DefaultLossPolicy is ACLTrait { bool public enabled; - constructor(address acl_) ACLTrait(acl_) {} + constructor(address pool, address) ACLTrait(ACLTrait(pool).acl()) {} function isLiquidatable(address, address, bytes calldata) external view returns (bool) { return enabled; diff --git a/contracts/market/legacy/MarketConfiguratorLegacy.sol b/contracts/market/legacy/MarketConfiguratorLegacy.sol index dddd838..26d9bfe 100644 --- a/contracts/market/legacy/MarketConfiguratorLegacy.sol +++ b/contracts/market/legacy/MarketConfiguratorLegacy.sol @@ -152,7 +152,7 @@ contract MarketConfiguratorLegacy is MarketConfigurator { address priceOracle = _priceOracle(creditManagers[0]); address interestRateModel = _interestRateModel(pool); address rateKeeper = _rateKeeper(quotaKeeper); - address lossPolicy = address(new DefaultLossPolicy(acl)); + address lossPolicy = address(new DefaultLossPolicy(pool, address(0))); _createMarket(pool, quotaKeeper, priceOracle, interestRateModel, rateKeeper, lossPolicy); diff --git a/contracts/test/configuration/ConfigurationTestHelper.sol b/contracts/test/configuration/ConfigurationTestHelper.sol index 1c45716..1d9654b 100644 --- a/contracts/test/configuration/ConfigurationTestHelper.sol +++ b/contracts/test/configuration/ConfigurationTestHelper.sol @@ -143,7 +143,7 @@ contract ConfigurationTestHelper is Test, GlobalSetup { IERC20(WETH).transfer(poolFactory, 1e18); - address _pool = MarketConfigurator(marketConfigurator).previewCreateMarket(3_10, WETH, name, symbol); + address _pool = marketConfigurator.previewCreateMarket(3_10, WETH, name, symbol); DeployParams memory interestRateModelParams = DeployParams({ postfix: "LINEAR", @@ -156,7 +156,7 @@ contract ConfigurationTestHelper is Test, GlobalSetup { DeployParams({postfix: "DEFAULT", salt: 0, constructorParams: abi.encode(_pool, addressProvider)}); vm.prank(admin); - _pool = MarketConfigurator(marketConfigurator).createMarket({ + _pool = marketConfigurator.createMarket({ minorVersion: 3_10, underlying: WETH, name: name, @@ -193,7 +193,7 @@ contract ConfigurationTestHelper is Test, GlobalSetup { bytes memory creditSuiteParams = abi.encode(creditManagerParams, facadeParams); vm.prank(admin); - return MarketConfigurator(marketConfigurator).createCreditSuite(3_10, address(pool), creditSuiteParams); + return marketConfigurator.createCreditSuite(3_10, address(pool), creditSuiteParams); } function _addUSDC() internal { diff --git a/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol b/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol index c363038..fbae4f7 100644 --- a/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol +++ b/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol @@ -18,6 +18,7 @@ import {DeployParams} from "../../interfaces/Types.sol"; import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; import {IContractsRegister} from "../../interfaces/IContractsRegister.sol"; import {IBytecodeRepository} from "../../interfaces/IBytecodeRepository.sol"; +import {IMarketConfigurator} from "../../interfaces/IMarketConfigurator.sol"; import { NO_VERSION_CONTROL, AP_BYTECODE_REPOSITORY, @@ -39,11 +40,13 @@ import { contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { address target; + address creditFactory; function setUp() public override { super.setUp(); target = address(new GeneralMock()); + creditFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_CREDIT_FACTORY, 3_10); } function _uploadCreditConfiguratorPatch() internal { @@ -68,7 +71,6 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { address bytecodeRepository = IAddressProvider(addressProvider).getAddressOrRevert(AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL); - address creditFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_CREDIT_FACTORY, 3_10); address expectedAdapter = IBytecodeRepository(bytecodeRepository).computeAddress( "ADAPTER::BALANCER_VAULT", @@ -78,6 +80,13 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { creditFactory ); + // Expect factory authorization and adapter allowance + vm.expectCall( + address(marketConfigurator), + abi.encodeCall( + IMarketConfigurator.authorizeFactory, (creditFactory, address(creditManager), expectedAdapter) + ) + ); vm.expectCall( address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.allowAdapter, (expectedAdapter)) ); @@ -87,7 +96,12 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { address(creditManager), abi.encodeCall(ICreditConfigureActions.allowAdapter, (params)) ); + // Verify adapter is allowed and factory is authorized assertEq(ICreditManagerV3(creditManager).adapterToContract(expectedAdapter), target, "Adapter must be allowed"); + assertTrue( + IMarketConfigurator(marketConfigurator).getAuthorizedFactory(expectedAdapter) == creditFactory, + "Factory must be authorized" + ); } function test_CS_02_forbidAdapter() public { @@ -97,21 +111,32 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { constructorParams: abi.encode(address(creditManager), target) }); - vm.startPrank(admin); + // First allow adapter + vm.prank(admin); marketConfigurator.configureCreditSuite( address(creditManager), abi.encodeCall(ICreditConfigureActions.allowAdapter, (params)) ); address adapter = ICreditManagerV3(creditManager).contractToAdapter(target); + // Expect factory unauthorized and adapter forbidden + vm.expectCall( + address(marketConfigurator), + abi.encodeCall(IMarketConfigurator.unauthorizeFactory, (creditFactory, address(creditManager), adapter)) + ); vm.expectCall(address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.forbidAdapter, (adapter))); + vm.prank(admin); marketConfigurator.configureCreditSuite( address(creditManager), abi.encodeCall(ICreditConfigureActions.forbidAdapter, (adapter)) ); - vm.stopPrank(); - assertEq(ICreditManagerV3(creditManager).contractToAdapter(target), address(0), "Adapter must not be allowed"); + // Verify adapter is forbidden and factory is unauthorized + assertEq(ICreditManagerV3(creditManager).contractToAdapter(target), address(0), "Adapter must be forbidden"); + assertTrue( + IMarketConfigurator(marketConfigurator).getAuthorizedFactory(adapter) == address(0), + "Factory must be unauthorized" + ); } function test_CS_03_setFees() public { @@ -230,7 +255,6 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { address bytecodeRepository = IAddressProvider(addressProvider).getAddressOrRevert(AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL); - address creditFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_CREDIT_FACTORY, 3_10); // Compute expected new configurator address address expectedNewConfigurator = IBytecodeRepository(bytecodeRepository).computeAddress( @@ -241,7 +265,20 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { creditFactory ); - // Expect call to current configurator to upgrade + // Expect factory authorization/unauthorized and configurator upgrade + vm.expectCall( + address(marketConfigurator), + abi.encodeCall( + IMarketConfigurator.unauthorizeFactory, + (creditFactory, address(creditManager), address(creditConfigurator)) + ) + ); + vm.expectCall( + address(marketConfigurator), + abi.encodeCall( + IMarketConfigurator.authorizeFactory, (creditFactory, address(creditManager), expectedNewConfigurator) + ) + ); vm.expectCall( address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.upgradeCreditConfigurator, (expectedNewConfigurator)) @@ -252,24 +289,32 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { address(creditManager), abi.encodeCall(ICreditConfigureActions.upgradeCreditConfigurator, ()) ); - // Verify configurator was upgraded + // Verify configurator was upgraded and factory authorization was transferred assertEq( ICreditManagerV3(creditManager).creditConfigurator(), expectedNewConfigurator, "Credit configurator must be upgraded" ); + assertTrue( + IMarketConfigurator(marketConfigurator).getAuthorizedFactory(expectedNewConfigurator) == creditFactory, + "Factory must be authorized for new configurator" + ); + assertTrue( + IMarketConfigurator(marketConfigurator).getAuthorizedFactory(address(creditConfigurator)) == address(0), + "Factory must be unauthorized for old configurator" + ); } function test_CS_08_upgradeCreditFacade() public { address bytecodeRepository = IAddressProvider(addressProvider).getAddressOrRevert(AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL); - address creditFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_CREDIT_FACTORY, 3_10); CreditFacadeParams memory params = CreditFacadeParams({degenNFT: address(0), expirable: true, migrateBotList: true}); address contractsRegister = marketConfigurator.contractsRegister(); address lossPolicy = IContractsRegister(contractsRegister).getLossPolicy(address(pool)); + address oldFacade = ICreditManagerV3(creditManager).creditFacade(); // Compute expected new facade address address expectedNewFacade = IBytecodeRepository(bytecodeRepository).computeAddress( @@ -279,7 +324,7 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { marketConfigurator.acl(), address(creditManager), lossPolicy, - ICreditFacadeV3(creditFacade).botList(), + ICreditFacadeV3(oldFacade).botList(), WETH, params.degenNFT, params.expirable @@ -288,7 +333,17 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { creditFactory ); - // Expect call to configurator to set new facade + // Expect factory authorization/unauthorized and facade upgrade + vm.expectCall( + address(marketConfigurator), + abi.encodeCall(IMarketConfigurator.unauthorizeFactory, (creditFactory, address(creditManager), oldFacade)) + ); + vm.expectCall( + address(marketConfigurator), + abi.encodeCall( + IMarketConfigurator.authorizeFactory, (creditFactory, address(creditManager), expectedNewFacade) + ) + ); vm.expectCall( address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.setCreditFacade, (expectedNewFacade, true)) @@ -299,8 +354,24 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { address(creditManager), abi.encodeCall(ICreditConfigureActions.upgradeCreditFacade, (params)) ); - // Verify facade was upgraded + // Verify facade was upgraded and factory authorization was transferred assertEq(ICreditManagerV3(creditManager).creditFacade(), expectedNewFacade, "Credit facade must be upgraded"); + assertTrue( + IMarketConfigurator(marketConfigurator).getAuthorizedFactory(expectedNewFacade) == creditFactory, + "Factory must be authorized for new facade" + ); + assertTrue( + IMarketConfigurator(marketConfigurator).getAuthorizedFactory(oldFacade) == address(0), + "Factory must be unauthorized for old facade" + ); + + // Verify it reverts when trying to use unregistered degenNFT + params.degenNFT = address(1); + vm.expectRevert(abi.encodeWithSelector(CreditFactory.DegenNFTIsNotRegisteredException.selector, address(1))); + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.upgradeCreditFacade, (params)) + ); } function test_CS_09_configureAdapter() public { @@ -342,6 +413,20 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { // Verify pool was allowed assertTrue(IUniswapV3Adapter(adapter).isPoolAllowed(WETH, USDC, 500), "Pool must be allowed"); + + // Verify it reverts when trying to configure non-existent adapter + address nonExistentTarget = address(1); + vm.expectRevert( + abi.encodeWithSelector(CreditFactory.TargetContractIsNotAllowedException.selector, nonExistentTarget) + ); + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), + abi.encodeCall( + ICreditConfigureActions.configureAdapterFor, + (nonExistentTarget, abi.encodeCall(IUniswapV3Adapter.setPoolStatusBatch, (pools))) + ) + ); } function test_CS_10_pause_unpause() public { diff --git a/contracts/test/configuration/InterestRateModelConfiguration.unit.t.sol b/contracts/test/configuration/InterestRateModelConfiguration.unit.t.sol new file mode 100644 index 0000000..e0d3e02 --- /dev/null +++ b/contracts/test/configuration/InterestRateModelConfiguration.unit.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {IContractsRegister} from "../../interfaces/IContractsRegister.sol"; +import {DeployParams} from "../../interfaces/Types.sol"; +import {MockIRM} from "../mocks/MockIRM.sol"; +import {GeneralMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/GeneralMock.sol"; +import {CrossChainCall} from "../helpers/GlobalSetup.sol"; + +contract InterestRateModelConfigurationUnitTest is ConfigurationTestHelper { + function setUp() public override { + super.setUp(); + } + + /// REGULAR CONFIGURATION TESTS /// + + function test_IRM_01_configure() public { + CrossChainCall[] memory calls = new CrossChainCall[](1); + bytes32 bytecodeHash = _uploadByteCodeAndSign(type(MockIRM).creationCode, "IRM::MOCK", 3_10); + calls[0] = _generateAllowSystemContractCall(bytecodeHash); + _submitProposalAndSign("Allow system contracts", calls); + + vm.prank(admin); + address newIRM = marketConfigurator.updateInterestRateModel( + address(pool), + DeployParams({postfix: "MOCK", salt: 0, constructorParams: abi.encode(address(pool), addressProvider)}) + ); + + bytes memory arbitraryData = abi.encodeCall(MockIRM.setFlag, (true)); + + vm.expectCall(newIRM, arbitraryData); + + vm.prank(admin); + marketConfigurator.configureInterestRateModel(address(pool), arbitraryData); + + assertTrue(MockIRM(payable(newIRM)).flag(), "IRM flag must be true"); + } +} diff --git a/contracts/test/configuration/LossPolicyConfiguration.unit.t.sol b/contracts/test/configuration/LossPolicyConfiguration.unit.t.sol new file mode 100644 index 0000000..8b0a51d --- /dev/null +++ b/contracts/test/configuration/LossPolicyConfiguration.unit.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {ILossPolicy} from "@gearbox-protocol/core-v3/contracts/interfaces/base/ILossPolicy.sol"; +import {DefaultLossPolicy} from "../../helpers/DefaultLossPolicy.sol"; +import {IContractsRegister} from "../../interfaces/IContractsRegister.sol"; + +contract LossPolicyConfigurationUnitTest is ConfigurationTestHelper { + address private _lossPolicy; + + function setUp() public override { + super.setUp(); + _lossPolicy = IContractsRegister(marketConfigurator.contractsRegister()).getLossPolicy(address(pool)); + } + + /// REGULAR CONFIGURATION TESTS /// + + function test_LP_01_configure() public { + vm.expectCall(_lossPolicy, abi.encodeCall(ILossPolicy.enable, ())); + + vm.prank(admin); + marketConfigurator.configureLossPolicy(address(pool), abi.encodeCall(ILossPolicy.enable, ())); + + assertTrue(DefaultLossPolicy(_lossPolicy).enabled(), "Loss policy must be enabled"); + } + + /// EMERGENCY CONFIGURATION TESTS /// + + function test_LP_02_emergency_configure() public { + vm.expectCall(_lossPolicy, abi.encodeCall(ILossPolicy.enable, ())); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigureLossPolicy(address(pool), abi.encodeCall(ILossPolicy.enable, ())); + + assertTrue(DefaultLossPolicy(_lossPolicy).enabled(), "Loss policy must be enabled"); + } +} diff --git a/contracts/test/mocks/MockIRM.sol b/contracts/test/mocks/MockIRM.sol new file mode 100644 index 0000000..bf39941 --- /dev/null +++ b/contracts/test/mocks/MockIRM.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +contract MockIRM { + uint256 public constant version = 3_10; + bytes32 public constant contractType = "IRM::MOCK"; + + bool public flag = false; + + constructor(address pool_, address) {} + + function availableToBorrow(uint256, uint256) external pure returns (uint256) { + return 0; + } + + function calcBorrowRate(uint256, uint256, bool) external pure returns (uint256) { + return 0; + } + + function setFlag(bool flag_) external { + flag = flag_; + } +} From d42d3656574ce861cc0ca5b2479e2aa0afd2e4ae Mon Sep 17 00:00:00 2001 From: Van0k Date: Thu, 6 Feb 2025 13:48:31 +0400 Subject: [PATCH 3/7] feat: improved price oracle and pool configuration tests --- .../PoolConfiguration.unit.t.sol | 15 ++- .../PriceOracleConfiguration.unit.t.sol | 112 +++++++++++++++++- 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/contracts/test/configuration/PoolConfiguration.unit.t.sol b/contracts/test/configuration/PoolConfiguration.unit.t.sol index cba069c..9825890 100644 --- a/contracts/test/configuration/PoolConfiguration.unit.t.sol +++ b/contracts/test/configuration/PoolConfiguration.unit.t.sol @@ -6,13 +6,17 @@ pragma solidity ^0.8.23; import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {PoolFactory} from "../../factories/PoolFactory.sol"; +import {IContractsRegister} from "../../interfaces/IContractsRegister.sol"; import {IPoolConfigureActions} from "../../interfaces/factories/IPoolConfigureActions.sol"; import {IPoolEmergencyConfigureActions} from "../../interfaces/factories/IPoolEmergencyConfigureActions.sol"; +import {IPriceOracleConfigureActions} from "../../interfaces/factories/IPriceOracleConfigureActions.sol"; import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; import {IPoolQuotaKeeperV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolQuotaKeeperV3.sol"; import {IPriceOracleV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol"; import {GeneralMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/GeneralMock.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v3/contracts/libraries/Constants.sol"; +import {ZeroPriceFeed} from "@gearbox-protocol/oracles-v3/contracts/oracles/ZeroPriceFeed.sol"; contract PoolConfigurationUnitTest is ConfigurationTestHelper { address private _target; @@ -21,7 +25,6 @@ contract PoolConfigurationUnitTest is ConfigurationTestHelper { function setUp() public override { super.setUp(); - _target = address(new GeneralMock()); _quotaKeeper = IPoolV3(pool).poolQuotaKeeper(); } @@ -71,6 +74,16 @@ contract PoolConfigurationUnitTest is ConfigurationTestHelper { (,,,, uint96 tokenLimit,) = IPoolQuotaKeeperV3(_quotaKeeper).getTokenQuotaParams(token); assertEq(tokenLimit, limit, "Incorrect token limit"); + + // Test that it reverts when trying to set non-zero limit for token with zero price + address priceOracle = IContractsRegister(marketConfigurator.contractsRegister()).getPriceOracle(address(pool)); + vm.mockCall(priceOracle, abi.encodeCall(IPriceOracleV3.getPrice, (token)), abi.encode(0)); + vm.expectRevert(abi.encodeWithSelector(PoolFactory.ZeroPriceFeedException.selector, token)); + + vm.prank(admin); + marketConfigurator.configurePool( + address(pool), abi.encodeCall(IPoolConfigureActions.setTokenLimit, (token, limit + 1)) + ); } function test_P_04_setTokenQuotaIncreaseFee() public { diff --git a/contracts/test/configuration/PriceOracleConfiguration.unit.t.sol b/contracts/test/configuration/PriceOracleConfiguration.unit.t.sol index fd5654a..20f065c 100644 --- a/contracts/test/configuration/PriceOracleConfiguration.unit.t.sol +++ b/contracts/test/configuration/PriceOracleConfiguration.unit.t.sol @@ -4,16 +4,25 @@ pragma solidity ^0.8.23; import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {PriceOracleFactory} from "../../factories/PriceOracleFactory.sol"; import {IPriceOracleConfigureActions} from "../../interfaces/factories/IPriceOracleConfigureActions.sol"; import {IPriceOracleEmergencyConfigureActions} from "../../interfaces/factories/IPriceOracleEmergencyConfigureActions.sol"; import {IPriceOracleV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol"; +import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; import {IPriceFeedStore} from "../../interfaces/IPriceFeedStore.sol"; +import {IPoolQuotaKeeperV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolQuotaKeeperV3.sol"; import {MockPriceFeed} from "../mocks/MockPriceFeed.sol"; import {IContractsRegister} from "../../interfaces/IContractsRegister.sol"; import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; import {AP_PRICE_FEED_STORE, NO_VERSION_CONTROL} from "../../libraries/ContractLiterals.sol"; +/// @notice Errors from PriceOracleFactory +error PriceFeedNotAllowedException(address token, address priceFeed); +error PriceFeedAllowedTooRecentlyException(address token, address priceFeed); +error TokenIsNotAddedException(address token); +error ZeroPriceFeedException(address token); + contract PriceOracleConfigurationUnitTest is ConfigurationTestHelper { address private _target; address private _priceFeedStore; @@ -36,6 +45,7 @@ contract PriceOracleConfigurationUnitTest is ConfigurationTestHelper { address priceFeed = address(new MockPriceFeed()); uint32 stalenessPeriod = 3600; + // Mock price feed store checks vm.mockCall( _priceFeedStore, abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (token, priceFeed)), abi.encode(true) ); @@ -54,6 +64,58 @@ contract PriceOracleConfigurationUnitTest is ConfigurationTestHelper { ); assertEq(IPriceOracleV3(_priceOracle).priceFeeds(token), priceFeed, "Incorrect price feed"); + + // Test that it reverts when price feed is not allowed + address notAllowedPriceFeed = address(new MockPriceFeed()); + vm.mockCall( + _priceFeedStore, + abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (token, notAllowedPriceFeed)), + abi.encode(false) + ); + + vm.expectRevert(abi.encodeWithSelector(PriceFeedNotAllowedException.selector, token, notAllowedPriceFeed)); + + vm.prank(admin); + marketConfigurator.configurePriceOracle( + address(pool), abi.encodeCall(IPriceOracleConfigureActions.setPriceFeed, (token, notAllowedPriceFeed)) + ); + + // Test that it reverts when token is not added to the market + address notAddedToken = address(1); + vm.mockCall( + _priceFeedStore, + abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (notAddedToken, priceFeed)), + abi.encode(true) + ); + + vm.expectRevert(abi.encodeWithSelector(TokenIsNotAddedException.selector, notAddedToken)); + + vm.prank(admin); + marketConfigurator.configurePriceOracle( + address(pool), abi.encodeCall(IPriceOracleConfigureActions.setPriceFeed, (notAddedToken, priceFeed)) + ); + + // Test that it reverts when price feed returns zero price for underlying + vm.mockCall(priceFeed, abi.encodeWithSignature("latestRoundData()"), abi.encode(0, 0, 0, 0, 0)); + + vm.expectRevert(abi.encodeWithSelector(ZeroPriceFeedException.selector, WETH)); + + vm.prank(admin); + marketConfigurator.configurePriceOracle( + address(pool), abi.encodeCall(IPriceOracleConfigureActions.setPriceFeed, (WETH, priceFeed)) + ); + address poolQuotaKeeper = IPoolV3(pool).poolQuotaKeeper(); + // Test that it reverts when price feed returns zero price for quoted token + vm.mockCall( + poolQuotaKeeper, abi.encodeCall(IPoolQuotaKeeperV3.getTokenQuotaParams, (token)), abi.encode(0, 0, 0, 1e18, 0, 0) + ); + + vm.expectRevert(abi.encodeWithSelector(ZeroPriceFeedException.selector, token)); + + vm.prank(admin); + marketConfigurator.configurePriceOracle( + address(pool), abi.encodeCall(IPriceOracleConfigureActions.setPriceFeed, (token, priceFeed)) + ); } function test_PO_02_setReservePriceFeed() public { @@ -61,10 +123,10 @@ contract PriceOracleConfigurationUnitTest is ConfigurationTestHelper { address priceFeed = address(new MockPriceFeed()); uint32 stalenessPeriod = 3600; + // Mock price feed store checks vm.mockCall( _priceFeedStore, abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (token, priceFeed)), abi.encode(true) ); - vm.mockCall( _priceFeedStore, abi.encodeCall(IPriceFeedStore.getStalenessPeriod, (priceFeed)), @@ -81,6 +143,37 @@ contract PriceOracleConfigurationUnitTest is ConfigurationTestHelper { ); assertEq(IPriceOracleV3(_priceOracle).reservePriceFeeds(token), priceFeed, "Incorrect reserve price feed"); + + // Test that it reverts when price feed is not allowed + address notAllowedPriceFeed = address(new MockPriceFeed()); + vm.mockCall( + _priceFeedStore, + abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (token, notAllowedPriceFeed)), + abi.encode(false) + ); + + vm.expectRevert(abi.encodeWithSelector(PriceFeedNotAllowedException.selector, token, notAllowedPriceFeed)); + + vm.prank(admin); + marketConfigurator.configurePriceOracle( + address(pool), + abi.encodeCall(IPriceOracleConfigureActions.setReservePriceFeed, (token, notAllowedPriceFeed)) + ); + + // Test that it reverts when token is not added to the market + address notAddedToken = address(1); + vm.mockCall( + _priceFeedStore, + abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (notAddedToken, priceFeed)), + abi.encode(true) + ); + + vm.expectRevert(abi.encodeWithSelector(TokenIsNotAddedException.selector, notAddedToken)); + + vm.prank(admin); + marketConfigurator.configurePriceOracle( + address(pool), abi.encodeCall(IPriceOracleConfigureActions.setReservePriceFeed, (notAddedToken, priceFeed)) + ); } /// EMERGENCY CONFIGURATION TESTS /// @@ -90,16 +183,15 @@ contract PriceOracleConfigurationUnitTest is ConfigurationTestHelper { address priceFeed = address(new MockPriceFeed()); uint32 stalenessPeriod = 3600; + // Mock price feed store checks vm.mockCall( _priceFeedStore, abi.encodeCall(IPriceFeedStore.isAllowedPriceFeed, (token, priceFeed)), abi.encode(true) ); - vm.mockCall( _priceFeedStore, abi.encodeCall(IPriceFeedStore.getStalenessPeriod, (priceFeed)), abi.encode(stalenessPeriod) ); - vm.mockCall( _priceFeedStore, abi.encodeCall(IPriceFeedStore.getAllowanceTimestamp, (token, priceFeed)), @@ -114,5 +206,19 @@ contract PriceOracleConfigurationUnitTest is ConfigurationTestHelper { ); assertEq(IPriceOracleV3(_priceOracle).priceFeeds(token), priceFeed, "Incorrect price feed"); + + // Test that it reverts when price feed was allowed too recently + vm.mockCall( + _priceFeedStore, + abi.encodeCall(IPriceFeedStore.getAllowanceTimestamp, (token, priceFeed)), + abi.encode(block.timestamp - 1 days + 1) + ); + + vm.expectRevert(abi.encodeWithSelector(PriceFeedAllowedTooRecentlyException.selector, token, priceFeed)); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigurePriceOracle( + address(pool), abi.encodeCall(IPriceOracleEmergencyConfigureActions.setPriceFeed, (token, priceFeed)) + ); } } From 9f0bb41c0422ac03decf5e067ecb5c3f10f2f2f2 Mon Sep 17 00:00:00 2001 From: Van0k Date: Thu, 6 Feb 2025 13:58:34 +0400 Subject: [PATCH 4/7] feat: revert DefaultLossPolicy change --- contracts/helpers/DefaultLossPolicy.sol | 2 +- .../legacy/MarketConfiguratorLegacy.sol | 2 +- .../configuration/ConfigurationTestHelper.sol | 15 ++++++++++- contracts/test/mocks/MockLossPolicy.sol | 25 +++++++++++++++++++ contracts/test/suite/NewChainDeploySuite.sol | 15 ++++++++++- 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 contracts/test/mocks/MockLossPolicy.sol diff --git a/contracts/helpers/DefaultLossPolicy.sol b/contracts/helpers/DefaultLossPolicy.sol index c9138cd..faa826e 100644 --- a/contracts/helpers/DefaultLossPolicy.sol +++ b/contracts/helpers/DefaultLossPolicy.sol @@ -12,7 +12,7 @@ contract DefaultLossPolicy is ACLTrait { bool public enabled; - constructor(address pool, address) ACLTrait(ACLTrait(pool).acl()) {} + constructor(address _acl) ACLTrait(_acl) {} function isLiquidatable(address, address, bytes calldata) external view returns (bool) { return enabled; diff --git a/contracts/market/legacy/MarketConfiguratorLegacy.sol b/contracts/market/legacy/MarketConfiguratorLegacy.sol index 26d9bfe..dddd838 100644 --- a/contracts/market/legacy/MarketConfiguratorLegacy.sol +++ b/contracts/market/legacy/MarketConfiguratorLegacy.sol @@ -152,7 +152,7 @@ contract MarketConfiguratorLegacy is MarketConfigurator { address priceOracle = _priceOracle(creditManagers[0]); address interestRateModel = _interestRateModel(pool); address rateKeeper = _rateKeeper(quotaKeeper); - address lossPolicy = address(new DefaultLossPolicy(pool, address(0))); + address lossPolicy = address(new DefaultLossPolicy(acl)); _createMarket(pool, quotaKeeper, priceOracle, interestRateModel, rateKeeper, lossPolicy); diff --git a/contracts/test/configuration/ConfigurationTestHelper.sol b/contracts/test/configuration/ConfigurationTestHelper.sol index 1d9654b..394a0d9 100644 --- a/contracts/test/configuration/ConfigurationTestHelper.sol +++ b/contracts/test/configuration/ConfigurationTestHelper.sol @@ -15,6 +15,7 @@ import {ICreditConfigureActions} from "../../factories/CreditFactory.sol"; import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; import {ERC20Mock} from "@gearbox-protocol/core-v3/contracts/test/mocks/token/ERC20Mock.sol"; import {MockPriceFeed} from "../mocks/MockPriceFeed.sol"; +import {MockLossPolicy} from "../mocks/MockLossPolicy.sol"; import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; import {ICreditManagerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol"; @@ -88,6 +89,8 @@ contract ConfigurationTestHelper is Test, GlobalSetup { _setupPriceFeedStore(); + _addMockLossPolicy(); + admin = makeAddr("admin"); emergencyAdmin = makeAddr("emergencyAdmin"); @@ -138,6 +141,16 @@ contract ConfigurationTestHelper is Test, GlobalSetup { _allowPriceFeed(USDC, CHAINLINK_USDC_USD); } + function _addMockLossPolicy() internal { + CrossChainCall[] memory calls = new CrossChainCall[](1); + + bytes32 bytecodeHash = _uploadByteCodeAndSign(type(MockLossPolicy).creationCode, "LOSS_POLICY::MOCK", 3_10); + + calls[0] = _generateAllowSystemContractCall(bytecodeHash); + + _submitProposalAndSign("Allow system contracts", calls); + } + function _deployTestPool() internal returns (address) { address poolFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_POOL_FACTORY, 3_10); @@ -153,7 +166,7 @@ contract ConfigurationTestHelper is Test, GlobalSetup { DeployParams memory rateKeeperParams = DeployParams({postfix: "TUMBLER", salt: 0, constructorParams: abi.encode(_pool, 7 days)}); DeployParams memory lossPolicyParams = - DeployParams({postfix: "DEFAULT", salt: 0, constructorParams: abi.encode(_pool, addressProvider)}); + DeployParams({postfix: "MOCK", salt: 0, constructorParams: abi.encode(_pool, addressProvider)}); vm.prank(admin); _pool = marketConfigurator.createMarket({ diff --git a/contracts/test/mocks/MockLossPolicy.sol b/contracts/test/mocks/MockLossPolicy.sol new file mode 100644 index 0000000..50bfa99 --- /dev/null +++ b/contracts/test/mocks/MockLossPolicy.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +contract MockLossPolicy { + uint256 public constant version = 3_10; + bytes32 public constant contractType = "LOSS_POLICY::MOCK"; + + bool public enabled; + + constructor(address pool, address addressProvider) {} + + function isLiquidatable(address, address, bytes calldata) external view returns (bool) { + return enabled; + } + + function enable() external { + enabled = true; + } + + function disable() external { + enabled = false; + } +} diff --git a/contracts/test/suite/NewChainDeploySuite.sol b/contracts/test/suite/NewChainDeploySuite.sol index 2fbba3e..17e54bb 100644 --- a/contracts/test/suite/NewChainDeploySuite.sol +++ b/contracts/test/suite/NewChainDeploySuite.sol @@ -70,6 +70,7 @@ import {DeployParams} from "../../interfaces/Types.sol"; import {CreditFacadeParams, CreditManagerParams} from "../../factories/CreditFactory.sol"; import {GlobalSetup} from "../../test/helpers/GlobalSetup.sol"; +import {MockLossPolicy} from "../../test/mocks/MockLossPolicy.sol"; contract NewChainDeploySuite is Test, GlobalSetup { address internal riskCurator; @@ -100,6 +101,18 @@ contract NewChainDeploySuite is Test, GlobalSetup { // Configure instance _setupPriceFeedStore(); riskCurator = vm.addr(_generatePrivateKey("RISK_CURATOR")); + + _addMockLossPolicy(); + } + + function _addMockLossPolicy() internal { + CrossChainCall[] memory calls = new CrossChainCall[](1); + + bytes32 bytecodeHash = _uploadByteCodeAndSign(type(MockLossPolicy).creationCode, "LOSS_POLICY::MOCK", 3_10); + + calls[0] = _generateAllowSystemContractCall(bytecodeHash); + + _submitProposalAndSign("Allow system contracts", calls); } function _setupPriceFeedStore() internal { @@ -141,7 +154,7 @@ contract NewChainDeploySuite is Test, GlobalSetup { DeployParams memory rateKeeperParams = DeployParams({postfix: "TUMBLER", salt: 0, constructorParams: abi.encode(pool, 7 days)}); DeployParams memory lossPolicyParams = - DeployParams({postfix: "DEFAULT", salt: 0, constructorParams: abi.encode(pool, ap)}); + DeployParams({postfix: "MOCK", salt: 0, constructorParams: abi.encode(pool, ap)}); gasBefore = gasleft(); From b96ece474a8dd1eca6e96ce8b7a76001e3e20e45 Mon Sep 17 00:00:00 2001 From: Van0k Date: Fri, 7 Feb 2025 17:04:46 +0400 Subject: [PATCH 5/7] feat: default degen nft --- contracts/helpers/DefaultDegenNFT.sol | 139 ++++ .../MarketConfigurator.unit.t.sol | 772 ++++++++++++++++++ .../test/helpers/DefaultDegenNFT.unit.t.sol | 177 ++++ 3 files changed, 1088 insertions(+) create mode 100644 contracts/helpers/DefaultDegenNFT.sol create mode 100644 contracts/test/configuration/MarketConfigurator.unit.t.sol create mode 100644 contracts/test/helpers/DefaultDegenNFT.unit.t.sol diff --git a/contracts/helpers/DefaultDegenNFT.sol b/contracts/helpers/DefaultDegenNFT.sol new file mode 100644 index 0000000..28d5b05 --- /dev/null +++ b/contracts/helpers/DefaultDegenNFT.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import {IDegenNFT} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IDegenNFT.sol"; +import {ICreditFacadeV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3.sol"; +import {ICreditManagerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol"; + +import {IContractsRegister} from "../interfaces/IContractsRegister.sol"; +import {IMarketConfigurator} from "../interfaces/IMarketConfigurator.sol"; + +contract DefaultDegenNFT is ERC721, IDegenNFT { + uint256 public constant override version = 3_10; + bytes32 public constant override contractType = "DEGEN_NFT::DEFAULT"; + + address public immutable marketConfigurator; + address public immutable contractsRegister; + address public minter; + + uint256 public totalSupply; + + string public baseURI; + + event SetMinter(address indexed newMinter); + + error CallerIsNotAdminException(address caller); + error CallerIsNotCreditFacadeOrEmergencyAdminException(address caller); + error CallerIsNotMinterException(address caller); + error NotImplementedException(); + + modifier onlyAdmin() { + _ensureCallerIsAdmin(); + _; + } + + modifier onlyMinter() { + _ensureCallerIsMinter(); + _; + } + + modifier onlyCreditFacadeOrEmergencyAdmin() { + _ensureCallerIsCreditFacadeOrEmergencyAdmin(); + _; + } + + constructor(address marketConfigurator_, string memory name_, string memory symbol_) ERC721(name_, symbol_) { + marketConfigurator = marketConfigurator_; + contractsRegister = IMarketConfigurator(marketConfigurator_).contractsRegister(); + } + + function _baseURI() internal view override returns (string memory) { + return baseURI; + } + + function tokenURI(uint256 tokenId) public view override returns (string memory) { + require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); + + return _baseURI(); + } + + function mint(address to, uint256 amount) external onlyMinter { + uint256 balanceBefore = balanceOf(to); + + for (uint256 i = 0; i < amount; ++i) { + uint256 tokenId = (uint256(uint160(to)) << 40) + balanceBefore + i; + _mint(to, tokenId); + } + + totalSupply += amount; + } + + function burn(address from, uint256 amount) external override onlyCreditFacadeOrEmergencyAdmin { + uint256 balance = balanceOf(from); + + for (uint256 i = 0; i < amount; ++i) { + uint256 tokenId = (uint256(uint160(from)) << 40) + balance - i - 1; + _burn(tokenId); + } + + totalSupply -= amount; + } + + function approve(address, uint256) public pure virtual override { + revert NotImplementedException(); + } + + function setApprovalForAll(address, bool) public pure virtual override { + revert NotImplementedException(); + } + + function transferFrom(address, address, uint256) public pure virtual override { + revert NotImplementedException(); + } + + function safeTransferFrom(address, address, uint256) public pure virtual override { + revert NotImplementedException(); + } + + function safeTransferFrom(address, address, uint256, bytes memory) public pure virtual override { + revert NotImplementedException(); + } + + function setMinter(address newMinter) external onlyAdmin { + if (newMinter == minter) return; + minter = newMinter; + emit SetMinter(newMinter); + } + + function setBaseUri(string calldata baseURI_) external onlyAdmin { + baseURI = baseURI_; + } + + function _ensureCallerIsAdmin() internal view { + if (msg.sender != IMarketConfigurator(marketConfigurator).admin()) revert CallerIsNotAdminException(msg.sender); + } + + function _ensureCallerIsCreditFacadeOrEmergencyAdmin() internal view { + if (msg.sender != IMarketConfigurator(marketConfigurator).emergencyAdmin() && !_callerIsCreditFacade()) { + revert CallerIsNotCreditFacadeOrEmergencyAdminException(msg.sender); + } + } + + function _callerIsCreditFacade() internal view returns (bool) { + address creditManager = ICreditFacadeV3(msg.sender).creditManager(); + if ( + ICreditManagerV3(creditManager).creditFacade() != msg.sender + || !IContractsRegister(contractsRegister).isCreditManager(creditManager) + ) return false; + + return true; + } + + function _ensureCallerIsMinter() internal view { + if (msg.sender != minter) revert CallerIsNotMinterException(msg.sender); + } +} diff --git a/contracts/test/configuration/MarketConfigurator.unit.t.sol b/contracts/test/configuration/MarketConfigurator.unit.t.sol new file mode 100644 index 0000000..ee4b77e --- /dev/null +++ b/contracts/test/configuration/MarketConfigurator.unit.t.sol @@ -0,0 +1,772 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {MarketConfigurator} from "../../market/MarketConfigurator.sol"; +import {IMarketConfigurator} from "../../interfaces/IMarketConfigurator.sol"; +import {MarketConfiguratorFactory} from "../../instance/MarketConfiguratorFactory.sol"; +import {IACL} from "../../interfaces/IACL.sol"; +import {IContractsRegister} from "../../interfaces/IContractsRegister.sol"; +import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; +import {IGovernor} from "../../interfaces/IGovernor.sol"; +import {IBytecodeRepository} from "../../interfaces/IBytecodeRepository.sol"; +import {GeneralMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/GeneralMock.sol"; +import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; +import {IPoolQuotaKeeperV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolQuotaKeeperV3.sol"; + +import { + AP_ACL, + AP_CONTRACTS_REGISTER, + AP_GOVERNOR, + AP_MARKET_CONFIGURATOR, + AP_TREASURY, + AP_TREASURY_SPLITTER, + AP_BYTECODE_REPOSITORY, + AP_MARKET_CONFIGURATOR_FACTORY, + NO_VERSION_CONTROL, + ROLE_PAUSABLE_ADMIN, + ROLE_UNPAUSABLE_ADMIN +} from "../../libraries/ContractLiterals.sol"; + +contract MarketConfiguratorUnitTest is ConfigurationTestHelper { + address public mcf; + address public treasury; + string constant CURATOR_NAME = "Test Curator"; + + function setUp() public override { + super.setUp(); + mcf = IAddressProvider(addressProvider).getAddressOrRevert(AP_MARKET_CONFIGURATOR_FACTORY, NO_VERSION_CONTROL); + } + + /// @notice Tests constructor deployment with governor, without treasury + function test_MC_01_constructor_with_governor() public { + // Compute future MC address + address expectedMC = IBytecodeRepository(bytecodeRepository).computeAddress( + AP_MARKET_CONFIGURATOR, + 3_10, + abi.encode(addressProvider, admin, emergencyAdmin, address(0), CURATOR_NAME, true), + bytes32(bytes20(admin)), + mcf + ); + + // Compute future governor address + address expectedGovernor = IBytecodeRepository(bytecodeRepository).computeAddress( + AP_GOVERNOR, 3_10, abi.encode(admin, emergencyAdmin, 1 days, false), bytes32(0), expectedMC + ); + + // Compute future ACL address + address expectedACL = IBytecodeRepository(bytecodeRepository).computeAddress( + AP_ACL, 3_10, abi.encode(expectedMC), bytes32(0), expectedMC + ); + + // Expect governor deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + (AP_GOVERNOR, 3_10, abi.encode(admin, emergencyAdmin, 1 days, false), bytes32(0)) + ) + ); + + // Expect ACL deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall(IBytecodeRepository.deploy, (AP_ACL, 3_10, abi.encode(expectedMC), bytes32(0))) + ); + + // Expect ContractsRegister deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, (AP_CONTRACTS_REGISTER, 3_10, abi.encode(expectedACL), bytes32(0)) + ) + ); + + vm.prank(admin); + address mc = MarketConfiguratorFactory(mcf).createMarketConfigurator( + emergencyAdmin, + address(0), + CURATOR_NAME, + true // deploy governor + ); + + assertEq(mc, expectedMC, "Incorrect market configurator address"); + + // Verify governor and admin setup + assertEq(MarketConfigurator(mc).admin(), IGovernor(expectedGovernor).timeLock(), "Incorrect admin"); + assertEq(MarketConfigurator(mc).emergencyAdmin(), emergencyAdmin, "Incorrect emergency admin"); + + // Verify treasury setup + assertEq( + MarketConfigurator(mc).treasury(), + IAddressProvider(addressProvider).getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL), + "Incorrect treasury" + ); + + // Verify roles + assertTrue( + IACL(MarketConfigurator(mc).acl()).hasRole(ROLE_PAUSABLE_ADMIN, mc), + "Market configurator must have pausable admin role" + ); + assertTrue( + IACL(MarketConfigurator(mc).acl()).hasRole(ROLE_UNPAUSABLE_ADMIN, mc), + "Market configurator must have unpausable admin role" + ); + } + + /// @notice Tests constructor deployment without governor, with treasury + function test_MC_02_constructor_without_governor() public { + address adminFeeTreasury = makeAddr("ADMIN_FEE_TREASURY"); + + // Compute future MC address + address expectedMC = IBytecodeRepository(bytecodeRepository).computeAddress( + AP_MARKET_CONFIGURATOR, + 3_10, + abi.encode(addressProvider, admin, emergencyAdmin, adminFeeTreasury, CURATOR_NAME, false), + bytes32(bytes20(admin)), + mcf + ); + + // Compute future ACL address + address expectedACL = IBytecodeRepository(bytecodeRepository).computeAddress( + AP_ACL, 3_10, abi.encode(expectedMC), bytes32(0), expectedMC + ); + + // Expect ACL deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall(IBytecodeRepository.deploy, (AP_ACL, 3_10, abi.encode(expectedMC), bytes32(0))) + ); + + // Expect ContractsRegister deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, (AP_CONTRACTS_REGISTER, 3_10, abi.encode(expectedACL), bytes32(0)) + ) + ); + + // Expect TreasurySplitter deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + (AP_TREASURY_SPLITTER, 3_10, abi.encode(addressProvider, admin, adminFeeTreasury), bytes32(0)) + ) + ); + + vm.prank(admin); + address mc = MarketConfiguratorFactory(mcf).createMarketConfigurator( + emergencyAdmin, + adminFeeTreasury, + CURATOR_NAME, + false // don't deploy governor + ); + + assertEq(mc, expectedMC, "Incorrect market configurator address"); + + // Verify admin setup + assertEq(MarketConfigurator(mc).admin(), admin, "Incorrect admin"); + assertEq(MarketConfigurator(mc).emergencyAdmin(), emergencyAdmin, "Incorrect emergency admin"); + + // Verify treasury deployment + address expectedTreasury = MarketConfigurator(mc).treasury(); + assertTrue(expectedTreasury.code.length > 0, "Treasury must be deployed"); + + // Verify roles + assertTrue( + IACL(MarketConfigurator(mc).acl()).hasRole(ROLE_PAUSABLE_ADMIN, mc), + "Market configurator must have pausable admin role" + ); + assertTrue( + IACL(MarketConfigurator(mc).acl()).hasRole(ROLE_UNPAUSABLE_ADMIN, mc), + "Market configurator must have unpausable admin role" + ); + } + + /// @notice Tests setting emergency admin + function test_MC_04_setEmergencyAdmin() public { + address newEmergencyAdmin = makeAddr("NEW_EMERGENCY_ADMIN"); + + // Test that only admin can set emergency admin + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.setEmergencyAdmin(newEmergencyAdmin); + + // Test successful emergency admin change + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.SetEmergencyAdmin(newEmergencyAdmin); + marketConfigurator.setEmergencyAdmin(newEmergencyAdmin); + + assertEq(marketConfigurator.emergencyAdmin(), newEmergencyAdmin, "Emergency admin not updated"); + } + + /// @notice Tests granting roles + function test_MC_05_grantRole() public { + bytes32 role = keccak256("TEST_ROLE"); + address account = makeAddr("ACCOUNT"); + + // Test that only admin can grant roles + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.grantRole(role, account); + + // Test successful role grant + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.GrantRole(role, account); + marketConfigurator.grantRole(role, account); + + assertTrue(IACL(marketConfigurator.acl()).hasRole(role, account), "Role not granted"); + } + + /// @notice Tests revoking roles + function test_MC_06_revokeRole() public { + bytes32 role = keccak256("TEST_ROLE"); + address account = makeAddr("ACCOUNT"); + + // Grant role first + vm.prank(admin); + marketConfigurator.grantRole(role, account); + + // Test that only admin can revoke roles + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.revokeRole(role, account); + + // Test successful role revocation + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.RevokeRole(role, account); + marketConfigurator.revokeRole(role, account); + + assertFalse(IACL(marketConfigurator.acl()).hasRole(role, account), "Role not revoked"); + } + + /// @notice Tests emergency role revocation + function test_MC_07_emergencyRevokeRole() public { + bytes32 role = keccak256("TEST_ROLE"); + address account = makeAddr("ACCOUNT"); + + // Grant role first + vm.prank(admin); + marketConfigurator.grantRole(role, account); + + // Test that only emergency admin can emergency revoke roles + vm.expectRevert( + abi.encodeWithSelector(IMarketConfigurator.CallerIsNotEmergencyAdminException.selector, address(this)) + ); + marketConfigurator.emergencyRevokeRole(role, account); + + // Test successful emergency role revocation + vm.prank(emergencyAdmin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.EmergencyRevokeRole(role, account); + marketConfigurator.emergencyRevokeRole(role, account); + + assertFalse(IACL(marketConfigurator.acl()).hasRole(role, account), "Role not revoked"); + } + + /// @notice Tests periphery contract management + function test_MC_08_periphery_contracts() public { + bytes32 domain = bytes32("TEST_DOMAIN"); + address peripheryContract = makeAddr("PERIPHERY_CONTRACT"); + + // Mock the periphery contract to return correct domain + vm.mockCall( + peripheryContract, + abi.encodeWithSignature("contractType()"), + abi.encode(bytes32(abi.encodePacked(domain, bytes16(0)))) + ); + + // Mock bytecode repository to recognize the contract + vm.mockCall( + bytecodeRepository, abi.encodeWithSignature("deployedContracts(address)", peripheryContract), abi.encode(1) + ); + + // Test that only admin can add periphery contracts + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.addPeripheryContract(peripheryContract); + + // Test successful periphery contract addition + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AddPeripheryContract(domain, peripheryContract); + marketConfigurator.addPeripheryContract(peripheryContract); + + // Verify contract was added + assertTrue(marketConfigurator.isPeripheryContract(domain, peripheryContract), "Contract not added"); + address[] memory contracts = marketConfigurator.getPeripheryContracts(domain); + assertEq(contracts.length, 1, "Incorrect number of contracts"); + assertEq(contracts[0], peripheryContract, "Incorrect contract address"); + + // Test adding same contract again (no event) + vm.prank(admin); + marketConfigurator.addPeripheryContract(peripheryContract); + + // Test that only admin can remove periphery contracts + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.removePeripheryContract(peripheryContract); + + // Test successful periphery contract removal + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.RemovePeripheryContract(domain, peripheryContract); + marketConfigurator.removePeripheryContract(peripheryContract); + + // Verify contract was removed + assertFalse(marketConfigurator.isPeripheryContract(domain, peripheryContract), "Contract not removed"); + contracts = marketConfigurator.getPeripheryContracts(domain); + assertEq(contracts.length, 0, "Contract list not empty"); + + // Test removing non-existent contract (no event) + vm.prank(admin); + marketConfigurator.removePeripheryContract(peripheryContract); + + // Test adding contract that's not in bytecode repository + vm.mockCall( + bytecodeRepository, abi.encodeWithSignature("deployedContracts(address)", peripheryContract), abi.encode(0) + ); + vm.prank(admin); + vm.expectRevert( + abi.encodeWithSelector(IMarketConfigurator.IncorrectPeripheryContractException.selector, peripheryContract) + ); + marketConfigurator.addPeripheryContract(peripheryContract); + + // Test adding contract that doesn't implement IVersion + address invalidContract = address(new GeneralMock()); + vm.mockCall( + bytecodeRepository, abi.encodeWithSignature("deployedContracts(address)", invalidContract), abi.encode(1) + ); + vm.prank(admin); + vm.expectRevert( + abi.encodeWithSelector(IMarketConfigurator.IncorrectPeripheryContractException.selector, invalidContract) + ); + marketConfigurator.addPeripheryContract(invalidContract); + } + + /// @notice Tests factory authorization + function test_MC_09_authorizeFactory() public { + address factory = makeAddr("FACTORY"); + address suite = makeAddr("SUITE"); + address target = makeAddr("TARGET"); + + // Test that only self can authorize factories + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotSelfException.selector, address(this))); + marketConfigurator.authorizeFactory(factory, suite, target); + + // Test successful factory authorization + vm.prank(address(marketConfigurator)); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(factory, suite, target); + marketConfigurator.authorizeFactory(factory, suite, target); + + // Verify factory was authorized + assertEq(marketConfigurator.getAuthorizedFactory(target), factory, "Factory not authorized"); + address[] memory targets = marketConfigurator.getFactoryTargets(factory, suite); + assertEq(targets.length, 1, "Incorrect number of targets"); + assertEq(targets[0], target, "Incorrect target address"); + + // Test authorizing already authorized target + vm.prank(address(marketConfigurator)); + vm.expectRevert( + abi.encodeWithSelector(IMarketConfigurator.UnauthorizedFactoryException.selector, factory, target) + ); + marketConfigurator.authorizeFactory(factory, suite, target); + } + + /// @notice Tests factory unauthorization + function test_MC_10_unauthorizeFactory() public { + address factory = makeAddr("FACTORY"); + address suite = makeAddr("SUITE"); + address target = makeAddr("TARGET"); + + // Authorize factory first + vm.prank(address(marketConfigurator)); + marketConfigurator.authorizeFactory(factory, suite, target); + + // Test that only self can unauthorize factories + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotSelfException.selector, address(this))); + marketConfigurator.unauthorizeFactory(factory, suite, target); + + // Test successful factory unauthorized + vm.prank(address(marketConfigurator)); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(factory, suite, target); + marketConfigurator.unauthorizeFactory(factory, suite, target); + + // Verify factory was unauthorized + assertEq(marketConfigurator.getAuthorizedFactory(target), address(0), "Factory not unauthorized"); + address[] memory targets = marketConfigurator.getFactoryTargets(factory, suite); + assertEq(targets.length, 0, "Target list not empty"); + + // Test unauthorized by wrong factory + address wrongFactory = makeAddr("WRONG_FACTORY"); + vm.prank(address(marketConfigurator)); + vm.expectRevert( + abi.encodeWithSelector(IMarketConfigurator.UnauthorizedFactoryException.selector, wrongFactory, target) + ); + marketConfigurator.unauthorizeFactory(wrongFactory, suite, target); + } + + /// @notice Tests pool factory upgrade function + function test_MC_11_upgradePoolFactory() public { + // Test that only admin can upgrade factories + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.upgradePoolFactory(address(pool)); + + // Test upgrading pool factory + address oldFactory = marketConfigurator.getMarketFactories(address(pool)).poolFactory; + address newFactory = makeAddr("NEW_FACTORY"); + address quotaKeeper = IPoolV3(pool).poolQuotaKeeper(); + + vm.mockCall(newFactory, abi.encodeWithSignature("version()"), abi.encode(3_11)); + vm.mockCall(newFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("POOL_FACTORY"))); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(newFactory, true); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(oldFactory, address(pool), address(pool)); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(newFactory, address(pool), address(pool)); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(oldFactory, address(pool), quotaKeeper); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(newFactory, address(pool), quotaKeeper); + + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpgradePoolFactory(address(pool), newFactory); + marketConfigurator.upgradePoolFactory(address(pool)); + + // Verify factory was upgraded and authorizations changed + assertEq( + marketConfigurator.getMarketFactories(address(pool)).poolFactory, newFactory, "Pool factory not upgraded" + ); + assertEq( + marketConfigurator.getAuthorizedFactory(address(pool)), newFactory, "Pool factory authorization not updated" + ); + assertEq( + marketConfigurator.getAuthorizedFactory(quotaKeeper), + newFactory, + "QuotaKeeper factory authorization not updated" + ); + + // Test upgrading from patch version + address patchFactory = makeAddr("PATCH_FACTORY"); + vm.mockCall(patchFactory, abi.encodeWithSignature("version()"), abi.encode(3_12)); + vm.mockCall(patchFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("POOL_FACTORY"))); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(patchFactory, true); + + vm.prank(admin); + marketConfigurator.upgradePoolFactory(address(pool)); + + assertEq( + marketConfigurator.getMarketFactories(address(pool)).poolFactory, patchFactory, "Pool factory not upgraded" + ); + } + + /// @notice Tests credit factory upgrade function + function test_MC_12_upgradeCreditFactory() public { + // Test that only admin can upgrade factories + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.upgradeCreditFactory(address(creditManager)); + + // Test upgrading credit factory + address oldFactory = marketConfigurator.getCreditFactory(address(creditManager)); + address newFactory = makeAddr("NEW_FACTORY"); + + vm.mockCall(newFactory, abi.encodeWithSignature("version()"), abi.encode(3_11)); + vm.mockCall(newFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("CREDIT_FACTORY"))); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(newFactory, true); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(oldFactory, address(creditManager), address(creditConfigurator)); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(newFactory, address(creditManager), address(creditConfigurator)); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(oldFactory, address(creditManager), address(creditFacade)); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(newFactory, address(creditManager), address(creditFacade)); + + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpgradeCreditFactory(address(creditManager), newFactory); + marketConfigurator.upgradeCreditFactory(address(creditManager)); + + // Verify factory was upgraded and authorizations changed + assertEq(marketConfigurator.getCreditFactory(address(creditManager)), newFactory, "Credit factory not upgraded"); + assertEq( + marketConfigurator.getAuthorizedFactory(address(creditConfigurator)), + newFactory, + "Configurator factory authorization not updated" + ); + assertEq( + marketConfigurator.getAuthorizedFactory(address(creditFacade)), + newFactory, + "Facade factory authorization not updated" + ); + + // Test upgrading from patch version + address patchFactory = makeAddr("PATCH_FACTORY"); + vm.mockCall(patchFactory, abi.encodeWithSignature("version()"), abi.encode(3_12)); + vm.mockCall(patchFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("CREDIT_FACTORY"))); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(patchFactory, true); + + vm.prank(admin); + marketConfigurator.upgradeCreditFactory(address(creditManager)); + + assertEq( + marketConfigurator.getCreditFactory(address(creditManager)), patchFactory, "Credit factory not upgraded" + ); + } + + /// @notice Tests price oracle factory upgrade function + function test_MC_13_upgradePriceOracleFactory() public { + // Test that only admin can upgrade factories + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.upgradePriceOracleFactory(address(pool)); + + // Test upgrading price oracle factory + address oldFactory = marketConfigurator.getMarketFactories(address(pool)).priceOracleFactory; + address newFactory = makeAddr("NEW_FACTORY"); + address priceOracle = IContractsRegister(marketConfigurator.contractsRegister()).getPriceOracle(address(pool)); + + vm.mockCall(newFactory, abi.encodeWithSignature("version()"), abi.encode(3_11)); + vm.mockCall(newFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("PRICE_ORACLE_FACTORY"))); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(newFactory, true); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(oldFactory, address(pool), priceOracle); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(newFactory, address(pool), priceOracle); + + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpgradePriceOracleFactory(address(pool), newFactory); + marketConfigurator.upgradePriceOracleFactory(address(pool)); + + // Verify factory was upgraded and authorizations changed + assertEq( + marketConfigurator.getMarketFactories(address(pool)).priceOracleFactory, + newFactory, + "Price oracle factory not upgraded" + ); + assertEq( + marketConfigurator.getAuthorizedFactory(priceOracle), + newFactory, + "Price oracle factory authorization not updated" + ); + + // Test upgrading from patch version + address patchFactory = makeAddr("PATCH_FACTORY"); + vm.mockCall(patchFactory, abi.encodeWithSignature("version()"), abi.encode(3_12)); + vm.mockCall( + patchFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("PRICE_ORACLE_FACTORY")) + ); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(patchFactory, true); + + vm.prank(admin); + marketConfigurator.upgradePriceOracleFactory(address(pool)); + + assertEq( + marketConfigurator.getMarketFactories(address(pool)).priceOracleFactory, + patchFactory, + "Price oracle factory not upgraded" + ); + } + + /// @notice Tests interest rate model factory upgrade function + function test_MC_14_upgradeInterestRateModelFactory() public { + // Test that only admin can upgrade factories + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.upgradeInterestRateModelFactory(address(pool)); + + // Test upgrading interest rate model factory + address oldFactory = marketConfigurator.getMarketFactories(address(pool)).interestRateModelFactory; + address newFactory = makeAddr("NEW_FACTORY"); + address interestRateModel = IPoolV3(pool).interestRateModel(); + + vm.mockCall(newFactory, abi.encodeWithSignature("version()"), abi.encode(3_11)); + vm.mockCall( + newFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("INTEREST_RATE_MODEL_FACTORY")) + ); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(newFactory, true); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(oldFactory, address(pool), interestRateModel); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(newFactory, address(pool), interestRateModel); + + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpgradeInterestRateModelFactory(address(pool), newFactory); + marketConfigurator.upgradeInterestRateModelFactory(address(pool)); + + // Verify factory was upgraded and authorizations changed + assertEq( + marketConfigurator.getMarketFactories(address(pool)).interestRateModelFactory, + newFactory, + "Interest rate model factory not upgraded" + ); + assertEq( + marketConfigurator.getAuthorizedFactory(interestRateModel), + newFactory, + "Interest rate model factory authorization not updated" + ); + + // Test upgrading from patch version + address patchFactory = makeAddr("PATCH_FACTORY"); + vm.mockCall(patchFactory, abi.encodeWithSignature("version()"), abi.encode(3_12)); + vm.mockCall( + patchFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("INTEREST_RATE_MODEL_FACTORY")) + ); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(patchFactory, true); + + vm.prank(admin); + marketConfigurator.upgradeInterestRateModelFactory(address(pool)); + + assertEq( + marketConfigurator.getMarketFactories(address(pool)).interestRateModelFactory, + patchFactory, + "Interest rate model factory not upgraded" + ); + } + + /// @notice Tests rate keeper factory upgrade function + function test_MC_15_upgradeRateKeeperFactory() public { + // Test that only admin can upgrade factories + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.upgradeRateKeeperFactory(address(pool)); + + // Test upgrading rate keeper factory + address oldFactory = marketConfigurator.getMarketFactories(address(pool)).rateKeeperFactory; + address newFactory = makeAddr("NEW_FACTORY"); + address quotaKeeper = IPoolV3(pool).poolQuotaKeeper(); + address rateKeeper = IPoolQuotaKeeperV3(quotaKeeper).gauge(); + + vm.mockCall(newFactory, abi.encodeWithSignature("version()"), abi.encode(3_11)); + vm.mockCall(newFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("RATE_KEEPER_FACTORY"))); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(newFactory, true); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(oldFactory, address(pool), rateKeeper); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(newFactory, address(pool), rateKeeper); + + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpgradeRateKeeperFactory(address(pool), newFactory); + marketConfigurator.upgradeRateKeeperFactory(address(pool)); + + // Verify factory was upgraded and authorizations changed + assertEq( + marketConfigurator.getMarketFactories(address(pool)).rateKeeperFactory, + newFactory, + "Rate keeper factory not upgraded" + ); + assertEq( + marketConfigurator.getAuthorizedFactory(rateKeeper), + newFactory, + "Rate keeper factory authorization not updated" + ); + + // Test upgrading from patch version + address patchFactory = makeAddr("PATCH_FACTORY"); + vm.mockCall(patchFactory, abi.encodeWithSignature("version()"), abi.encode(3_12)); + vm.mockCall(patchFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("RATE_KEEPER_FACTORY"))); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(patchFactory, true); + + vm.prank(admin); + marketConfigurator.upgradeRateKeeperFactory(address(pool)); + + assertEq( + marketConfigurator.getMarketFactories(address(pool)).rateKeeperFactory, + patchFactory, + "Rate keeper factory not upgraded" + ); + } + + /// @notice Tests loss policy factory upgrade function + function test_MC_16_upgradeLossPolicyFactory() public { + // Test that only admin can upgrade factories + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.upgradeLossPolicyFactory(address(pool)); + + // Test upgrading loss policy factory + address oldFactory = marketConfigurator.getMarketFactories(address(pool)).lossPolicyFactory; + address newFactory = makeAddr("NEW_FACTORY"); + address lossPolicy = creditFacade.lossPolicy(); + + vm.mockCall(newFactory, abi.encodeWithSignature("version()"), abi.encode(3_11)); + vm.mockCall(newFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("LOSS_POLICY_FACTORY"))); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(newFactory, true); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(oldFactory, address(pool), lossPolicy); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(newFactory, address(pool), lossPolicy); + + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpgradeLossPolicyFactory(address(pool), newFactory); + marketConfigurator.upgradeLossPolicyFactory(address(pool)); + + // Verify factory was upgraded and authorizations changed + assertEq( + marketConfigurator.getMarketFactories(address(pool)).lossPolicyFactory, + newFactory, + "Loss policy factory not upgraded" + ); + assertEq( + marketConfigurator.getAuthorizedFactory(lossPolicy), + newFactory, + "Loss policy factory authorization not updated" + ); + + // Test upgrading from patch version + address patchFactory = makeAddr("PATCH_FACTORY"); + vm.mockCall(patchFactory, abi.encodeWithSignature("version()"), abi.encode(3_12)); + vm.mockCall(patchFactory, abi.encodeWithSignature("contractType()"), abi.encode(bytes32("LOSS_POLICY_FACTORY"))); + + vm.prank(Ownable(addressProvider).owner()); + IAddressProvider(addressProvider).setAddress(patchFactory, true); + + vm.prank(admin); + marketConfigurator.upgradeLossPolicyFactory(address(pool)); + + assertEq( + marketConfigurator.getMarketFactories(address(pool)).lossPolicyFactory, + patchFactory, + "Loss policy factory not upgraded" + ); + } +} diff --git a/contracts/test/helpers/DefaultDegenNFT.unit.t.sol b/contracts/test/helpers/DefaultDegenNFT.unit.t.sol new file mode 100644 index 0000000..62b335c --- /dev/null +++ b/contracts/test/helpers/DefaultDegenNFT.unit.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {ConfigurationTestHelper} from "../configuration/ConfigurationTestHelper.sol"; +import {DefaultDegenNFT} from "../../helpers/DefaultDegenNFT.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {GeneralMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/GeneralMock.sol"; + +contract InvalidCreditFacade { + address public creditManager; + + constructor(address _creditManager) { + creditManager = _creditManager; + } +} + +contract DefaultDegenNFTTest is ConfigurationTestHelper { + DefaultDegenNFT public degenNFT; + address public user1; + address public user2; + address public invalidCreditFacade; + + function setUp() public override { + super.setUp(); + degenNFT = new DefaultDegenNFT(address(marketConfigurator), "Test Degen NFT", "DEGEN"); + user1 = makeAddr("user1"); + user2 = makeAddr("user2"); + invalidCreditFacade = address(new InvalidCreditFacade(address(creditManager))); + } + + function test_DD_01_mint() public { + vm.prank(admin); + degenNFT.setMinter(address(this)); + + degenNFT.mint(user1, 3); + + assertEq(degenNFT.balanceOf(user1), 3, "Incorrect balance after mint"); + assertEq(degenNFT.totalSupply(), 3, "Incorrect total supply after mint"); + + uint256 expectedTokenId; + for (uint256 i = 0; i < 3; ++i) { + expectedTokenId = (uint256(uint160(user1)) << 40) + i; + assertTrue(degenNFT.ownerOf(expectedTokenId) == user1, "Incorrect token owner"); + } + } + + function test_DD_02_mintMultipleUsers() public { + vm.prank(admin); + degenNFT.setMinter(address(this)); + + degenNFT.mint(user1, 2); + degenNFT.mint(user2, 3); + + assertEq(degenNFT.balanceOf(user1), 2, "Incorrect user1 balance"); + assertEq(degenNFT.balanceOf(user2), 3, "Incorrect user2 balance"); + assertEq(degenNFT.totalSupply(), 5, "Incorrect total supply"); + } + + function test_DD_03_burnByCreditFacade() public { + vm.prank(admin); + degenNFT.setMinter(address(this)); + degenNFT.mint(user1, 5); + + vm.prank(address(creditFacade)); + degenNFT.burn(user1, 3); + + assertEq(degenNFT.balanceOf(user1), 2, "Incorrect balance after burn"); + assertEq(degenNFT.totalSupply(), 2, "Incorrect total supply after burn"); + } + + function test_DD_04_burnByEmergencyAdmin() public { + vm.prank(admin); + degenNFT.setMinter(address(this)); + degenNFT.mint(user1, 5); + + vm.prank(emergencyAdmin); + degenNFT.burn(user1, 3); + + assertEq(degenNFT.balanceOf(user1), 2, "Incorrect balance after burn"); + assertEq(degenNFT.totalSupply(), 2, "Incorrect total supply after burn"); + } + + function test_DD_05_burnRevertIfInsufficientBalance() public { + vm.prank(admin); + degenNFT.setMinter(address(this)); + degenNFT.mint(user1, 2); + + vm.prank(address(creditFacade)); + vm.expectRevert("ERC721: invalid token ID"); + degenNFT.burn(user1, 3); + } + + function test_DD_06_onlyMinterCanMint() public { + vm.prank(admin); + degenNFT.setMinter(user1); + + vm.prank(user2); + vm.expectRevert(abi.encodeWithSelector(DefaultDegenNFT.CallerIsNotMinterException.selector, user2)); + degenNFT.mint(user2, 1); + } + + function test_DD_07_onlyCreditFacadeOrEmergencyAdminCanBurn() public { + vm.prank(admin); + degenNFT.setMinter(address(this)); + degenNFT.mint(user1, 1); + + vm.prank(invalidCreditFacade); + vm.expectRevert( + abi.encodeWithSelector( + DefaultDegenNFT.CallerIsNotCreditFacadeOrEmergencyAdminException.selector, invalidCreditFacade + ) + ); + degenNFT.burn(user1, 1); + } + + function test_DD_08_onlyAdminCanSetMinter() public { + vm.prank(user1); + vm.expectRevert(abi.encodeWithSelector(DefaultDegenNFT.CallerIsNotAdminException.selector, user1)); + degenNFT.setMinter(user2); + + vm.prank(admin); + degenNFT.setMinter(user2); + assertEq(degenNFT.minter(), user2, "Minter not set correctly"); + } + + function test_DD_09_setMinter() public { + vm.prank(admin); + degenNFT.setMinter(user1); + assertEq(degenNFT.minter(), user1, "Minter not set correctly"); + + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit DefaultDegenNFT.SetMinter(user2); + degenNFT.setMinter(user2); + } + + function test_DD_10_setBaseUri() public { + string memory newUri = "https://api.example.com/token/"; + + vm.prank(admin); + degenNFT.setBaseUri(newUri); + + vm.prank(admin); + degenNFT.setMinter(address(this)); + degenNFT.mint(user1, 1); + + uint256 tokenId = uint256(uint160(user1)) << 40; + assertEq(degenNFT.tokenURI(tokenId), newUri, "Base URI not set correctly"); + } + + function test_DD_11_transferRestrictions() public { + vm.prank(admin); + degenNFT.setMinter(address(this)); + degenNFT.mint(user1, 1); + + uint256 tokenId = uint256(uint160(user1)) << 40; + + vm.prank(user1); + vm.expectRevert(DefaultDegenNFT.NotImplementedException.selector); + degenNFT.transferFrom(user1, user2, tokenId); + + vm.prank(user1); + vm.expectRevert(DefaultDegenNFT.NotImplementedException.selector); + degenNFT.safeTransferFrom(user1, user2, tokenId); + + vm.prank(user1); + vm.expectRevert(DefaultDegenNFT.NotImplementedException.selector); + degenNFT.approve(user2, tokenId); + + vm.prank(user1); + vm.expectRevert(DefaultDegenNFT.NotImplementedException.selector); + degenNFT.setApprovalForAll(user2, true); + } +} From e5e48361544eaf24498d748432b19f3942701f0b Mon Sep 17 00:00:00 2001 From: Van0k Date: Mon, 10 Feb 2025 16:44:46 +0400 Subject: [PATCH 6/7] fix: fix setup funds transfer in tests --- contracts/test/configuration/ConfigurationTestHelper.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/configuration/ConfigurationTestHelper.sol b/contracts/test/configuration/ConfigurationTestHelper.sol index 394a0d9..a46bc42 100644 --- a/contracts/test/configuration/ConfigurationTestHelper.sol +++ b/contracts/test/configuration/ConfigurationTestHelper.sol @@ -154,7 +154,7 @@ contract ConfigurationTestHelper is Test, GlobalSetup { function _deployTestPool() internal returns (address) { address poolFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_POOL_FACTORY, 3_10); - IERC20(WETH).transfer(poolFactory, 1e18); + IERC20(WETH).transfer(address(marketConfigurator), 1e18); address _pool = marketConfigurator.previewCreateMarket(3_10, WETH, name, symbol); From 32eed865220b732b09e552b939cf5266f1f27bc7 Mon Sep 17 00:00:00 2001 From: Dima Lekhovitsky Date: Mon, 10 Feb 2025 19:11:29 +0200 Subject: [PATCH 7/7] fix: minor fixes In this commit: - upgrade deps - CF and CC no longer expect ACL as constructor argument - credit factory reads WETH from AP in runtime and not in constructor - aliased loss policy conforms to default constructor signature - add missing serialization - revert on non-v3 minor version - `getLatestMinorVersion` and `getLatestPatchVersion` accept non-exact versions --- contracts/factories/CreditFactory.sol | 33 ++++++------------- contracts/factories/LossPolicyFactory.sol | 7 +--- contracts/factories/PriceOracleFactory.sol | 15 +++++---- contracts/global/BytecodeRepository.sol | 2 ++ contracts/helpers/DefaultDegenNFT.sol | 4 +++ contracts/helpers/DefaultIRM.sol | 2 ++ contracts/helpers/DefaultLossPolicy.sol | 11 +++++-- contracts/instance/AddressProvider.sol | 2 ++ contracts/interfaces/IMarketConfigurator.sol | 2 ++ .../factories/ICreditConfigureActions.sol | 20 ++++++++++- .../interfaces/factories/ICreditFactory.sol | 6 ---- contracts/market/MarketConfigurator.sol | 2 ++ .../configuration/ConfigurationTestHelper.sol | 2 -- .../CreditSuiteConfiguration.unit.t.sol | 3 +- .../mocks/MockCreditConfiguratorPatch.sol | 2 +- contracts/test/mocks/MockLossPolicy.sol | 6 +++- contracts/test/mocks/MockPriceFeed.sol | 2 ++ contracts/test/suite/NewChainDeploySuite.sol | 3 +- lib/@gearbox-protocol/core-v3 | 2 +- lib/@gearbox-protocol/integrations-v3 | 2 +- lib/@gearbox-protocol/oracles-v3 | 2 +- 21 files changed, 74 insertions(+), 56 deletions(-) diff --git a/contracts/factories/CreditFactory.sol b/contracts/factories/CreditFactory.sol index 38d8c66..c065007 100644 --- a/contracts/factories/CreditFactory.sol +++ b/contracts/factories/CreditFactory.sol @@ -9,8 +9,12 @@ import {ICreditFacadeV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IC import {ICreditManagerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol"; import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; -import {ICreditFactory, CreditFacadeParams} from "../interfaces/factories/ICreditFactory.sol"; -import {ICreditConfigureActions} from "../interfaces/factories/ICreditConfigureActions.sol"; +import {ICreditFactory} from "../interfaces/factories/ICreditFactory.sol"; +import { + CreditFacadeParams, + CreditManagerParams, + ICreditConfigureActions +} from "../interfaces/factories/ICreditConfigureActions.sol"; import {ICreditEmergencyConfigureActions} from "../interfaces/factories/ICreditEmergencyConfigureActions.sol"; import {IFactory} from "../interfaces/factories/IFactory.sol"; import {IContractsRegister} from "../interfaces/IContractsRegister.sol"; @@ -34,19 +38,6 @@ import { import {AbstractFactory} from "./AbstractFactory.sol"; -struct CreditManagerParams { - uint8 maxEnabledTokens; - uint16 feeInterest; - uint16 feeLiquidation; - uint16 liquidationPremium; - uint16 feeLiquidationExpired; - uint16 liquidationPremiumExpired; - uint128 minDebt; - uint128 maxDebt; - string name; - DeployParams accountFactoryParams; -} - contract CreditFactory is AbstractFactory, ICreditFactory { /// @notice Contract version uint256 public constant override version = 3_10; @@ -57,9 +48,6 @@ contract CreditFactory is AbstractFactory, ICreditFactory { /// @notice Address of the bot list contract address public immutable botList; - /// @notice Address of the WETH token - address public immutable weth; - error DegenNFTIsNotRegisteredException(address degenNFT); error TargetContractIsNotAllowedException(address targetCotnract); @@ -68,7 +56,6 @@ contract CreditFactory is AbstractFactory, ICreditFactory { /// @param addressProvider_ Address provider contract address constructor(address addressProvider_) AbstractFactory(addressProvider_) { botList = _getAddressOrRevert(AP_BOT_LIST, NO_VERSION_CONTROL); - weth = _tryGetAddress(AP_WETH_TOKEN, NO_VERSION_CONTROL); } // ---------- // @@ -314,8 +301,7 @@ contract CreditFactory is AbstractFactory, ICreditFactory { } function _deployCreditConfigurator(address marketConfigurator, address creditManager) internal returns (address) { - address acl = IMarketConfigurator(marketConfigurator).acl(); - bytes memory constructorParams = abi.encode(acl, creditManager); + bytes memory constructorParams = abi.encode(creditManager); return _deployLatestPatch({ contractType: AP_CREDIT_CONFIGURATOR, @@ -329,7 +315,6 @@ contract CreditFactory is AbstractFactory, ICreditFactory { internal returns (address) { - address acl = IMarketConfigurator(marketConfigurator).acl(); address contractsRegister = IMarketConfigurator(marketConfigurator).contractsRegister(); address lossPolicy = IContractsRegister(contractsRegister).getLossPolicy(ICreditManagerV3(creditManager).pool()); @@ -346,8 +331,10 @@ contract CreditFactory is AbstractFactory, ICreditFactory { botList_ = ICreditFacadeV3(prevCreditFacade).botList(); } + address weth = _tryGetAddress(AP_WETH_TOKEN, NO_VERSION_CONTROL); + bytes memory constructorParams = - abi.encode(acl, creditManager, lossPolicy, botList_, weth, params.degenNFT, params.expirable); + abi.encode(creditManager, lossPolicy, botList_, weth, params.degenNFT, params.expirable); return _deployLatestPatch({ contractType: AP_CREDIT_FACADE, diff --git a/contracts/factories/LossPolicyFactory.sol b/contracts/factories/LossPolicyFactory.sol index 485db83..deba0ca 100644 --- a/contracts/factories/LossPolicyFactory.sol +++ b/contracts/factories/LossPolicyFactory.sol @@ -37,12 +37,7 @@ contract LossPolicyFactory is AbstractMarketFactory, ILossPolicyFactory { onlyMarketConfigurators returns (DeployResult memory) { - if (params.postfix == "ALIASED") { - address decodedPool = abi.decode(params.constructorParams, (address)); - if (decodedPool != pool) revert InvalidConstructorParamsException(); - } else { - _validateDefaultConstructorParams(pool, params.constructorParams); - } + _validateDefaultConstructorParams(pool, params.constructorParams); address lossPolicy = _deployLatestPatch({ contractType: _getContractType(DOMAIN_LOSS_POLICY, params.postfix), diff --git a/contracts/factories/PriceOracleFactory.sol b/contracts/factories/PriceOracleFactory.sol index a4184c9..1230362 100644 --- a/contracts/factories/PriceOracleFactory.sol +++ b/contracts/factories/PriceOracleFactory.sol @@ -176,7 +176,7 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { address priceOracle = _priceOracle(pool); bytes4 selector = bytes4(callData); - if (selector == IPriceOracleConfigureActions.setPriceFeed.selector) { + if (selector == IPriceOracleEmergencyConfigureActions.setPriceFeed.selector) { (address token, address priceFeed) = abi.decode(callData[4:], (address, address)); _validatePriceFeed(pool, token, priceFeed, true); if (block.timestamp < IPriceFeedStore(priceFeedStore).getAllowanceTimestamp(token, priceFeed) + 1 days) { @@ -209,6 +209,7 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { } function _getPriceFeed(address priceOracle, address token, bool reserve) internal view returns (address) { + // FIXME: doesn't work like that for price oracle v3.0 return reserve ? IPriceOracleV3(priceOracle).reservePriceFeeds(token) : IPriceOracleV3(priceOracle).priceFeeds(token); @@ -227,8 +228,8 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { Call[] memory calls = CallBuilder.build( reserve - ? _setReservePriceFeed(priceOracle, token, priceFeed, stalenessPeriod) - : _setPriceFeed(priceOracle, token, priceFeed, stalenessPeriod) + ? _setReservePriceFeedCall(priceOracle, token, priceFeed, stalenessPeriod) + : _setPriceFeedCall(priceOracle, token, priceFeed, stalenessPeriod) ); return _addUpdatableFeeds(priceOracle, priceFeed, calls); } @@ -239,7 +240,7 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { returns (Call[] memory) { try IUpdatablePriceFeed(priceFeed).updatable() returns (bool updatable) { - if (updatable) calls = calls.append(_addUpdatablePriceFeed(priceOracle, priceFeed)); + if (updatable) calls = calls.append(_addUpdatablePriceFeedCall(priceOracle, priceFeed)); } catch {} address[] memory underlyingFeeds = IPriceFeed(priceFeed).getUnderlyingFeeds(); uint256 numFeeds = underlyingFeeds.length; @@ -249,7 +250,7 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { return calls; } - function _setPriceFeed(address priceOracle, address token, address priceFeed, uint32 stalenessPeriod) + function _setPriceFeedCall(address priceOracle, address token, address priceFeed, uint32 stalenessPeriod) internal pure returns (Call memory) @@ -257,7 +258,7 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { return Call(priceOracle, abi.encodeCall(IPriceOracleV3.setPriceFeed, (token, priceFeed, stalenessPeriod))); } - function _setReservePriceFeed(address priceOracle, address token, address priceFeed, uint32 stalenessPeriod) + function _setReservePriceFeedCall(address priceOracle, address token, address priceFeed, uint32 stalenessPeriod) internal pure returns (Call memory) @@ -266,7 +267,7 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { Call(priceOracle, abi.encodeCall(IPriceOracleV3.setReservePriceFeed, (token, priceFeed, stalenessPeriod))); } - function _addUpdatablePriceFeed(address priceOracle, address priceFeed) internal pure returns (Call memory) { + function _addUpdatablePriceFeedCall(address priceOracle, address priceFeed) internal pure returns (Call memory) { return Call(priceOracle, abi.encodeCall(IPriceOracleV3.addUpdatablePriceFeed, (priceFeed))); } } diff --git a/contracts/global/BytecodeRepository.sol b/contracts/global/BytecodeRepository.sol index 1be59cb..55db9b9 100644 --- a/contracts/global/BytecodeRepository.sol +++ b/contracts/global/BytecodeRepository.sol @@ -502,6 +502,7 @@ contract BytecodeRepository is ImmutableOwnableTrait, SanityCheckTrait, IBytecod /// @param majorVersion Major version number /// @return uint256 Latest minor version number function getLatestMinorVersion(bytes32 _contractType, uint256 majorVersion) external view returns (uint256) { + majorVersion -= majorVersion % 100; return latestMinorVersion[_contractType][majorVersion]; } @@ -510,6 +511,7 @@ contract BytecodeRepository is ImmutableOwnableTrait, SanityCheckTrait, IBytecod /// @param minorVersion Minor version number /// @return uint256 Latest patch version number function getLatestPatchVersion(bytes32 _contractType, uint256 minorVersion) external view returns (uint256) { + minorVersion -= minorVersion % 10; return latestPatchVersion[_contractType][minorVersion]; } diff --git a/contracts/helpers/DefaultDegenNFT.sol b/contracts/helpers/DefaultDegenNFT.sol index 28d5b05..60b2dfc 100644 --- a/contracts/helpers/DefaultDegenNFT.sol +++ b/contracts/helpers/DefaultDegenNFT.sol @@ -51,6 +51,10 @@ contract DefaultDegenNFT is ERC721, IDegenNFT { contractsRegister = IMarketConfigurator(marketConfigurator_).contractsRegister(); } + function serialize() external view override returns (bytes memory) { + return abi.encode(marketConfigurator, minter, name(), symbol(), baseURI, totalSupply); + } + function _baseURI() internal view override returns (string memory) { return baseURI; } diff --git a/contracts/helpers/DefaultIRM.sol b/contracts/helpers/DefaultIRM.sol index 88663f7..9371817 100644 --- a/contracts/helpers/DefaultIRM.sol +++ b/contracts/helpers/DefaultIRM.sol @@ -10,6 +10,8 @@ contract DefaultIRM is IInterestRateModel { uint256 public constant override version = 3_10; bytes32 public constant override contractType = AP_INTEREST_RATE_MODEL_DEFAULT; + function serialize() external pure override returns (bytes memory) {} + function calcBorrowRate(uint256, uint256, bool) external pure override returns (uint256) { return 0; } diff --git a/contracts/helpers/DefaultLossPolicy.sol b/contracts/helpers/DefaultLossPolicy.sol index faa826e..7b3443a 100644 --- a/contracts/helpers/DefaultLossPolicy.sol +++ b/contracts/helpers/DefaultLossPolicy.sol @@ -3,16 +3,23 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; +import {ILossPolicy} from "@gearbox-protocol/core-v3/contracts/interfaces/base/ILossPolicy.sol"; import {ACLTrait} from "@gearbox-protocol/core-v3/contracts/traits/ACLTrait.sol"; import {AP_LOSS_POLICY_DEFAULT} from "../libraries/ContractLiterals.sol"; -contract DefaultLossPolicy is ACLTrait { +contract DefaultLossPolicy is ILossPolicy, ACLTrait { uint256 public constant version = 3_10; bytes32 public constant contractType = AP_LOSS_POLICY_DEFAULT; bool public enabled; - constructor(address _acl) ACLTrait(_acl) {} + // QUESTION: shouldn't it take pool address and AP so that it can be used with loss policy factory? + // that would make it hard to use in legacy market configurator + constructor(address acl_) ACLTrait(acl_) {} + + function serialize() external view override returns (bytes memory) { + return abi.encode(enabled); + } function isLiquidatable(address, address, bytes calldata) external view returns (bool) { return enabled; diff --git a/contracts/instance/AddressProvider.sol b/contracts/instance/AddressProvider.sol index dbc72bf..ba44950 100644 --- a/contracts/instance/AddressProvider.sol +++ b/contracts/instance/AddressProvider.sol @@ -70,12 +70,14 @@ contract AddressProvider is ImmutableOwnableTrait, IAddressProvider { } function getLatestMinorVersion(string memory key, uint256 majorVersion) external view override returns (uint256) { + majorVersion -= majorVersion % 100; uint256 latestMinorVersion = latestMinorVersions[key][majorVersion]; if (latestMinorVersion == 0) revert VersionNotFoundException(); return latestMinorVersion; } function getLatestPatchVersion(string memory key, uint256 minorVersion) external view override returns (uint256) { + minorVersion -= minorVersion % 10; uint256 latestPatchVersion = latestPatchVersions[key][minorVersion]; if (latestPatchVersion == 0) revert VersionNotFoundException(); return latestPatchVersion; diff --git a/contracts/interfaces/IMarketConfigurator.sol b/contracts/interfaces/IMarketConfigurator.sol index ed1005f..627733d 100644 --- a/contracts/interfaces/IMarketConfigurator.sol +++ b/contracts/interfaces/IMarketConfigurator.sol @@ -103,6 +103,8 @@ interface IMarketConfigurator is IVersion, IDeployerTrait { error CreditSuiteNotRegisteredException(address creditManager); + error IncorrectMinorVersionException(uint256 version); + error IncorrectPeripheryContractException(address peripheryContract); error MarketNotRegisteredException(address pool); diff --git a/contracts/interfaces/factories/ICreditConfigureActions.sol b/contracts/interfaces/factories/ICreditConfigureActions.sol index c8f4c67..ca7e25d 100644 --- a/contracts/interfaces/factories/ICreditConfigureActions.sol +++ b/contracts/interfaces/factories/ICreditConfigureActions.sol @@ -3,9 +3,27 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; -import {CreditFacadeParams} from "./ICreditFactory.sol"; import {DeployParams} from "../Types.sol"; +struct CreditManagerParams { + uint8 maxEnabledTokens; + uint16 feeInterest; + uint16 feeLiquidation; + uint16 liquidationPremium; + uint16 feeLiquidationExpired; + uint16 liquidationPremiumExpired; + uint128 minDebt; + uint128 maxDebt; + string name; + DeployParams accountFactoryParams; +} + +struct CreditFacadeParams { + address degenNFT; + bool expirable; + bool migrateBotList; +} + interface ICreditConfigureActions { function upgradeCreditConfigurator() external; function upgradeCreditFacade(CreditFacadeParams calldata params) external; diff --git a/contracts/interfaces/factories/ICreditFactory.sol b/contracts/interfaces/factories/ICreditFactory.sol index a143431..d53ea7a 100644 --- a/contracts/interfaces/factories/ICreditFactory.sol +++ b/contracts/interfaces/factories/ICreditFactory.sol @@ -6,12 +6,6 @@ pragma solidity ^0.8.23; import {Call, DeployResult} from "../Types.sol"; import {IFactory} from "./IFactory.sol"; -struct CreditFacadeParams { - address degenNFT; - bool expirable; - bool migrateBotList; -} - interface ICreditFactory is IFactory { function deployCreditSuite(address pool, bytes calldata encodedParams) external returns (DeployResult memory); diff --git a/contracts/market/MarketConfigurator.sol b/contracts/market/MarketConfigurator.sol index b1d9c1b..d97e495 100644 --- a/contracts/market/MarketConfigurator.sol +++ b/contracts/market/MarketConfigurator.sol @@ -796,6 +796,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { } function _getLatestMarketFactories(uint256 minorVersion) internal view returns (MarketFactories memory) { + if (minorVersion / 100 != 3) revert IncorrectMinorVersionException(minorVersion); return MarketFactories({ poolFactory: _getLatestPatch(AP_POOL_FACTORY, minorVersion), priceOracleFactory: _getLatestPatch(AP_PRICE_ORACLE_FACTORY, minorVersion), @@ -806,6 +807,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { } function _getLatestCreditFactory(uint256 minorVersion) internal view returns (address) { + if (minorVersion / 100 != 3) revert IncorrectMinorVersionException(minorVersion); return _getLatestPatch(AP_CREDIT_FACTORY, minorVersion); } diff --git a/contracts/test/configuration/ConfigurationTestHelper.sol b/contracts/test/configuration/ConfigurationTestHelper.sol index a46bc42..b1835f3 100644 --- a/contracts/test/configuration/ConfigurationTestHelper.sol +++ b/contracts/test/configuration/ConfigurationTestHelper.sol @@ -152,8 +152,6 @@ contract ConfigurationTestHelper is Test, GlobalSetup { } function _deployTestPool() internal returns (address) { - address poolFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_POOL_FACTORY, 3_10); - IERC20(WETH).transfer(address(marketConfigurator), 1e18); address _pool = marketConfigurator.previewCreateMarket(3_10, WETH, name, symbol); diff --git a/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol b/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol index fbae4f7..aa0b41d 100644 --- a/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol +++ b/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol @@ -260,7 +260,7 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { address expectedNewConfigurator = IBytecodeRepository(bytecodeRepository).computeAddress( "CREDIT_CONFIGURATOR", 3_11, - abi.encode(marketConfigurator.acl(), address(creditManager)), + abi.encode(address(creditManager)), bytes32(bytes20(address(marketConfigurator))), creditFactory ); @@ -321,7 +321,6 @@ contract CreditSuiteConfigurationUnitTest is ConfigurationTestHelper { "CREDIT_FACADE", 3_10, abi.encode( - marketConfigurator.acl(), address(creditManager), lossPolicy, ICreditFacadeV3(oldFacade).botList(), diff --git a/contracts/test/mocks/MockCreditConfiguratorPatch.sol b/contracts/test/mocks/MockCreditConfiguratorPatch.sol index 89f9ae2..eb155d1 100644 --- a/contracts/test/mocks/MockCreditConfiguratorPatch.sol +++ b/contracts/test/mocks/MockCreditConfiguratorPatch.sol @@ -6,7 +6,7 @@ import {CreditConfiguratorV3} from "@gearbox-protocol/core-v3/contracts/credit/C import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; contract MockCreditConfiguratorPatch is CreditConfiguratorV3, Test { - constructor(address _acl, address _creditManager) CreditConfiguratorV3(_acl, _creditManager) { + constructor(address _creditManager) CreditConfiguratorV3(_creditManager) { vm.mockCall(address(this), abi.encodeCall(IVersion.version, ()), abi.encode(311)); } } diff --git a/contracts/test/mocks/MockLossPolicy.sol b/contracts/test/mocks/MockLossPolicy.sol index 50bfa99..e778ce5 100644 --- a/contracts/test/mocks/MockLossPolicy.sol +++ b/contracts/test/mocks/MockLossPolicy.sol @@ -3,7 +3,9 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; -contract MockLossPolicy { +import {ILossPolicy} from "@gearbox-protocol/core-v3/contracts/interfaces/base/ILossPolicy.sol"; + +contract MockLossPolicy is ILossPolicy { uint256 public constant version = 3_10; bytes32 public constant contractType = "LOSS_POLICY::MOCK"; @@ -11,6 +13,8 @@ contract MockLossPolicy { constructor(address pool, address addressProvider) {} + function serialize() external pure override returns (bytes memory) {} + function isLiquidatable(address, address, bytes calldata) external view returns (bool) { return enabled; } diff --git a/contracts/test/mocks/MockPriceFeed.sol b/contracts/test/mocks/MockPriceFeed.sol index 97e6b26..933b276 100644 --- a/contracts/test/mocks/MockPriceFeed.sol +++ b/contracts/test/mocks/MockPriceFeed.sol @@ -14,6 +14,8 @@ contract MockPriceFeed is IPriceFeed { _price = 1e18; // Default price of 1 } + function serialize() external pure override returns (bytes memory) {} + function lastUpdateTime() external view returns (uint256) { return _lastUpdateTime; } diff --git a/contracts/test/suite/NewChainDeploySuite.sol b/contracts/test/suite/NewChainDeploySuite.sol index 8442afd..a692ef3 100644 --- a/contracts/test/suite/NewChainDeploySuite.sol +++ b/contracts/test/suite/NewChainDeploySuite.sol @@ -10,7 +10,7 @@ import {PriceFeedStore} from "../../instance/PriceFeedStore.sol"; import {IBytecodeRepository} from "../../interfaces/IBytecodeRepository.sol"; import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; import {IInstanceManager} from "../../interfaces/IInstanceManager.sol"; -import {ICreditConfigureActions} from "../../factories/CreditFactory.sol"; +import {CreditManagerParams, CreditFacadeParams, ICreditConfigureActions} from "../../factories/CreditFactory.sol"; import {IWETH} from "@gearbox-protocol/core-v3/contracts/interfaces/external/IWETH.sol"; import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; @@ -67,7 +67,6 @@ import {CreditFacadeV3} from "@gearbox-protocol/core-v3/contracts/credit/CreditF import {CreditConfiguratorV3} from "@gearbox-protocol/core-v3/contracts/credit/CreditConfiguratorV3.sol"; import {DeployParams} from "../../interfaces/Types.sol"; -import {CreditFacadeParams, CreditManagerParams} from "../../factories/CreditFactory.sol"; import {GlobalSetup} from "../../test/helpers/GlobalSetup.sol"; import {MockLossPolicy} from "../../test/mocks/MockLossPolicy.sol"; diff --git a/lib/@gearbox-protocol/core-v3 b/lib/@gearbox-protocol/core-v3 index 66f9b8e..b9a9ceb 160000 --- a/lib/@gearbox-protocol/core-v3 +++ b/lib/@gearbox-protocol/core-v3 @@ -1 +1 @@ -Subproject commit 66f9b8e7be833964c935cd43d1e8002b9f08d9ea +Subproject commit b9a9ceb9dacb13bd906f64a47a39ac8bfb0bfe47 diff --git a/lib/@gearbox-protocol/integrations-v3 b/lib/@gearbox-protocol/integrations-v3 index 0d9636a..1449dc8 160000 --- a/lib/@gearbox-protocol/integrations-v3 +++ b/lib/@gearbox-protocol/integrations-v3 @@ -1 +1 @@ -Subproject commit 0d9636a6d93639aa442f3ce48d8694eee8a95a86 +Subproject commit 1449dc87b2c3768a48a39e256c0c77935d0f366c diff --git a/lib/@gearbox-protocol/oracles-v3 b/lib/@gearbox-protocol/oracles-v3 index 951b716..f8d1010 160000 --- a/lib/@gearbox-protocol/oracles-v3 +++ b/lib/@gearbox-protocol/oracles-v3 @@ -1 +1 @@ -Subproject commit 951b71670465e2f1b86d3140117d6e74cf55ee7a +Subproject commit f8d10105d5a3d90ac2aa5f0b54e16310c1b5ed43