diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index ad473c8..b31aeb9 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -7,27 +7,37 @@ on: env: CI: true FORK_RPC_URL: ${{ secrets.MAINNET_TESTS_FORK }} - TIMELOCK_ADDRESS: "0xa133C9A92Fb8dDB962Af1cbae58b2723A0bdf23b" jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Use Latest Corepack + run: | + echo "Before: corepack version => $(corepack --version || echo 'not installed')" + npm install -g corepack@latest + echo "After : corepack version => $(corepack --version)" + corepack enable + pnpm --version - name: Setup node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - cache: "yarn" + cache: "pnpm" node-version-file: ".nvmrc" - name: Install node dependencies - run: yarn install --frozen-lockfile + run: pnpm install --frozen-lockfile - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable - name: Install forge dependencies run: forge i @@ -36,4 +46,4 @@ jobs: run: forge b - name: Run tests - run: forge test + run: forge test --fork-url ${{ secrets.MAINNET_TESTS_FORK }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ce438d..0e3b2e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,24 +1,23 @@ name: Release on: - push: - branches: - - "main" - - "next" + push: + branches: + - "main" + - "next" env: - HUSKY: 0 - CI: true + HUSKY: 0 + CI: true jobs: - release: - runs-on: ubuntu-latest + release: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 + steps: + - uses: actions/checkout@v4 - - name: Semantic Release - uses: cycjimmy/semantic-release-action@v3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 444d802..5162337 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ broadcast .DS_Store .env addresses.json +generated.ts diff --git a/.nvmrc b/.nvmrc index b6a7d89..2bd5a0a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16 +22 diff --git a/.releaserc.json b/.releaserc.json index 9dbdf5e..cbd4d56 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -12,7 +12,6 @@ "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", - "@semantic-release/npm", "@semantic-release/github" ] } \ No newline at end of file diff --git a/contracts/factories/CreditFactory.sol b/contracts/factories/CreditFactory.sol index f71e66e..7905dd0 100644 --- a/contracts/factories/CreditFactory.sol +++ b/contracts/factories/CreditFactory.sol @@ -4,12 +4,19 @@ pragma solidity ^0.8.23; import {IAccountFactory} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IAccountFactory.sol"; +import {IAdapter} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IAdapter.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 {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.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"; import {IMarketConfigurator} from "../interfaces/IMarketConfigurator.sol"; @@ -32,81 +39,17 @@ 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; -} - -struct CreditFacadeParams { - address degenNFT; - bool expirable; - bool migrateBotList; -} - -interface IConfigureActions { - function upgradeCreditConfigurator() external; - function upgradeCreditFacade(CreditFacadeParams calldata params) external; - function allowAdapter(DeployParams calldata params) external; - function forbidAdapter(address adapter) external; - function configureAdapterFor(address targetContract, bytes calldata data) external; - function setFees( - uint16 feeLiquidation, - uint16 liquidationPremium, - uint16 feeLiquidationExpired, - uint16 liquidationPremiumExpired - ) external; - function setMaxDebtPerBlockMultiplier(uint8 newMaxDebtLimitPerBlockMultiplier) external; - function addCollateralToken(address token, uint16 liquidationThreshold) external; - function rampLiquidationThreshold( - address token, - uint16 liquidationThresholdFinal, - uint40 rampStart, - uint24 rampDuration - ) external; - function forbidToken(address token) external; - function allowToken(address token) external; - function setExpirationDate(uint40 newExpirationDate) external; - function pause() external; - function unpause() external; -} - -interface IEmergencyConfigureActions { - function forbidAdapter(address adapter) external; - function forbidToken(address token) external; - function forbidBorrowing() external; - function pause() external; -} - contract CreditFactory is AbstractFactory, ICreditFactory { - /// @notice Contract version uint256 public constant override version = 3_10; - - /// @notice Contract type bytes32 public constant override contractType = AP_CREDIT_FACTORY; - /// @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); - /// @notice Constructor - /// @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); } // ---------- // @@ -141,14 +84,15 @@ contract CreditFactory is AbstractFactory, ICreditFactory { }); } - function computeCreditManagerAddress(address marketConfigurator, address pool, bytes calldata encodedParams) - external - view - override - returns (address) - { + function computeCreditManagerAddress( + address marketConfigurator, + address pool, + address underlying, + address priceOracle, + bytes calldata encodedParams + ) external view override returns (address) { (CreditManagerParams memory params,) = abi.decode(encodedParams, (CreditManagerParams, CreditFacadeParams)); - return _computeCreditManagerAddress(marketConfigurator, pool, params); + return _computeCreditManagerAddress(marketConfigurator, pool, underlying, priceOracle, params); } // ------------ // @@ -184,15 +128,16 @@ contract CreditFactory is AbstractFactory, ICreditFactory { returns (Call[] memory) { bytes4 selector = bytes4(callData); - if (selector == IConfigureActions.upgradeCreditConfigurator.selector) { + if (selector == ICreditConfigureActions.upgradeCreditConfigurator.selector) { address creditConfigurator = _creditConfigurator(creditManager); address newCreditConfigurator = _deployCreditConfigurator(msg.sender, creditManager); return CallBuilder.build( _upgradeCreditConfigurator(creditConfigurator, newCreditConfigurator), _unauthorizeFactory(msg.sender, creditManager, creditConfigurator), - _authorizeFactory(msg.sender, creditManager, newCreditConfigurator) + _authorizeFactory(msg.sender, creditManager, newCreditConfigurator), + _makeAllTokensQuoted(newCreditConfigurator) ); - } else if (selector == IConfigureActions.upgradeCreditFacade.selector) { + } else if (selector == ICreditConfigureActions.upgradeCreditFacade.selector) { CreditFacadeParams memory params = abi.decode(callData[4:], (CreditFacadeParams)); address creditFacade = _creditFacade(creditManager); address newCreditFacade = _deployCreditFacade(msg.sender, creditManager, params); @@ -201,34 +146,40 @@ contract CreditFactory is AbstractFactory, ICreditFactory { _unauthorizeFactory(msg.sender, creditManager, creditFacade), _authorizeFactory(msg.sender, creditManager, newCreditFacade) ); - } else if (selector == IConfigureActions.allowAdapter.selector) { + } else if (selector == ICreditConfigureActions.allowAdapter.selector) { DeployParams memory params = abi.decode(callData[4:], (DeployParams)); address adapter = _deployAdapter(msg.sender, creditManager, params); - return CallBuilder.build( - _authorizeFactory(msg.sender, creditManager, adapter), - _allowAdapter(_creditConfigurator(creditManager), adapter) - ); - } else if (selector == IConfigureActions.forbidAdapter.selector) { + address oldAdapter = ICreditManagerV3(creditManager).contractToAdapter(IAdapter(adapter).targetContract()); + Call memory unauthorizeCall = _unauthorizeFactory(msg.sender, creditManager, oldAdapter); + Call memory authorizeCall = _authorizeFactory(msg.sender, creditManager, adapter); + Call memory allowCall = _allowAdapter(_creditConfigurator(creditManager), adapter); + return oldAdapter != address(0) + ? CallBuilder.build(unauthorizeCall, authorizeCall, allowCall) + : CallBuilder.build(authorizeCall, allowCall); + } else if (selector == ICreditConfigureActions.forbidAdapter.selector) { address adapter = abi.decode(callData[4:], (address)); return CallBuilder.build( - _authorizeFactory(msg.sender, creditManager, adapter), + _unauthorizeFactory(msg.sender, creditManager, adapter), _forbidAdapter(_creditConfigurator(creditManager), adapter) ); - } else if (selector == IConfigureActions.configureAdapterFor.selector) { + } else if (selector == ICreditConfigureActions.configureAdapterFor.selector) { (address targetContract, bytes memory data) = abi.decode(callData[4:], (address, bytes)); address adapter = ICreditManagerV3(creditManager).contractToAdapter(targetContract); if (adapter == address(0)) revert TargetContractIsNotAllowedException(targetContract); return CallBuilder.build(Call(adapter, data)); } else if ( - selector == IConfigureActions.setFees.selector - || selector == IConfigureActions.setMaxDebtPerBlockMultiplier.selector - || selector == IConfigureActions.addCollateralToken.selector - || selector == IConfigureActions.rampLiquidationThreshold.selector - || selector == IConfigureActions.forbidToken.selector || selector == IConfigureActions.allowToken.selector - || selector == IConfigureActions.setExpirationDate.selector + selector == ICreditConfigureActions.setFees.selector + || selector == ICreditConfigureActions.setMaxDebtPerBlockMultiplier.selector + || selector == ICreditConfigureActions.addCollateralToken.selector + || selector == ICreditConfigureActions.rampLiquidationThreshold.selector + || selector == ICreditConfigureActions.forbidToken.selector + || selector == ICreditConfigureActions.allowToken.selector + || selector == ICreditConfigureActions.setExpirationDate.selector ) { return CallBuilder.build(Call(_creditConfigurator(creditManager), callData)); - } else if (selector == IConfigureActions.pause.selector || selector == IConfigureActions.unpause.selector) { + } else if ( + selector == ICreditConfigureActions.pause.selector || selector == ICreditConfigureActions.unpause.selector + ) { return CallBuilder.build(Call(_creditFacade(creditManager), callData)); } else { revert ForbiddenConfigurationCallException(selector); @@ -242,18 +193,18 @@ contract CreditFactory is AbstractFactory, ICreditFactory { returns (Call[] memory) { bytes4 selector = bytes4(callData); - if (selector == IEmergencyConfigureActions.forbidAdapter.selector) { + if (selector == ICreditEmergencyConfigureActions.forbidAdapter.selector) { address adapter = abi.decode(callData[4:], (address)); return CallBuilder.build( _unauthorizeFactory(msg.sender, creditManager, adapter), _forbidAdapter(_creditConfigurator(creditManager), adapter) ); } else if ( - selector == IEmergencyConfigureActions.forbidBorrowing.selector - || selector == IEmergencyConfigureActions.forbidToken.selector + selector == ICreditEmergencyConfigureActions.forbidBorrowing.selector + || selector == ICreditEmergencyConfigureActions.forbidToken.selector ) { return CallBuilder.build(Call(_creditConfigurator(creditManager), callData)); - } else if (selector == IEmergencyConfigureActions.pause.selector) { + } else if (selector == ICreditEmergencyConfigureActions.pause.selector) { return CallBuilder.build(Call(_creditFacade(creditManager), callData)); } else { revert ForbiddenEmergencyConfigurationCallException(selector); @@ -297,8 +248,9 @@ contract CreditFactory is AbstractFactory, ICreditFactory { CreditManagerParams memory params ) internal returns (address) { bytes32 postfix = _getTokenSpecificPostfix(IPoolV3(pool).asset()); - bytes memory constructorParams = - _buildCreditManagerConstructorParams(marketConfigurator, pool, accountFactory, params); + address contractsRegister = IMarketConfigurator(marketConfigurator).contractsRegister(); + address priceOracle = IContractsRegister(contractsRegister).getPriceOracle(pool); + bytes memory constructorParams = _buildCreditManagerConstructorParams(pool, accountFactory, priceOracle, params); return _deployLatestPatch({ contractType: _getContractType(DOMAIN_CREDIT_MANAGER, postfix), minorVersion: version, @@ -307,15 +259,16 @@ contract CreditFactory is AbstractFactory, ICreditFactory { }); } - function _computeCreditManagerAddress(address marketConfigurator, address pool, CreditManagerParams memory params) - internal - view - returns (address) - { + function _computeCreditManagerAddress( + address marketConfigurator, + address pool, + address underlying, + address priceOracle, + CreditManagerParams memory params + ) internal view returns (address) { address accountFactory = _computeAccountFactoryAddress(marketConfigurator, params.accountFactoryParams); - bytes32 postfix = _getTokenSpecificPostfix(IPoolV3(pool).asset()); - bytes memory constructorParams = - _buildCreditManagerConstructorParams(marketConfigurator, pool, accountFactory, params); + bytes32 postfix = _getTokenSpecificPostfix(underlying); + bytes memory constructorParams = _buildCreditManagerConstructorParams(pool, accountFactory, priceOracle, params); return _computeAddressLatestPatch({ contractType: _getContractType(DOMAIN_CREDIT_MANAGER, postfix), minorVersion: version, @@ -326,14 +279,11 @@ contract CreditFactory is AbstractFactory, ICreditFactory { } function _buildCreditManagerConstructorParams( - address marketConfigurator, address pool, address accountFactory, + address priceOracle, CreditManagerParams memory params - ) internal view returns (bytes memory) { - address contractsRegister = IMarketConfigurator(marketConfigurator).contractsRegister(); - address priceOracle = IContractsRegister(contractsRegister).getPriceOracle(pool); - + ) internal pure returns (bytes memory) { return abi.encode( pool, accountFactory, @@ -349,8 +299,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, @@ -364,7 +313,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()); @@ -381,8 +329,10 @@ contract CreditFactory is AbstractFactory, ICreditFactory { botList_ = ICreditFacadeV3(prevCreditFacade).botList(); } + address weth = _getAddress(AP_WETH_TOKEN, NO_VERSION_CONTROL); + bytes memory constructorParams = - abi.encode(acl, creditManager, lossPolicy, botList_, weth, params.degenNFT, params.expirable); + abi.encode(addressProvider, creditManager, lossPolicy, botList_, weth, params.degenNFT, params.expirable); return _deployLatestPatch({ contractType: AP_CREDIT_FACADE, @@ -458,4 +408,8 @@ contract CreditFactory is AbstractFactory, ICreditFactory { { return Call(creditConfigurator, abi.encodeCall(ICreditConfiguratorV3.setDebtLimits, (minDebt, maxDebt))); } + + function _makeAllTokensQuoted(address creditConfigurator) internal pure returns (Call memory) { + return Call(creditConfigurator, abi.encodeCall(ICreditConfiguratorV3.makeAllTokensQuoted, ())); + } } diff --git a/contracts/factories/InterestRateModelFactory.sol b/contracts/factories/InterestRateModelFactory.sol index 91dee66..5e2067b 100644 --- a/contracts/factories/InterestRateModelFactory.sol +++ b/contracts/factories/InterestRateModelFactory.sol @@ -15,14 +15,9 @@ import {AbstractFactory} from "./AbstractFactory.sol"; import {AbstractMarketFactory} from "./AbstractMarketFactory.sol"; contract InterestRateModelFactory is AbstractMarketFactory, IInterestRateModelFactory { - /// @notice Contract version uint256 public constant override version = 3_10; - - /// @notice Contract type bytes32 public constant override contractType = AP_INTEREST_RATE_MODEL_FACTORY; - /// @notice Constructor - /// @param addressProvider_ Address provider contract address constructor(address addressProvider_) AbstractFactory(addressProvider_) {} // ---------- // diff --git a/contracts/factories/LossPolicyFactory.sol b/contracts/factories/LossPolicyFactory.sol index 485db83..86b873e 100644 --- a/contracts/factories/LossPolicyFactory.sol +++ b/contracts/factories/LossPolicyFactory.sol @@ -17,14 +17,9 @@ import {AbstractFactory} from "./AbstractFactory.sol"; import {AbstractMarketFactory} from "./AbstractMarketFactory.sol"; contract LossPolicyFactory is AbstractMarketFactory, ILossPolicyFactory { - /// @notice Contract version uint256 public constant override version = 3_10; - - /// @notice Contract type bytes32 public constant override contractType = AP_LOSS_POLICY_FACTORY; - /// @notice Constructor - /// @param addressProvider_ Address provider contract address constructor(address addressProvider_) AbstractFactory(addressProvider_) {} // ---------- // @@ -37,12 +32,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), @@ -90,7 +80,7 @@ contract LossPolicyFactory is AbstractMarketFactory, ILossPolicyFactory { returns (Call[] memory) { bytes4 selector = bytes4(callData); - if (selector != ILossPolicy.enable.selector && selector != ILossPolicy.disable.selector) { + if (selector != ILossPolicy.setAccessMode.selector && selector != ILossPolicy.setChecksEnabled.selector) { revert ForbiddenEmergencyConfigurationCallException(selector); } return CallBuilder.build(Call(_lossPolicy(pool), callData)); diff --git a/contracts/factories/PoolFactory.sol b/contracts/factories/PoolFactory.sol index d5a801d..47abb78 100644 --- a/contracts/factories/PoolFactory.sol +++ b/contracts/factories/PoolFactory.sol @@ -16,6 +16,8 @@ import {DefaultIRM} from "../helpers/DefaultIRM.sol"; import {IFactory} from "../interfaces/factories/IFactory.sol"; import {IMarketFactory} from "../interfaces/factories/IMarketFactory.sol"; import {IPoolFactory} from "../interfaces/factories/IPoolFactory.sol"; +import {IPoolConfigureActions} from "../interfaces/factories/IPoolConfigureActions.sol"; +import {IPoolEmergencyConfigureActions} from "../interfaces/factories/IPoolEmergencyConfigureActions.sol"; import {IAddressProvider} from "../interfaces/IAddressProvider.sol"; import {IMarketConfigurator} from "../interfaces/IMarketConfigurator.sol"; import {Call, DeployResult} from "../interfaces/Types.sol"; @@ -26,47 +28,19 @@ import {AP_POOL_FACTORY, AP_POOL_QUOTA_KEEPER, DOMAIN_POOL} from "../libraries/C import {AbstractFactory} from "./AbstractFactory.sol"; import {AbstractMarketFactory} from "./AbstractMarketFactory.sol"; -interface IConfigureActions { - function setTotalDebtLimit(uint256 limit) external; - function setCreditManagerDebtLimit(address creditManager, uint256 limit) external; - function setTokenLimit(address token, uint96 limit) external; - function setTokenQuotaIncreaseFee(address token, uint16 fee) external; - function pause() external; - function unpause() external; -} - -interface IEmergencyConfigureActions { - function setCreditManagerDebtLimitToZero(address creditManager) external; - function setTokenLimitToZero(address token) external; - function pause() external; -} - contract PoolFactory is AbstractMarketFactory, IPoolFactory { using SafeERC20 for IERC20; - /// @notice Contract version uint256 public constant override version = 3_10; - - /// @notice Contract type bytes32 public constant override contractType = AP_POOL_FACTORY; - /// @notice Address of the default IRM address public immutable defaultInterestRateModel; - /// @notice Thrown when trying to shutdown a credit suite with non-zero outstanding debt error CantShutdownCreditSuiteWithNonZeroDebtException(address creditManager); - - /// @notice Thrown when trying to shutdown a market with non-zero outstanding debt error CantShutdownMarketWithNonZeroDebtException(address pool); - - /// @notice Thrown when trying to deploy a pool without funding factory to mint dead shares - error InsufficientFundsForDeploymentException(); - - /// @notice Thrown when to set non-zero quota limit for a token with zero price feed + error InsufficientFundsForDeploymentException(address underlying); error ZeroPriceFeedException(address token); - /// @notice Constructor - /// @param addressProvider_ Address provider contract address constructor(address addressProvider_) AbstractFactory(addressProvider_) { defaultInterestRateModel = address(new DefaultIRM()); } @@ -84,8 +58,9 @@ contract PoolFactory is AbstractMarketFactory, IPoolFactory { address pool = _deployPool(msg.sender, underlying, name, symbol); address quotaKeeper = _deployQuotaKeeper(msg.sender, pool); - // NOTE: should use batching to avoid getting frontrun - if (IERC20(underlying).balanceOf(address(this)) < 1e5) revert InsufficientFundsForDeploymentException(); + // NOTE: mint dead shares to protect against inflation attack + if (IERC20(underlying).balanceOf(msg.sender) < 1e5) revert InsufficientFundsForDeploymentException(underlying); + IERC20(underlying).safeTransferFrom(msg.sender, address(this), 1e5); IERC20(underlying).forceApprove(pool, 1e5); IPoolV3(pool).deposit(1e5, address(0xdead)); @@ -194,18 +169,18 @@ contract PoolFactory is AbstractMarketFactory, IPoolFactory { { bytes4 selector = bytes4(callData); if ( - selector == IConfigureActions.setTotalDebtLimit.selector - || selector == IConfigureActions.setCreditManagerDebtLimit.selector - || selector == IConfigureActions.pause.selector || selector == IConfigureActions.unpause.selector + selector == IPoolConfigureActions.setTotalDebtLimit.selector + || selector == IPoolConfigureActions.setCreditManagerDebtLimit.selector + || selector == IPoolConfigureActions.pause.selector || selector == IPoolConfigureActions.unpause.selector ) { return CallBuilder.build(Call(pool, callData)); - } else if (selector == IConfigureActions.setTokenLimit.selector) { + } else if (selector == IPoolConfigureActions.setTokenLimit.selector) { (address token, uint96 limit) = abi.decode(callData[4:], (address, uint96)); if (limit != 0 && IPriceOracleV3(_priceOracle(pool)).getPrice(token) == 0) { revert ZeroPriceFeedException(token); } return CallBuilder.build(_setTokenLimit(_quotaKeeper(pool), token, limit)); - } else if (selector == IConfigureActions.setTokenQuotaIncreaseFee.selector) { + } else if (selector == IPoolConfigureActions.setTokenQuotaIncreaseFee.selector) { return CallBuilder.build(Call(_quotaKeeper(pool), callData)); } else { revert ForbiddenConfigurationCallException(selector); @@ -219,13 +194,13 @@ contract PoolFactory is AbstractMarketFactory, IPoolFactory { returns (Call[] memory) { bytes4 selector = bytes4(callData); - if (selector == IEmergencyConfigureActions.setCreditManagerDebtLimitToZero.selector) { + if (selector == IPoolEmergencyConfigureActions.setCreditManagerDebtLimitToZero.selector) { address creditManager = abi.decode(callData[4:], (address)); return CallBuilder.build(_setCreditManagerDebtLimit(pool, creditManager, 0)); - } else if (selector == IEmergencyConfigureActions.setTokenLimitToZero.selector) { + } else if (selector == IPoolEmergencyConfigureActions.setTokenLimitToZero.selector) { address token = abi.decode(callData[4:], (address)); return CallBuilder.build(_setTokenLimit(_quotaKeeper(pool), token, 0)); - } else if (selector == IEmergencyConfigureActions.pause.selector) { + } else if (selector == IPoolEmergencyConfigureActions.pause.selector) { return CallBuilder.build(Call(pool, callData)); } else { revert ForbiddenEmergencyConfigurationCallException(selector); diff --git a/contracts/factories/PriceOracleFactory.sol b/contracts/factories/PriceOracleFactory.sol index 5b33a2a..7e54a9f 100644 --- a/contracts/factories/PriceOracleFactory.sol +++ b/contracts/factories/PriceOracleFactory.sol @@ -3,12 +3,14 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; -import {IPriceFeed, IUpdatablePriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol"; +import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol"; import {IPriceOracleV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol"; import {IFactory} from "../interfaces/factories/IFactory.sol"; import {IMarketFactory} from "../interfaces/factories/IMarketFactory.sol"; import {IPriceOracleFactory} from "../interfaces/factories/IPriceOracleFactory.sol"; +import {IPriceOracleConfigureActions} from "../interfaces/factories/IPriceOracleConfigureActions.sol"; +import {IPriceOracleEmergencyConfigureActions} from "../interfaces/factories/IPriceOracleEmergencyConfigureActions.sol"; import {IMarketConfigurator} from "../interfaces/IMarketConfigurator.sol"; import {IPriceFeedStore} from "../interfaces/IPriceFeedStore.sol"; import {Call, DeployResult} from "../interfaces/Types.sol"; @@ -20,49 +22,30 @@ import { AP_PRICE_ORACLE_FACTORY, NO_VERSION_CONTROL } from "../libraries/ContractLiterals.sol"; -import {NestedPriceFeeds} from "../libraries/NestedPriceFeeds.sol"; import {AbstractFactory} from "./AbstractFactory.sol"; import {AbstractMarketFactory} from "./AbstractMarketFactory.sol"; -interface IConfigureActions { - function setPriceFeed(address token, address priceFeed) external; - function setReservePriceFeed(address token, address priceFeed) external; -} - -interface IEmergencyConfigureActions { - function setPriceFeed(address token, address priceFeed) external; +interface IPriceOracleLegacy { + /// @dev Older signature for fetching main and reserve feeds, reverts if price feed is not set + function priceFeedsRaw(address token, bool reserve) external view returns (address); } contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { - using CallBuilder for Call[]; - using NestedPriceFeeds for IPriceFeed; - - /// @notice Contract version uint256 public constant override version = 3_10; - - /// @notice Contract type bytes32 public constant override contractType = AP_PRICE_ORACLE_FACTORY; - /// @notice Address of the price feed store contract address public immutable priceFeedStore; + address public immutable zeroPriceFeed; - /// @notice Thrown when trying to set price feed for a token that is not allowed in the price feed store error PriceFeedNotAllowedException(address token, address priceFeed); - - /// @notice Thrown when trying to set a price feed that was allowed too recently error PriceFeedAllowedTooRecentlyException(address token, address priceFeed); - - /// @notice Thrown when trying to set price feed for a token that has not been added to the market error TokenIsNotAddedException(address token); - - /// @notice Thrown when trying to set zero price feed for pool's underlying or a token with non-zero quota error ZeroPriceFeedException(address token); - /// @notice Constructor - /// @param addressProvider_ Address provider contract address constructor(address addressProvider_) AbstractFactory(addressProvider_) { priceFeedStore = _getAddressOrRevert(AP_PRICE_FEED_STORE, NO_VERSION_CONTROL); + zeroPriceFeed = IPriceFeedStore(priceFeedStore).zeroPriceFeed(); } // ---------- // @@ -85,6 +68,21 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { }); } + function computePriceOracleAddress(address marketConfigurator, address pool) + external + view + override + returns (address) + { + address acl = IMarketConfigurator(marketConfigurator).acl(); + return _computeAddressLatestPatch({ + contractType: AP_PRICE_ORACLE, + minorVersion: version, + constructorParams: abi.encode(acl), + salt: bytes32(bytes20(pool)), + deployer: address(this) + }); + } // ------------ // // MARKET HOOKS // // ------------ // @@ -97,7 +95,7 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { { address underlying = _underlying(pool); _revertOnZeroPriceFeed(underlying, underlyingPriceFeed); - return _setPriceFeed(priceOracle, underlying, underlyingPriceFeed, false); + return CallBuilder.build(_setPriceFeed(priceOracle, underlying, underlyingPriceFeed, false)); } function onUpdatePriceOracle(address pool, address newPriceOracle, address oldPriceOracle) @@ -106,21 +104,26 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { override(AbstractMarketFactory, IMarketFactory) returns (Call[] memory calls) { - calls = CallBuilder.build(_unauthorizeFactory(msg.sender, pool, oldPriceOracle)); - address underlying = _underlying(pool); - calls = calls.extend( - _setPriceFeed(newPriceOracle, underlying, _getPriceFeed(oldPriceOracle, underlying, false), false) - ); - address[] memory tokens = _quotedTokens(_quotaKeeper(pool)); - uint256 numTokens = tokens.length; + uint256 numTokens = 1 + tokens.length; + + calls = new Call[](1 + 2 * numTokens); + calls[0] = _unauthorizeFactory(msg.sender, pool, oldPriceOracle); + + uint256 numCalls = 1; for (uint256 i; i < numTokens; ++i) { - address main = _getPriceFeed(oldPriceOracle, tokens[i], false); - calls = calls.extend(_setPriceFeed(newPriceOracle, tokens[i], main, false)); + address token = i == 0 ? underlying : tokens[i - 1]; - address reserve = _getPriceFeed(oldPriceOracle, tokens[i], true); - if (reserve != address(0)) calls = calls.extend(_setPriceFeed(newPriceOracle, tokens[i], reserve, true)); + address main = _getPriceFeed(oldPriceOracle, token, false); + calls[numCalls++] = _setPriceFeed(newPriceOracle, token, main, false); + + address reserve = _getPriceFeed(oldPriceOracle, token, true); + if (reserve != address(0)) calls[numCalls++] = _setPriceFeed(newPriceOracle, token, reserve, true); + } + + assembly { + mstore(calls, numCalls) } } @@ -130,7 +133,7 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { override(AbstractMarketFactory, IMarketFactory) returns (Call[] memory) { - return _setPriceFeed(_priceOracle(pool), token, priceFeed, false); + return CallBuilder.build(_setPriceFeed(_priceOracle(pool), token, priceFeed, false)); } // ------------- // @@ -146,14 +149,14 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { address priceOracle = _priceOracle(pool); bytes4 selector = bytes4(callData); - if (selector == IConfigureActions.setPriceFeed.selector) { + if (selector == IPriceOracleConfigureActions.setPriceFeed.selector) { (address token, address priceFeed) = abi.decode(callData[4:], (address, address)); _validatePriceFeed(pool, token, priceFeed, true); - return _setPriceFeed(priceOracle, token, priceFeed, false); - } else if (selector == IConfigureActions.setReservePriceFeed.selector) { + return CallBuilder.build(_setPriceFeed(priceOracle, token, priceFeed, false)); + } else if (selector == IPriceOracleConfigureActions.setReservePriceFeed.selector) { (address token, address priceFeed) = abi.decode(callData[4:], (address, address)); _validatePriceFeed(pool, token, priceFeed, false); - return _setPriceFeed(priceOracle, token, priceFeed, true); + return CallBuilder.build(_setPriceFeed(priceOracle, token, priceFeed, true)); } else { revert ForbiddenConfigurationCallException(selector); } @@ -168,13 +171,13 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { address priceOracle = _priceOracle(pool); bytes4 selector = bytes4(callData); - if (selector == IConfigureActions.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) { revert PriceFeedAllowedTooRecentlyException(token, priceFeed); } - return _setPriceFeed(priceOracle, token, priceFeed, false); + return CallBuilder.build(_setPriceFeed(priceOracle, token, priceFeed, false)); } else { revert ForbiddenEmergencyConfigurationCallException(selector); } @@ -201,6 +204,13 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { } function _getPriceFeed(address priceOracle, address token, bool reserve) internal view returns (address) { + if (IPriceOracleV3(priceOracle).version() < 3_10) { + try IPriceOracleLegacy(priceOracle).priceFeedsRaw(token, reserve) returns (address priceFeed) { + return priceFeed; + } catch { + return address(0); + } + } return reserve ? IPriceOracleV3(priceOracle).reservePriceFeeds(token) : IPriceOracleV3(priceOracle).priceFeeds(token); @@ -209,38 +219,20 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { function _setPriceFeed(address priceOracle, address token, address priceFeed, bool reserve) internal view - returns (Call[] memory) + returns (Call memory) { - if (!IPriceFeedStore(priceFeedStore).isAllowedPriceFeed(token, priceFeed)) { - revert PriceFeedNotAllowedException(token, priceFeed); - } - uint32 stalenessPeriod = IPriceFeedStore(priceFeedStore).getStalenessPeriod(priceFeed); + bool isValid = IPriceFeedStore(priceFeedStore).isAllowedPriceFeed(token, priceFeed) + || reserve && priceFeed == zeroPriceFeed; + if (!isValid) revert PriceFeedNotAllowedException(token, priceFeed); - Call[] memory calls = CallBuilder.build( - reserve - ? _setReservePriceFeed(priceOracle, token, priceFeed, stalenessPeriod) - : _setPriceFeed(priceOracle, token, priceFeed, stalenessPeriod) - ); - return _addUpdatableFeeds(priceOracle, priceFeed, calls); - } + uint32 stalenessPeriod = IPriceFeedStore(priceFeedStore).getStalenessPeriod(priceFeed); - function _addUpdatableFeeds(address priceOracle, address priceFeed, Call[] memory calls) - internal - view - returns (Call[] memory) - { - try IUpdatablePriceFeed(priceFeed).updatable() returns (bool updatable) { - if (updatable) calls = calls.append(_addUpdatablePriceFeed(priceOracle, priceFeed)); - } catch {} - address[] memory underlyingFeeds = IPriceFeed(priceFeed).getUnderlyingFeeds(); - uint256 numFeeds = underlyingFeeds.length; - for (uint256 i; i < numFeeds; ++i) { - calls = _addUpdatableFeeds(priceOracle, underlyingFeeds[i], calls); - } - return calls; + return reserve + ? _setReservePriceFeedCall(priceOracle, token, priceFeed, stalenessPeriod) + : _setPriceFeedCall(priceOracle, token, priceFeed, stalenessPeriod); } - 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) @@ -248,7 +240,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) @@ -256,8 +248,4 @@ contract PriceOracleFactory is AbstractMarketFactory, IPriceOracleFactory { return Call(priceOracle, abi.encodeCall(IPriceOracleV3.setReservePriceFeed, (token, priceFeed, stalenessPeriod))); } - - function _addUpdatablePriceFeed(address priceOracle, address priceFeed) internal pure returns (Call memory) { - return Call(priceOracle, abi.encodeCall(IPriceOracleV3.addUpdatablePriceFeed, (priceFeed))); - } } diff --git a/contracts/factories/RateKeeperFactory.sol b/contracts/factories/RateKeeperFactory.sol index 8731591..23c7db9 100644 --- a/contracts/factories/RateKeeperFactory.sol +++ b/contracts/factories/RateKeeperFactory.sol @@ -25,17 +25,11 @@ import {AbstractMarketFactory} from "./AbstractMarketFactory.sol"; contract RateKeeperFactory is AbstractMarketFactory, IRateKeeperFactory { using CallBuilder for Call[]; - /// @notice Contract version uint256 public constant override version = 3_10; - - /// @notice Contract type bytes32 public constant override contractType = AP_RATE_KEEPER_FACTORY; - /// @notice Address of the GEAR staking contract address public immutable gearStaking; - /// @notice Constructor - /// @param addressProvider_ Address provider contract address constructor(address addressProvider_) AbstractFactory(addressProvider_) { gearStaking = _getAddressOrRevert(AP_GEAR_STAKING, NO_VERSION_CONTROL); } @@ -119,7 +113,7 @@ contract RateKeeperFactory is AbstractMarketFactory, IRateKeeperFactory { calls = calls.append(_setFrozenEpoch(oldRateKeeper, true)); } if (type_ == "RATE_KEEPER::GAUGE") { - calls = calls.append(_setFrozenEpoch(oldRateKeeper, false)); + calls = calls.append(_setFrozenEpoch(newRateKeeper, false)); } calls = calls.append(_unauthorizeFactory(msg.sender, pool, oldRateKeeper)); } diff --git a/contracts/global/BytecodeRepository.sol b/contracts/global/BytecodeRepository.sol index 1be59cb..dee7d20 100644 --- a/contracts/global/BytecodeRepository.sol +++ b/contracts/global/BytecodeRepository.sol @@ -4,567 +4,594 @@ pragma solidity ^0.8.23; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; -import {IBytecodeRepository} from "../interfaces/IBytecodeRepository.sol"; -import {AP_BYTECODE_REPOSITORY} from "../libraries/ContractLiterals.sol"; -import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; -import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol"; -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + import {LibString} from "@solady/utils/LibString.sol"; -import "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol"; +import {SSTORE2} from "@solady/utils/SSTORE2.sol"; + +import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; +import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol"; -import {Bytecode, BytecodePointer, AuditorSignature} from "../interfaces/Types.sol"; import {EIP712Mainnet} from "../helpers/EIP712Mainnet.sol"; +import {IBytecodeRepository} from "../interfaces/IBytecodeRepository.sol"; +import {AuditReport, Bytecode, BytecodePointer} from "../interfaces/Types.sol"; +import {AP_BYTECODE_REPOSITORY} from "../libraries/ContractLiterals.sol"; import {Domain} from "../libraries/Domain.sol"; import {ImmutableOwnableTrait} from "../traits/ImmutableOwnableTrait.sol"; -import {SSTORE2} from "@solady/utils/SSTORE2.sol"; -/** - * @title BytecodeRepository - * - * @notice - * The `BytecodeRepository` is a singleton contract responsible for deploying all contracts in the system for risk curators. - * - * Each contract is identified by two parameters: - * - ContractType: bytes32. Can be further split into two parts: - * - Domain: Represents the fundamental category or name of the smart contract. For example, - * contracts like `Pools` or `CreditManagers` use the contract name as the domain. - * - Postfix: For contracts that offer different implementations under the same interface - * (such as interest rate models, adapters, or price feeds), the domain remains fixed. - * Variations are distinguished using a postfix. This is established by convention. - * - Version: uint256: Specifies the version of the contract in semver. (example: 3_10) - * - * ContractType Convention: - * - The contract type follows a capitalized snake_case naming convention - * without any version information (example: "POOL", "CREDIT_MANAGER") - * - List of domains: - * RK_ - rate keepers. Supports IRateKeeperBase interface. - * IRM_ - interest rate models - * - * This structure ensures consistency and clarity when deploying and managing contracts within the system. - */ +/// @title Bytecode repository contract BytecodeRepository is ImmutableOwnableTrait, SanityCheckTrait, IBytecodeRepository, EIP712Mainnet { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; - using ECDSA for bytes32; + using EnumerableSet for EnumerableSet.UintSet; using LibString for bytes32; using LibString for string; using LibString for uint256; - using Domain for string; - using SSTORE2 for bytes; - // - // CONSTANTS - // + using Domain for bytes32; + + /// @dev Internal struct with version info for a given contract type + struct VersionInfo { + address owner; + uint256 latest; + mapping(uint256 majorVersion => uint256) latestByMajor; + mapping(uint256 minorVersion => uint256) latestByMinor; + EnumerableSet.UintSet versionsSet; + } - /// @notice Meta info about contract type & version + /// @notice Contract version uint256 public constant override version = 3_10; + + /// @notice Contract type bytes32 public constant override contractType = AP_BYTECODE_REPOSITORY; - bytes32 public constant BYTECODE_TYPEHASH = + /// @notice Bytecode typehash + bytes32 public constant override BYTECODE_TYPEHASH = keccak256("Bytecode(bytes32 contractType,uint256 version,bytes initCode,address author,string source)"); - bytes32 public constant AUDITOR_SIGNATURE_TYPEHASH = - keccak256("SignBytecodeHash(bytes32 bytecodeHash,string reportUrl)"); - - // - // STORAGE - // - - // bytecodeHash => Bytecode - mapping(bytes32 => BytecodePointer) internal _bytecodeByHash; - - // bytecodeHash => array of AuditorSignature - mapping(bytes32 => AuditorSignature[]) internal _auditorSignaturesByHash; + /// @notice Audit report typehash + bytes32 public constant override AUDIT_REPORT_TYPEHASH = + keccak256("AuditReport(bytes32 bytecodeHash,address auditor,string reportUrl)"); - EnumerableSet.Bytes32Set internal _bytecodeHashes; + /// @dev Mapping from `deployedContract` deployed from the repository to its bytecode hash + mapping(address deployedContract => bytes32) _deployedContractBytecodeHashes; - // contractType => version => bytecodeHash - mapping(bytes32 => mapping(uint256 => bytes32)) public approvedBytecodeHash; + /// @dev Mapping from `bytecodeHash` to pointer to bytecode with given hash + mapping(bytes32 bytecodeHash => BytecodePointer) internal _bytecodeByHash; - // address => bytecodeHash - mapping(address => bytes32) public deployedContracts; + /// @dev Mapping from `bytecodeHash` to its audit reports + mapping(bytes32 bytecodeHash => AuditReport[]) internal _auditReports; - // Forbidden initCodes - mapping(bytes32 => bool) public forbiddenInitCode; + /// @dev Mapping from `cType` to `ver` to allowed bytecode hash + mapping(bytes32 cType => mapping(uint256 ver => bytes32 bytecodeHash)) internal _allowedBytecodeHashes; - // Allowed system contracts - mapping(bytes32 => bool) public allowedSystemContracts; + /// @dev Set of system domains + EnumerableSet.Bytes32Set internal _systemDomainsSet; - // Distinguish system vs. public domains - EnumerableSet.Bytes32Set private _publicDomains; + /// @dev Set of public domains + EnumerableSet.Bytes32Set internal _publicDomainsSet; - // if contractType is public - mapping(bytes32 => address) public contractTypeOwner; + /// @dev Set of approved auditors + EnumerableSet.AddressSet internal _auditorsSet; - // Auditors - EnumerableSet.AddressSet private _auditors; + /// @dev Mapping from `auditor` to their name + mapping(address auditor => string) internal _auditorNames; - // Auditor => name - mapping(address => string) public auditorName; + /// @dev Mapping from `initCodeHash` to whether it is forbidden + mapping(bytes32 initCodeHash => bool) internal _isInitCodeForbidden; - // Postfixes are used to deploy unique contract versions inherited from - // the base contract but differ when used with specific tokens. - // For example, the USDT pool, which supports fee computation without errors - mapping(address => bytes32) public tokenSpecificPostfixes; + /// @dev Mapping from `token` to its specific postfix + mapping(address token => bytes32) internal _tokenSpecificPostfixes; - // Version control - mapping(bytes32 => uint256) public latestVersion; - mapping(bytes32 => mapping(uint256 => uint256)) public latestMinorVersion; - mapping(bytes32 => mapping(uint256 => uint256)) public latestPatchVersion; + /// @dev Mapping from `cType` to its version info + mapping(bytes32 cType => VersionInfo) internal _versionInfo; - constructor(address _owner) + /// @notice Constructor + /// @param owner_ Owner of the bytecode repository + constructor(address owner_) EIP712Mainnet(contractType.fromSmallString(), version.toString()) - ImmutableOwnableTrait(_owner) + ImmutableOwnableTrait(owner_) {} - /// @notice Computes a unique hash for _bytecode metadata - /// @param _bytecode Bytecode metadata including contract type, version, _bytecode, author and source - /// @return bytes32 Hash of the metadata - function computeBytecodeHash(Bytecode memory _bytecode) public pure returns (bytes32) { + // --------------- // + // EIP-712 GETTERS // + // --------------- // + + /// @notice Returns the domain separator + function domainSeparatorV4() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @notice Computes bytecode's struct hash + function computeBytecodeHash(Bytecode calldata bytecode) public pure override returns (bytes32) { return keccak256( abi.encode( BYTECODE_TYPEHASH, - _bytecode.contractType, - _bytecode.version, - keccak256(_bytecode.initCode), - _bytecode.author, - keccak256(bytes(_bytecode.source)) + bytecode.contractType, + bytecode.version, + keccak256(bytecode.initCode), + bytecode.author, + keccak256(bytes(bytecode.source)) ) ); } - /// @notice Uploads new _bytecode to the repository - /// @param _bytecode Bytecode metadata to upload - /// @dev Only the author can upload on mainnet - function uploadBytecode(Bytecode memory _bytecode) external nonZeroAddress(_bytecode.author) { - if (block.chainid == 1 && msg.sender != _bytecode.author) { - revert OnlyAuthorCanSyncException(); - } - // Check if _bytecode is already uploaded - bytes32 bytecodeHash = computeBytecodeHash(_bytecode); - - if (isBytecodeUploaded(bytecodeHash)) { - revert BytecodeAlreadyExistsException(); - } - - // Verify author's signature of the _bytecode metadata - address recoveredAuthor = ECDSA.recover(_hashTypedDataV4(bytecodeHash), _bytecode.authorSignature); - if (recoveredAuthor != _bytecode.author) { - revert InvalidAuthorSignatureException(); - } - - // Revert if the initCode is forbidden - revertIfInitCodeForbidden(_bytecode.initCode); + /// @notice Computes struct hash for auditor signature + function computeAuditReportHash(bytes32 bytecodeHash, address auditor, string calldata reportUrl) + public + pure + override + returns (bytes32) + { + return keccak256(abi.encode(AUDIT_REPORT_TYPEHASH, bytecodeHash, auditor, keccak256(bytes(reportUrl)))); + } - // Check if the contract name and version already exists - if (approvedBytecodeHash[_bytecode.contractType][_bytecode.version] != 0) { - revert ContractTypeVersionAlreadyExistsException(); - } + // ------------------- // + // DEPLOYING CONTRACTS // + // ------------------- // - address initCodePointer = _bytecode.initCode.write(); + /// @notice Whether `deployedContract` was deployed from the repository + function isDeployedFromRepository(address deployedContract) external view override returns (bool) { + return _deployedContractBytecodeHashes[deployedContract] != bytes32(0); + } - _bytecodeByHash[bytecodeHash] = BytecodePointer({ - contractType: _bytecode.contractType, - version: _bytecode.version, - initCodePointer: initCodePointer, - author: _bytecode.author, - source: _bytecode.source, - authorSignature: _bytecode.authorSignature - }); + /// @notice Returns bytecode hash for `deployedContract` deployed from the repository + function getDeployedContractBytecodeHash(address deployedContract) external view override returns (bytes32) { + return _deployedContractBytecodeHashes[deployedContract]; + } - _bytecodeHashes.add(bytecodeHash); + /// @notice Computes the address at which a contract of a given type and version + /// with given constructor parameters and salt would be deployed + /// @dev Deployer's address is mixed with salt to prevent front-running using collisions + function computeAddress(bytes32 cType, uint256 ver, bytes memory constructorParams, bytes32 salt, address deployer) + external + view + override + returns (address) + { + bytes32 bytecodeHash = _allowedBytecodeHashes[cType][ver]; + BytecodePointer storage bytecode = _bytecodeByHash[bytecodeHash]; + bytes memory initCode = SSTORE2.read(bytecode.initCodePointer); - emit UploadBytecode( - bytecodeHash, - _bytecode.contractType.fromSmallString(), - _bytecode.version, - _bytecode.author, - _bytecode.source - ); + bytes32 uniqueSalt = keccak256(abi.encode(salt, deployer)); + bytes memory bytecodeWithParams = abi.encodePacked(initCode, constructorParams); + return Create2.computeAddress(uniqueSalt, keccak256(bytecodeWithParams)); } - /// @notice Deploys a contract using stored _bytecode - /// @param _contractType Type identifier of the contract - /// @param _version Version of the contract to deploy - /// @param constructorParams Constructor parameters for deployment - /// @param salt Unique salt for CREATE2 deployment - /// @return newContract Address of the deployed contract - function deploy(bytes32 _contractType, uint256 _version, bytes memory constructorParams, bytes32 salt) + /// @notice Deploys a contract of a given type and version with given constructor parameters and salt. + /// Tries to transfer ownership over the deployed contract to the caller. + /// Bytecode must be allowed either as system or public contract, which, in turn, requires it + /// to be uploaded and have at least one signed report from approved auditor. + /// @dev Deployer's address is mixed with salt to prevent front-running using collisions + /// @dev Reverts if contract's init code is forbidden + /// @dev Reverts if contract was previously deployed at the same address + /// @dev Reverts if deployed contract's type or version does not match passed parameters + function deploy(bytes32 cType, uint256 ver, bytes memory constructorParams, bytes32 salt) external + override returns (address newContract) { - // Retrieve bytecodeHash - bytes32 bytecodeHash = approvedBytecodeHash[_contractType][_version]; - if (bytecodeHash == 0) { - revert BytecodeIsNotApprovedException(_contractType, _version); - } + bytes32 bytecodeHash = _allowedBytecodeHashes[cType][ver]; + if (bytecodeHash == 0) revert BytecodeIsNotAllowedException(cType, ver); - if (!isAuditBytecode(bytecodeHash)) { - revert BytecodeIsNotAuditedException(); - } - - BytecodePointer storage _bytecode = _bytecodeByHash[bytecodeHash]; - - bytes memory initCode = SSTORE2.read(_bytecode.initCodePointer); - - // Revert if the initCode is forbidden - revertIfInitCodeForbidden(initCode); + BytecodePointer storage bytecode = _bytecodeByHash[bytecodeHash]; + bytes memory initCode = SSTORE2.read(bytecode.initCodePointer); + _revertIfInitCodeIsForbidden(initCode); - // Combine code + constructor params + bytes32 uniqueSalt = keccak256(abi.encode(salt, msg.sender)); bytes memory bytecodeWithParams = abi.encodePacked(initCode, constructorParams); + newContract = Create2.computeAddress(uniqueSalt, keccak256(bytecodeWithParams)); - bytes32 saltUnique = keccak256(abi.encode(salt, msg.sender)); - - // Compute CREATE2 address - newContract = Create2.computeAddress(saltUnique, keccak256(bytecodeWithParams)); - - // Check if the contract already deployed - if (newContract.code.length != 0) { - revert BytecodeAlreadyExistsAtAddressException(newContract); + if (newContract.code.length != 0) revert ContractIsAlreadyDeployedException(newContract); + Create2.deploy(0, uniqueSalt, bytecodeWithParams); + if (IVersion(newContract).contractType() != cType || IVersion(newContract).version() != ver) { + revert InvalidBytecodeException(bytecodeHash); } - // Deploy - Create2.deploy(0, saltUnique, bytecodeWithParams); + _deployedContractBytecodeHashes[newContract] = bytecodeHash; + emit DeployContract(bytecodeHash, cType, ver, newContract); - // Verify IVersion - if (IVersion(newContract).contractType() != _contractType || IVersion(newContract).version() != _version) { - revert IncorrectBytecodeException(bytecodeHash); - } + try Ownable(newContract).transferOwnership(msg.sender) {} catch {} + } - // add to deployedContracts - deployedContracts[newContract] = bytecodeHash; + // ------------------ // + // UPLOADING BYTECODE // + // ------------------ // - emit DeployContract(newContract, bytecodeHash, _contractType.fromSmallString(), _version); + /// @notice Returns bytecode with `bytecodeHash` + /// @dev Reverts if bytecode is not uploaded + function getBytecode(bytes32 bytecodeHash) external view override returns (Bytecode memory) { + BytecodePointer memory bytecode = _bytecodeByHash[bytecodeHash]; + if (bytecode.initCodePointer == address(0)) revert BytecodeIsNotUploadedException(bytecodeHash); + return Bytecode({ + contractType: bytecode.contractType, + version: bytecode.version, + initCode: SSTORE2.read(bytecode.initCodePointer), + author: bytecode.author, + source: bytecode.source, + authorSignature: bytecode.authorSignature + }); + } - // Auto-transfer ownership if IOwnable - try Ownable(newContract).transferOwnership(msg.sender) {} catch {} + /// @notice Whether bytecode with `bytecodeHash` is uploaded + function isBytecodeUploaded(bytes32 bytecodeHash) public view override returns (bool) { + return _bytecodeByHash[bytecodeHash].initCodePointer != address(0); } - /// @notice Computes the address where a contract would be deployed - /// @param _contractType Type identifier of the contract - /// @param _version Version of the contract - /// @param constructorParams Constructor parameters - /// @param salt Unique salt for CREATE2 deployment - /// @return Address where the contract would be deployed - function computeAddress( - bytes32 _contractType, - uint256 _version, - bytes memory constructorParams, - bytes32 salt, - address deployer - ) external view returns (address) { - // Retrieve bytecodeHash - bytes32 bytecodeHash = approvedBytecodeHash[_contractType][_version]; - if (bytecodeHash == 0) { - revert BytecodeIsNotApprovedException(_contractType, _version); + /// @notice Uploads new contract bytecode to the repository. + /// Simply uploading the bytecode is not enough to deploy a contract with it, see `deploy` for details. + /// @dev Reverts if bytecode's contract type is invalid or version is less than `100` or greater than `999` + /// @dev Reverts if bytecode for given contract type and version is already allowed + /// @dev Reverts if author is zero address or if their signature is invalid + /// @dev On mainnet, only author of the bytecode can upload it + function uploadBytecode(Bytecode calldata bytecode) external override nonZeroAddress(bytecode.author) { + bytes32 bytecodeHash = computeBytecodeHash(bytecode); + if (isBytecodeUploaded(bytecodeHash)) return; + + _validateContractType(bytecode.contractType); + _validateVersion(bytecode.contractType, bytecode.version); + if (_allowedBytecodeHashes[bytecode.contractType][bytecode.version] != 0) { + revert BytecodeIsAlreadyAllowedException(bytecode.contractType, bytecode.version); } - BytecodePointer storage _bytecode = _bytecodeByHash[bytecodeHash]; - bytes memory initCode = SSTORE2.read(_bytecode.initCodePointer); + if (block.chainid == 1 && msg.sender != bytecode.author) revert CallerIsNotBytecodeAuthorException(msg.sender); + address author = ECDSA.recover(_hashTypedDataV4(bytecodeHash), bytecode.authorSignature); + if (author != bytecode.author) revert InvalidAuthorSignatureException(author); - // Combine code + constructor params - bytes memory bytecodeWithParams = abi.encodePacked(initCode, constructorParams); + address initCodePointer = SSTORE2.write(bytecode.initCode); + _bytecodeByHash[bytecodeHash] = BytecodePointer({ + contractType: bytecode.contractType, + version: bytecode.version, + initCodePointer: initCodePointer, + author: bytecode.author, + source: bytecode.source, + authorSignature: bytecode.authorSignature + }); + emit UploadBytecode( + bytecodeHash, + bytecode.contractType, + bytecode.version, + bytecode.author, + bytecode.source, + bytecode.authorSignature + ); + } - bytes32 saltUnique = keccak256(abi.encode(salt, deployer)); + // ----------------- // + // AUDITING BYTECODE // + // ----------------- // - // Return CREATE2 address - return Create2.computeAddress(saltUnique, keccak256(bytecodeWithParams)); + /// @notice Whether bytecode with `bytecodeHash` is signed at least by one approved auditor + function isBytecodeAudited(bytes32 bytecodeHash) public view override returns (bool) { + uint256 len = _auditReports[bytecodeHash].length; + for (uint256 i; i < len; ++i) { + AuditReport memory report = _auditReports[bytecodeHash][i]; + if (isAuditor(report.auditor)) return true; + } + return false; } - /// @notice Allows auditors to sign _bytecode metadata - /// @param bytecodeHash Hash of the _bytecode metadata to sign - /// @param reportUrl URL of the audit report - /// @param signature Cryptographic signature of the auditor - function signBytecodeHash(bytes32 bytecodeHash, string calldata reportUrl, bytes memory signature) external { - // Must point to existing metadata - if (!isBytecodeUploaded(bytecodeHash)) { - revert BytecodeIsNotUploadedException(bytecodeHash); - } + /// @notice Returns all audit reports for `bytecodeHash` + function getAuditReports(bytes32 bytecodeHash) external view override returns (AuditReport[] memory) { + return _auditReports[bytecodeHash]; + } - // Re-create typed data - bytes32 structHash = - keccak256(abi.encode(AUDITOR_SIGNATURE_TYPEHASH, bytecodeHash, keccak256(bytes(reportUrl)))); - // Hash with our pinned domain - address signer = ECDSA.recover(_hashTypedDataV4(structHash), signature); + /// @notice Returns audit report at `index` for `bytecodeHash` + function getAuditReport(bytes32 bytecodeHash, uint256 index) external view override returns (AuditReport memory) { + return _auditReports[bytecodeHash][index]; + } - // Must match msg.sender and be an approved auditor - if (!_auditors.contains(signer)) { - revert SignerIsNotAuditorException(signer); - } + /// @notice Returns number of audit reports for `bytecodeHash` + function getNumAuditReports(bytes32 bytecodeHash) external view override returns (uint256) { + return _auditReports[bytecodeHash].length; + } - // do not allow duplicates - uint256 len = _auditorSignaturesByHash[bytecodeHash].length; - for (uint256 i = 0; i < len; ++i) { - if (keccak256(_auditorSignaturesByHash[bytecodeHash][i].signature) == keccak256(signature)) { - revert AuditorAlreadySignedException(); + /// @notice Submits signed audit report for bytecode with `bytecodeHash` + /// @dev Reverts if bytecode is not uploaded + /// @dev Reverts if auditor is not approved, already signed bytecode, or their signature is invalid + function submitAuditReport(bytes32 bytecodeHash, AuditReport calldata auditReport) external override { + if (!isBytecodeUploaded(bytecodeHash)) revert BytecodeIsNotUploadedException(bytecodeHash); + if (!_auditorsSet.contains(auditReport.auditor)) revert AuditorIsNotApprovedException(auditReport.auditor); + + bytes32 reportHash = computeAuditReportHash(bytecodeHash, auditReport.auditor, auditReport.reportUrl); + address auditor = ECDSA.recover(_hashTypedDataV4(reportHash), auditReport.signature); + if (auditor != auditReport.auditor) revert InvalidAuditorSignatureException(auditor); + + AuditReport[] storage reports = _auditReports[bytecodeHash]; + uint256 len = reports.length; + for (uint256 i; i < len; ++i) { + if (keccak256(reports[i].signature) == keccak256(auditReport.signature)) { + revert BytecodeIsAlreadySignedByAuditorException(bytecodeHash, auditor); } } - _auditorSignaturesByHash[bytecodeHash].push( - AuditorSignature({reportUrl: reportUrl, auditor: signer, signature: signature}) - ); - - emit AuditBytecode(bytecodeHash, signer, reportUrl, signature); + reports.push(auditReport); + emit AuditBytecode(bytecodeHash, auditor, auditReport.reportUrl, auditReport.signature); + } - BytecodePointer storage _bytecode = _bytecodeByHash[bytecodeHash]; + // ----------------- // + // ALLOWING BYTECODE // + // ----------------- // - bytes32 _contractType = _bytecode.contractType; - address author = _bytecode.author; + /// @notice Returns the allowed bytecode hash for `cType` and `ver` + function getAllowedBytecodeHash(bytes32 cType, uint256 ver) external view override returns (bytes32) { + return _allowedBytecodeHashes[cType][ver]; + } - bool isSystemContract = allowedSystemContracts[bytecodeHash]; + /// @notice Returns the owner of `cType` + function getContractTypeOwner(bytes32 cType) external view override returns (address) { + return _versionInfo[cType].owner; + } - address currentOwner = contractTypeOwner[_contractType]; - bool isContractTypeInPublicDomain = isInPublicDomain(_contractType); + /// @notice Marks bytecode with `bytecodeHash` as allowed system contract. + /// Adds bytecode's domain to the list of system domains. + /// @dev Can only be called by the owner + /// @dev Reverts if bytecode is not uploaded or not audited + /// @dev Reverts if bytecode's contract type is in the list of public domains + /// @dev Reverts if bytecode with this contract type and version is already allowed + function allowSystemContract(bytes32 bytecodeHash) external override onlyOwner { + if (!isBytecodeUploaded(bytecodeHash)) revert BytecodeIsNotUploadedException(bytecodeHash); + if (!isBytecodeAudited(bytecodeHash)) revert BytecodeIsNotAuditedException(bytecodeHash); + + BytecodePointer storage bytecode = _bytecodeByHash[bytecodeHash]; + bytes32 cType = bytecode.contractType; + _addSystemDomain(cType.extractDomain()); + + _allowContract(bytecodeHash, cType, bytecode.version); + } - if (currentOwner == address(0) || isSystemContract) { - contractTypeOwner[_contractType] = author; - } else if (isContractTypeInPublicDomain && (currentOwner != author)) { - revert NotDomainOwnerException(); + /// @notice Marks bytecode with `bytecodeHash` as allowed public contract. + /// Sets bytecode's author as contract type owner. + /// @dev Reverts if bytecode is not uploaded or not audited + /// @dev Reverts if bytecode's contract type is not in the list of public domains + /// @dev Reverts if bytecode's author is not contract type owner + /// @dev Reverts if bytecode with this contract type and version is already allowed + function allowPublicContract(bytes32 bytecodeHash) external override { + if (!isBytecodeUploaded(bytecodeHash)) revert BytecodeIsNotUploadedException(bytecodeHash); + if (!isBytecodeAudited(bytecodeHash)) revert BytecodeIsNotAuditedException(bytecodeHash); + + BytecodePointer storage bytecode = _bytecodeByHash[bytecodeHash]; + bytes32 cType = bytecode.contractType; + if (!isPublicDomain(cType.extractDomain())) revert ContractTypeIsNotInPublicDomainException(cType); + + address author = bytecode.author; + address contractTypeOwner = _versionInfo[cType].owner; + if (contractTypeOwner == address(0)) { + _versionInfo[cType].owner = author; + emit SetContractTypeOwner(cType, author); + } else if (contractTypeOwner != author) { + revert AuthorIsNotContractTypeOwnerException(cType, author); } - if (isSystemContract || isContractTypeInPublicDomain) { - _approveContract(_contractType, _bytecode.version, bytecodeHash); - } + _allowContract(bytecodeHash, cType, bytecode.version); } - /// @notice Allows owner to mark contracts as system contracts - /// @param bytecodeHash Hash of the _bytecode metadata to allow - function allowSystemContract(bytes32 bytecodeHash) external onlyOwner { - allowedSystemContracts[bytecodeHash] = true; + /// @notice Forbids all previously allowed public contracts of a given type, removes type owner and version info. + /// Exists primarily to cleanup the repository after public domain squatting by a compromised auditor. + /// @dev Can only be called by the owner + function removePublicContractType(bytes32 cType) external override onlyOwner { + if (!isPublicDomain(cType.extractDomain())) return; - if (isBytecodeUploaded(bytecodeHash) && isAuditBytecode(bytecodeHash)) { - BytecodePointer storage _bytecode = _bytecodeByHash[bytecodeHash]; - contractTypeOwner[_bytecode.contractType] = _bytecode.author; - _approveContract(_bytecode.contractType, _bytecode.version, bytecodeHash); + VersionInfo storage info = _versionInfo[cType]; + if (info.owner != address(0)) { + info.owner = address(0); + emit RemoveContractTypeOwner(cType); + } + info.latest = 0; + uint256[] memory versions = info.versionsSet.values(); + uint256 numVersions = versions.length; + for (uint256 i; i < numVersions; ++i) { + uint256 ver = versions[i]; + info.versionsSet.remove(ver); + info.latestByMajor[_getMajorVersion(ver)] = 0; + info.latestByMinor[_getMinorVersion(ver)] = 0; + + bytes32 bytecodeHash = _allowedBytecodeHashes[cType][ver]; + _allowedBytecodeHashes[cType][ver] = bytes32(0); + emit ForbidContract(bytecodeHash, cType, ver); } } - /// @notice Internal function to approve contract _bytecode - /// @param bytecodeHash Hash of the _bytecode metadata to approve - function _approveContract(bytes32 _contractType, uint256 _version, bytes32 bytecodeHash) internal { - if (approvedBytecodeHash[_contractType][_version] == 0) { - approvedBytecodeHash[_contractType][_version] = bytecodeHash; + /// @dev Allows bytecode with `bytecodeHash` for `cType` and `ver`, updates version info for `cType` + /// @dev Reverts if bytecode is already allowed + function _allowContract(bytes32 bytecodeHash, bytes32 cType, uint256 ver) internal { + if (_allowedBytecodeHashes[cType][ver] == bytecodeHash) return; + if (_allowedBytecodeHashes[cType][ver] != 0) revert BytecodeIsAlreadyAllowedException(cType, ver); + _allowedBytecodeHashes[cType][ver] = bytecodeHash; + emit AllowContract(bytecodeHash, cType, ver); - uint256 majorVersion = (_version / 100) * 100; - uint256 minorVersion = ((_version / 10) % 10) * 10 + majorVersion; + _updateVersionInfo(cType, ver); + } - if (latestVersion[_contractType] < _version) { - latestVersion[_contractType] = _version; - } - if (latestMinorVersion[_contractType][majorVersion] < _version) { - latestMinorVersion[_contractType][majorVersion] = _version; - } - if (latestPatchVersion[_contractType][minorVersion] < _version) { - latestPatchVersion[_contractType][minorVersion] = _version; - } + // ------------------ // + // DOMAINS MANAGEMENT // + // ------------------ // - emit ApproveContract(bytecodeHash, _contractType, _version); - } + /// @notice Whether `domain` is in the list of system domains + function isSystemDomain(bytes32 domain) public view override returns (bool) { + return _systemDomainsSet.contains(domain); } - // - // Auditor management - // - /// @notice Adds a new auditor - /// @param auditor Address of the auditor - /// @param name Name of the auditor - function addAuditor(address auditor, string memory name) external onlyOwner nonZeroAddress(auditor) { - bool added = _auditors.add(auditor); - if (added) { - auditorName[auditor] = name; - emit AddAuditor(auditor, name); - } + /// @notice Returns list of all system domains + function getSystemDomains() external view override returns (bytes32[] memory) { + return _systemDomainsSet.values(); } - /// @notice Removes an auditor - /// @param auditor Address of the auditor to remove - function removeAuditor(address auditor) external onlyOwner { - bool removed = _auditors.remove(auditor); - if (removed) { - emit RemoveAuditor(auditor); - } + /// @notice Whether `domain` is in the list of public domains + function isPublicDomain(bytes32 domain) public view override returns (bool) { + return _publicDomainsSet.contains(domain); } - /// @notice Checks if an address is an approved auditor - /// @param auditor Address to check - /// @return bool True if address is an approved auditor - function isAuditor(address auditor) public view returns (bool) { - return _auditors.contains(auditor); + /// @notice Returns list of all public domains + function getPublicDomains() external view override returns (bytes32[] memory) { + return _publicDomainsSet.values(); } - /// @notice Returns list of all approved auditors - /// @return Array of auditor addresses - function getAuditors() external view returns (address[] memory) { - return _auditors.values(); + /// @notice Adds `domain` to the list of public domains + /// @dev Can only be called by the owner + /// @dev Reverts if `domain` is invalid or is already in the list of system domains + function addPublicDomain(bytes32 domain) external override onlyOwner { + _validateDomain(domain); + _addPublicDomain(domain); } - // - // DOMAIN MANAGEMENT - // - - /// @notice Adds a new public domain - /// @param domain Domain identifier to add - /// @dev Non-revertable to avoid blocking InstanceManager - function addPublicDomain(bytes32 domain) external onlyOwner { - if (domain == bytes32(0)) { - return; - } - - if (LibString.fromSmallString(domain).contains("::")) { - return; - } + /// @dev Adds `domain` to the list of public domains + /// @dev Reverts if `domain` is already in the list of system domains + function _addPublicDomain(bytes32 domain) internal { + if (isSystemDomain(domain)) revert DomainIsAlreadyMarketAsSystemException(domain); + if (_publicDomainsSet.add(domain)) emit AddPublicDomain(domain); + } - if (_publicDomains.add(domain)) { - emit AddPublicDomain(domain); - } + /// @dev Adds `domain` to the list of system domains + /// @dev Reverts if `domain` is already in the list of public domains + function _addSystemDomain(bytes32 domain) internal { + if (isPublicDomain(domain)) revert DomainIsAlreadyMarketAsPublicException(domain); + if (_systemDomainsSet.add(domain)) emit AddSystemDomain(domain); } - /// @notice Removes a public domain - /// @param domain Domain identifier to remove - function removePublicDomain(bytes32 domain) external onlyOwner { - if (_publicDomains.remove(domain)) { - emit RemovePublicDomain(domain); - } + // ------------------- // + // AUDITORS MANAGEMENT // + // ------------------- // + + /// @notice Whether `auditor` is an approved auditor + function isAuditor(address auditor) public view override returns (bool) { + return _auditorsSet.contains(auditor); } - /// @notice Marks initCode as forbidden - /// @param initCodeHash Hash of initCode to forbid - function forbidInitCode(bytes32 initCodeHash) external onlyOwner { - forbiddenInitCode[initCodeHash] = true; - emit ForbidBytecode(initCodeHash); + /// @notice Returns list of all approved auditors + function getAuditors() external view override returns (address[] memory) { + return _auditorsSet.values(); } - /// @notice Sets token-specific postfix - /// @param token Token address - /// @param postfix Postfix to associate with token - function setTokenSpecificPostfix(address token, bytes32 postfix) external onlyOwner { - tokenSpecificPostfixes[token] = postfix; - emit SetTokenSpecificPostfix(token, postfix); + /// @notice Returns `auditor`'s name + function getAuditorName(address auditor) external view override returns (string memory) { + return _auditorNames[auditor]; } - /// @notice Removes contract type owner - /// @param _contractType Contract type to remove owner from - /// @dev Used to remove malicious auditors and cybersquatters - function removeContractTypeOwner(bytes32 _contractType) external onlyOwner { - if (contractTypeOwner[_contractType] != address(0)) { - contractTypeOwner[_contractType] = address(0); - emit RemoveContractTypeOwner(_contractType); - } + /// @notice Adds `auditor` to the list of approved auditors + /// @dev Can only be called by the owner + /// @dev Reverts if `auditor` is zero address + function addAuditor(address auditor, string memory name) external override onlyOwner nonZeroAddress(auditor) { + if (!_auditorsSet.add(auditor)) return; + _auditorNames[auditor] = name; + emit AddAuditor(auditor, name); } - function revokeApproval(bytes32 _contractType, uint256 _version, bytes32 _bytecodeHash) external onlyOwner { - if (approvedBytecodeHash[_contractType][_version] == _bytecodeHash) { - approvedBytecodeHash[_contractType][_version] = bytes32(0); - emit RevokeApproval(_bytecodeHash, _contractType, _version); - } + /// @notice Removes `auditor` from the list of approved auditors + /// @dev Can only be called by the owner + function removeAuditor(address auditor) external override onlyOwner { + if (!_auditorsSet.remove(auditor)) return; + delete _auditorNames[auditor]; + emit RemoveAuditor(auditor); } - // GETTERS + // -------------------- // + // FORBIDDING INIT CODE // + // -------------------- // - /// @notice Checks if a contract name belongs to public domain - /// @param _contractType Contract type to check - /// @return bool True if contract is in public domain - function isInPublicDomain(bytes32 _contractType) public view returns (bool) { - string memory contractTypeStr = _contractType.fromSmallString(); - return isPublicDomain(contractTypeStr.extractDomain().toSmallString()); + /// @notice Whether init code with `initCodeHash` is forbidden + function isInitCodeForbidden(bytes32 initCodeHash) external view override returns (bool) { + return _isInitCodeForbidden[initCodeHash]; } - /// @notice Checks if a domain is public - /// @param domain Domain to check - /// @return bool True if domain is public - function isPublicDomain(bytes32 domain) public view returns (bool) { - return _publicDomains.contains(domain); + /// @notice Permanently marks init code with `initCodeHash` as forbidden + /// @dev Can only be called by the owner + function forbidInitCode(bytes32 initCodeHash) external override onlyOwner { + if (_isInitCodeForbidden[initCodeHash]) return; + _isInitCodeForbidden[initCodeHash] = true; + emit ForbidInitCode(initCodeHash); } - /// @notice Returns list of all public domains - /// @return Array of public domain identifiers - function listPublicDomains() external view returns (bytes32[] memory) { - return _publicDomains.values(); + /// @dev Reverts if `initCode` is forbidden + function _revertIfInitCodeIsForbidden(bytes memory initCode) internal view { + bytes32 initCodeHash = keccak256(initCode); + if (_isInitCodeForbidden[initCodeHash]) revert InitCodeIsForbiddenException(initCodeHash); } - /// @notice Gets token-specific postfix - /// @param token Token address to query - /// @return bytes32 Postfix associated with token - function getTokenSpecificPostfix(address token) external view returns (bytes32) { - return tokenSpecificPostfixes[token]; - } + // ------------------------ // + // TOKENS WITH CUSTOM LOGIC // + // ------------------------ // - /// @notice Gets latest version for a contract type - /// @param _contractType Contract type to query - /// @return uint256 Latest version number (0 if none exists) - function getLatestVersion(bytes32 _contractType) external view returns (uint256) { - return latestVersion[_contractType]; + /// @notice Returns `token`'s specific postfix, if any + function getTokenSpecificPostfix(address token) external view override returns (bytes32) { + return _tokenSpecificPostfixes[token]; } - /// @notice Gets latest minor version for a major version - /// @param _contractType Contract type to query - /// @param majorVersion Major version number - /// @return uint256 Latest minor version number - function getLatestMinorVersion(bytes32 _contractType, uint256 majorVersion) external view returns (uint256) { - return latestMinorVersion[_contractType][majorVersion]; + /// @notice Sets `token`'s specific `postfix` (does nothing if `postfix` contains "::") + /// @dev Can only be called by the owner + function setTokenSpecificPostfix(address token, bytes32 postfix) external override onlyOwner { + if (_tokenSpecificPostfixes[token] == postfix || LibString.fromSmallString(postfix).contains("::")) return; + _tokenSpecificPostfixes[token] = postfix; + emit SetTokenSpecificPostfix(token, postfix); } - /// @notice Gets latest patch version for a minor version - /// @param _contractType Contract type to query - /// @param minorVersion Minor version number - /// @return uint256 Latest patch version number - function getLatestPatchVersion(bytes32 _contractType, uint256 minorVersion) external view returns (uint256) { - return latestPatchVersion[_contractType][minorVersion]; + // --------------- // + // VERSION CONTROL // + // --------------- // + + /// @notice Returns all versions for `cType` + function getVersions(bytes32 cType) external view override returns (uint256[] memory) { + return _versionInfo[cType].versionsSet.values(); } - function auditorSignaturesByHash(bytes32 bytecodeHash) external view returns (AuditorSignature[] memory) { - return _auditorSignaturesByHash[bytecodeHash]; + /// @notice Returns the latest known bytecode version for given `cType` + /// @dev Reverts if `cType` has no bytecode entries + function getLatestVersion(bytes32 cType) external view override returns (uint256 ver) { + ver = _versionInfo[cType].latest; + if (ver == 0) revert VersionNotFoundException(cType); } - function auditorSignaturesByHash(bytes32 bytecodeHash, uint256 index) - external - view - returns (AuditorSignature memory) - { - return _auditorSignaturesByHash[bytecodeHash][index]; + /// @notice Returns the latest known version for given `cType` with matching `majorVersion` + /// @dev Reverts if `majorVersion` is less than `100` or greater than `999` + /// @dev Reverts if `cType` has no bytecode entries with matching `majorVersion` + function getLatestMinorVersion(bytes32 cType, uint256 majorVersion) external view override returns (uint256 ver) { + _validateVersion(cType, majorVersion); + ver = _versionInfo[cType].latestByMajor[_getMajorVersion(majorVersion)]; + if (ver == 0) revert VersionNotFoundException(cType); } - // - // HELPERS - // - function isBytecodeUploaded(bytes32 bytecodeHash) public view returns (bool) { - return _bytecodeByHash[bytecodeHash].author != address(0); + /// @notice Returns the latest known version for given `cType` with matching `minorVersion` + /// @dev Reverts if `minorVersion` is less than `100` or greater than `999` + /// @dev Reverts if `cType` has no bytecode entries with matching `minorVersion` + function getLatestPatchVersion(bytes32 cType, uint256 minorVersion) external view override returns (uint256 ver) { + _validateVersion(cType, minorVersion); + ver = _versionInfo[cType].latestByMinor[_getMinorVersion(minorVersion)]; + if (ver == 0) revert VersionNotFoundException(cType); } - function revertIfInitCodeForbidden(bytes memory initCode) public view { - bytes32 initCodeHash = keccak256(initCode); - if (forbiddenInitCode[initCodeHash]) { - revert BytecodeForbiddenException(initCodeHash); - } + /// @dev Updates version info for `cType` based on `ver` + function _updateVersionInfo(bytes32 cType, uint256 ver) internal { + VersionInfo storage info = _versionInfo[cType]; + if (ver > info.latest) info.latest = ver; + uint256 majorVersion = _getMajorVersion(ver); + if (ver > info.latestByMajor[majorVersion]) info.latestByMajor[majorVersion] = ver; + uint256 minorVersion = _getMinorVersion(ver); + if (ver > info.latestByMinor[minorVersion]) info.latestByMinor[minorVersion] = ver; + info.versionsSet.add(ver); } - function isAuditBytecode(bytes32 bytecodeHash) public view returns (bool) { - uint256 len = _auditorSignaturesByHash[bytecodeHash].length; + /// @dev Returns the major version of a given version + function _getMajorVersion(uint256 ver) internal pure returns (uint256) { + return ver - ver % 100; + } - for (uint256 i = 0; i < len; ++i) { - AuditorSignature memory sig = _auditorSignaturesByHash[bytecodeHash][i]; - if (isAuditor(sig.auditor)) { - return true; - } - } + /// @dev Returns the minor version of a given version + function _getMinorVersion(uint256 ver) internal pure returns (uint256) { + return ver - ver % 10; + } - return false; + /// @dev Reverts if `cType` is invalid + function _validateContractType(bytes32 cType) internal pure { + if (!cType.isValidContractType()) revert InvalidContractTypeException(cType); } - function bytecodeByHash(bytes32 bytecodeHash) external view returns (Bytecode memory) { - BytecodePointer memory _bytecode = _bytecodeByHash[bytecodeHash]; - return Bytecode({ - contractType: _bytecode.contractType, - version: _bytecode.version, - initCode: SSTORE2.read(_bytecode.initCodePointer), - author: _bytecode.author, - source: _bytecode.source, - authorSignature: _bytecode.authorSignature - }); + /// @dev Reverts if `domain` is invalid + function _validateDomain(bytes32 domain) internal pure { + if (!domain.isValidDomain()) revert InvalidDomainException(domain); } - function domainSeparatorV4() external view returns (bytes32) { - return _domainSeparatorV4(); + /// @dev Reverts if `ver` is less than `100` or greater than `999` + function _validateVersion(bytes32 cType, uint256 ver) internal pure { + if (ver < 100 || ver > 999) revert InvalidVersionException(cType, ver); } } diff --git a/contracts/global/CrossChainMultisig.sol b/contracts/global/CrossChainMultisig.sol index b5e3643..0554c14 100644 --- a/contracts/global/CrossChainMultisig.sol +++ b/contracts/global/CrossChainMultisig.sol @@ -3,325 +3,386 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {LibString} from "@solady/utils/LibString.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {SignedProposal, CrossChainCall} from "../interfaces/ICrossChainMultisig.sol"; -import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {LibString} from "@solady/utils/LibString.sol"; -import {EIP712Mainnet} from "../helpers/EIP712Mainnet.sol"; - -import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import {ICrossChainMultisig} from "../interfaces/ICrossChainMultisig.sol"; +import {CrossChainCall, ICrossChainMultisig} from "../interfaces/ICrossChainMultisig.sol"; +import {SignedBatch, SignedRecoveryModeMessage} from "../interfaces/Types.sol"; +import {EIP712Mainnet} from "../helpers/EIP712Mainnet.sol"; import {AP_CROSS_CHAIN_MULTISIG} from "../libraries/ContractLiterals.sol"; +/// @title Cross-chain multisig contract CrossChainMultisig is EIP712Mainnet, Ownable, ReentrancyGuard, ICrossChainMultisig { + using Address for address; using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; using LibString for bytes32; - using LibString for string; using LibString for uint256; - /// @notice Meta info about contract type & version + /// @notice Contract version uint256 public constant override version = 3_10; + + /// @notice Contract type bytes32 public constant override contractType = AP_CROSS_CHAIN_MULTISIG; - // EIP-712 type hash for Proposal only - bytes32 public constant CROSS_CHAIN_CALL_TYPEHASH = + /// @notice Cross-chain call typehash + bytes32 public constant override CROSS_CHAIN_CALL_TYPEHASH = keccak256("CrossChainCall(uint256 chainId,address target,bytes callData)"); - bytes32 public constant PROPOSAL_TYPEHASH = keccak256("Proposal(string name,bytes32 proposalHash,bytes32 prevHash)"); - uint8 public confirmationThreshold; + /// @notice Batch typehash + /// @dev This typehash is used to identify batches + bytes32 public constant override BATCH_TYPEHASH = + keccak256("Batch(string name,CrossChainCall[] calls,bytes32 prevHash)"); + + /// @notice Compact batch typehash + /// @dev This typehash is used for signing to avoid cluttering the message with calls + bytes32 public constant override COMPACT_BATCH_TYPEHASH = + keccak256("CompactBatch(string name,bytes32 batchHash,bytes32 prevHash)"); + + /// @notice Recovery mode typehash + bytes32 public constant override RECOVERY_MODE_TYPEHASH = + keccak256("RecoveryMode(uint256 chainId,bytes32 startingBatchHash)"); + + /// @notice Confirmation threshold + uint8 public override confirmationThreshold; - bytes32 public lastProposalHash; + /// @notice Whether recovery mode is enabled + bool public override isRecoveryModeEnabled = false; - EnumerableSet.AddressSet internal _signers; + /// @dev Set of approved signers + EnumerableSet.AddressSet internal _signersSet; - bytes32[] internal _executedProposalHashes; + /// @dev List of executed batch hashes + bytes32[] internal _executedBatchHashes; - mapping(bytes32 => EnumerableSet.Bytes32Set) internal _connectedProposalHashes; - mapping(bytes32 => SignedProposal) internal _signedProposals; + /// @dev Mapping from `batchHash` to the set of connected batch hashes + mapping(bytes32 batchHash => EnumerableSet.Bytes32Set) internal _connectedBatchHashes; + /// @dev Mapping from `batchHash` to signed batch + mapping(bytes32 batchHash => SignedBatch) internal _signedBatches; + + /// @dev Ensures that function can only be called on Ethereum Mainnet modifier onlyOnMainnet() { if (block.chainid != 1) revert CantBeExecutedOnCurrentChainException(); _; } - modifier onlyOnNotMainnet() { + /// @dev Ensures that function can only be called outside Ethereum Mainnet + modifier onlyNotOnMainnet() { if (block.chainid == 1) revert CantBeExecutedOnCurrentChainException(); _; } + /// @dev Ensures that function can only be called by the contract itself modifier onlySelf() { - if (msg.sender != address(this)) revert OnlySelfException(); + if (msg.sender != address(this)) revert CallerIsNotSelfException(msg.sender); _; } - // It's deployed with the same set of parameters on all chains, so it's qddress should be the same - // @param: initialSigners - Array of initial signers - // @param: _confirmationThreshold - Confirmation threshold - // @param: _owner - Owner of the contract. used on Mainnet only, however, it should be same on all chains - // to make CREATE2 address the same on all chains - constructor(address[] memory initialSigners, uint8 _confirmationThreshold, address _owner) + /// @notice Constructor + /// @param signers_ Array of initial signers + /// @param confirmationThreshold_ Confirmation threshold + /// @param owner_ Owner of the contract, assumed to be Gearbox DAO + constructor(address[] memory signers_, uint8 confirmationThreshold_, address owner_) EIP712Mainnet(contractType.fromSmallString(), version.toString()) - Ownable() { - uint256 len = initialSigners.length; - - for (uint256 i = 0; i < len; ++i) { - _addSigner(initialSigners[i]); // U:[SM-1] + uint256 len = signers_.length; + for (uint256 i; i < len; ++i) { + _addSigner(signers_[i]); } - - _setConfirmationThreshold(_confirmationThreshold); // U:[SM-1] - _transferOwnership(_owner); // U:[SM-1] + _setConfirmationThreshold(confirmationThreshold_); + _transferOwnership(owner_); } - // @dev: Submit a new proposal - // Executed by Gearbox DAO on Mainnet - // @param: calls - Array of CrossChainCall structs - // @param: prevHash - Hash of the previous proposal (zero if first proposal) - function submitProposal(string calldata name, CrossChainCall[] calldata calls, bytes32 prevHash) - external - onlyOwner - onlyOnMainnet - nonReentrant - { - _verifyProposal({calls: calls, prevHash: prevHash}); + // --------------- // + // EIP-712 GETTERS // + // --------------- // - bytes32 proposalHash = hashProposal({name: name, calls: calls, prevHash: prevHash}); + /// @notice Returns the domain separator + function domainSeparatorV4() external view override returns (bytes32) { + return _domainSeparatorV4(); + } - // Copy proposal to storage - SignedProposal storage signedProposal = _signedProposals[proposalHash]; + /// @notice Computes struct hash for cross-chain call + function computeCrossChainCallHash(CrossChainCall calldata call) public pure override returns (bytes32) { + return keccak256(abi.encode(CROSS_CHAIN_CALL_TYPEHASH, call.chainId, call.target, keccak256(call.callData))); + } + /// @notice Computes struct hash for batch + function computeBatchHash(string calldata name, CrossChainCall[] calldata calls, bytes32 prevHash) + public + pure + returns (bytes32) + { uint256 len = calls.length; - for (uint256 i = 0; i < len; ++i) { - signedProposal.calls.push(calls[i]); + bytes32[] memory callHashes = new bytes32[](len); + for (uint256 i; i < len; ++i) { + callHashes[i] = computeCrossChainCallHash(calls[i]); } - signedProposal.prevHash = prevHash; - signedProposal.name = name; - - _connectedProposalHashes[lastProposalHash].add(proposalHash); + return keccak256( + abi.encode(BATCH_TYPEHASH, keccak256(bytes(name)), keccak256(abi.encodePacked(callHashes)), prevHash) + ); + } - emit SubmitProposal(proposalHash); + /// @notice Computes struct hash for compact batch + function computeCompactBatchHash(string memory name, bytes32 batchHash, bytes32 prevHash) + public + pure + override + returns (bytes32) + { + return keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes(name)), batchHash, prevHash)); } - // @dev: Sign a proposal - // Executed by any signer to make cross-chain distribution possible - // @param: proposalHash - Hash of the proposal to sign - // @param: signature - Signature of the proposal - function signProposal(bytes32 proposalHash, bytes calldata signature) external onlyOnMainnet nonReentrant { - SignedProposal storage signedProposal = _signedProposals[proposalHash]; - if (signedProposal.prevHash != lastProposalHash) { - revert InvalidPrevHashException(); - } - bytes32 digest = - _hashTypedDataV4(computeSignProposalHash(signedProposal.name, proposalHash, signedProposal.prevHash)); + /// @notice Computes struct hash for recovery mode + function computeRecoveryModeHash(uint256 chainId, bytes32 startingBatchHash) + public + pure + override + returns (bytes32) + { + return keccak256(abi.encode(RECOVERY_MODE_TYPEHASH, chainId, startingBatchHash)); + } - address signer = ECDSA.recover(digest, signature); - if (!_signers.contains(signer)) revert SignerDoesNotExistException(); + // ---------- // + // GOVERNANCE // + // ---------- // - signedProposal.signatures.push(signature); + /// @notice Returns the hash of the last executed batch + function lastBatchHash() public view override returns (bytes32) { + uint256 len = _executedBatchHashes.length; + return len == 0 ? bytes32(0) : _executedBatchHashes[len - 1]; + } - uint256 validSignatures = _verifySignatures({signatures: signedProposal.signatures, digest: digest}); + /// @notice Returns list of executed batch hashes + function getExecutedBatchHashes() external view returns (bytes32[] memory) { + return _executedBatchHashes; + } - emit SignProposal(proposalHash, signer); + /// @notice Returns list of batch hashes connected to the last executed batch + function getCurrentBatchHashes() external view returns (bytes32[] memory) { + return _connectedBatchHashes[lastBatchHash()].values(); + } - if (validSignatures >= confirmationThreshold) { - _verifyProposal({calls: signedProposal.calls, prevHash: signedProposal.prevHash}); - _executeProposal({calls: signedProposal.calls, proposalHash: proposalHash}); - } + /// @notice Returns list of batch hashes connected to a batch with `batchHash` + function getConnectedBatchHashes(bytes32 batchHash) external view override returns (bytes32[] memory) { + return _connectedBatchHashes[batchHash].values(); } - // @dev: Execute a proposal on other chain permissionlessly - function executeProposal(SignedProposal calldata signedProposal) external onlyOnNotMainnet nonReentrant { - bytes32 proposalHash = hashProposal(signedProposal.name, signedProposal.calls, signedProposal.prevHash); + /// @notice Returns batch by hash + function getBatch(bytes32 batchHash) external view returns (SignedBatch memory result) { + return _signedBatches[batchHash]; + } - // Check proposal is valid - _verifyProposal({calls: signedProposal.calls, prevHash: signedProposal.prevHash}); + /// @notice Allows Gearbox DAO to submit a new batch on Ethereum Mainnet + /// @dev Can only be executed by Gearbox DAO on Ethereum Mainnet + /// @dev If batch contains `disableRecoveryMode` self-call, it must be its only call + /// @dev Reverts if `prevHash` is not the hash of the last executed batch + /// @dev Reverts if `calls` is empty or contains local self-calls + function submitBatch(string calldata name, CrossChainCall[] calldata calls, bytes32 prevHash) + external + override + onlyOwner + onlyOnMainnet + nonReentrant + { + bytes32 batchHash = computeBatchHash(name, calls, prevHash); + if (!_connectedBatchHashes[lastBatchHash()].add(batchHash)) return; - bytes32 digest = - _hashTypedDataV4(computeSignProposalHash(signedProposal.name, proposalHash, signedProposal.prevHash)); + _verifyBatch(calls, prevHash); - // Check if enough signatures are valid - uint256 validSignatures = _verifySignatures({signatures: signedProposal.signatures, digest: digest}); - if (validSignatures < confirmationThreshold) revert NotEnoughSignaturesException(); + SignedBatch storage signedBatch = _signedBatches[batchHash]; + signedBatch.name = name; + signedBatch.prevHash = prevHash; + uint256 len = calls.length; + for (uint256 i; i < len; ++i) { + signedBatch.calls.push(calls[i]); + } - _executeProposal({calls: signedProposal.calls, proposalHash: proposalHash}); + emit SubmitBatch(batchHash); } - function _verifyProposal(CrossChainCall[] memory calls, bytes32 prevHash) internal view { - if (prevHash != lastProposalHash) revert InvalidPrevHashException(); - if (calls.length == 0) revert NoCallsInProposalException(); + /// @notice Submits a signature for a compact batch message for a batch with `batchHash`. + /// If the number of signatures reaches confirmation threshold, the batch is executed. + /// @dev Can only be executed on Ethereum Mainnet (though permissionlessly to ease signers' life) + /// @dev Reverts if batch with `batchHash` hasn't been submitted or is not connected to the last executed batch + /// @dev Reverts if signer is not approved or their signature has already been submitted + function signBatch(bytes32 batchHash, bytes calldata signature) external override onlyOnMainnet nonReentrant { + SignedBatch storage signedBatch = _signedBatches[batchHash]; + if (signedBatch.calls.length == 0) revert BatchIsNotSubmittedException(batchHash); + if (signedBatch.prevHash != lastBatchHash()) revert InvalidPrevHashException(); + + bytes32 digest = _hashTypedDataV4(computeCompactBatchHash(signedBatch.name, batchHash, signedBatch.prevHash)); + address signer = ECDSA.recover(digest, signature); + if (!_signersSet.contains(signer)) revert SignerIsNotApprovedException(signer); + + signedBatch.signatures.push(signature); + emit SignBatch(batchHash, signer); - uint256 len = calls.length; - for (uint256 i = 0; i < len; ++i) { - CrossChainCall memory call = calls[i]; - if (call.chainId != 0 && call.target == address(this)) { - revert InconsistentSelfCallOnOtherChainException(); - } - } + uint256 validSignatures = _verifySignatures(signedBatch.signatures, digest); + if (validSignatures >= confirmationThreshold) _executeBatch(signedBatch.calls, batchHash); } - function _verifySignatures(bytes[] memory signatures, bytes32 digest) - internal - view - returns (uint256 validSignatures) - { - address[] memory proposalSigners = new address[](signatures.length); - // Check for duplicate signatures - uint256 len = signatures.length; + /// @notice Executes a proposal outside Ethereum Mainnet permissionlessly + /// @dev In the current implementation, signers are trusted not to deviate and only sign batches + /// submitted by Gearbox DAO on Ethereum Mainnet. In future versions, DAO decisions will be + /// propagated to other chains using bridges or `L1SLOAD`. + /// @dev If batch contains `disableRecoveryMode` self-call, it must be its only call + /// @dev Reverts if batch's `prevHash` is not the hash of the last executed batch + /// @dev Reverts if batch is empty or contains local self-calls + /// @dev Reverts if signatures have duplicates or the number of valid signatures is insufficient + function executeBatch(SignedBatch calldata signedBatch) external override onlyNotOnMainnet nonReentrant { + _verifyBatch(signedBatch.calls, signedBatch.prevHash); - for (uint256 i = 0; i < len; ++i) { - address signer = ECDSA.recover(digest, signatures[i]); + bytes32 batchHash = computeBatchHash(signedBatch.name, signedBatch.calls, signedBatch.prevHash); - // It's not reverted to avoid the case, when 2 proposals are submitted - // and the first one is about removing a signer. The signer could add his signature - // to the second proposal (it's still possible) and lock the system forever - if (_signers.contains(signer)) { - validSignatures++; - } - for (uint256 j = 0; j < i; ++j) { - if (proposalSigners[j] == signer) { - revert AlreadySignedException(); + bytes32 digest = _hashTypedDataV4(computeCompactBatchHash(signedBatch.name, batchHash, signedBatch.prevHash)); + uint256 validSignatures = _verifySignatures(signedBatch.signatures, digest); + if (validSignatures < confirmationThreshold) revert InsufficientNumberOfSignaturesException(); + + _executeBatch(signedBatch.calls, batchHash); + } + + /// @dev Ensures that batch is connected to the last executed batch, is non-empty and contains no local self-calls + /// @dev If batch contains `disableRecoveryMode` self-call, it must be its only call + function _verifyBatch(CrossChainCall[] memory calls, bytes32 prevHash) internal view { + if (prevHash != lastBatchHash()) revert InvalidPrevHashException(); + + uint256 len = calls.length; + if (len == 0) revert InvalidBatchException(); + for (uint256 i; i < len; ++i) { + if (calls[i].target == address(this)) { + if (calls[i].chainId != 0) revert InvalidBatchException(); + if (bytes4(calls[i].callData) == ICrossChainMultisig.disableRecoveryMode.selector && len != 1) { + revert InvalidBatchException(); } } - proposalSigners[i] = signer; } } - // @dev: Execute proposal calls and update state - // @param: calls - Array of cross-chain calls to execute - // @param: proposalHash - Hash of the proposal being executed - function _executeProposal(CrossChainCall[] memory calls, bytes32 proposalHash) internal { - // Execute each call in the proposal + /// @dev Executes a batch of calls skipping local calls from other chains, updates the last executed batch hash + /// @dev In recovery mode, only self-calls are executed + function _executeBatch(CrossChainCall[] memory calls, bytes32 batchHash) internal { uint256 len = calls.length; - for (uint256 i = 0; i < len; ++i) { - CrossChainCall memory call = calls[i]; - uint256 chainId = call.chainId; - + for (uint256 i; i < len; ++i) { + if (isRecoveryModeEnabled && calls[i].target != address(this)) continue; + uint256 chainId = calls[i].chainId; if (chainId == 0 || chainId == block.chainid) { - // QUESTION: add try{} catch{} to achieve 100% execution - Address.functionCall(call.target, call.callData, "Call execution failed"); + calls[i].target.functionCall(calls[i].callData, "Call execution failed"); } } + _executedBatchHashes.push(batchHash); + emit ExecuteBatch(batchHash); + } - _executedProposalHashes.push(proposalHash); - lastProposalHash = proposalHash; + // ------------------ // + // SIGNERS MANAGEMENT // + // ------------------ // - emit ExecuteProposal(proposalHash); + /// @notice Returns whether `account` is an approved signer + function isSigner(address account) external view override returns (bool) { + return _signersSet.contains(account); } - // - // MULTISIG CONFIGURATION FUNCTIONS - // - - // @notice: Add a new signer to the multisig - // @param: newSigner - Address of the new signer - function addSigner(address newSigner) external onlySelf { - _addSigner(newSigner); + /// @notice Returns list of approved signers + function getSigners() external view override returns (address[] memory) { + return _signersSet.values(); } - function _addSigner(address newSigner) internal { - if (!_signers.add(newSigner)) revert SignerAlreadyExistsException(); - emit AddSigner(newSigner); + /// @notice Adds `signer` to the list of approved signers + /// @dev Can only be called by the contract itself + /// @dev Reverts if signer is zero address or is already approved + function addSigner(address signer) external override onlySelf { + _addSigner(signer); } - // @notice: Remove a signer from the multisig - // @param: signer - Address of the signer to remove - function removeSigner(address signer) external onlySelf { - if (!_signers.remove(signer)) revert SignerDoesNotExistException(); + /// @notice Removes `signer` from the list of approved signers + /// @dev Can only be called by the contract itself + /// @dev Reverts if signer is not approved + /// @dev Reverts if removing the signer makes multisig have less than `confirmationThreshold` signers + function removeSigner(address signer) external override onlySelf { + if (!_signersSet.remove(signer)) revert SignerIsNotApprovedException(signer); + if (_signersSet.length() < confirmationThreshold) revert InvalidConfirmationThresholdException(); emit RemoveSigner(signer); } - // @notice: Set the confirmation threshold for the multisig - // @param: newConfirmationThreshold - New confirmation threshold - function setConfirmationThreshold(uint8 newConfirmationThreshold) external onlySelf { + /// @notice Sets the minimum number of signatures required to execute a batch to `newConfirmationThreshold` + /// @dev Can only be called by the contract itself + /// @dev Reverts if the new confirmation threshold is 0 or greater than the number of signers + function setConfirmationThreshold(uint8 newConfirmationThreshold) external override onlySelf { _setConfirmationThreshold(newConfirmationThreshold); } - function _setConfirmationThreshold(uint8 newConfirmationThreshold) internal { - if (newConfirmationThreshold == 0 || newConfirmationThreshold > _signers.length()) { - revert InvalidConfirmationThresholdValueException(); - } - confirmationThreshold = newConfirmationThreshold; // U:[SM-1] - emit SetConfirmationThreshold(newConfirmationThreshold); // U:[SM-1] + /// @dev `addSigner` implementation + function _addSigner(address signer) internal { + if (signer == address(0)) revert InvalidSignerAddressException(); + if (!_signersSet.add(signer)) revert SignerIsAlreadyApprovedException(signer); + emit AddSigner(signer); } - // - // HELPERS - // - function hashProposal(string calldata name, CrossChainCall[] calldata calls, bytes32 prevHash) - public - pure - returns (bytes32) - { - bytes32[] memory callsHash = new bytes32[](calls.length); - uint256 len = calls.length; - for (uint256 i = 0; i < len; ++i) { - CrossChainCall memory call = calls[i]; - callsHash[i] = keccak256(abi.encode(CROSS_CHAIN_CALL_TYPEHASH, call.chainId, call.target, call.callData)); + /// @dev `setConfirmationThreshold` implementation + function _setConfirmationThreshold(uint8 newConfirmationThreshold) internal { + if (newConfirmationThreshold == 0 || newConfirmationThreshold > _signersSet.length()) { + revert InvalidConfirmationThresholdException(); } - - return keccak256(abi.encode(keccak256(bytes(name)), keccak256(abi.encodePacked(callsHash)), prevHash)); + if (newConfirmationThreshold == confirmationThreshold) return; + confirmationThreshold = newConfirmationThreshold; + emit SetConfirmationThreshold(newConfirmationThreshold); } - function computeSignProposalHash(string memory name, bytes32 proposalHash, bytes32 prevHash) - public - pure - returns (bytes32) + /// @dev Ensures that the list of signatures has no duplicates and returns the number of valid signatures + function _verifySignatures(bytes[] memory signatures, bytes32 digest) + internal + view + returns (uint256 validSignatures) { - return keccak256(abi.encode(PROPOSAL_TYPEHASH, keccak256(bytes(name)), proposalHash, prevHash)); - } - - // - // GETTERS - // - function getCurrentProposalHashes() external view returns (bytes32[] memory) { - return _connectedProposalHashes[lastProposalHash].values(); - } - - function getCurrentProposals() external view returns (SignedProposal[] memory result) { - uint256 len = _connectedProposalHashes[lastProposalHash].length(); - result = new SignedProposal[](len); - for (uint256 i = 0; i < len; ++i) { - result[i] = _signedProposals[_connectedProposalHashes[lastProposalHash].at(i)]; - } - } - - function getSigners() external view returns (address[] memory) { - return _signers.values(); - } - - function getExecutedProposals() external view returns (SignedProposal[] memory result) { - uint256 len = _executedProposalHashes.length; - result = new SignedProposal[](len); - for (uint256 i = 0; i < len; ++i) { - result[i] = _signedProposals[_executedProposalHashes[i]]; + uint256 len = signatures.length; + address[] memory signers = new address[](len); + for (uint256 i; i < len; ++i) { + address signer = ECDSA.recover(digest, signatures[i]); + if (_signersSet.contains(signer)) validSignatures++; + for (uint256 j; j < i; ++j) { + if (signers[j] == signer) revert DuplicateSignatureException(signer); + } + signers[i] = signer; } } - function getProposal(bytes32 proposalHash) external view returns (SignedProposal memory result) { - return _signedProposals[proposalHash]; - } + // ------------- // + // RECOVERY MODE // + // ------------- // - function getExecutedProposalHashes() external view returns (bytes32[] memory) { - return _executedProposalHashes; - } + /// @notice If `message.chainId` matches current chain, enables recovery mode, in which only self-calls are executed + /// @dev Can only be executed outside Ethereum Mainnet + /// @dev Reverts if starting batch of recovery mode is not the last executed batch + /// @dev Reverts if the number of signatures is insufficient + function enableRecoveryMode(SignedRecoveryModeMessage calldata message) + external + override + onlyNotOnMainnet + nonReentrant + { + if (isRecoveryModeEnabled || message.chainId != block.chainid) return; + if (message.startingBatchHash != lastBatchHash()) revert InvalidRecoveryModeMessageException(); - function isSigner(address account) external view returns (bool) { - return _signers.contains(account); - } + bytes32 digest = _hashTypedDataV4(computeRecoveryModeHash(message.chainId, message.startingBatchHash)); + uint256 validSignatures = _verifySignatures(message.signatures, digest); + if (validSignatures < confirmationThreshold) revert InsufficientNumberOfSignaturesException(); - function getSignedProposal(bytes32 proposalHash) external view returns (SignedProposal memory) { - return _signedProposals[proposalHash]; + isRecoveryModeEnabled = true; + emit EnableRecoveryMode(message.startingBatchHash); } - function domainSeparatorV4() external view returns (bytes32) { - return _domainSeparatorV4(); + /// @notice If `chainId` matches current chain, disables recovery mode + /// @dev Can only be executed by the contract itself + function disableRecoveryMode(uint256 chainId) external override onlySelf { + if (!isRecoveryModeEnabled || chainId != block.chainid) return; + isRecoveryModeEnabled = false; + emit DisableRecoveryMode(); } } diff --git a/contracts/helpers/DefaultDegenNFT.sol b/contracts/helpers/DefaultDegenNFT.sol new file mode 100644 index 0000000..60b2dfc --- /dev/null +++ b/contracts/helpers/DefaultDegenNFT.sol @@ -0,0 +1,143 @@ +// 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 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; + } + + 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/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 f6144df..e382df0 100644 --- a/contracts/helpers/DefaultLossPolicy.sol +++ b/contracts/helpers/DefaultLossPolicy.sol @@ -3,26 +3,38 @@ // (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 { - uint256 public constant version = 3_10; - bytes32 public constant contractType = AP_LOSS_POLICY_DEFAULT; - - bool public enabled; +contract DefaultLossPolicy is ILossPolicy, ACLTrait { + uint256 public constant override version = 3_10; + bytes32 public constant override contractType = AP_LOSS_POLICY_DEFAULT; + AccessMode public override accessMode = AccessMode.Permissioned; + bool public override checksEnabled = false; constructor(address acl_) ACLTrait(acl_) {} - function isLiquidatable(address, address, bytes calldata) external view returns (bool) { - return enabled; + function serialize() external view override returns (bytes memory) { + return abi.encode(accessMode, checksEnabled); + } + + function isLiquidatableWithLoss(address, address caller, Params calldata) external view override returns (bool) { + AccessMode accessMode_ = accessMode; + if (accessMode_ == AccessMode.Forbidden) return false; + if (accessMode_ == AccessMode.Permissioned && !_hasRole("LOSS_LIQUIDATOR", caller)) return false; + return !checksEnabled; } - function enable() external configuratorOnly { - enabled = true; + function setAccessMode(AccessMode mode) external override configuratorOnly { + if (accessMode == mode) return; + accessMode = mode; + emit SetAccessMode(mode); } - function disable() external configuratorOnly { - enabled = false; + function setChecksEnabled(bool enabled) external override configuratorOnly { + if (checksEnabled == enabled) return; + checksEnabled = enabled; + emit SetChecksEnabled(enabled); } } diff --git a/contracts/helpers/EIP712Mainnet.sol b/contracts/helpers/EIP712Mainnet.sol index 0190514..8174624 100644 --- a/contracts/helpers/EIP712Mainnet.sol +++ b/contracts/helpers/EIP712Mainnet.sol @@ -38,8 +38,6 @@ abstract contract EIP712Mainnet is IERC5267 { bytes32 private constant _TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); - // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to - // invalidate the cached domain separator if the chain id changes. bytes32 private immutable _cachedDomainSeparator; uint256 private constant _HARDCODED_CHAIN_ID = 1; address private immutable _cachedThis; @@ -75,7 +73,7 @@ abstract contract EIP712Mainnet is IERC5267 { } /** - * @dev Returns the domain separator for the current chain. + * @dev Returns the domain separator. */ function _domainSeparatorV4() internal view returns (bytes32) { if (address(this) == _cachedThis) { diff --git a/contracts/helpers/ProxyCall.sol b/contracts/helpers/ProxyCall.sol index 56fb5cd..ac6400e 100644 --- a/contracts/helpers/ProxyCall.sol +++ b/contracts/helpers/ProxyCall.sol @@ -6,22 +6,12 @@ pragma solidity ^0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ImmutableOwnableTrait} from "../traits/ImmutableOwnableTrait.sol"; -/// @title ProxyCall -/// @notice Contract that allows an immutable owner to make calls on its behalf contract ProxyCall is ImmutableOwnableTrait { using Address for address; - /// @notice Emitted when a call is made through the proxy - event ProxyCallExecuted(address target, bytes data); - constructor() ImmutableOwnableTrait(msg.sender) {} - /// @notice Makes a call to target contract with provided data - /// @param target Address of contract to call - /// @param data Call data to execute - /// @return result The raw return data from the call function proxyCall(address target, bytes calldata data) external onlyOwner returns (bytes memory result) { - // Make the call using OpenZeppelin's Address library return target.functionCall(data); } } diff --git a/contracts/instance/AddressProvider.sol b/contracts/instance/AddressProvider.sol index 5ef5769..86d5766 100644 --- a/contracts/instance/AddressProvider.sol +++ b/contracts/instance/AddressProvider.sol @@ -5,129 +5,165 @@ pragma solidity ^0.8.23; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {LibString} from "@solady/utils/LibString.sol"; - import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; -import {AddressNotFoundException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol"; -import {IAddressProvider, ContractValue} from "../interfaces/IAddressProvider.sol"; +import {IAddressProvider} from "../interfaces/IAddressProvider.sol"; +import {AddressProviderEntry} from "../interfaces/Types.sol"; import {AP_ADDRESS_PROVIDER, NO_VERSION_CONTROL} from "../libraries/ContractLiterals.sol"; import {ImmutableOwnableTrait} from "../traits/ImmutableOwnableTrait.sol"; -struct ContractKey { - string key; - uint256 version; -} - -/// @title Address provider V3 +/// @title Address provider /// @notice Stores addresses of important contracts contract AddressProvider is ImmutableOwnableTrait, IAddressProvider { - using EnumerableSet for EnumerableSet.AddressSet; - // using LibString for string; - using LibString for bytes32; + using EnumerableSet for EnumerableSet.Bytes32Set; + using EnumerableSet for EnumerableSet.UintSet; + + /// @dev Internal struct with version info for a given key + struct VersionInfo { + uint256 latest; + mapping(uint256 majorVersion => uint256) latestByMajor; + mapping(uint256 minorVersion => uint256) latestByMinor; + EnumerableSet.UintSet versionsSet; + } /// @notice Contract version uint256 public constant override version = 3_10; + + /// @notice Contract type bytes32 public constant override contractType = AP_ADDRESS_PROVIDER; - /// @notice Mapping from (contract key, version) to contract addresses - mapping(string => mapping(uint256 => address)) public override addresses; + /// @dev Set of saved entry keys + EnumerableSet.Bytes32Set internal _keysSet; - mapping(string => uint256) public latestVersions; - mapping(string => mapping(uint256 => uint256)) public latestMinorVersions; - mapping(string => mapping(uint256 => uint256)) public latestPatchVersions; + /// @dev Mapping from `key` to version info + mapping(bytes32 key => VersionInfo) internal _versionInfo; - ContractKey[] internal contractKeys; + /// @dev Mapping from `(key, ver)` pair to saved address + mapping(bytes32 key => mapping(uint256 ver => address)) internal _addresses; - error VersionNotFoundException(); + /// @notice Constructor + /// @param owner_ Contract owner + constructor(address owner_) ImmutableOwnableTrait(owner_) {} - constructor(address _owner) ImmutableOwnableTrait(_owner) { - // The first event is emitted for the address provider itself to aid in contract discovery - emit SetAddress(AP_ADDRESS_PROVIDER.fromSmallString(), version, address(this)); - } + // ------- // + // GETTERS // + // ------- // - /// @notice Returns the address of a contract with a given key and version - function getAddressOrRevert(string memory key, uint256 _version) - public - view - virtual - override - returns (address result) - { - result = addresses[key][_version]; - if (result == address(0)) revert AddressNotFoundException(); + /// @notice Returns the address by given `key` and `ver` + function getAddress(bytes32 key, uint256 ver) external view override returns (address) { + return _addresses[key][ver]; } - /// @notice Returns the address of a contract with a given key and version - function getAddressOrRevert(bytes32 key, uint256 _version) public view virtual override returns (address result) { - return getAddressOrRevert(key.fromSmallString(), _version); + /// @notice Returns the address by given `key` and `ver`, reverts if not found + function getAddressOrRevert(bytes32 key, uint256 ver) external view override returns (address result) { + result = _addresses[key][ver]; + if (result == address(0)) revert AddressNotFoundException(key, ver); } - function getLatestVersion(string memory key) external view override returns (uint256) { - uint256 latestVersion = latestVersions[key]; - if (latestVersion == 0) revert VersionNotFoundException(); - return latestVersion; + /// @notice Returns all known keys + function getKeys() external view override returns (bytes32[] memory) { + return _keysSet.values(); } - function getLatestMinorVersion(string memory key, uint256 majorVersion) external view override returns (uint256) { - uint256 latestMinorVersion = latestMinorVersions[key][majorVersion]; - if (latestMinorVersion == 0) revert VersionNotFoundException(); - return latestMinorVersion; + /// @notice Returns all known versions for given `key` + function getVersions(bytes32 key) external view override returns (uint256[] memory) { + return _versionInfo[key].versionsSet.values(); } - function getLatestPatchVersion(string memory key, uint256 minorVersion) external view override returns (uint256) { - uint256 latestPatchVersion = latestPatchVersions[key][minorVersion]; - if (latestPatchVersion == 0) revert VersionNotFoundException(); - return latestPatchVersion; + /// @notice Returns all saved entries + function getAllEntries() external view override returns (AddressProviderEntry[] memory entries) { + uint256 numKeys = _keysSet.length(); + + uint256 numEntries; + for (uint256 i; i < numKeys; ++i) { + numEntries += _versionInfo[_keysSet.at(i)].versionsSet.length(); + } + + entries = new AddressProviderEntry[](numEntries); + uint256 idx; + for (uint256 i; i < numKeys; ++i) { + bytes32 key = _keysSet.at(i); + VersionInfo storage info = _versionInfo[key]; + uint256 numVersions = info.versionsSet.length(); + for (uint256 j; j < numVersions; ++j) { + uint256 ver = info.versionsSet.at(j); + entries[idx++] = AddressProviderEntry(key, ver, _addresses[key][ver]); + } + } } - /// @notice Sets the address for the passed contract key - /// @param key Contract key - /// @param value Contract address - /// @param saveVersion Whether to save contract's version - function setAddress(string memory key, address value, bool saveVersion) external override onlyOwner { - _setAddress(key, value, saveVersion ? IVersion(value).version() : NO_VERSION_CONTROL); + /// @notice Returns the latest version for given `key` (excluding `NO_VERSION_CONTROL`) + /// @dev Reverts if `key` has no versions except `NO_VERSION_CONTROL` + function getLatestVersion(bytes32 key) external view override returns (uint256 ver) { + ver = _versionInfo[key].latest; + if (ver == 0) revert VersionNotFoundException(key); } - function setAddress(bytes32 key, address value, bool saveVersion) external override onlyOwner { - _setAddress(key.fromSmallString(), value, saveVersion ? IVersion(value).version() : NO_VERSION_CONTROL); + /// @notice Returns the latest minor version for given `majorVersion` + /// @dev Reverts if `majorVersion` is less than `100` + /// @dev Reverts if `key` has no entries with matching `majorVersion` + function getLatestMinorVersion(bytes32 key, uint256 majorVersion) external view override returns (uint256 ver) { + _validateVersion(key, majorVersion); + ver = _versionInfo[key].latestByMajor[_getMajorVersion(majorVersion)]; + if (ver == 0) revert VersionNotFoundException(key); } - /// @notice Sets the address for the passed contract key - /// @param addr Contract address - /// @param saveVersion Whether to save contract's version - function setAddress(address addr, bool saveVersion) external override onlyOwner { - _setAddress( - IVersion(addr).contractType().fromSmallString(), - addr, - saveVersion ? IVersion(addr).version() : NO_VERSION_CONTROL - ); + /// @notice Returns the latest patch version for given `minorVersion` + /// @dev Reverts if `minorVersion` is less than `100` + /// @dev Reverts if `key` has no entries with matching `minorVersion` + function getLatestPatchVersion(bytes32 key, uint256 minorVersion) external view override returns (uint256 ver) { + _validateVersion(key, minorVersion); + ver = _versionInfo[key].latestByMinor[_getMinorVersion(minorVersion)]; + if (ver == 0) revert VersionNotFoundException(key); } - /// @dev Implementation of `setAddress` - function _setAddress(string memory key, address value, uint256 _version) internal virtual { - addresses[key][_version] = value; - uint256 latestVersion = latestVersions[key]; - - if (_version > latestVersion) { - latestVersions[key] = _version; - uint256 minorVersion = version / 100 * 100; - uint256 patchVersion = version / 10 * 10; - latestMinorVersions[key][minorVersion] = _version; - latestPatchVersions[key][patchVersion] = _version; + // ------------- // + // CONFIGURATION // + // ------------- // + + /// @notice Sets the address for given `key` to `value`, optionally saving contract's version + /// @dev Reverts if caller is not the owner + /// @dev Reverts if `value` is zero address + /// @dev If `saveVersion` is true, reverts if version is less than 100 + function setAddress(bytes32 key, address value, bool saveVersion) external override onlyOwner { + if (value == address(0)) revert ZeroAddressException(key); + uint256 ver = NO_VERSION_CONTROL; + if (saveVersion) { + ver = IVersion(value).version(); + _validateVersion(key, ver); } - contractKeys.push(ContractKey(key, _version)); - emit SetAddress(key, _version, value); + if (_addresses[key][ver] == value) return; + _keysSet.add(key); + VersionInfo storage info = _versionInfo[key]; + info.versionsSet.add(ver); + _addresses[key][ver] = value; + emit SetAddress(key, ver, value); + + if (ver == NO_VERSION_CONTROL) return; + if (ver > info.latest) info.latest = ver; + uint256 majorVersion = _getMajorVersion(ver); + if (ver > info.latestByMajor[majorVersion]) info.latestByMajor[majorVersion] = ver; + uint256 minorVersion = _getMinorVersion(ver); + if (ver > info.latestByMinor[minorVersion]) info.latestByMinor[minorVersion] = ver; } - function getAllSavedContracts() external view returns (ContractValue[] memory) { - ContractValue[] memory result = new ContractValue[](contractKeys.length); - for (uint256 i = 0; i < contractKeys.length; i++) { - result[i] = ContractValue( - contractKeys[i].key, addresses[contractKeys[i].key][contractKeys[i].version], contractKeys[i].version - ); - } - return result; + // --------- // + // INTERNALS // + // --------- // + + /// @dev Returns the major version of a given version + function _getMajorVersion(uint256 ver) internal pure returns (uint256) { + return ver - ver % 100; + } + + /// @dev Returns the minor version of a given version + function _getMinorVersion(uint256 ver) internal pure returns (uint256) { + return ver - ver % 10; + } + + /// @dev Reverts if `ver` is less than `100` + function _validateVersion(bytes32 key, uint256 ver) internal pure { + if (ver < 100) revert InvalidVersionException(key, ver); } } diff --git a/contracts/instance/InstanceManager.sol b/contracts/instance/InstanceManager.sol index d8bd9b0..170aaf9 100644 --- a/contracts/instance/InstanceManager.sol +++ b/contracts/instance/InstanceManager.sol @@ -3,65 +3,85 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {LibString} from "@solady/utils/LibString.sol"; + import {BytecodeRepository} from "../global/BytecodeRepository.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {ProxyCall} from "../helpers/ProxyCall.sol"; +import {IInstanceManager} from "../interfaces/IInstanceManager.sol"; import { - AP_INSTANCE_MANAGER, - AP_CROSS_CHAIN_GOVERNANCE, - AP_TREASURY, - NO_VERSION_CONTROL, AP_BYTECODE_REPOSITORY, - AP_ADDRESS_PROVIDER, - AP_INSTANCE_MANAGER_PROXY, + AP_CROSS_CHAIN_GOVERNANCE, AP_CROSS_CHAIN_GOVERNANCE_PROXY, - AP_TREASURY_PROXY, AP_GEAR_STAKING, AP_GEAR_TOKEN, + AP_INSTANCE_MANAGER, + AP_INSTANCE_MANAGER_PROXY, + AP_TREASURY, + AP_TREASURY_PROXY, AP_WETH_TOKEN, - AP_MARKET_CONFIGURATOR_FACTORY + NO_VERSION_CONTROL } from "../libraries/ContractLiterals.sol"; -import {IAddressProvider} from "../interfaces/IAddressProvider.sol"; -import {IInstanceManager} from "../interfaces/IInstanceManager.sol"; -import {ProxyCall} from "../helpers/ProxyCall.sol"; -import {LibString} from "@solady/utils/LibString.sol"; + import {AddressProvider} from "./AddressProvider.sol"; +/// @title Instance manager contract InstanceManager is Ownable, IInstanceManager { using LibString for string; + using LibString for bytes32; - /// @notice Meta info about contract type & version + /// @notice Contract version uint256 public constant override version = 3_10; + + /// @notice Contract type bytes32 public constant override contractType = AP_INSTANCE_MANAGER; - address public immutable addressProvider; - address public immutable bytecodeRepository; + /// @notice Address provider + address public immutable override addressProvider; - address public instanceManagerProxy; - address public treasuryProxy; - address public crossChainGovernanceProxy; + /// @notice Bytecode repository + address public immutable override bytecodeRepository; - bool public isActivated; + /// @notice Instance manager proxy + address public immutable override instanceManagerProxy; - error InvalidKeyException(string key); + /// @notice Treasury proxy + address public immutable override treasuryProxy; + /// @notice Cross-chain governance proxy + address public immutable override crossChainGovernanceProxy; + + /// @notice Whether the instance is activated + bool public override isActivated; + + /// @notice Pending governance + address public override pendingGovernance; + + /// @dev Reverts if caller is not cross-chain governance modifier onlyCrossChainGovernance() { - require( - msg.sender - == IAddressProvider(addressProvider).getAddressOrRevert(AP_CROSS_CHAIN_GOVERNANCE, NO_VERSION_CONTROL), - "Only cross chain governance can call this function" - ); + if (msg.sender != _getAddressOrRevert(AP_CROSS_CHAIN_GOVERNANCE, NO_VERSION_CONTROL)) { + revert CallerIsNotCrossChainGovernanceException(msg.sender); + } + _; + } + + /// @dev Reverts if caller is not pending governance + modifier onlyPendingGovernance() { + if (msg.sender != pendingGovernance) revert CallerIsNotPendingGovernanceException(msg.sender); _; } + /// @dev Reverts if caller is not the DAO treasury modifier onlyTreasury() { - require( - msg.sender == IAddressProvider(addressProvider).getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL), - "Only financial multisig can call this function" - ); + if (msg.sender != _getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL)) { + revert CallerIsNotTreasuryException(msg.sender); + } _; } - constructor(address _owner) { + /// @notice Constructor + /// @param owner_ Contract owner (upon contract creation, must be cross-chain governance) + constructor(address owner_) { instanceManagerProxy = address(new ProxyCall()); treasuryProxy = address(new ProxyCall()); crossChainGovernanceProxy = address(new ProxyCall()); @@ -70,93 +90,144 @@ contract InstanceManager is Ownable, IInstanceManager { addressProvider = address(new AddressProvider(address(this))); _setAddress(AP_BYTECODE_REPOSITORY, address(bytecodeRepository), false); - _setAddress(AP_CROSS_CHAIN_GOVERNANCE, _owner, false); + _setAddress(AP_CROSS_CHAIN_GOVERNANCE, owner_, false); _setAddress(AP_INSTANCE_MANAGER_PROXY, instanceManagerProxy, false); _setAddress(AP_TREASURY_PROXY, treasuryProxy, false); _setAddress(AP_CROSS_CHAIN_GOVERNANCE_PROXY, crossChainGovernanceProxy, false); _setAddress(AP_INSTANCE_MANAGER, address(this), false); - _transferOwnership(_owner); + _transferOwnership(owner_); } - function activate(address _instanceOwner, address _treasury, address _weth, address _gear) external onlyOwner { - if (!isActivated) { - _transferOwnership(_instanceOwner); + /// @notice Returns the instance owner + function owner() public view override(Ownable, IInstanceManager) returns (address) { + return super.owner(); + } - _setAddress(AP_TREASURY, _treasury, false); - _setAddress(AP_WETH_TOKEN, _weth, false); - _setAddress(AP_GEAR_TOKEN, _gear, false); - isActivated = true; - } + // ------------- // + // CONFIGURATION // + // ------------- // + + /// @notice Activates the instance, setting the instance owner and saving the treasury, WETH and GEAR addresses + /// @dev Can only be called once by the cross-chain governance + function activate(address instanceOwner, address treasury, address weth, address gear) + external + override + onlyOwner + { + if (isActivated) return; + _transferOwnership(instanceOwner); + + _setAddress(AP_TREASURY, treasury, false); + _setAddress(AP_WETH_TOKEN, weth, false); + _setAddress(AP_GEAR_TOKEN, gear, false); + isActivated = true; } - function deploySystemContract(bytes32 _contractType, uint256 _version, bool _saveVersion) + /// @notice Deploys a system contract and saves its address in the address provider + /// @dev System contracts must accept address provider as the only constructor argument + /// @dev Reverts if caller is not the cross-chain governance + function deploySystemContract(bytes32 contractType_, uint256 version_, bool saveVersion) external + override onlyCrossChainGovernance { - address newSystemContract; - if ( - _contractType == AP_GEAR_STAKING && _version == 3_10 - && (block.chainid == 1 || block.chainid == 10 || block.chainid == 42161) - ) { - if (block.chainid == 1) { - newSystemContract = 0x2fcbD02d5B1D52FC78d4c02890D7f4f47a459c33; - } else if (block.chainid == 10) { - newSystemContract = 0x8D2622f1CA3B42b637e2ff6753E6b69D3ab9Adfd; - } else if (block.chainid == 42161) { - newSystemContract = 0xf3599BEfe8E79169Afd5f0b7eb0A1aA322F193D9; - } - } else { - newSystemContract = _deploySystemContract(_contractType, _version); - } + address newSystemContract = contractType_ == AP_GEAR_STAKING && version_ == 3_10 && _isLegacyChain() + ? _getLegacyGearStakingAddress() + : _deploySystemContract(contractType_, version_); - _setAddress(_contractType, newSystemContract, _saveVersion); + _setAddress(contractType_, newSystemContract, saveVersion); } - function _deploySystemContract(bytes32 _contractType, uint256 _version) internal returns (address) { - try ProxyCall(crossChainGovernanceProxy).proxyCall( - address(bytecodeRepository), - abi.encodeCall(BytecodeRepository.deploy, (_contractType, _version, abi.encode(addressProvider), 0)) - ) returns (bytes memory result) { - return abi.decode(result, (address)); - } catch { - return address(0); - } + /// @notice Allows cross-chain governance to set a global address in the address provider + /// @dev `key` must start with the "GLOBAL::" prefix + function setGlobalAddress(bytes32 key, address addr, bool saveVersion) external override onlyCrossChainGovernance { + _setAddressWithPrefix(key, "GLOBAL::", addr, saveVersion); } - function setGlobalAddress(string memory key, address addr, bool saveVersion) external onlyCrossChainGovernance { - _setAddressWithPrefix(key, "GLOBAL_", addr, saveVersion); + /// @notice Allows instance owner to set a local address in the address provider + /// @dev `key` must start with the "LOCAL::" prefix + function setLocalAddress(bytes32 key, address addr, bool saveVersion) external override onlyOwner { + _setAddressWithPrefix(key, "LOCAL::", addr, saveVersion); } - function setLocalAddress(string memory key, address addr, bool saveVersion) external onlyOwner { - _setAddressWithPrefix(key, "LOCAL_", addr, saveVersion); + /// @notice Allows cross-chain governance to configure global contracts such as bytecode repository, GEAR staking, etc. + function configureGlobal(address target, bytes calldata data) external override onlyCrossChainGovernance { + ProxyCall(crossChainGovernanceProxy).proxyCall(target, data); } - function _setAddressWithPrefix(string memory key, string memory prefix, address addr, bool saveVersion) internal { - if (!key.startsWith(prefix)) { - revert InvalidKeyException(key); - } - IAddressProvider(addressProvider).setAddress(key, addr, saveVersion); + /// @notice Allows instance owner to configure local contracts such as price feed store, bot list, etc. + function configureLocal(address target, bytes calldata data) external override onlyOwner { + ProxyCall(instanceManagerProxy).proxyCall(target, data); } - function configureGlobal(address target, bytes calldata data) external onlyCrossChainGovernance { - _configureGlobal(target, data); + /// @notice Allows DAO treasury to configure financial contracts such as fee splitters + function configureTreasury(address target, bytes calldata data) external override onlyTreasury { + ProxyCall(treasuryProxy).proxyCall(target, data); } - function _configureGlobal(address target, bytes memory data) internal { - ProxyCall(crossChainGovernanceProxy).proxyCall(target, data); + /// @notice Sets `newCrossChainGovernance` as the pending cross-chain governance + /// @dev Can only be called by the current cross-chain governance + function setPendingGovernance(address newGovernance) external override onlyCrossChainGovernance { + pendingGovernance = newGovernance; + emit SetPendingGovernance(newGovernance); } - function configureLocal(address target, bytes calldata data) external onlyOwner { - ProxyCall(instanceManagerProxy).proxyCall(target, data); + /// @notice Accepts the cross-chain governance role + /// @dev Can only be called by the pending governance + function acceptGovernance() external override onlyPendingGovernance { + _setAddress(AP_CROSS_CHAIN_GOVERNANCE, msg.sender, false); + pendingGovernance = address(0); + emit AcceptGovernance(msg.sender); } - function configureTreasury(address target, bytes calldata data) external onlyTreasury { - ProxyCall(treasuryProxy).proxyCall(target, data); + // --------- // + // INTERNALS // + // --------- // + + /// @dev Internal wrapper around address provider's `getAddressOrRevert` to reduce code size + function _getAddressOrRevert(bytes32 key, uint256 ver) internal view returns (address) { + return AddressProvider(addressProvider).getAddressOrRevert(key, ver); } + /// @dev Internal wrapper around address provider's `setAddress` to reduce code size function _setAddress(bytes32 key, address value, bool saveVersion) internal { - IAddressProvider(addressProvider).setAddress(key, value, saveVersion); + AddressProvider(addressProvider).setAddress(key, value, saveVersion); + } + + /// @dev Sets address in the address provider, ensuring that `key` starts with `prefix` + function _setAddressWithPrefix(bytes32 key, string memory prefix, address addr, bool saveVersion) internal { + if (!key.fromSmallString().startsWith(prefix)) revert InvalidKeyException(key); + _setAddress(key, addr, saveVersion); + } + + /// @dev Deploys a system contract and returns its address + function _deploySystemContract(bytes32 contractType_, uint256 version_) internal returns (address) { + bytes memory result = ProxyCall(crossChainGovernanceProxy).proxyCall( + address(bytecodeRepository), + abi.encodeCall(BytecodeRepository.deploy, (contractType_, version_, abi.encode(addressProvider), 0)) + ); + return abi.decode(result, (address)); + } + + /// @dev Whether there is a legacy instance on this chain + function _isLegacyChain() internal view returns (bool) { + return block.chainid == 1 || block.chainid == 10 || block.chainid == 146 || block.chainid == 42161; + } + + /// @dev Returns the address of the legacy GEAR staking contract on this chain + function _getLegacyGearStakingAddress() internal view returns (address) { + if (block.chainid == 1) { + return 0x2fcbD02d5B1D52FC78d4c02890D7f4f47a459c33; + } else if (block.chainid == 10) { + return 0x8D2622f1CA3B42b637e2ff6753E6b69D3ab9Adfd; + } else if (block.chainid == 146) { + return 0xe88846b6C85AA67688e453c7eaeeeb40F51e1F0a; + } else if (block.chainid == 42161) { + return 0xf3599BEfe8E79169Afd5f0b7eb0A1aA322F193D9; + } else { + revert(); + } } } diff --git a/contracts/instance/MarketConfiguratorFactory.sol b/contracts/instance/MarketConfiguratorFactory.sol index 5121140..3e93507 100644 --- a/contracts/instance/MarketConfiguratorFactory.sol +++ b/contracts/instance/MarketConfiguratorFactory.sol @@ -3,7 +3,6 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IContractsRegister} from "../interfaces/IContractsRegister.sol"; @@ -14,17 +13,13 @@ import { AP_CROSS_CHAIN_GOVERNANCE, AP_MARKET_CONFIGURATOR, AP_MARKET_CONFIGURATOR_FACTORY, - AP_MARKET_CONFIGURATOR_LEGACY, NO_VERSION_CONTROL } from "../libraries/ContractLiterals.sol"; -import {MarketConfiguratorLegacy} from "../market/legacy/MarketConfiguratorLegacy.sol"; -import {MarketConfigurator} from "../market/MarketConfigurator.sol"; - import {DeployerTrait} from "../traits/DeployerTrait.sol"; +/// @title Market configurator factory contract MarketConfiguratorFactory is DeployerTrait, IMarketConfiguratorFactory { - using Address for address; using EnumerableSet for EnumerableSet.AddressSet; /// @notice Contract version @@ -47,48 +42,61 @@ contract MarketConfiguratorFactory is DeployerTrait, IMarketConfiguratorFactory _; } - /// @dev Reverts if caller is not one of market configurators - modifier onlyMarketConfigurators() { - if (!_registeredMarketConfiguratorsSet.contains(msg.sender)) { - revert CallerIsNotMarketConfiguratorException(msg.sender); - } - _; - } - + /// @dev Reverts if `msg.sender` is not the admin of `marketConfigurator` modifier onlyMarketConfiguratorAdmin(address marketConfigurator) { - if (!_registeredMarketConfiguratorsSet.contains(marketConfigurator)) { - revert AddressIsNotMarketConfiguratorException(marketConfigurator); - } - if (msg.sender != MarketConfigurator(marketConfigurator).admin()) { + if (msg.sender != IMarketConfigurator(marketConfigurator).admin()) { revert CallerIsNotMarketConfiguratorAdminException(msg.sender); } _; } + /// @notice Constructor + /// @param addressProvider_ Address provider contract address constructor(address addressProvider_) DeployerTrait(addressProvider_) {} + // ------- // + // GETTERS // + // ------- // + + /// @notice Returns whether `account` is a registered market configurator function isMarketConfigurator(address account) external view override returns (bool) { return _registeredMarketConfiguratorsSet.contains(account); } + /// @notice Returns all registered market configurators function getMarketConfigurators() external view override returns (address[] memory) { return _registeredMarketConfiguratorsSet.values(); } - function getMarketConfigurator(uint256 index) external view returns (address) { + /// @notice Returns the market configurator at `index` + function getMarketConfigurator(uint256 index) external view override returns (address) { return _registeredMarketConfiguratorsSet.at(index); } - function getNumMarketConfigurators() external view returns (uint256) { + /// @notice Returns the number of registered market configurators + function getNumMarketConfigurators() external view override returns (uint256) { return _registeredMarketConfiguratorsSet.length(); } + /// @notice Returns all shutdown market configurators function getShutdownMarketConfigurators() external view override returns (address[] memory) { return _shutdownMarketConfiguratorsSet.values(); } + // ------------- // + // CONFIGURATION // + // ------------- // + + /// @notice Allows anyone to permissionlessly deploy a new market configurator + /// @param emergencyAdmin Address to set as emergency admin + /// @param adminFeeTreasury Address to set as the second admin of the fee splitter, with the first one being the + /// DAO treasury. If `address(0)`, the splitter is not deployed and all fees will be sent to the treasury. + /// @param curatorName Risk curator name + /// @param deployGovernor If true, a governor contract is deployed and set as market configurator's admin. + /// `msg.sender` is set as its owner, queue and execution admin, while `emergencyAdmin` is set as veto admin. + /// Otherwise, `msg.sender` is set as the admin of the market configurator. + /// @return marketConfigurator Address of the newly deployed market configurator function createMarketConfigurator( - address admin, address emergencyAdmin, address adminFeeTreasury, string calldata curatorName, @@ -98,7 +106,7 @@ contract MarketConfiguratorFactory is DeployerTrait, IMarketConfiguratorFactory contractType: AP_MARKET_CONFIGURATOR, minorVersion: 3_10, constructorParams: abi.encode( - addressProvider, admin, emergencyAdmin, adminFeeTreasury, curatorName, deployGovernor + addressProvider, msg.sender, emergencyAdmin, adminFeeTreasury, curatorName, deployGovernor ), salt: bytes32(bytes20(msg.sender)) }); @@ -107,30 +115,38 @@ contract MarketConfiguratorFactory is DeployerTrait, IMarketConfiguratorFactory emit CreateMarketConfigurator(marketConfigurator, curatorName); } + /// @notice Allows the admin of `marketConfigurator` to shut it down + /// @dev Reverts if caller is not the admin of `marketConfigurator` + /// @dev Reverts if `marketConfigurator` is not registered or already shutdown + /// @dev Reverts if `marketConfigurator` has non-shutdown pools function shutdownMarketConfigurator(address marketConfigurator) external override onlyMarketConfiguratorAdmin(marketConfigurator) { - if (_shutdownMarketConfiguratorsSet.add(marketConfigurator)) { + if (!_shutdownMarketConfiguratorsSet.add(marketConfigurator)) { revert MarketConfiguratorIsAlreadyShutdownException(marketConfigurator); } - address contractsRegister = MarketConfigurator(marketConfigurator).contractsRegister(); + if (!_registeredMarketConfiguratorsSet.remove(marketConfigurator)) { + revert MarketConfiguratorIsNotRegisteredException(marketConfigurator); + } + address contractsRegister = IMarketConfigurator(marketConfigurator).contractsRegister(); if (IContractsRegister(contractsRegister).getPools().length != 0) { - revert CantShutdownMarketConfiguratorException(); + revert CantShutdownMarketConfiguratorException(marketConfigurator); } - _registeredMarketConfiguratorsSet.remove(marketConfigurator); emit ShutdownMarketConfigurator(marketConfigurator); } + /// @notice Allows cross-chain governance to register an externally deployed legacy market configurator + /// @dev Reverts if caller is not cross-chain governance + /// @dev Reverts if `marketConfigurator` is already registered or shutdown function addMarketConfigurator(address marketConfigurator) external override onlyCrossChainGovernance { - if (_registeredMarketConfiguratorsSet.contains(marketConfigurator)) { + if (!_registeredMarketConfiguratorsSet.add(marketConfigurator)) { revert MarketConfiguratorIsAlreadyAddedException(marketConfigurator); } if (_shutdownMarketConfiguratorsSet.contains(marketConfigurator)) { revert MarketConfiguratorIsAlreadyShutdownException(marketConfigurator); } - _registeredMarketConfiguratorsSet.add(marketConfigurator); emit CreateMarketConfigurator(marketConfigurator, IMarketConfigurator(marketConfigurator).curatorName()); } } diff --git a/contracts/instance/PriceFeedStore.sol b/contracts/instance/PriceFeedStore.sol index 8f5d174..44c28b3 100644 --- a/contracts/instance/PriceFeedStore.sol +++ b/contracts/instance/PriceFeedStore.sol @@ -3,168 +3,312 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; +import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol"; +import {IPriceFeed, IUpdatablePriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol"; import {PriceFeedValidationTrait} from "@gearbox-protocol/core-v3/contracts/traits/PriceFeedValidationTrait.sol"; -import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol"; +import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol"; -import {IPriceFeedStore} from "../interfaces/IPriceFeedStore.sol"; -import {AP_PRICE_FEED_STORE, AP_INSTANCE_MANAGER_PROXY, NO_VERSION_CONTROL} from "../libraries/ContractLiterals.sol"; import {IAddressProvider} from "../interfaces/IAddressProvider.sol"; -import {PriceFeedInfo} from "../interfaces/Types.sol"; +import {IBytecodeRepository} from "../interfaces/IBytecodeRepository.sol"; +import {IPriceFeedStore, PriceUpdate} from "../interfaces/IPriceFeedStore.sol"; +import {Call, ConnectedPriceFeed, PriceFeedInfo} from "../interfaces/Types.sol"; + +import { + AP_BYTECODE_REPOSITORY, + AP_INSTANCE_MANAGER_PROXY, + AP_PRICE_FEED_STORE, + AP_ZERO_PRICE_FEED, + NO_VERSION_CONTROL +} from "../libraries/ContractLiterals.sol"; +import {NestedPriceFeeds} from "../libraries/NestedPriceFeeds.sol"; + +import {DeployerTrait} from "../traits/DeployerTrait.sol"; import {ImmutableOwnableTrait} from "../traits/ImmutableOwnableTrait.sol"; -contract PriceFeedStore is ImmutableOwnableTrait, SanityCheckTrait, PriceFeedValidationTrait, IPriceFeedStore { +/// @title Price feed store +contract PriceFeedStore is + DeployerTrait, + ImmutableOwnableTrait, + PriceFeedValidationTrait, + SanityCheckTrait, + IPriceFeedStore +{ + using Address for address; using EnumerableSet for EnumerableSet.AddressSet; + using NestedPriceFeeds for IPriceFeed; - // - // CONSTANTS - // - - /// @notice Meta info about contract type & version + /// @notice Contract version uint256 public constant override version = 3_10; + + /// @notice Contract type bytes32 public constant override contractType = AP_PRICE_FEED_STORE; - // - // VARIABLES - // + /// @notice Zero price feed address + address public immutable override zeroPriceFeed; /// @dev Set of all known price feeds EnumerableSet.AddressSet internal _knownPriceFeeds; - /// @dev Set of all known price feeds + /// @dev Set of all known tokens EnumerableSet.AddressSet internal _knownTokens; - /// @dev Mapping from token address to its set of allowed price feeds + /// @dev Set of all updatable price feeds + EnumerableSet.AddressSet internal _updatablePriceFeeds; + + /// @dev Mapping from `token` to its set of allowed price feeds mapping(address token => EnumerableSet.AddressSet) internal _allowedPriceFeeds; - /// @dev Mapping from a (token, priceFeed) pair to a timestamp when price feed was allowed for token - mapping(address token => mapping(address priceFeed => uint256)) _allowanceTimestamps; + /// @dev Mapping from a `(token, priceFeed)` pair to a timestamp when `priceFeed` was allowed for `token` + mapping(address token => mapping(address priceFeed => uint256)) internal _allowanceTimestamps; - /// @notice Mapping from price feed address to its data - mapping(address => PriceFeedInfo) internal _priceFeedInfo; + /// @dev Mapping from `priceFeed` to its info + mapping(address priceFeed => PriceFeedInfo) internal _priceFeedInfo; - constructor(address _addressProvider) + /// @notice Constructor + /// @param addressProvider_ Address provider contract address + constructor(address addressProvider_) + DeployerTrait(addressProvider_) ImmutableOwnableTrait( - IAddressProvider(_addressProvider).getAddressOrRevert(AP_INSTANCE_MANAGER_PROXY, NO_VERSION_CONTROL) + IAddressProvider(addressProvider_).getAddressOrRevert(AP_INSTANCE_MANAGER_PROXY, NO_VERSION_CONTROL) ) - {} + { + zeroPriceFeed = _deploy(AP_ZERO_PRICE_FEED, 3_10, "", bytes32(0)); + } + + // ------- // + // GETTERS // + // ------- // - /// @notice Returns the list of price feeds available for a token - function getPriceFeeds(address token) external view returns (address[] memory) { + /// @notice Returns the list of price feeds allowed for `token` + function getPriceFeeds(address token) public view override returns (address[] memory) { return _allowedPriceFeeds[token].values(); } - /// @notice Returns whether a price feed is allowed to be used for a token - function isAllowedPriceFeed(address token, address priceFeed) external view returns (bool) { + /// @notice Returns whether `priceFeed` is allowed for `token` + function isAllowedPriceFeed(address token, address priceFeed) external view override returns (bool) { return _allowedPriceFeeds[token].contains(priceFeed); } - /// @notice Returns the staleness period for a price feed - function getStalenessPeriod(address priceFeed) external view returns (uint32) { + /// @notice Returns the staleness period of `priceFeed` + /// @dev Reverts if `priceFeed` is not known + function getStalenessPeriod(address priceFeed) external view override returns (uint32) { + if (!_knownPriceFeeds.contains(priceFeed)) revert PriceFeedIsNotKnownException(priceFeed); return _priceFeedInfo[priceFeed].stalenessPeriod; } - /// @notice Returns the timestamp when priceFeed was allowed for token - function getAllowanceTimestamp(address token, address priceFeed) external view returns (uint256) { + /// @notice Returns the timestamp when `priceFeed` was allowed for `token` + /// @dev Reverts if `priceFeed` is not allowed for `token` + function getAllowanceTimestamp(address token, address priceFeed) external view override returns (uint256) { if (!_allowedPriceFeeds[token].contains(priceFeed)) revert PriceFeedIsNotAllowedException(token, priceFeed); return _allowanceTimestamps[token][priceFeed]; } - function getKnownTokens() external view returns (address[] memory) { - return _knownTokens.values(); + /// @notice Returns whether `token` is known + function isKnownToken(address token) external view override returns (bool) { + return _knownTokens.contains(token); } - function getKnownPriceFeeds() external view returns (address[] memory) { - return _knownPriceFeeds.values(); + /// @notice Returns the list of known tokens + function getKnownTokens() external view override returns (address[] memory) { + return _knownTokens.values(); } - /** - * @notice Adds a new price feed - * @param priceFeed The address of the new price feed - * @param stalenessPeriod Staleness period of the new price feed - * @dev Reverts if the price feed's result is stale based on the staleness period - */ - function addPriceFeed(address priceFeed, uint32 stalenessPeriod) external onlyOwner nonZeroAddress(priceFeed) { - if (_knownPriceFeeds.contains(priceFeed)) revert PriceFeedAlreadyAddedException(priceFeed); - - _validatePriceFeed(priceFeed, stalenessPeriod); - - bytes32 priceFeedType; - uint256 priceFeedVersion; + /// @notice Returns the list of tokens with their allowed price feeds + function getTokenPriceFeedsMap() external view override returns (ConnectedPriceFeed[] memory connectedPriceFeeds) { + address[] memory tokens = _knownTokens.values(); + uint256 len = tokens.length; - try IPriceFeed(priceFeed).contractType() returns (bytes32 _cType) { - priceFeedType = _cType; - priceFeedVersion = IPriceFeed(priceFeed).version(); - } catch { - priceFeedType = "PRICE_FEED::EXTERNAL"; - priceFeedVersion = 0; + connectedPriceFeeds = new ConnectedPriceFeed[](len); + for (uint256 i; i < len; ++i) { + connectedPriceFeeds[i].token = tokens[i]; + connectedPriceFeeds[i].priceFeeds = getPriceFeeds(tokens[i]); } + } - _knownPriceFeeds.add(priceFeed); - _priceFeedInfo[priceFeed].author = msg.sender; - _priceFeedInfo[priceFeed].priceFeedType = priceFeedType; - _priceFeedInfo[priceFeed].stalenessPeriod = stalenessPeriod; - _priceFeedInfo[priceFeed].version = priceFeedVersion; + /// @notice Returns whether `priceFeed` is known + function isKnownPriceFeed(address priceFeed) external view override returns (bool) { + return _knownPriceFeeds.contains(priceFeed); + } + + /// @notice Returns the list of known price feeds + function getKnownPriceFeeds() external view override returns (address[] memory) { + return _knownPriceFeeds.values(); + } - emit AddPriceFeed(priceFeed, stalenessPeriod); + /// @notice Returns the info for `priceFeed` + function priceFeedInfo(address priceFeed) external view override returns (PriceFeedInfo memory) { + return _priceFeedInfo[priceFeed]; } - /** - * @notice Sets the staleness period for an existing price feed - * @param priceFeed The address of the price feed - * @param stalenessPeriod New staleness period for the price feed - * @dev Reverts if the price feed is not added to the global list - */ - function setStalenessPeriod(address priceFeed, uint32 stalenessPeriod) + // ------------- // + // CONFIGURATION // + // ------------- // + + /// @notice Adds a new price feed to the store + /// @param priceFeed The address of the new price feed + /// @param stalenessPeriod Staleness period of the new price feed + /// @param name Name of the new price feed + /// @dev Reverts if caller is not owner + /// @dev Reverts if `priceFeed` is zero address or is already added + /// @dev Validates `priceFeed`'s tree and adds all updatable price feeds from it to the store. + function addPriceFeed(address priceFeed, uint32 stalenessPeriod, string calldata name) external + override onlyOwner nonZeroAddress(priceFeed) { - if (!_knownPriceFeeds.contains(priceFeed)) revert PriceFeedNotKnownException(priceFeed); - uint32 oldStalenessPeriod = _priceFeedInfo[priceFeed].stalenessPeriod; + if (!_knownPriceFeeds.add(priceFeed)) revert PriceFeedIsAlreadyAddedException(priceFeed); + + _validatePriceFeed(priceFeed, stalenessPeriod); + bool isExternal = _validatePriceFeedTree(priceFeed); + + _priceFeedInfo[priceFeed] = PriceFeedInfo({ + stalenessPeriod: stalenessPeriod, + priceFeedType: isExternal ? bytes32("PRICE_FEED::EXTERNAL") : IPriceFeed(priceFeed).contractType(), + version: isExternal ? 0 : IPriceFeed(priceFeed).version(), + name: name + }); + + emit AddPriceFeed(priceFeed, stalenessPeriod, name); + } - if (stalenessPeriod != oldStalenessPeriod) { - _validatePriceFeed(priceFeed, stalenessPeriod); - _priceFeedInfo[priceFeed].stalenessPeriod = stalenessPeriod; - emit SetStalenessPeriod(priceFeed, stalenessPeriod); + /// @notice Forbids `priceFeed` for all tokens and removes it from the store + /// @dev Reverts if caller is not owner + /// @dev Reverts if `priceFeed` is not known + function removePriceFeed(address priceFeed) external override onlyOwner { + if (!_knownPriceFeeds.remove(priceFeed)) revert PriceFeedIsNotKnownException(priceFeed); + delete _priceFeedInfo[priceFeed]; + + uint256 numTokens = _knownTokens.length(); + for (uint256 i; i < numTokens; ++i) { + address token = _knownTokens.at(i); + if (_allowedPriceFeeds[token].remove(priceFeed)) { + _allowanceTimestamps[token][priceFeed] = 0; + emit ForbidPriceFeed(token, priceFeed); + } } + + emit RemovePriceFeed(priceFeed); + } + + /// @notice Sets `priceFeed`'s staleness period to `stalenessPeriod` + /// @dev Reverts if caller is not owner + /// @dev Reverts if `priceFeed` is not known + function setStalenessPeriod(address priceFeed, uint32 stalenessPeriod) external override onlyOwner { + if (!_knownPriceFeeds.contains(priceFeed)) revert PriceFeedIsNotKnownException(priceFeed); + if (_priceFeedInfo[priceFeed].stalenessPeriod == stalenessPeriod) return; + + _validatePriceFeed(priceFeed, stalenessPeriod); + _priceFeedInfo[priceFeed].stalenessPeriod = stalenessPeriod; + + emit SetStalenessPeriod(priceFeed, stalenessPeriod); } - /** - * @notice Allows a price feed for use with a particular token - * @param token Address of the token - * @param priceFeed Address of the price feed - * @dev Reverts if the price feed is not added to the global list - */ - function allowPriceFeed(address token, address priceFeed) external onlyOwner nonZeroAddress(token) { - if (!_knownPriceFeeds.contains(priceFeed)) revert PriceFeedNotKnownException(priceFeed); + /// @notice Allows `priceFeed` for `token` + /// @dev Reverts if caller is not owner + /// @dev Reverts if `token` is zero address + /// @dev Reverts if `priceFeed` is not known or is already allowed for `token` + function allowPriceFeed(address token, address priceFeed) external override onlyOwner nonZeroAddress(token) { + if (!_knownPriceFeeds.contains(priceFeed)) revert PriceFeedIsNotKnownException(priceFeed); + if (!_allowedPriceFeeds[token].add(priceFeed)) revert PriceFeedIsAlreadyAllowedException(token, priceFeed); - _allowedPriceFeeds[token].add(priceFeed); _allowanceTimestamps[token][priceFeed] = block.timestamp; _knownTokens.add(token); emit AllowPriceFeed(token, priceFeed); } - /** - * @notice Forbids a price feed for use with a particular token - * @param token Address of the token - * @param priceFeed Address of the price feed - * @dev Reverts if the price feed is not added to the global list or the per-token list - */ - function forbidPriceFeed(address token, address priceFeed) external onlyOwner nonZeroAddress(token) { - if (!_knownPriceFeeds.contains(priceFeed)) revert PriceFeedNotKnownException(priceFeed); - if (!_allowedPriceFeeds[token].contains(priceFeed)) revert PriceFeedIsNotAllowedException(token, priceFeed); + /// @notice Forbids `priceFeed` for `token` + /// @dev Reverts if caller is not owner + /// @dev Reverts if `priceFeed` is not known or is not allowed for `token` + function forbidPriceFeed(address token, address priceFeed) external override onlyOwner { + if (!_knownPriceFeeds.contains(priceFeed)) revert PriceFeedIsNotKnownException(priceFeed); + if (!_allowedPriceFeeds[token].remove(priceFeed)) revert PriceFeedIsNotAllowedException(token, priceFeed); - _allowedPriceFeeds[token].remove(priceFeed); _allowanceTimestamps[token][priceFeed] = 0; emit ForbidPriceFeed(token, priceFeed); } - function priceFeedInfo(address priceFeed) external view returns (PriceFeedInfo memory) { - return _priceFeedInfo[priceFeed]; + /// @notice Executes price feed configuration `calls` with owner privileges + /// @dev Reverts if caller is not owner + /// @dev Reverts if any of call targets is not a known price feed + /// @dev Reverts if any of calls renounces ownership over price feed + function configurePriceFeeds(Call[] calldata calls) external override onlyOwner { + uint256 numCalls = calls.length; + for (uint256 i; i < numCalls; ++i) { + if (!_knownPriceFeeds.contains(calls[i].target)) revert PriceFeedIsNotKnownException(calls[i].target); + bytes4 selector = bytes4(calls[i].callData); + if (selector == Ownable.renounceOwnership.selector) { + revert ForbiddenConfigurationMethodException(selector); + } + calls[i].target.functionCall(calls[i].callData); + } + } + + // ------------- // + // PRICE UPDATES // + // ------------- // + + /// @notice Returns the list of updatable price feeds + function getUpdatablePriceFeeds() external view override returns (address[] memory) { + return _updatablePriceFeeds.values(); + } + + /// @notice Performs on-demand price feed updates + /// @dev Reverts if any of the price feeds is not added to the updatable price feeds set + function updatePrices(PriceUpdate[] calldata updates) external override { + uint256 numUpdates = updates.length; + for (uint256 i; i < numUpdates; ++i) { + if (!_updatablePriceFeeds.contains(updates[i].priceFeed)) { + revert PriceFeedIsNotUpdatableException(updates[i].priceFeed); + } + IUpdatablePriceFeed(updates[i].priceFeed).updatePrice(updates[i].data); + } + } + + // --------- // + // INTERNALS // + // --------- // + + /// @dev Validates `priceFeed`'s tree and adds all updatable price feeds from it to the store. + /// Returns whether `priceFeed` is deployed externally or via BCR. + /// Externally deployed price feeds are assumed to be non-updatable leaves of the tree. + function _validatePriceFeedTree(address priceFeed) internal returns (bool) { + if (_validatePriceFeedDeployment(priceFeed)) return true; + + if (_isUpdatable(priceFeed) && _updatablePriceFeeds.add(priceFeed)) emit AddUpdatablePriceFeed(priceFeed); + address[] memory underlyingFeeds = IPriceFeed(priceFeed).getUnderlyingFeeds(); + uint256 numFeeds = underlyingFeeds.length; + for (uint256 i; i < numFeeds; ++i) { + _validatePriceFeedTree(underlyingFeeds[i]); + } + + return false; + } + + /// @dev Returns whether `priceFeed` is deployed externally or via BCR. + /// For latter case, also ensures that price feed is owned by the store. + function _validatePriceFeedDeployment(address priceFeed) internal returns (bool) { + if (!IBytecodeRepository(bytecodeRepository).isDeployedFromRepository(priceFeed)) return true; + + try Ownable2Step(priceFeed).acceptOwnership() {} catch {} + try Ownable(priceFeed).owner() returns (address owner_) { + if (owner_ != address(this)) revert PriceFeedIsNotOwnedByStore(priceFeed); + } catch {} + + return false; + } + + /// @dev Returns whether `priceFeed` is updatable + function _isUpdatable(address priceFeed) internal view returns (bool) { + try IUpdatablePriceFeed(priceFeed).updatable() returns (bool updatable) { + return updatable; + } catch { + return false; + } } } diff --git a/contracts/interfaces/IAddressProvider.sol b/contracts/interfaces/IAddressProvider.sol index 518df13..006d0e2 100644 --- a/contracts/interfaces/IAddressProvider.sol +++ b/contracts/interfaces/IAddressProvider.sol @@ -7,36 +7,41 @@ import {IAddressProvider as IAddressProviderBase} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IAddressProvider.sol"; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; import {IImmutableOwnableTrait} from "./base/IImmutableOwnableTrait.sol"; - -struct ContractValue { - string key; - address value; - uint256 version; -} +import {AddressProviderEntry} from "./Types.sol"; /// @title Address provider interface interface IAddressProvider is IAddressProviderBase, IVersion, IImmutableOwnableTrait { - event SetAddress(string indexed key, uint256 indexed version, address indexed value); - - function addresses(string memory key, uint256 _version) external view returns (address); - - function getAddressOrRevert(string memory key, uint256 _version) external view returns (address); - - function getAllSavedContracts() external view returns (ContractValue[] memory); - - function getLatestVersion(string memory key) external view returns (uint256); - - function getLatestMinorVersion(string memory key, uint256 majorVersion) external view returns (uint256); - - function getLatestPatchVersion(string memory key, uint256 minorVersion) external view returns (uint256); + // ------ // + // EVENTS // + // ------ // + + event SetAddress(bytes32 indexed key, uint256 indexed ver, address indexed value); + + // ------ // + // ERRORS // + // ------ // + + error AddressNotFoundException(bytes32 key, uint256 ver); + error InvalidVersionException(bytes32 key, uint256 ver); + error VersionNotFoundException(bytes32 key); + error ZeroAddressException(bytes32 key); + + // ------- // + // GETTERS // + // ------- // + + function getAddress(bytes32 key, uint256 ver) external view returns (address); + function getAddressOrRevert(bytes32 key, uint256 ver) external view override returns (address); + function getKeys() external view returns (bytes32[] memory); + function getVersions(bytes32 key) external view returns (uint256[] memory); + function getAllEntries() external view returns (AddressProviderEntry[] memory); + function getLatestVersion(bytes32 key) external view returns (uint256); + function getLatestMinorVersion(bytes32 key, uint256 majorVersion) external view returns (uint256); + function getLatestPatchVersion(bytes32 key, uint256 minorVersion) external view returns (uint256); // ------------- // // CONFIGURATION // // ------------- // - function setAddress(string memory key, address addr, bool saveVersion) external; - function setAddress(bytes32 key, address value, bool saveVersion) external; - - function setAddress(address addr, bool saveVersion) external; } diff --git a/contracts/interfaces/IBytecodeRepository.sol b/contracts/interfaces/IBytecodeRepository.sol index 6449c97..261c583 100644 --- a/contracts/interfaces/IBytecodeRepository.sol +++ b/contracts/interfaces/IBytecodeRepository.sol @@ -5,230 +5,160 @@ pragma solidity ^0.8.23; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; import {IImmutableOwnableTrait} from "./base/IImmutableOwnableTrait.sol"; -import {Bytecode, AuditorSignature} from "./Types.sol"; +import {AuditReport, Bytecode} from "./Types.sol"; +/// @title Bytecode repository interface interface IBytecodeRepository is IVersion, IImmutableOwnableTrait { - // - // ERRORS - // - error BytecodeIsNotApprovedException(bytes32 contractType, uint256 version); + // ------ // + // EVENTS // + // ------ // - // Thrown if the deployed contract has a different contractType/version than it's indexed in the repository - error IncorrectBytecodeException(bytes32 bytecodeHash); - - // Thrown if the bytecode provided is empty - error EmptyBytecodeException(); - - // Thrown if someone tries to deploy the contract with the same address - error BytecodeAlreadyExistsAtAddressException(address); - - // Thrown if domain + postfix length is more than 30 symbols (doesn't fit into bytes32) - error TooLongContractTypeException(string); - - // Thrown if requested bytecode wasn't found in the repository - error BytecodeIsNotUploadedException(bytes32 bytecodeHash); - - // Thrown if someone tries to replace existing bytecode with the same contact type & version - error BytecodeAlreadyExistsException(); - - // Thrown if requested bytecode wasn't found in the repository - error BytecodeIsNotAuditedException(); - - // Thrown if someone tries to deploy a contract which wasn't audited enough - error ContractIsNotAuditedException(); - - error SignerIsNotAuditorException(address signer); - - // Thrown when an attempt is made to add an auditor that already exists - error AuditorAlreadyAddedException(); - - // Thrown when an auditor is not found in the repository - error AuditorNotFoundException(); - - // Thrown if the caller is not the deployer of the bytecode - error NotDeployerException(); - - // Thrown if the caller does not have valid auditor permissions - error NoValidAuditorPermissionsAException(); - - /// @notice Thrown when trying to deploy contract with forbidden bytecode - error BytecodeForbiddenException(bytes32 bytecodeHash); - - /// @notice Thrown when trying to deploy contract with incorrect domain ownership - error NotDomainOwnerException(); - - /// @notice Thrown when trying to deploy contract with incorrect domain ownership - error NotAllowedSystemContractException(bytes32 bytecodeHash); - - /// @notice Thrown when trying to deploy contract with incorrect contract type - error ContractTypeVersionAlreadyExistsException(); - - error OnlyAuthorCanSyncException(); - - error AuditorAlreadySignedException(); - - error NoValidAuditorSignatureException(); - - error InvalidAuthorSignatureException(); - // - // EVENTS - // - - // Emitted when new smart contract was deployed + event AddAuditor(address indexed auditor, string name); + event AddPublicDomain(bytes32 indexed domain); + event AddSystemDomain(bytes32 indexed domain); + event AllowContract(bytes32 indexed bytecodeHash, bytes32 indexed contractType, uint256 indexed version); + event AuditBytecode(bytes32 indexed bytecodeHash, address indexed auditor, string reportUrl, bytes signature); event DeployContract( - address indexed addr, bytes32 indexed bytecodeHash, string contractType, uint256 indexed version + bytes32 indexed bytecodeHash, bytes32 indexed contractType, uint256 indexed version, address contractAddress ); - - // Event emitted when a new auditor is added to the repository - event AddAuditor(address indexed auditor, string name); - - // Event emitted when an auditor is forbidden from the repository + event ForbidContract(bytes32 indexed bytecodeHash, bytes32 indexed contractType, uint256 indexed version); + event ForbidInitCode(bytes32 indexed initCodeHash); event RemoveAuditor(address indexed auditor); - - // Event emitted when new bytecode is uploaded to the repository + event RemoveContractTypeOwner(bytes32 indexed contractType); + event SetContractTypeOwner(bytes32 indexed contractType, address indexed owner); + event SetTokenSpecificPostfix(address indexed token, bytes32 indexed postfix); event UploadBytecode( bytes32 indexed bytecodeHash, - string contractType, + bytes32 indexed contractType, uint256 indexed version, - address indexed author, - string source + address author, + string source, + bytes signature ); - // Event emitted when bytecode is signed by an auditor - event AuditBytecode(bytes32 indexed bytecodeHash, address indexed auditor, string reportUrl, bytes signature); + // ------ // + // ERRORS // + // ------ // - // Event emitted when a public domain is added - event AddPublicDomain(bytes32 indexed domain); - - // Event emitted when a public domain is removed - event RemovePublicDomain(bytes32 indexed domain); - - // Event emitted when contract type owner is removed - event RemoveContractTypeOwner(bytes32 indexed contractType); - - // Event emitted when bytecode is forbidden - event ForbidBytecode(bytes32 indexed bytecodeHash); - - // Event emitted when token specific postfix is set - event SetTokenSpecificPostfix(address indexed token, bytes32 indexed postfix); - - // Event emitted when bytecode is approved - event ApproveContract(bytes32 indexed bytecodeHash, bytes32 indexed contractType, uint256 version); - - // Event emitted when bytecode is revoked - event RevokeApproval(bytes32 indexed bytecodeHash, bytes32 indexed contractType, uint256 version); - - // FUNCTIONS + error AuditorIsNotApprovedException(address auditor); + error AuthorIsNotContractTypeOwnerException(bytes32 contractType, address author); + error BytecodeIsAlreadyAllowedException(bytes32 contractType, uint256 version); + error BytecodeIsAlreadySignedByAuditorException(bytes32 bytecodeHash, address auditor); + error BytecodeIsNotAllowedException(bytes32 contractType, uint256 version); + error BytecodeIsNotAuditedException(bytes32 bytecodeHash); + error BytecodeIsNotUploadedException(bytes32 bytecodeHash); + error CallerIsNotBytecodeAuthorException(address caller); + error ContractIsAlreadyDeployedException(address deployedContract); + error ContractTypeIsNotInPublicDomainException(bytes32 contractType); + error DomainIsAlreadyMarketAsPublicException(bytes32 domain); + error DomainIsAlreadyMarketAsSystemException(bytes32 domain); + error InitCodeIsForbiddenException(bytes32 initCodeHash); + error InvalidAuditorSignatureException(address auditor); + error InvalidAuthorSignatureException(address author); + error InvalidBytecodeException(bytes32 bytecodeHash); + error InvalidContractTypeException(bytes32 contractType); + error InvalidDomainException(bytes32 domain); + error InvalidVersionException(bytes32 contractType, uint256 version); + error VersionNotFoundException(bytes32 contractType); + + // --------------- // + // EIP-712 GETTERS // + // --------------- // - function deploy(bytes32 type_, uint256 version_, bytes memory constructorParams, bytes32 salt) + function BYTECODE_TYPEHASH() external view returns (bytes32); + function AUDIT_REPORT_TYPEHASH() external view returns (bytes32); + function domainSeparatorV4() external view returns (bytes32); + function computeBytecodeHash(Bytecode calldata bytecode) external view returns (bytes32); + function computeAuditReportHash(bytes32 bytecodeHash, address auditor, string calldata reportUrl) external - returns (address); + view + returns (bytes32); + // ------------------- // + // DEPLOYING CONTRACTS // + // ------------------- // + + function isDeployedFromRepository(address deployedContract) external view returns (bool); + function getDeployedContractBytecodeHash(address deployedContract) external view returns (bytes32); function computeAddress( - bytes32 type_, - uint256 version_, - bytes memory constructorParams, + bytes32 contractType, + uint256 version, + bytes calldata constructorParams, bytes32 salt, address deployer ) external view returns (address); + function deploy(bytes32 contractType, uint256 version, bytes calldata constructorParams, bytes32 salt) + external + returns (address); - function getTokenSpecificPostfix(address token) external view returns (bytes32); - - function getLatestVersion(bytes32 type_) external view returns (uint256); - - function getLatestMinorVersion(bytes32 type_, uint256 majorVersion) external view returns (uint256); + // ------------------ // + // UPLOADING BYTECODE // + // ------------------ // - function getLatestPatchVersion(bytes32 type_, uint256 minorVersion) external view returns (uint256); + function getBytecode(bytes32 bytecodeHash) external view returns (Bytecode memory); + function isBytecodeUploaded(bytes32 bytecodeHash) external view returns (bool); + function uploadBytecode(Bytecode calldata bytecode) external; - /// @notice Computes a unique hash for bytecode metadata - function computeBytecodeHash(Bytecode calldata bytecode) external pure returns (bytes32); + // ----------------- // + // AUDITING BYTECODE // + // ----------------- // - /// @notice Uploads new bytecode to the repository - function uploadBytecode(Bytecode calldata bytecode) external; + function isBytecodeAudited(bytes32 bytecodeHash) external view returns (bool); + function getAuditReports(bytes32 bytecodeHash) external view returns (AuditReport[] memory); + function getAuditReport(bytes32 bytecodeHash, uint256 index) external view returns (AuditReport memory); + function getNumAuditReports(bytes32 bytecodeHash) external view returns (uint256); + function submitAuditReport(bytes32 bytecodeHash, AuditReport calldata auditReport) external; - /// @notice Allows auditors to sign bytecode metadata - function signBytecodeHash(bytes32 bytecodeHash, string calldata reportUrl, bytes memory signature) external; + // ----------------- // + // ALLOWING BYTECODE // + // ----------------- // - /// @notice Allows owner to mark contracts as system contracts + function getAllowedBytecodeHash(bytes32 contractType, uint256 version) external view returns (bytes32); + function getContractTypeOwner(bytes32 contractType) external view returns (address); function allowSystemContract(bytes32 bytecodeHash) external; + function allowPublicContract(bytes32 bytecodeHash) external; + function removePublicContractType(bytes32 contractType) external; - /// @notice Adds a new auditor - function addAuditor(address auditor, string memory name) external; + // ------------------ // + // DOMAINS MANAGEMENT // + // ------------------ // - /// @notice Removes an auditor - function removeAuditor(address auditor) external; + function isSystemDomain(bytes32 domain) external view returns (bool); + function getSystemDomains() external view returns (bytes32[] memory); + function isPublicDomain(bytes32 domain) external view returns (bool); + function getPublicDomains() external view returns (bytes32[] memory); + function addPublicDomain(bytes32 domain) external; - /// @notice Checks if an address is an approved auditor - function isAuditor(address auditor) external view returns (bool); + // ------------------- // + // AUDITORS MANAGEMENT // + // ------------------- // - /// @notice Returns list of all approved auditors + function isAuditor(address auditor) external view returns (bool); function getAuditors() external view returns (address[] memory); + function getAuditorName(address auditor) external view returns (string memory); + function addAuditor(address auditor, string calldata name) external; + function removeAuditor(address auditor) external; - /// @notice Adds a new public domain - function addPublicDomain(bytes32 domain) external; - - /// @notice Removes a public domain - function removePublicDomain(bytes32 domain) external; + // -------------------- // + // FORBIDDING INIT CODE // + // -------------------- // - /// @notice Marks initCode as forbidden + function isInitCodeForbidden(bytes32 initCodeHash) external view returns (bool); function forbidInitCode(bytes32 initCodeHash) external; - /// @notice Sets token-specific postfix - function setTokenSpecificPostfix(address token, bytes32 postfix) external; - - /// @notice Removes contract type owner - function removeContractTypeOwner(bytes32 contractType) external; + // ------------------------ // + // TOKENS WITH CUSTOM LOGIC // + // ------------------------ // - /// @notice Revokes approval for a specific bytecode - function revokeApproval(bytes32 contractType, uint256 version, bytes32 bytecodeHash) external; - - /// @notice Checks if a contract name belongs to public domain - function isInPublicDomain(bytes32 contractType) external view returns (bool); - - /// @notice Checks if a domain is public - function isPublicDomain(bytes32 domain) external view returns (bool); - - /// @notice Returns list of all public domains - function listPublicDomains() external view returns (bytes32[] memory); - - /// @notice Gets bytecode metadata by hash - function bytecodeByHash(bytes32 hash) external view returns (Bytecode memory); - - /// @notice Gets approved bytecode hash for contract type and version - function approvedBytecodeHash(bytes32 contractType, uint256 version) external view returns (bytes32); - - /// @notice Gets deployed contract's bytecode hash - function deployedContracts(address contractAddress) external view returns (bytes32); - - /// @notice Checks if initCode is forbidden - function forbiddenInitCode(bytes32 initCodeHash) external view returns (bool); - - /// @notice Checks if contract is allowed as system contract - function allowedSystemContracts(bytes32 bytecodeHash) external view returns (bool); - - /// @notice Gets contract type owner - function contractTypeOwner(bytes32 contractType) external view returns (address); - - /// @notice Gets auditor name - function auditorName(address auditor) external view returns (string memory); - - /// @notice Gets auditor signatures for a bytecode hash - function auditorSignaturesByHash(bytes32 bytecodeHash) external view returns (AuditorSignature[] memory); - - /// @notice Gets specific auditor signature for a bytecode hash - function auditorSignaturesByHash(bytes32 bytecodeHash, uint256 index) - external - view - returns (AuditorSignature memory); - - /// @notice Checks if bytecode is uploaded - function isBytecodeUploaded(bytes32 bytecodeHash) external view returns (bool); - - /// @notice Checks if initCode is forbidden and reverts if it is - function revertIfInitCodeForbidden(bytes memory initCode) external view; + function getTokenSpecificPostfix(address token) external view returns (bytes32); + function setTokenSpecificPostfix(address token, bytes32 postfix) external; - /// @notice Checks if bytecode is audited - function isAuditBytecode(bytes32 bytecodeHash) external view returns (bool); + // --------------- // + // VERSION CONTROL // + // --------------- // - function BYTECODE_TYPEHASH() external view returns (bytes32); + function getVersions(bytes32 contractType) external view returns (uint256[] memory); + function getLatestVersion(bytes32 contractType) external view returns (uint256); + function getLatestMinorVersion(bytes32 contractType, uint256 majorVersion) external view returns (uint256); + function getLatestPatchVersion(bytes32 contractType, uint256 minorVersion) external view returns (uint256); } diff --git a/contracts/interfaces/ICrossChainMultisig.sol b/contracts/interfaces/ICrossChainMultisig.sol index bb20bed..07ee9be 100644 --- a/contracts/interfaces/ICrossChainMultisig.sol +++ b/contracts/interfaces/ICrossChainMultisig.sol @@ -3,144 +3,90 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; -import {CrossChainCall, SignedProposal} from "./Types.sol"; +import {CrossChainCall, SignedBatch, SignedRecoveryModeMessage} from "./Types.sol"; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; +/// @title Cross-chain multisig interface interface ICrossChainMultisig is IVersion { - // - // Events - // - /// @notice Emitted when a new signer is added to the multisig - /// @param signer Address of the newly added signer - event AddSigner(address indexed signer); + // ------ // + // EVENTS // + // ------ // - /// @notice Emitted when a signer is removed from the multisig - /// @param signer Address of the removed signer + event AddSigner(address indexed signer); + event DisableRecoveryMode(); + event EnableRecoveryMode(bytes32 indexed startingBatchHash); + event ExecuteBatch(bytes32 indexed batchHash); event RemoveSigner(address indexed signer); + event SetConfirmationThreshold(uint8 newConfirmationThreshold); + event SignBatch(bytes32 indexed batchHash, address indexed signer); + event SubmitBatch(bytes32 indexed batchHash); - /// @notice Emitted when the confirmation threshold is updated - /// @param newconfirmationThreshold New number of required signatures - event SetConfirmationThreshold(uint8 newconfirmationThreshold); - - /// @notice Emitted when a new proposal is submitted - /// @param proposalHash Hash of the submitted proposal - event SubmitProposal(bytes32 indexed proposalHash); - - /// @notice Emitted when a signer signs a proposal - /// @param proposalHash Hash of the signed proposal - /// @param signer Address of the signer - event SignProposal(bytes32 indexed proposalHash, address indexed signer); - - /// @notice Emitted when a proposal is successfully executed - /// @param proposalHash Hash of the executed proposal - event ExecuteProposal(bytes32 indexed proposalHash); - - // Errors - - /// @notice Thrown when an invalid confirmation threshold is set - error InvalidconfirmationThresholdException(); - - /// @notice Thrown when a signer attempts to sign a proposal multiple times - error AlreadySignedException(); - - /// @notice Thrown when the previous proposal hash doesn't match the expected value - error InvalidPrevHashException(); - - /// @notice Thrown when trying to interact with a non-existent proposal - error ProposalDoesNotExistException(); + // ------ // + // ERRORS // + // ------ // - /// @notice Thrown when trying to add a signer that already exists - error SignerAlreadyExistsException(); - - /// @notice Thrown when trying to remove a non-existent signer - error SignerDoesNotExistException(); - - /// @notice Thrown when trying to execute a proposal on the wrong chain + error BatchIsNotSubmittedException(bytes32 batchHash); + error CallerIsNotSelfException(address caller); error CantBeExecutedOnCurrentChainException(); + error DuplicateSignatureException(address signer); + error InsufficientNumberOfSignaturesException(); + error InvalidBatchException(); + error InvalidConfirmationThresholdException(); + error InvalidPrevHashException(); + error InvalidRecoveryModeMessageException(); + error InvalidSignerAddressException(); + error SignerIsAlreadyApprovedException(address signer); + error SignerIsNotApprovedException(address signer); - /// @notice Thrown when a restricted function is called by non-multisig address - error OnlySelfException(); - - /// @notice Thrown when submitting a proposal with no calls - error NoCallsInProposalException(); - - /// @notice Thrown when trying to execute a proposal with insufficient signatures - error NotEnoughSignaturesException(); - - /// @notice Thrown when self-calls are inconsistent with the target chain - error InconsistentSelfCallOnOtherChainException(); - - /// @notice Thrown when setting an invalid confirmation threshold value - error InvalidConfirmationThresholdValueException(); - - /// @notice Submits a new proposal to the multisig - /// @param calls Array of cross-chain calls to be executed - /// @param prevHash Hash of the previous proposal (for ordering) - function submitProposal(string calldata name, CrossChainCall[] calldata calls, bytes32 prevHash) external; - - /// @notice Allows a signer to sign a submitted proposal - /// @param proposalHash Hash of the proposal to sign - /// @param signature Signature of the signer - function signProposal(bytes32 proposalHash, bytes calldata signature) external; - - /// @notice Executes a proposal once it has enough signatures - /// @param proposal The signed proposal to execute - function executeProposal(SignedProposal calldata proposal) external; - - /// @notice Adds a new signer to the multisig - /// @param signer Address of the signer to add - function addSigner(address signer) external; - - /// @notice Removes a signer from the multisig - /// @param signer Address of the signer to remove - function removeSigner(address signer) external; - - /// @notice Sets a new confirmation threshold - /// @param newThreshold New threshold value - function setConfirmationThreshold(uint8 newThreshold) external; + // --------------- // + // EIP-712 GETTERS // + // --------------- // - /// @notice Hashes a proposal according to EIP-712 - /// @param name Name of the proposal - /// @param calls Array of cross-chain calls - /// @param prevHash Hash of the previous proposal - /// @return bytes32 Hash of the proposal - function hashProposal(string calldata name, CrossChainCall[] calldata calls, bytes32 prevHash) + function domainSeparatorV4() external view returns (bytes32); + function CROSS_CHAIN_CALL_TYPEHASH() external view returns (bytes32); + function BATCH_TYPEHASH() external view returns (bytes32); + function COMPACT_BATCH_TYPEHASH() external view returns (bytes32); + function RECOVERY_MODE_TYPEHASH() external view returns (bytes32); + function computeCrossChainCallHash(CrossChainCall calldata call) external view returns (bytes32); + function computeBatchHash(string memory name, CrossChainCall[] calldata calls, bytes32 prevHash) external view returns (bytes32); + function computeCompactBatchHash(string memory name, bytes32 batchHash, bytes32 prevHash) + external + view + returns (bytes32); + function computeRecoveryModeHash(uint256 chainId, bytes32 startingBatchHash) external view returns (bytes32); - // - // GETTERS - // - - /// @notice Returns the current confirmation threshold - function confirmationThreshold() external view returns (uint8); - - /// @notice Returns the hash of the last executed proposal - function lastProposalHash() external view returns (bytes32); - - /// @notice Returns the signed proposal details for a given hash - function getSignedProposal(bytes32 proposalHash) external view returns (SignedProposal memory); - - /// @notice Returns all currently pending proposals - function getCurrentProposals() external view returns (SignedProposal[] memory); - - /// @notice Returns all executed proposals - function getExecutedProposals() external view returns (SignedProposal[] memory); + // ---------- // + // GOVERNANCE // + // ---------- // - /// @notice Returns the array of executed proposal hashes - function getExecutedProposalHashes() external view returns (bytes32[] memory); + function lastBatchHash() external view returns (bytes32); + function getExecutedBatchHashes() external view returns (bytes32[] memory); + function getCurrentBatchHashes() external view returns (bytes32[] memory); + function getConnectedBatchHashes(bytes32 batchHash) external view returns (bytes32[] memory); + function getBatch(bytes32 batchHash) external view returns (SignedBatch memory); + function submitBatch(string calldata name, CrossChainCall[] calldata calls, bytes32 prevHash) external; + function signBatch(bytes32 batchHash, bytes calldata signature) external; + function executeBatch(SignedBatch calldata batch) external; - /// @notice Returns a single executed proposal - function getProposal(bytes32 proposalHash) external view returns (SignedProposal memory); + // ------------------ // + // SIGNERS MANAGEMENT // + // ------------------ // - /// @notice Returns the list of current signers + function isSigner(address account) external view returns (bool); function getSigners() external view returns (address[] memory); + function confirmationThreshold() external view returns (uint8); + function addSigner(address signer) external; + function removeSigner(address signer) external; + function setConfirmationThreshold(uint8 newThreshold) external; - /// @notice Checks if an address is a signer - /// @param account Address to check - function isSigner(address account) external view returns (bool); + // ------------- // + // RECOVERY MODE // + // ------------- // - /// @notice Returns the domain separator used for EIP-712 signing - function domainSeparatorV4() external view returns (bytes32); + function isRecoveryModeEnabled() external view returns (bool); + function enableRecoveryMode(SignedRecoveryModeMessage calldata message) external; + function disableRecoveryMode(uint256 chainId) external; } diff --git a/contracts/interfaces/IGovernor.sol b/contracts/interfaces/IGovernor.sol index aecc4e1..6a15ee4 100644 --- a/contracts/interfaces/IGovernor.sol +++ b/contracts/interfaces/IGovernor.sol @@ -102,6 +102,9 @@ interface IGovernor is IVersion { /// @notice Thrown when a queue admin tries to add transactions to the batch not initiated by themselves error CallerNotBatchInitiatorException(); + /// @notice Thrown when trying to renounce ownership + error CannotRenounceOwnershipException(); + /// @notice Thrown when trying to queue a transaction that is already queued error TransactionAlreadyQueuedException(); diff --git a/contracts/interfaces/IInstanceManager.sol b/contracts/interfaces/IInstanceManager.sol index 5fab6db..63db909 100644 --- a/contracts/interfaces/IInstanceManager.sol +++ b/contracts/interfaces/IInstanceManager.sol @@ -5,19 +5,48 @@ pragma solidity ^0.8.23; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; +/// @title Instance manager interface interface IInstanceManager is IVersion { + // ------ // + // EVENTS // + // ------ // + + event SetPendingGovernance(address indexed newGovernance); + event AcceptGovernance(address indexed newGovernance); + + // ------ // + // ERRORS // + // ------ // + + error CallerIsNotCrossChainGovernanceException(address caller); + error CallerIsNotPendingGovernanceException(address caller); + error CallerIsNotTreasuryException(address caller); + error InvalidKeyException(bytes32 key); + + // ------- // + // GETTERS // + // ------- // + function addressProvider() external view returns (address); function bytecodeRepository() external view returns (address); function instanceManagerProxy() external view returns (address); function treasuryProxy() external view returns (address); function crossChainGovernanceProxy() external view returns (address); function isActivated() external view returns (bool); + function owner() external view returns (address); + function pendingGovernance() external view returns (address); + + // ------------- // + // CONFIGURATION // + // ------------- // function activate(address instanceOwner, address treasury, address weth, address gear) external; function deploySystemContract(bytes32 contractType, uint256 version, bool saveVersion) external; - function setGlobalAddress(string memory key, address addr, bool saveVersion) external; - function setLocalAddress(string memory key, address addr, bool saveVersion) external; + function setGlobalAddress(bytes32 key, address addr, bool saveVersion) external; + function setLocalAddress(bytes32 key, address addr, bool saveVersion) external; function configureGlobal(address target, bytes calldata data) external; function configureLocal(address target, bytes calldata data) external; function configureTreasury(address target, bytes calldata data) external; + function setPendingGovernance(address newGovernance) external; + function acceptGovernance() external; } diff --git a/contracts/interfaces/IMarketConfigurator.sol b/contracts/interfaces/IMarketConfigurator.sol index 582ce00..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); @@ -170,6 +172,15 @@ interface IMarketConfigurator is IVersion, IDeployerTrait { view returns (address creditManager); + function previewCreateCreditSuite( + uint256 marketMinorVersion, + uint256 creditSuiteMinorVersion, + address underlying, + string calldata name, + string calldata symbol, + bytes calldata encodedParams + ) external view returns (address creditManager); + function createCreditSuite(uint256 minorVersion, address pool, bytes calldata encdodedParams) external returns (address creditManager); diff --git a/contracts/interfaces/IMarketConfiguratorFactory.sol b/contracts/interfaces/IMarketConfiguratorFactory.sol index 49f6229..342e390 100644 --- a/contracts/interfaces/IMarketConfiguratorFactory.sol +++ b/contracts/interfaces/IMarketConfiguratorFactory.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.23; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; import {IDeployerTrait} from "./base/IDeployerTrait.sol"; +/// @title Market configurator factory interface interface IMarketConfiguratorFactory is IVersion, IDeployerTrait { // ------ // // EVENTS // @@ -18,29 +19,33 @@ interface IMarketConfiguratorFactory is IVersion, IDeployerTrait { // ERRORS // // ------ // - error AddressIsNotMarketConfiguratorException(address addr); error CallerIsNotCrossChainGovernanceException(address caller); - error CallerIsNotMarketConfiguratorException(address caller); error CallerIsNotMarketConfiguratorAdminException(address caller); - error CantShutdownMarketConfiguratorException(); + error CantShutdownMarketConfiguratorException(address marketConfigurator); error MarketConfiguratorIsAlreadyAddedException(address marketConfigurator); error MarketConfiguratorIsAlreadyShutdownException(address marketConfigruator); + error MarketConfiguratorIsNotRegisteredException(address marketConfigurator); - function isMarketConfigurator(address account) external view returns (bool); + // ------- // + // GETTERS // + // ------- // + function isMarketConfigurator(address account) external view returns (bool); function getMarketConfigurators() external view returns (address[] memory); - + function getMarketConfigurator(uint256 index) external view returns (address); + function getNumMarketConfigurators() external view returns (uint256); function getShutdownMarketConfigurators() external view returns (address[] memory); + // ------------- // + // CONFIGURATION // + // ------------- // + function createMarketConfigurator( - address admin, address emergencyAdmin, address adminFeeTreasury, string calldata curatorName, bool deployGovernor ) external returns (address marketConfigurator); - function shutdownMarketConfigurator(address marketConfigurator) external; - function addMarketConfigurator(address marketConfigurator) external; } diff --git a/contracts/interfaces/IPriceFeedStore.sol b/contracts/interfaces/IPriceFeedStore.sol index bfef182..f6d4804 100644 --- a/contracts/interfaces/IPriceFeedStore.sol +++ b/contracts/interfaces/IPriceFeedStore.sol @@ -3,52 +3,95 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; +import { + IPriceFeedStore as IPriceFeedStoreBase, + PriceUpdate +} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeedStore.sol"; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; +import {IDeployerTrait} from "./base/IDeployerTrait.sol"; import {IImmutableOwnableTrait} from "./base/IImmutableOwnableTrait.sol"; +import {Call, ConnectedPriceFeed, PriceFeedInfo} from "./Types.sol"; -interface IPriceFeedStore is IVersion, IImmutableOwnableTrait { - // - // ERRORS - // +/// @title Price feed store interface +interface IPriceFeedStore is IPriceFeedStoreBase, IVersion, IDeployerTrait, IImmutableOwnableTrait { + // ------ // + // ERRORS // + // ------ // /// @notice Thrown when attempting to use a price feed that is not known by the price feed store - error PriceFeedNotKnownException(address priceFeed); + error PriceFeedIsNotKnownException(address priceFeed); /// @notice Thrown when attempting to add a price feed that is already known by the price feed store - error PriceFeedAlreadyAddedException(address priceFeed); + error PriceFeedIsAlreadyAddedException(address priceFeed); - /// @notice Thrown when attempting to remove a price feed that is not allowed for a token + /// @notice Thrown when attempting to forbid a price feed that is not allowed for a token error PriceFeedIsNotAllowedException(address token, address priceFeed); - // - // EVENTS - // + /// @notice Thrown when attempting to allow a price feed that is already allowed for a token + error PriceFeedIsAlreadyAllowedException(address token, address priceFeed); - /// @notice Emitted when a new price feed is added to PriceFeedStore - event AddPriceFeed(address priceFeed, uint32 stalenessPeriod); + /// @notice Thrown when attempting to add a price feed that is not owned by the store + error PriceFeedIsNotOwnedByStore(address priceFeed); - /// @notice Emitted when the staleness period is changed in an existing price feed - event SetStalenessPeriod(address priceFeed, uint32 stalenessPeriod); + /// @notice Thrown when attempting to update a price feed that is not added to the updatable price feeds set + error PriceFeedIsNotUpdatableException(address priceFeed); + + /// @notice Thrown when attempting to call a forbidden configuration method + error ForbiddenConfigurationMethodException(bytes4 selector); + + // ------ // + // EVENTS // + // ------ // + + /// @notice Emitted when a new price feed is added to the store + event AddPriceFeed(address indexed priceFeed, uint32 stalenessPeriod, string name); + + /// @notice Emitted when a price feed is removed from the store + event RemovePriceFeed(address indexed priceFeed); + + /// @notice Emitted when the staleness period is set for a price feed + event SetStalenessPeriod(address indexed priceFeed, uint32 stalenessPeriod); /// @notice Emitted when a price feed is allowed for a token - event AllowPriceFeed(address token, address priceFeed); + event AllowPriceFeed(address indexed token, address indexed priceFeed); /// @notice Emitted when a price feed is forbidden for a token - event ForbidPriceFeed(address token, address priceFeed); + event ForbidPriceFeed(address indexed token, address indexed priceFeed); - // - // GETTERS - // + /// @notice Emitted when a price feed is added to the updatable price feeds set + event AddUpdatablePriceFeed(address indexed priceFeed); + + // ------- // + // GETTERS // + // ------- // + + function zeroPriceFeed() external view returns (address); function getPriceFeeds(address token) external view returns (address[] memory); function isAllowedPriceFeed(address token, address priceFeed) external view returns (bool); - function getStalenessPeriod(address priceFeed) external view returns (uint32); + function getStalenessPeriod(address priceFeed) external view override returns (uint32); function getAllowanceTimestamp(address token, address priceFeed) external view returns (uint256); + function getTokenPriceFeedsMap() external view returns (ConnectedPriceFeed[] memory); + function getKnownTokens() external view returns (address[] memory); + function isKnownToken(address token) external view returns (bool); + function getKnownPriceFeeds() external view returns (address[] memory); + function isKnownPriceFeed(address priceFeed) external view returns (bool); + function priceFeedInfo(address priceFeed) external view returns (PriceFeedInfo memory); + + // ------------- // + // CONFIGURATION // + // ------------- // - // - // CONFIGURATION - // - function addPriceFeed(address priceFeed, uint32 stalenessPeriod) external; + function addPriceFeed(address priceFeed, uint32 stalenessPeriod, string calldata name) external; + function removePriceFeed(address priceFeed) external; function setStalenessPeriod(address priceFeed, uint32 stalenessPeriod) external; function allowPriceFeed(address token, address priceFeed) external; function forbidPriceFeed(address token, address priceFeed) external; + function configurePriceFeeds(Call[] calldata calls) external; + + // ------------- // + // PRICE UPDATES // + // ------------- // + + function getUpdatablePriceFeeds() external view returns (address[] memory); + function updatePrices(PriceUpdate[] calldata updates) external override; } diff --git a/contracts/interfaces/Types.sol b/contracts/interfaces/Types.sol index e994646..d381c97 100644 --- a/contracts/interfaces/Types.sol +++ b/contracts/interfaces/Types.sol @@ -3,24 +3,52 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; +struct AddressProviderEntry { + bytes32 key; + uint256 ver; + address value; +} + +struct AuditReport { + address auditor; + string reportUrl; + bytes signature; +} + +struct Bytecode { + bytes32 contractType; + uint256 version; + bytes initCode; + address author; + string source; + bytes authorSignature; +} + +struct BytecodePointer { + bytes32 contractType; + uint256 version; + address initCodePointer; + address author; + string source; + bytes authorSignature; +} + struct Call { address target; bytes callData; } +struct ConnectedPriceFeed { + address token; + address[] priceFeeds; +} + struct CrossChainCall { uint256 chainId; // 0 means to be executed on all chains address target; bytes callData; } -struct SignedProposal { - string name; - bytes32 prevHash; - CrossChainCall[] calls; - bytes[] signatures; -} - struct DeployParams { bytes32 postfix; bytes32 salt; @@ -41,42 +69,23 @@ struct MarketFactories { } struct PriceFeedInfo { - address author; + string name; uint32 stalenessPeriod; bytes32 priceFeedType; uint256 version; } -// The `BytecodeInfoMeta` struct holds metadata about a bytecode in BytecodeRepository -// -// - `author`: A person who first upload smart-contract to BCR -// - `contractType`: A bytes32 identifier representing the type of the contract. -// - `version`: A uint256 indicating the version of the contract. -// - `sources`: An array of `Source` structs, each containing a comment and a link related to the contract's source. -// - `auditors`: An array of addresses representing the auditors who have reviewed the contract. -// - `reports`: An array of `SecurityReport` structs, each containing information about security audits conducted on the contract. -struct Bytecode { - bytes32 contractType; - uint256 version; - bytes initCode; - address author; - string source; - bytes authorSignature; -} - -struct BytecodePointer { - bytes32 contractType; - uint256 version; - address initCodePointer; - address author; - string source; - bytes authorSignature; +struct SignedBatch { + string name; + bytes32 prevHash; + CrossChainCall[] calls; + bytes[] signatures; } -struct AuditorSignature { - string reportUrl; - address auditor; - bytes signature; +struct SignedRecoveryModeMessage { + uint256 chainId; + bytes32 startingBatchHash; + bytes[] signatures; } struct Split { diff --git a/contracts/interfaces/factories/ICreditConfigureActions.sol b/contracts/interfaces/factories/ICreditConfigureActions.sol new file mode 100644 index 0000000..ca7e25d --- /dev/null +++ b/contracts/interfaces/factories/ICreditConfigureActions.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +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; + function allowAdapter(DeployParams calldata params) external; + function forbidAdapter(address adapter) external; + function configureAdapterFor(address targetContract, bytes calldata data) external; + function setFees( + uint16 feeLiquidation, + uint16 liquidationPremium, + uint16 feeLiquidationExpired, + uint16 liquidationPremiumExpired + ) external; + function setMaxDebtPerBlockMultiplier(uint8 newMaxDebtLimitPerBlockMultiplier) external; + function addCollateralToken(address token, uint16 liquidationThreshold) external; + function rampLiquidationThreshold( + address token, + uint16 liquidationThresholdFinal, + uint40 rampStart, + uint24 rampDuration + ) external; + function forbidToken(address token) external; + function allowToken(address token) external; + function setExpirationDate(uint40 newExpirationDate) external; + function pause() external; + function unpause() external; +} diff --git a/contracts/interfaces/factories/ICreditEmergencyConfigureActions.sol b/contracts/interfaces/factories/ICreditEmergencyConfigureActions.sol new file mode 100644 index 0000000..b581e91 --- /dev/null +++ b/contracts/interfaces/factories/ICreditEmergencyConfigureActions.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +interface ICreditEmergencyConfigureActions { + function forbidAdapter(address adapter) external; + function forbidToken(address token) external; + function forbidBorrowing() external; + function pause() external; +} diff --git a/contracts/interfaces/factories/ICreditFactory.sol b/contracts/interfaces/factories/ICreditFactory.sol index 47ff0e1..d53ea7a 100644 --- a/contracts/interfaces/factories/ICreditFactory.sol +++ b/contracts/interfaces/factories/ICreditFactory.sol @@ -9,10 +9,13 @@ import {IFactory} from "./IFactory.sol"; interface ICreditFactory is IFactory { function deployCreditSuite(address pool, bytes calldata encodedParams) external returns (DeployResult memory); - function computeCreditManagerAddress(address marketConfigurator, address pool, bytes calldata encodedParams) - external - view - returns (address); + function computeCreditManagerAddress( + address marketConfigurator, + address pool, + address underlying, + address priceOracle, + bytes calldata encodedParams + ) external view returns (address); // ------------ // // CREDIT HOOKS // diff --git a/contracts/interfaces/factories/IPoolConfigureActions.sol b/contracts/interfaces/factories/IPoolConfigureActions.sol new file mode 100644 index 0000000..ef7d10c --- /dev/null +++ b/contracts/interfaces/factories/IPoolConfigureActions.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +interface IPoolConfigureActions { + function setTotalDebtLimit(uint256 limit) external; + function setCreditManagerDebtLimit(address creditManager, uint256 limit) external; + function setTokenLimit(address token, uint96 limit) external; + function setTokenQuotaIncreaseFee(address token, uint16 fee) external; + function pause() external; + function unpause() external; +} diff --git a/contracts/interfaces/factories/IPoolEmergencyConfigureActions.sol b/contracts/interfaces/factories/IPoolEmergencyConfigureActions.sol new file mode 100644 index 0000000..dcb7208 --- /dev/null +++ b/contracts/interfaces/factories/IPoolEmergencyConfigureActions.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +interface IPoolEmergencyConfigureActions { + function setCreditManagerDebtLimitToZero(address creditManager) external; + function setTokenLimitToZero(address token) external; + function pause() external; +} diff --git a/contracts/interfaces/factories/IPriceOracleConfigureActions.sol b/contracts/interfaces/factories/IPriceOracleConfigureActions.sol new file mode 100644 index 0000000..8696789 --- /dev/null +++ b/contracts/interfaces/factories/IPriceOracleConfigureActions.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +interface IPriceOracleConfigureActions { + function setPriceFeed(address token, address priceFeed) external; + function setReservePriceFeed(address token, address priceFeed) external; +} diff --git a/contracts/interfaces/factories/IPriceOracleEmergencyConfigureActions.sol b/contracts/interfaces/factories/IPriceOracleEmergencyConfigureActions.sol new file mode 100644 index 0000000..d96f237 --- /dev/null +++ b/contracts/interfaces/factories/IPriceOracleEmergencyConfigureActions.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +interface IPriceOracleEmergencyConfigureActions { + function setPriceFeed(address token, address priceFeed) external; +} diff --git a/contracts/interfaces/factories/IPriceOracleFactory.sol b/contracts/interfaces/factories/IPriceOracleFactory.sol index 8b2c6da..0975851 100644 --- a/contracts/interfaces/factories/IPriceOracleFactory.sol +++ b/contracts/interfaces/factories/IPriceOracleFactory.sol @@ -8,4 +8,6 @@ import {IMarketFactory} from "./IMarketFactory.sol"; interface IPriceOracleFactory is IMarketFactory { function deployPriceOracle(address pool) external returns (DeployResult memory); + + function computePriceOracleAddress(address marketConfigurator, address pool) external view returns (address); } diff --git a/contracts/libraries/ContractLiterals.sol b/contracts/libraries/ContractLiterals.sol index e0c60b9..df0dcb1 100644 --- a/contracts/libraries/ContractLiterals.sol +++ b/contracts/libraries/ContractLiterals.sol @@ -5,79 +5,63 @@ pragma solidity ^0.8.23; uint256 constant NO_VERSION_CONTROL = 0; +// Contract types and prefixes +bytes32 constant AP_ACCOUNT_FACTORY_DEFAULT = "ACCOUNT_FACTORY::DEFAULT"; bytes32 constant AP_ACL = "ACL"; -bytes32 constant AP_CONTRACTS_REGISTER = "CONTRACTS_REGISTER"; -bytes32 constant AP_GOVERNOR = "GOVERNOR"; -bytes32 constant AP_TREASURY_SPLITTER = "TREASURY_SPLITTER"; - bytes32 constant AP_ADDRESS_PROVIDER = "ADDRESS_PROVIDER"; +bytes32 constant AP_BOT_LIST = "BOT_LIST"; +bytes32 constant AP_BYTECODE_REPOSITORY = "BYTECODE_REPOSITORY"; +bytes32 constant AP_CONTRACTS_REGISTER = "CONTRACTS_REGISTER"; +bytes32 constant AP_CREDIT_CONFIGURATOR = "CREDIT_CONFIGURATOR"; +bytes32 constant AP_CREDIT_FACADE = "CREDIT_FACADE"; +bytes32 constant AP_CREDIT_FACTORY = "CREDIT_FACTORY"; +bytes32 constant AP_CREDIT_MANAGER = "CREDIT_MANAGER"; bytes32 constant AP_CROSS_CHAIN_GOVERNANCE = "CROSS_CHAIN_GOVERNANCE"; +bytes32 constant AP_CROSS_CHAIN_GOVERNANCE_PROXY = "CROSS_CHAIN_GOVERNANCE_PROXY"; bytes32 constant AP_CROSS_CHAIN_MULTISIG = "CROSS_CHAIN_MULTISIG"; +bytes32 constant AP_GEAR_STAKING = "GEAR_STAKING"; +bytes32 constant AP_GEAR_TOKEN = "GEAR_TOKEN"; +bytes32 constant AP_GOVERNOR = "GOVERNOR"; bytes32 constant AP_INSTANCE_MANAGER = "INSTANCE_MANAGER"; - -// PROXIES bytes32 constant AP_INSTANCE_MANAGER_PROXY = "INSTANCE_MANAGER_PROXY"; -bytes32 constant AP_TREASURY_PROXY = "TREASURY_PROXY"; -bytes32 constant AP_CROSS_CHAIN_GOVERNANCE_PROXY = "CROSS_CHAIN_GOVERNANCE_PROXY"; - -bytes32 constant AP_POOL = "POOL"; -bytes32 constant AP_POOL_QUOTA_KEEPER = "POOL_QUOTA_KEEPER"; -bytes32 constant AP_INTEREST_RATE_MODEL_LINEAR = "IRM::LINEAR"; bytes32 constant AP_INTEREST_RATE_MODEL_DEFAULT = "IRM::DEFAULT"; -bytes32 constant AP_RATE_KEEPER_TUMBLER = "RATE_KEEPER::TUMBLER"; -bytes32 constant AP_RATE_KEEPER_GAUGE = "RATE_KEEPER::GAUGE"; +bytes32 constant AP_INTEREST_RATE_MODEL_FACTORY = "INTEREST_RATE_MODEL_FACTORY"; +bytes32 constant AP_INTEREST_RATE_MODEL_LINEAR = "IRM::LINEAR"; +bytes32 constant AP_LOSS_POLICY_ALIASED = "LOSS_POLICY::ALIASED"; bytes32 constant AP_LOSS_POLICY_DEFAULT = "LOSS_POLICY::DEFAULT"; -bytes32 constant AP_ACCOUNT_FACTORY_DEFAULT = "ACCOUNT_FACTORY::DEFAULT"; - -bytes32 constant AP_CREDIT_MANAGER = "CREDIT_MANAGER"; -bytes32 constant AP_CREDIT_FACADE = "CREDIT_FACADE"; -bytes32 constant AP_CREDIT_CONFIGURATOR = "CREDIT_CONFIGURATOR"; - -bytes32 constant AP_PRICE_ORACLE = "PRICE_ORACLE"; - -bytes32 constant AP_TREASURY = "TREASURY"; -bytes32 constant AP_GEAR_TOKEN = "GEAR_TOKEN"; -bytes32 constant AP_WETH_TOKEN = "WETH_TOKEN"; -bytes32 constant AP_ROUTER = "ROUTER"; -bytes32 constant AP_BOT_LIST = "BOT_LIST"; -bytes32 constant AP_GEAR_STAKING = "GEAR_STAKING"; -bytes32 constant AP_ZAPPER_REGISTER = "ZAPPER_REGISTER"; - -bytes32 constant AP_INFLATION_ATTACK_BLOCKER = "INFLATION_ATTACK_BLOCKER"; -bytes32 constant AP_ZERO_PRICE_FEED = "ZERO_PRICE_FEED"; -bytes32 constant AP_DEGEN_DISTRIBUTOR = "DEGEN_DISTRIBUTOR"; -bytes32 constant AP_MULTI_PAUSE = "MULTI_PAUSE"; - -bytes32 constant AP_BYTECODE_REPOSITORY = "BYTECODE_REPOSITORY"; -bytes32 constant AP_PRICE_FEED_STORE = "PRICE_FEED_STORE"; - -bytes32 constant AP_DEGEN_NFT = "DEGEN_NFT"; +bytes32 constant AP_LOSS_POLICY_FACTORY = "LOSS_POLICY_FACTORY"; bytes32 constant AP_MARKET_CONFIGURATOR = "MARKET_CONFIGURATOR"; -bytes32 constant AP_MARKET_CONFIGURATOR_LEGACY = "MARKET_CONFIGURATOR_LEGACY"; - +bytes32 constant AP_MARKET_CONFIGURATOR_FACTORY = "MARKET_CONFIGURATOR_FACTORY"; +bytes32 constant AP_MARKET_CONFIGURATOR_LEGACY = "MARKET_CONFIGURATOR::LEGACY"; +bytes32 constant AP_POOL = "POOL"; bytes32 constant AP_POOL_FACTORY = "POOL_FACTORY"; -bytes32 constant AP_CREDIT_FACTORY = "CREDIT_FACTORY"; -bytes32 constant AP_INTEREST_RATE_MODEL_FACTORY = "INTEREST_RATE_MODEL_FACTORY"; +bytes32 constant AP_POOL_QUOTA_KEEPER = "POOL_QUOTA_KEEPER"; +bytes32 constant AP_PRICE_FEED_STORE = "PRICE_FEED_STORE"; +bytes32 constant AP_PRICE_ORACLE = "PRICE_ORACLE"; bytes32 constant AP_PRICE_ORACLE_FACTORY = "PRICE_ORACLE_FACTORY"; bytes32 constant AP_RATE_KEEPER_FACTORY = "RATE_KEEPER_FACTORY"; -bytes32 constant AP_LOSS_POLICY_FACTORY = "LOSS_POLICY_FACTORY"; -bytes32 constant AP_MARKET_CONFIGURATOR_FACTORY = "MARKET_CONFIGURATOR_FACTORY"; +bytes32 constant AP_RATE_KEEPER_GAUGE = "RATE_KEEPER::GAUGE"; +bytes32 constant AP_RATE_KEEPER_TUMBLER = "RATE_KEEPER::TUMBLER"; +bytes32 constant AP_TREASURY = "TREASURY"; +bytes32 constant AP_TREASURY_PROXY = "TREASURY_PROXY"; +bytes32 constant AP_TREASURY_SPLITTER = "TREASURY_SPLITTER"; +bytes32 constant AP_WETH_TOKEN = "WETH_TOKEN"; +bytes32 constant AP_ZERO_PRICE_FEED = "PRICE_FEED::ZERO"; +// Common domains bytes32 constant DOMAIN_ACCOUNT_FACTORY = "ACCOUNT_FACTORY"; -bytes32 constant DOMAIN_POOL = "POOL"; -bytes32 constant DOMAIN_CREDIT_MANAGER = "CREDIT_MANAGER"; bytes32 constant DOMAIN_ADAPTER = "ADAPTER"; +bytes32 constant DOMAIN_BOT = "BOT"; +bytes32 constant DOMAIN_CREDIT_MANAGER = "CREDIT_MANAGER"; bytes32 constant DOMAIN_DEGEN_NFT = "DEGEN_NFT"; +bytes32 constant DOMAIN_IRM = "IRM"; bytes32 constant DOMAIN_LOSS_POLICY = "LOSS_POLICY"; -bytes32 constant DOMAIN_RATE_KEEPER = "RATE_KEEPER"; +bytes32 constant DOMAIN_POOL = "POOL"; bytes32 constant DOMAIN_PRICE_FEED = "PRICE_FEED"; -bytes32 constant DOMAIN_IRM = "IRM"; +bytes32 constant DOMAIN_RATE_KEEPER = "RATE_KEEPER"; bytes32 constant DOMAIN_ZAPPER = "ZAPPER"; -// ----- // -// ROLES // -// ----- // - +// Roles bytes32 constant ROLE_EMERGENCY_LIQUIDATOR = "EMERGENCY_LIQUIDATOR"; bytes32 constant ROLE_PAUSABLE_ADMIN = "PAUSABLE_ADMIN"; bytes32 constant ROLE_UNPAUSABLE_ADMIN = "UNPAUSABLE_ADMIN"; diff --git a/contracts/libraries/Domain.sol b/contracts/libraries/Domain.sol index 47b7252..0df8294 100644 --- a/contracts/libraries/Domain.sol +++ b/contracts/libraries/Domain.sol @@ -3,26 +3,59 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; -import {IAddressProvider} from "../interfaces/IAddressProvider.sol"; -import {AP_INSTANCE_MANAGER} from "./ContractLiterals.sol"; import {LibString} from "@solady/utils/LibString.sol"; library Domain { using LibString for string; using LibString for bytes32; - function extractDomain(string memory str) internal pure returns (string memory) { + uint128 internal constant UNDERSCORE = 1 << 95; + + function getContractType(bytes32 domain, bytes32 postfix) internal pure returns (bytes32) { + if (postfix == 0) return domain; + return string.concat(domain.fromSmallString(), "::", postfix.fromSmallString()).toSmallString(); + } + + function extractDomain(bytes32 contractType) internal pure returns (bytes32) { + string memory str = contractType.fromSmallString(); uint256 separatorIndex = str.indexOf("::"); - // If no separator found, treat the whole name as domain - if (separatorIndex == LibString.NOT_FOUND) { - return str; - } + // If no separator found, treat the whole type as domain + if (separatorIndex == LibString.NOT_FOUND) return str.toSmallString(); - return str.slice(0, separatorIndex); + return str.slice(0, separatorIndex).toSmallString(); } - function extractDomain(bytes32 contractType) internal pure returns (bytes32) { - return extractDomain(contractType.fromSmallString()).toSmallString(); + function extractPostfix(bytes32 contractType) internal pure returns (bytes32) { + string memory str = contractType.fromSmallString(); + uint256 separatorIndex = str.indexOf("::"); + + // if no separator found, return empty postfix + if (separatorIndex == LibString.NOT_FOUND) return bytes32(0); + + return str.slice(separatorIndex + 2).toSmallString(); + } + + function isValidContractType(bytes32 contractType) internal pure returns (bool) { + bytes32 domain = extractDomain(contractType); + if (!isValidDomain(domain)) return false; + + bytes32 postfix = extractPostfix(contractType); + if (!isValidPostfix(postfix)) return false; + + // avoid the "DOMAIN::" case + return contractType == getContractType(domain, postfix); + } + + function isValidDomain(bytes32 domain) internal pure returns (bool) { + return domain != 0 && _isValidString(domain.fromSmallString()); + } + + function isValidPostfix(bytes32 postfix) internal pure returns (bool) { + return _isValidString(postfix.fromSmallString()); + } + + function _isValidString(string memory str) internal pure returns (bool) { + return str.is7BitASCII(LibString.ALPHANUMERIC_7_BIT_ASCII | UNDERSCORE); } } diff --git a/contracts/libraries/NestedPriceFeeds.sol b/contracts/libraries/NestedPriceFeeds.sol index 44c7f2d..41926ee 100644 --- a/contracts/libraries/NestedPriceFeeds.sol +++ b/contracts/libraries/NestedPriceFeeds.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.23; import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol"; +import {OptionalCall} from "@gearbox-protocol/core-v3/contracts/libraries/OptionalCall.sol"; interface NestedPriceFeedWithSingleUnderlying is IPriceFeed { function priceFeed() external view returns (address); @@ -21,6 +22,8 @@ interface NestedPriceFeedWithMultipleUnderlyings is IPriceFeed { } library NestedPriceFeeds { + using OptionalCall for address; + uint256 constant MAX_UNDERLYING_PRICE_FEEDS = 8; enum NestingType { @@ -39,13 +42,11 @@ library NestedPriceFeeds { } function getNestingType(IPriceFeed priceFeed) internal view returns (NestingType) { - try NestedPriceFeedWithSingleUnderlying(address(priceFeed)).priceFeed() returns (address) { - return NestingType.SINGLE_UNDERLYING; - } catch {} + (bool success,) = address(priceFeed).staticCallOptionalSafe(abi.encodeWithSignature("priceFeed()"), 10000); + if (success) return NestingType.SINGLE_UNDERLYING; - try NestedPriceFeedWithMultipleUnderlyings(address(priceFeed)).priceFeed0() returns (address) { - return NestingType.MULTIPLE_UNDERLYING; - } catch {} + (success,) = address(priceFeed).staticCallOptionalSafe(abi.encodeWithSignature("priceFeed0()"), 10000); + if (success) return NestingType.MULTIPLE_UNDERLYING; return NestingType.NO_NESTING; } @@ -99,7 +100,8 @@ library NestedPriceFeeds { } else if (index == 7) { selector = priceFeed.priceFeed7.selector; } - (bool success, bytes memory result) = address(priceFeed).staticcall(abi.encodePacked(selector)); + (bool success, bytes memory result) = + address(priceFeed).staticCallOptionalSafe(abi.encodePacked(selector), 10000); if (!success || result.length == 0) return address(0); return abi.decode(result, (address)); } diff --git a/contracts/market/ACL.sol b/contracts/market/ACL.sol index 5218f60..9357d41 100644 --- a/contracts/market/ACL.sol +++ b/contracts/market/ACL.sol @@ -3,14 +3,14 @@ // (c) Gearbox Holdings, 2024 pragma solidity ^0.8.23; -import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IACL} from "../interfaces/IACL.sol"; import {AP_ACL} from "../libraries/ContractLiterals.sol"; +import {ImmutableOwnableTrait} from "../traits/ImmutableOwnableTrait.sol"; /// @title Access control list -contract ACL is IACL, Ownable2Step { +contract ACL is IACL, ImmutableOwnableTrait { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; @@ -27,19 +27,17 @@ contract ACL is IACL, Ownable2Step { mapping(bytes32 role => EnumerableSet.AddressSet) internal _roleHolders; /// @notice Constructor - /// @param owner_ Initial owner - constructor(address owner_) { - _transferOwnership(owner_); - } + /// @param owner_ Owner of the ACL + constructor(address owner_) ImmutableOwnableTrait(owner_) {} /// @notice Returns configurator function getConfigurator() external view override returns (address) { - return owner(); + return owner; } /// @notice Whether `account` is configurator function isConfigurator(address account) external view override returns (bool) { - return account == owner(); + return account == owner; } /// @notice Returns the list of all existing roles diff --git a/contracts/market/Governor.sol b/contracts/market/Governor.sol index 2cff710..6bf0714 100644 --- a/contracts/market/Governor.sol +++ b/contracts/market/Governor.sol @@ -215,6 +215,11 @@ contract Governor is Ownable2Step, IGovernor { } } + /// @dev Forbids renouncing ownership + function renounceOwnership() public pure override { + revert CannotRenounceOwnershipException(); + } + // --------- // // INTERNALS // // --------- // diff --git a/contracts/market/MarketConfigurator.sol b/contracts/market/MarketConfigurator.sol index d56b2a3..a887127 100644 --- a/contracts/market/MarketConfigurator.sol +++ b/contracts/market/MarketConfigurator.sol @@ -3,6 +3,8 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; +import {SafeERC20} from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {LibString} from "@solady/utils/LibString.sol"; @@ -51,56 +53,81 @@ import {Domain} from "../libraries/Domain.sol"; import {DeployerTrait} from "../traits/DeployerTrait.sol"; /// @title Market configurator +/// @notice Allows risk curator to deploy and configure market and credit suites contract MarketConfigurator is DeployerTrait, IMarketConfigurator { using Address for address; using EnumerableSet for EnumerableSet.AddressSet; using LibString for string; using LibString for bytes32; + using SafeERC20 for IERC20; // --------------- // // STATE VARIABLES // // --------------- // + /// @notice Admin address address public immutable override admin; + + /// @notice Emergency admin address address public override emergencyAdmin; + + /// @dev Risk curator name as small string bytes32 internal immutable _curatorName; + /// @notice ACL contract address address public immutable override acl; + + /// @notice Contracts register contract address address public immutable override contractsRegister; + + /// @notice Treasury address address public immutable override treasury; + /// @dev Mapping from `domain` to set of periphery contracts mapping(bytes32 domain => EnumerableSet.AddressSet) internal _peripheryContracts; + /// @dev Mapping from `pool` to market factories mapping(address pool => MarketFactories) internal _marketFactories; + + /// @dev Mapping from `creditManager` to credit factory mapping(address creditManager => address) internal _creditFactories; + /// @dev Mapping from `target` to factory authorized to configure it mapping(address target => address) internal _authorizedFactories; + + /// @dev Mapping from `factory` and market/credit `suite` to set of contracts in this suite + /// that factory is authorized to configure mapping(address factory => mapping(address suite => EnumerableSet.AddressSet)) internal _factoryTargets; // --------- // // MODIFIERS // // --------- // + /// @dev Reverts if caller is not the contract itself modifier onlySelf() { _ensureCallerIsSelf(); _; } + /// @dev Reverts if caller is not the admin modifier onlyAdmin() { _ensureCallerIsAdmin(); _; } + /// @dev Reverts if caller is not the emergency admin modifier onlyEmergencyAdmin() { _ensureCallerIsEmergencyAdmin(); _; } + /// @dev Reverts if `pool` is not registered modifier onlyRegisteredMarket(address pool) { _ensureRegisteredMarket(pool); _; } + /// @dev Reverts if `creditManager` is not registered modifier onlyRegisteredCreditSuite(address creditManager) { _ensureRegisteredCreditSuite(creditManager); _; @@ -110,6 +137,17 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // CONSTRUCTOR // // ----------- // + /// @notice Constructor + /// @param addressProvider_ Address provider contract address + /// @param admin_ Address to set as market configurator's admin or governor's owner, depending on `deployGovernor_` + /// @param emergencyAdmin_ Address to set as emergency admin + /// @param adminFeeTreasury_ Address to set as the second admin of the fee splitter, with the first one being the + /// DAO treasury. If `address(0)`, the splitter is not deployed and all fees will be sent to the treasury. + /// @param curatorName_ Risk curator name + /// @param deployGovernor_ If true, a governor contract is deployed and set as market configurator's admin. + /// `admin_` is set as its owner, queue and execution admin, while `emergencyAdmin_` is set as veto admin. + /// Otherwise, `admin_` is set as the admin of the market configurator. + /// @dev Market configurator is granted pausable and unpausable admin roles in ACL constructor( address addressProvider_, address admin_, @@ -188,22 +226,30 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // ROLES MANAGEMENT // // ---------------- // + /// @notice Sets `newEmergencyAdmin` as emergency admin + /// @dev Reverts if caller is not the admin function setEmergencyAdmin(address newEmergencyAdmin) external override onlyAdmin { if (newEmergencyAdmin == emergencyAdmin) return; emergencyAdmin = newEmergencyAdmin; emit SetEmergencyAdmin(newEmergencyAdmin); } + /// @notice Grants role `role` to account `account` + /// @dev Reverts if caller is not the admin function grantRole(bytes32 role, address account) external override onlyAdmin { _grantRole(role, account); emit GrantRole(role, account); } + /// @notice Revokes role `role` from account `account` + /// @dev Reverts if caller is not the admin function revokeRole(bytes32 role, address account) external override onlyAdmin { _revokeRole(role, account); emit RevokeRole(role, account); } + /// @notice Emergency revokes role `role` from account `account` + /// @dev Reverts if caller is not the emergency admin function emergencyRevokeRole(bytes32 role, address account) external override onlyEmergencyAdmin { _revokeRole(role, account); emit EmergencyRevokeRole(role, account); @@ -213,6 +259,11 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // MARKET MANAGEMENT // // ----------------- // + /// @notice Returns the address of a pool that would be created with given parameters + /// @param minorVersion Minor version to use for deployment + /// @param underlying Market's underlying token + /// @param name Market's name + /// @param symbol Market's symbol function previewCreateMarket(uint256 minorVersion, address underlying, string calldata name, string calldata symbol) external view @@ -223,6 +274,18 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { return IPoolFactory(factories.poolFactory).computePoolAddress(address(this), underlying, name, symbol); } + /// @notice Creates and registers new market with given parameters. + /// Executes `onCreateMarket` hook of all market factories. + /// @param minorVersion Minor version to use for deployment + /// @param underlying Market's underlying token + /// @param name Market's name + /// @param symbol Market's symbol + /// @param interestRateModelParams Parameters for interest rate model deployment + /// @param rateKeeperParams Parameters for rate keeper deployment + /// @param lossPolicyParams Parameters for loss policy deployment + /// @param underlyingPriceFeed Price feed for underlying token + /// @return pool Address of the created pool + /// @dev Reverts if caller is not the admin function createMarket( uint256 minorVersion, address underlying, @@ -234,7 +297,12 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { address underlyingPriceFeed ) external override onlyAdmin returns (address pool) { MarketFactories memory factories = _getLatestMarketFactories(minorVersion); + + // NOTE: some implementations of pool factory might need underlying to mint dead shares + IERC20(underlying).forceApprove(factories.poolFactory, type(uint256).max); pool = _deployPool(factories.poolFactory, underlying, name, symbol); + IERC20(underlying).forceApprove(factories.poolFactory, 0); + address priceOracle = _deployPriceOracle(factories.priceOracleFactory, pool); address interestRateModel = _deployInterestRateModel(factories.interestRateModelFactory, pool, interestRateModelParams); @@ -253,12 +321,20 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit CreateMarket(pool, priceOracle, interestRateModel, rateKeeper, lossPolicy, factories); } + /// @notice Shuts down market for `pool`. + /// Executes `onShutdownMarket` hook of all market factories. + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function shutdownMarket(address pool) external override onlyAdmin onlyRegisteredMarket(pool) { _executeMarketHooks(pool, abi.encodeCall(IMarketFactory.onShutdownMarket, (pool))); IContractsRegister(contractsRegister).shutdownMarket(pool); emit ShutdownMarket(pool); } + /// @notice Adds token `token` with price feed `priceFeed` to market for `pool`. + /// Executes `onAddToken` hook of all market factories. + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function addToken(address pool, address token, address priceFeed) external override @@ -269,11 +345,17 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit AddToken(pool, token); } + /// @notice Configures `pool` by executing `configure` hook of market's pool factory + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function configurePool(address pool, bytes calldata data) external override onlyAdmin onlyRegisteredMarket(pool) { _configure(_marketFactories[pool].poolFactory, pool, data); emit ConfigurePool(pool, data); } + /// @notice Configures `pool` by executing `emergencyConfigure` hook of market's pool factory + /// @dev Reverts if caller is not the emergency admin + /// @dev Reverts if pool is not registered function emergencyConfigurePool(address pool, bytes calldata data) external override @@ -284,6 +366,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit EmergencyConfigurePool(pool, data); } + /// @dev Deploys pool via factory and executes installation hooks function _deployPool(address factory, address underlying, string calldata name, string calldata symbol) internal returns (address) @@ -297,6 +380,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // CREDIT SUITE MANAGEMENT // // ----------------------- // + /// @notice Returns the address of a credit manager that would be created in existing market + /// @param minorVersion Minor version to use for deployment + /// @param pool Pool to create credit suite for + /// @param encodedParams Encoded parameters for credit suite deployment function previewCreateCreditSuite(uint256 minorVersion, address pool, bytes calldata encodedParams) external view @@ -304,9 +391,49 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { returns (address) { address factory = _getLatestCreditFactory(minorVersion); - return ICreditFactory(factory).computeCreditManagerAddress(address(this), pool, encodedParams); + return ICreditFactory(factory).computeCreditManagerAddress( + address(this), + pool, + IPoolV3(pool).asset(), + IContractsRegister(contractsRegister).getPriceOracle(pool), + encodedParams + ); + } + + /// @notice Returns the address of a credit manager that would be created in a new market + /// @param marketMinorVersion Minor version to use for market deployment + /// @param creditSuiteMinorVersion Minor version to use for credit manager deployment + /// @param underlying Market's underlying token + /// @param name Market's name + /// @param symbol Market's symbol + /// @param encodedParams Encoded parameters for credit manager deployment + function previewCreateCreditSuite( + uint256 marketMinorVersion, + uint256 creditSuiteMinorVersion, + address underlying, + string calldata name, + string calldata symbol, + bytes calldata encodedParams + ) external view override returns (address) { + MarketFactories memory factories = _getLatestMarketFactories(marketMinorVersion); + address pool = IPoolFactory(factories.poolFactory).computePoolAddress(address(this), underlying, name, symbol); + address priceOracle = + IPriceOracleFactory(factories.priceOracleFactory).computePriceOracleAddress(address(this), pool); + + address factory = _getLatestCreditFactory(creditSuiteMinorVersion); + return ICreditFactory(factory).computeCreditManagerAddress( + address(this), pool, underlying, priceOracle, encodedParams + ); } + /// @notice Creates and registers new credit suite in a market for `pool`. + /// Executes `onCreateCreditSuite` hook of all market factories. + /// @param minorVersion Minor version to use for deployment + /// @param pool Pool to create credit suite for + /// @param encodedParams Encoded parameters for credit suite deployment + /// @return creditManager Address of the created credit manager + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function createCreditSuite(uint256 minorVersion, address pool, bytes calldata encodedParams) external override @@ -323,6 +450,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit CreateCreditSuite(creditManager, factory); } + /// @notice Shuts down credit suite for `creditManager`. + /// Executes `onShutdownCreditSuite` hook of all market factories. + /// @dev Reverts if caller is not the admin + /// @dev Reverts if credit suite is not registered function shutdownCreditSuite(address creditManager) external override @@ -335,6 +466,9 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit ShutdownCreditSuite(creditManager); } + /// @notice Configures credit suite for `creditManager` by executing `configure` hook of its factory + /// @dev Reverts if caller is not the admin + /// @dev Reverts if credit suite is not registered function configureCreditSuite(address creditManager, bytes calldata data) external override @@ -345,6 +479,9 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit ConfigureCreditSuite(creditManager, data); } + /// @notice Configures credit suite for `creditManager` by executing `emergencyConfigure` hook of its factory + /// @dev Reverts if caller is not the emergency admin + /// @dev Reverts if credit manager is not registered function emergencyConfigureCreditSuite(address creditManager, bytes calldata data) external override @@ -355,6 +492,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit EmergencyConfigureCreditSuite(creditManager, data); } + /// @dev Deploys credit suite via factory and executes installation hooks function _deployCreditSuite(address factory, address pool, bytes calldata encodedParams) internal returns (address) @@ -368,6 +506,11 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // PRICE ORACLE MANAGEMENT // // ----------------------- // + /// @notice Updates price oracle in market for `pool`. + /// Executes `onUpdatePriceOracle` hook of all market and credit factories. + /// @param pool Pool to update price oracle for + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function updatePriceOracle(address pool) external override @@ -395,6 +538,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit UpdatePriceOracle(pool, priceOracle); } + /// @notice Configures price oracle in market for `pool` by executing + /// `configure` hook of market's price oracle factory + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function configurePriceOracle(address pool, bytes calldata data) external override @@ -405,6 +552,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit ConfigurePriceOracle(pool, data); } + /// @notice Emergency configures price oracle in market for `pool` by executing + /// `emergencyConfigure` hook of market's price oracle factory + /// @dev Reverts if caller is not the emergency admin + /// @dev Reverts if pool is not registered function emergencyConfigurePriceOracle(address pool, bytes calldata data) external override @@ -415,6 +566,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit EmergencyConfigurePriceOracle(pool, data); } + /// @dev Deploys price oracle via factory and executes installation hooks function _deployPriceOracle(address factory, address pool) internal returns (address) { DeployResult memory deployResult = IPriceOracleFactory(factory).deployPriceOracle(pool); _executeHook(factory, deployResult.onInstallOps); @@ -425,6 +577,12 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // IRM MANAGEMENT // // -------------- // + /// @notice Updates interest rate model in market for `pool`. + /// Executes `onUpdateInterestRateModel` hook of all market factories. + /// @param pool Pool to update interest rate model for + /// @param params Parameters for interest rate model deployment + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function updateInterestRateModel(address pool, DeployParams calldata params) external override @@ -442,6 +600,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit UpdateInterestRateModel(pool, interestRateModel); } + /// @notice Configures interest rate model in market for `pool` by executing + /// `configure` hook of market's interest rate model factory + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function configureInterestRateModel(address pool, bytes calldata data) external override @@ -452,6 +614,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit ConfigureInterestRateModel(pool, data); } + /// @notice Emergency configures interest rate model in market for `pool` by executing + /// `emergencyConfigure` hook of market's interest rate model factory + /// @dev Reverts if caller is not the emergency admin + /// @dev Reverts if pool is not registered function emergencyConfigureInterestRateModel(address pool, bytes calldata data) external override @@ -462,6 +628,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit EmergencyConfigureInterestRateModel(pool, data); } + /// @dev Deploys interest rate model via factory and executes installation hooks function _deployInterestRateModel(address factory, address pool, DeployParams calldata params) internal returns (address) @@ -475,6 +642,12 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // RATE KEEPER MANAGEMENT // // ---------------------- // + /// @notice Updates rate keeper in market for `pool`. + /// Executes `onUpdateRateKeeper` hook of all market factories. + /// @param pool Pool to update rate keeper for + /// @param params Parameters for rate keeper deployment + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function updateRateKeeper(address pool, DeployParams calldata params) external override @@ -489,6 +662,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit UpdateRateKeeper(pool, rateKeeper); } + /// @notice Configures rate keeper in market for `pool` by executing + /// `configure` hook of market's rate keeper factory + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function configureRateKeeper(address pool, bytes calldata data) external override @@ -499,6 +676,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit ConfigureRateKeeper(pool, data); } + /// @notice Emergency configures rate keeper in market for `pool` by executing + /// `emergencyConfigure` hook of market's rate keeper factory + /// @dev Reverts if caller is not the emergency admin + /// @dev Reverts if pool is not registered function emergencyConfigureRateKeeper(address pool, bytes calldata data) external override @@ -509,6 +690,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit EmergencyConfigureRateKeeper(pool, data); } + /// @dev Deploys rate keeper via factory and executes installation hooks function _deployRateKeeper(address factory, address pool, DeployParams calldata params) internal returns (address) @@ -522,6 +704,12 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // LOSS POLICY MANAGEMENT // // ---------------------- // + /// @notice Updates loss policy in market for `pool`. + /// Executes `onUpdateLossPolicy` hook of all market and credit factories. + /// @param pool Pool to update loss policy for + /// @param params Parameters for loss policy deployment + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function updateLossPolicy(address pool, DeployParams calldata params) external override @@ -547,6 +735,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit UpdateLossPolicy(pool, lossPolicy); } + /// @notice Configures loss policy in market for `pool` by executing + /// `configure` hook of market's loss policy factory + /// @dev Reverts if caller is not the admin + /// @dev Reverts if pool is not registered function configureLossPolicy(address pool, bytes calldata data) external override @@ -557,6 +749,10 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit ConfigureLossPolicy(pool, data); } + /// @notice Emergency configures loss policy in market for `pool` by executing + /// `emergencyConfigure` hook of market's loss policy factory + /// @dev Reverts if caller is not the emergency admin + /// @dev Reverts if pool is not registered function emergencyConfigureLossPolicy(address pool, bytes calldata data) external override @@ -567,6 +763,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit EmergencyConfigureLossPolicy(pool, data); } + /// @dev Deploys loss policy for a pool via factory and executes installation hooks function _deployLossPolicy(address factory, address pool, DeployParams calldata params) internal returns (address) @@ -580,32 +777,46 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // FACTORIES // // --------- // + /// @notice Returns factories in market for `pool` function getMarketFactories(address pool) external view override returns (MarketFactories memory) { return _marketFactories[pool]; } + /// @notice Returns credit factory of credit suite for `creditManager` function getCreditFactory(address creditManager) external view override returns (address) { return _creditFactories[creditManager]; } + /// @notice Returns factory authorized to configure given `target` function getAuthorizedFactory(address target) external view override returns (address) { return _authorizedFactories[target]; } + /// @notice Returns all targets `factory` is authorized to configure in a given market/credit `suite` function getFactoryTargets(address factory, address suite) external view override returns (address[] memory) { return _factoryTargets[factory][suite].values(); } + /// @notice Authorizes `factory` to configure `target` in a given market/credit `suite` + /// @dev Reverts if caller is not the contract itself + /// @dev Reverts if other factory is already authorized to configure `target` function authorizeFactory(address factory, address suite, address target) external override onlySelf { + if (_authorizedFactories[target] == factory) return; if (_authorizedFactories[target] != address(0)) revert UnauthorizedFactoryException(factory, target); _authorizeFactory(factory, suite, target); } + /// @notice Unauthorizes `factory` to configure `target` in a given market/credit `suite` + /// @dev Reverts if caller is not the contract itself + /// @dev Reverts if `factory` is not authorized to configure `target` function unauthorizeFactory(address factory, address suite, address target) external override onlySelf { + if (_authorizedFactories[target] == address(0)) return; if (_authorizedFactories[target] != factory) revert UnauthorizedFactoryException(factory, target); _unauthorizeFactory(factory, suite, target); } + /// @notice Upgrades pool factory of market for `pool` to latest patch + /// @dev Reverts if caller is not the admin function upgradePoolFactory(address pool) external override onlyAdmin { address oldFactory = _marketFactories[pool].poolFactory; address newFactory = _getLatestPatch(oldFactory); @@ -615,6 +826,8 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit UpgradePoolFactory(pool, newFactory); } + /// @notice Upgrades price oracle factory of market for `pool` to latest patch + /// @dev Reverts if caller is not the admin function upgradePriceOracleFactory(address pool) external override onlyAdmin { address oldFactory = _marketFactories[pool].priceOracleFactory; address newFactory = _getLatestPatch(oldFactory); @@ -624,6 +837,8 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit UpgradePriceOracleFactory(pool, newFactory); } + /// @notice Upgrades interest rate model factory of market for `pool` to latest patch + /// @dev Reverts if caller is not the admin function upgradeInterestRateModelFactory(address pool) external override onlyAdmin { address oldFactory = _marketFactories[pool].interestRateModelFactory; address newFactory = _getLatestPatch(oldFactory); @@ -633,6 +848,8 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit UpgradeInterestRateModelFactory(pool, newFactory); } + /// @notice Upgrades rate keeper factory of market for `pool` to latest patch + /// @dev Reverts if caller is not the admin function upgradeRateKeeperFactory(address pool) external override onlyAdmin { address oldFactory = _marketFactories[pool].rateKeeperFactory; address newFactory = _getLatestPatch(oldFactory); @@ -642,6 +859,8 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit UpgradeRateKeeperFactory(pool, newFactory); } + /// @notice Upgrades loss policy factory of market for `pool` to latest patch + /// @dev Reverts if caller is not the admin function upgradeLossPolicyFactory(address pool) external override onlyAdmin { address oldFactory = _marketFactories[pool].lossPolicyFactory; address newFactory = _getLatestPatch(oldFactory); @@ -651,6 +870,8 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { emit UpgradeLossPolicyFactory(pool, newFactory); } + /// @notice Upgrades credit factory of credit suite for `creditManager` to latest patch + /// @dev Reverts if caller is not the admin function upgradeCreditFactory(address creditManager) external override onlyAdmin { address oldFactory = _creditFactories[creditManager]; address newFactory = _getLatestPatch(oldFactory); @@ -664,16 +885,21 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // PERIPHERY // // --------- // + /// @notice Returns all periphery contracts registered in a given `domain` function getPeripheryContracts(bytes32 domain) external view override returns (address[] memory) { return _peripheryContracts[domain].values(); } + /// @notice Whether `peripheryContract` is registered in `domain` function isPeripheryContract(bytes32 domain, address peripheryContract) external view override returns (bool) { return _peripheryContracts[domain].contains(peripheryContract); } + /// @notice Adds `peripheryContract` to a list of registered periphery contracts in its domain + /// @dev Reverts if caller is not the admin + /// @dev Reverts if contract is not deployed via bytecode repository function addPeripheryContract(address peripheryContract) external override onlyAdmin { - if (IBytecodeRepository(bytecodeRepository).deployedContracts(peripheryContract) == 0) { + if (!IBytecodeRepository(bytecodeRepository).isDeployedFromRepository(peripheryContract)) { revert IncorrectPeripheryContractException(peripheryContract); } bytes32 domain = _getDomain(peripheryContract); @@ -682,6 +908,8 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { } } + /// @notice Removes `peripheryContract` from a list of registered periphery contracts in its domain + /// @dev Reverts if caller is not the admin function removePeripheryContract(address peripheryContract) external override onlyAdmin { bytes32 domain = _getDomain(peripheryContract); if (_peripheryContracts[domain].remove(peripheryContract)) { @@ -689,6 +917,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { } } + /// @dev Returns domain of a `peripheryContract` function _getDomain(address peripheryContract) internal view returns (bytes32) { try IVersion(peripheryContract).contractType() returns (bytes32 type_) { return Domain.extractDomain(type_); @@ -701,50 +930,60 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { // INTERNALS // // --------- // + /// @dev Ensures caller is the contract itself function _ensureCallerIsSelf() internal view { if (msg.sender != address(this)) revert CallerIsNotSelfException(msg.sender); } + /// @dev Ensures caller is the admin function _ensureCallerIsAdmin() internal view { if (msg.sender != admin) revert CallerIsNotAdminException(msg.sender); } + /// @dev Ensures caller is the emergency admin function _ensureCallerIsEmergencyAdmin() internal view { if (msg.sender != emergencyAdmin) revert CallerIsNotEmergencyAdminException(msg.sender); } + /// @dev Ensures pool is registered function _ensureRegisteredMarket(address pool) internal view { if (!IContractsRegister(contractsRegister).isPool(pool)) { revert MarketNotRegisteredException(pool); } } + /// @dev Ensures credit manager is registered function _ensureRegisteredCreditSuite(address creditManager) internal view { if (!IContractsRegister(contractsRegister).isCreditManager(creditManager)) { revert CreditSuiteNotRegisteredException(creditManager); } } + /// @dev Grants role to account in ACL /// @dev `MarketConfiguratorLegacy` performs additional actions, hence the `virtual` modifier function _grantRole(bytes32 role, address account) internal virtual { IACL(acl).grantRole(role, account); } + /// @dev Revokes role from account in ACL /// @dev `MarketConfiguratorLegacy` performs additional actions, hence the `virtual` modifier function _revokeRole(bytes32 role, address account) internal virtual { IACL(acl).revokeRole(role, account); } + /// @dev Registers market in contracts register /// @dev `MarketConfiguratorLegacy` performs additional actions, hence the `virtual` modifier function _registerMarket(address pool, address priceOracle, address lossPolicy) internal virtual { IContractsRegister(contractsRegister).registerMarket(pool, priceOracle, lossPolicy); } + /// @dev Registers credit suite in contracts register /// @dev `MarketConfiguratorLegacy` performs additional actions, hence the `virtual` modifier function _registerCreditSuite(address creditManager) internal virtual { IContractsRegister(contractsRegister).registerCreditSuite(creditManager); } + /// @dev Validates that `factory` is authorized to configure `target` /// @dev `MarketConfiguratorLegacy` performs additional checks, hence the `virtual` modifier function _validateCallTarget(address target, address factory) internal virtual { if (target != address(this) && _authorizedFactories[target] != factory) { @@ -752,17 +991,19 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { } } + /// @dev Returns latest patch in the address provider for given contract type with matching minor version function _getLatestPatch(bytes32 key, uint256 minorVersion) internal view returns (address) { - return _getAddressOrRevert( - key, IAddressProvider(addressProvider).getLatestPatchVersion(key.fromSmallString(), minorVersion) - ); + return _getAddressOrRevert(key, IAddressProvider(addressProvider).getLatestPatchVersion(key, minorVersion)); } + /// @dev Returns latest patch for given `factory` function _getLatestPatch(address factory) internal view returns (address) { return _getLatestPatch(IVersion(factory).contractType(), IVersion(factory).version()); } + /// @dev Returns latest market factories for given `minorVersion` 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), @@ -772,28 +1013,33 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { }); } + /// @dev Returns latest credit factory for given `minorVersion` function _getLatestCreditFactory(uint256 minorVersion) internal view returns (address) { + if (minorVersion / 100 != 3) revert IncorrectMinorVersionException(minorVersion); return _getLatestPatch(AP_CREDIT_FACTORY, minorVersion); } + /// @dev Authorizes `factory` to configure `target` in a market/credit `suite` function _authorizeFactory(address factory, address suite, address target) internal { _authorizedFactories[target] = factory; _factoryTargets[factory][suite].add(target); emit AuthorizeFactory(factory, suite, target); } + /// @dev Unauthorizes `factory` to configure `target` in a market/credit `suite` function _unauthorizeFactory(address factory, address suite, address target) internal { _authorizedFactories[target] = address(0); _factoryTargets[factory][suite].remove(target); emit UnauthorizeFactory(factory, suite, target); } + /// @dev Migrates all targets from old factory to new factory in a market or credit suite function _migrateFactoryTargets(address oldFactory, address newFactory, address suite) internal { - EnumerableSet.AddressSet storage targets = _factoryTargets[oldFactory][suite]; - uint256 numTargets = targets.length(); + address[] memory targets = _factoryTargets[oldFactory][suite].values(); + uint256 numTargets = targets.length; for (uint256 i; i < numTargets; ++i) { - address target = targets.at(i); - targets.remove(target); + address target = targets[i]; + _factoryTargets[oldFactory][suite].remove(target); _factoryTargets[newFactory][suite].add(target); _authorizedFactories[target] = newFactory; emit UnauthorizeFactory(oldFactory, suite, target); @@ -801,6 +1047,7 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { } } + /// @dev Executes calls returned by a hook in all market factories function _executeMarketHooks(address pool, bytes memory data) internal { MarketFactories memory factories = _marketFactories[pool]; _executeHook(factories.poolFactory, data); @@ -810,18 +1057,22 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { _executeHook(factories.lossPolicyFactory, data); } + /// @dev Executes calls returned by factory hook function _executeHook(address factory, bytes memory data) internal { _executeHook(factory, abi.decode(factory.functionCall(data), (Call[]))); } + /// @dev Executes calls returned by factory configuration hook function _configure(address factory, address target, bytes calldata callData) internal { _executeHook(factory, IFactory(factory).configure(target, callData)); } + /// @dev Executes calls returned by factory emergency configuration hook function _emergencyConfigure(address factory, address target, bytes calldata callData) internal { _executeHook(factory, IFactory(factory).emergencyConfigure(target, callData)); } + /// @dev Executes array of calls after validating targets function _executeHook(address factory, Call[] memory calls) internal { uint256 len = calls.length; for (uint256 i; i < len; ++i) { @@ -833,22 +1084,27 @@ contract MarketConfigurator is DeployerTrait, IMarketConfigurator { } } + /// @dev Returns all registered credit managers function _registeredCreditManagers() internal view returns (address[] memory) { return IContractsRegister(contractsRegister).getCreditManagers(); } + /// @dev Returns all registered credit managers for `pool` function _registeredCreditManagers(address pool) internal view returns (address[] memory creditManagers) { return IContractsRegister(contractsRegister).getCreditManagers(pool); } + /// @dev Returns quota keeper of `pool` function _quotaKeeper(address pool) internal view returns (address) { return IPoolV3(pool).poolQuotaKeeper(); } + /// @dev Returns interest rate model if `pool` function _interestRateModel(address pool) internal view returns (address) { return IPoolV3(pool).interestRateModel(); } + /// @dev Returns rate keeper of `quotaKeeper` function _rateKeeper(address quotaKeeper) internal view returns (address) { return IPoolQuotaKeeperV3(quotaKeeper).gauge(); } diff --git a/contracts/market/TimeLock.sol b/contracts/market/TimeLock.sol index 4e3670f..4cfec82 100644 --- a/contracts/market/TimeLock.sol +++ b/contracts/market/TimeLock.sol @@ -60,6 +60,7 @@ contract TimeLock is ITimeLock { onlyAdmin { bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + if (!queuedTransactions[txHash]) return; queuedTransactions[txHash] = false; emit CancelTransaction(txHash, target, value, signature, data, eta); @@ -73,7 +74,7 @@ contract TimeLock is ITimeLock { { bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); if (!queuedTransactions[txHash]) revert TransactionIsNotQueuedException(txHash); - if (block.timestamp > eta) revert TimelockNotSurpassedException(); + if (block.timestamp < eta) revert TimelockNotSurpassedException(); if (block.timestamp > eta + GRACE_PERIOD) revert StaleTransactionException(txHash); queuedTransactions[txHash] = false; diff --git a/contracts/market/legacy/MarketConfiguratorLegacy.sol b/contracts/market/legacy/MarketConfiguratorLegacy.sol index 7f1a6ba..91cf628 100644 --- a/contracts/market/legacy/MarketConfiguratorLegacy.sol +++ b/contracts/market/legacy/MarketConfiguratorLegacy.sol @@ -4,10 +4,9 @@ pragma solidity ^0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.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 {IPoolQuotaKeeperV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolQuotaKeeperV3.sol"; import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; @@ -21,6 +20,7 @@ import {Call, MarketFactories} from "../../interfaces/Types.sol"; import { AP_MARKET_CONFIGURATOR_LEGACY, AP_CROSS_CHAIN_GOVERNANCE_PROXY, + DOMAIN_ZAPPER, NO_VERSION_CONTROL, ROLE_EMERGENCY_LIQUIDATOR, ROLE_PAUSABLE_ADMIN, @@ -51,13 +51,32 @@ interface IContractsRegisterLegacy { function addCreditManager(address creditManager) external; } +interface IZapperRegisterLegacy { + function zappers(address pool) external view returns (address[] memory); +} + +struct PeripheryContract { + bytes32 domain; + address addr; +} + +struct LegacyParams { + address acl; + address contractsRegister; + address gearStaking; + address priceOracle; + address zapperRegister; + address[] pausableAdmins; + address[] unpausableAdmins; + address[] emergencyLiquidators; + PeripheryContract[] peripheryContracts; +} + contract MarketConfiguratorLegacy is MarketConfigurator { using Address for address; + using EnumerableSet for EnumerableSet.AddressSet; - /// @notice Contract version uint256 public constant override version = 3_10; - - /// @notice Contract type bytes32 public constant override contractType = AP_MARKET_CONFIGURATOR_LEGACY; address public immutable crossChainGovernanceProxy; @@ -66,56 +85,60 @@ contract MarketConfiguratorLegacy is MarketConfigurator { address public immutable contractsRegisterLegacy; address public immutable gearStakingLegacy; + error ACLOwnershipNotTransferredException(); error AddressIsNotPausableAdminException(address admin); error AddressIsNotUnpausableAdminException(address admin); error CallerIsNotCrossChainGovernanceProxyException(address caller); error CallsToLegacyContractsAreForbiddenException(); error CollateralTokenIsNotQuotedException(address creditManager, address token); - error CreditManagerIsMisconfiguredException(address creditManager); + error CreditSuiteAlreadyInitializedException(address creditManager); + error CreditSuiteIsNotInitializedException(address creditManager); + error InconsistentPriceOracleException(address creditManager); + error MarketAlreadyInitializedException(address pool); + error MarketIsNotInitializedException(address pool); modifier onlyCrossChainGovernanceProxy() { - if (msg.sender != crossChainGovernanceProxy) revert CallerIsNotCrossChainGovernanceProxyException(msg.sender); + _ensureCallerIsCrossChainGovernanceProxy(); _; } - /// @dev There's no way to validate that `pausableAdmins_` and `unpausableAdmins_` are exhaustive - /// because the legacy ACL contract doesn't provide needed getters, so don't screw up :) constructor( address addressProvider_, address admin_, address emergencyAdmin_, string memory curatorName_, bool deployGovernor_, - address aclLegacy_, - address contractsRegisterLegacy_, - address gearStakingLegacy_, - address[] memory pausableAdmins_, - address[] memory unpausableAdmins_, - address[] memory emergencyLiquidators_ + LegacyParams memory legacyParams_ ) MarketConfigurator(addressProvider_, admin_, emergencyAdmin_, address(0), curatorName_, deployGovernor_) { crossChainGovernanceProxy = _getAddressOrRevert(AP_CROSS_CHAIN_GOVERNANCE_PROXY, NO_VERSION_CONTROL); - aclLegacy = aclLegacy_; - contractsRegisterLegacy = contractsRegisterLegacy_; - gearStakingLegacy = gearStakingLegacy_; + aclLegacy = legacyParams_.acl; + contractsRegisterLegacy = legacyParams_.contractsRegister; + gearStakingLegacy = legacyParams_.gearStaking; - uint256 num = pausableAdmins_.length; + // NOTE: there's no way to validate that `legacyParams_.pausableAdmins` and `legacyParams_.unpausableAdmins` + // are exhaustive because the legacy ACL contract doesn't provide needed getters, so don't screw up :) + uint256 num = legacyParams_.pausableAdmins.length; for (uint256 i; i < num; ++i) { - address admin = pausableAdmins_[i]; - if (!IACLLegacy(aclLegacy).isPausableAdmin(admin)) revert AddressIsNotPausableAdminException(admin); - IACL(acl).grantRole(ROLE_PAUSABLE_ADMIN, admin); - emit GrantRole(ROLE_PAUSABLE_ADMIN, admin); + address pausableAdmin = legacyParams_.pausableAdmins[i]; + if (!IACLLegacy(aclLegacy).isPausableAdmin(pausableAdmin)) { + revert AddressIsNotPausableAdminException(pausableAdmin); + } + IACL(acl).grantRole(ROLE_PAUSABLE_ADMIN, pausableAdmin); + emit GrantRole(ROLE_PAUSABLE_ADMIN, pausableAdmin); } - num = unpausableAdmins_.length; + num = legacyParams_.unpausableAdmins.length; for (uint256 i; i < num; ++i) { - address admin = unpausableAdmins_[i]; - if (!IACLLegacy(aclLegacy).isUnpausableAdmin(admin)) revert AddressIsNotUnpausableAdminException(admin); - IACL(acl).grantRole(ROLE_UNPAUSABLE_ADMIN, admin); - emit GrantRole(ROLE_UNPAUSABLE_ADMIN, admin); + address unpausableAdmin = legacyParams_.unpausableAdmins[i]; + if (!IACLLegacy(aclLegacy).isUnpausableAdmin(unpausableAdmin)) { + revert AddressIsNotUnpausableAdminException(unpausableAdmin); + } + IACL(acl).grantRole(ROLE_UNPAUSABLE_ADMIN, unpausableAdmin); + emit GrantRole(ROLE_UNPAUSABLE_ADMIN, unpausableAdmin); } - num = emergencyLiquidators_.length; + num = legacyParams_.emergencyLiquidators.length; for (uint256 i; i < num; ++i) { - address liquidator = emergencyLiquidators_[i]; + address liquidator = legacyParams_.emergencyLiquidators[i]; IACL(acl).grantRole(ROLE_EMERGENCY_LIQUIDATOR, liquidator); emit GrantRole(ROLE_EMERGENCY_LIQUIDATOR, liquidator); } @@ -131,66 +154,78 @@ contract MarketConfiguratorLegacy is MarketConfigurator { if (numCreditManagers == 0) continue; address quotaKeeper = _quotaKeeper(pool); - address priceOracle = _priceOracle(creditManagers[0]); - address interestRateModel = _interestRateModel(pool); - address rateKeeper = _rateKeeper(quotaKeeper); address lossPolicy = address(new DefaultLossPolicy(acl)); - - _createMarket(pool, quotaKeeper, priceOracle, interestRateModel, rateKeeper, lossPolicy); + IContractsRegister(contractsRegister).registerMarket(pool, legacyParams_.priceOracle, lossPolicy); for (uint256 j; j < numCreditManagers; ++j) { address creditManager = creditManagers[j]; - if (!_isV3Contract(creditManager) || _priceOracle(creditManager) != priceOracle) { - revert CreditManagerIsMisconfiguredException(creditManager); + if (!_isV3Contract(creditManager)) continue; + + if (ICreditManagerV3(creditManager).priceOracle() != legacyParams_.priceOracle) { + revert InconsistentPriceOracleException(creditManager); } uint256 numTokens = ICreditManagerV3(creditManager).collateralTokensCount(); + uint256 quotedTokensMask = ICreditManagerV3(creditManager).quotedTokensMask(); for (uint256 k = 1; k < numTokens; ++k) { - address token = ICreditManagerV3(creditManager).getTokenByMask(1 << k); - if (!IPoolQuotaKeeperV3(quotaKeeper).isQuotedToken(token)) { + uint256 tokenMask = 1 << k; + address token = ICreditManagerV3(creditManager).getTokenByMask(tokenMask); + if (!IPoolQuotaKeeperV3(quotaKeeper).isQuotedToken(token) || quotedTokensMask & tokenMask == 0) { revert CollateralTokenIsNotQuotedException(creditManager, token); } } - _createCreditSuite(creditManager); + IContractsRegister(contractsRegister).registerCreditSuite(creditManager); + } + + address[] memory zappers = IZapperRegisterLegacy(legacyParams_.zapperRegister).zappers(pool); + uint256 numZappers = zappers.length; + for (uint256 j; j < numZappers; ++j) { + _peripheryContracts[DOMAIN_ZAPPER].add(zappers[j]); + emit AddPeripheryContract(DOMAIN_ZAPPER, zappers[j]); } } + + uint256 numPeripheryContracts = legacyParams_.peripheryContracts.length; + for (uint256 i; i < numPeripheryContracts; ++i) { + PeripheryContract memory pc = legacyParams_.peripheryContracts[i]; + _peripheryContracts[pc.domain].add(pc.addr); + emit AddPeripheryContract(pc.domain, pc.addr); + } } - function _createMarket( - address pool, - address quotaKeeper, - address priceOracle, - address interestRateModel, - address rateKeeper, - address lossPolicy - ) internal { - IContractsRegister(contractsRegister).registerMarket(pool, priceOracle, lossPolicy); - MarketFactories memory factories = _getLatestMarketFactories(version); + function initializeMarket(address pool) external { + _ensureRegisteredMarket(pool); + if (_marketFactories[pool].poolFactory != address(0)) revert MarketAlreadyInitializedException(pool); + + MarketFactories memory factories = _getLatestMarketFactories(3_10); _marketFactories[pool] = factories; + address quotaKeeper = _quotaKeeper(pool); + address priceOracle = IContractsRegister(contractsRegister).getPriceOracle(pool); + address interestRateModel = _interestRateModel(pool); + address rateKeeper = _rateKeeper(quotaKeeper); + address lossPolicy = IContractsRegister(contractsRegister).getLossPolicy(pool); + + // NOTE: authorize factories for contracts that might still be used after the migration; legacy price oracle + // is left unauthorized since it's not gonna be used, IRM is unauthorized since it's not configurable _authorizeFactory(factories.poolFactory, pool, pool); _authorizeFactory(factories.poolFactory, pool, quotaKeeper); - _authorizeFactory(factories.priceOracleFactory, pool, priceOracle); - _authorizeFactory(factories.interestRateModelFactory, pool, interestRateModel); _authorizeFactory(factories.rateKeeperFactory, pool, rateKeeper); _authorizeFactory(factories.lossPolicyFactory, pool, lossPolicy); emit CreateMarket(pool, priceOracle, interestRateModel, rateKeeper, lossPolicy, factories); } - function _createCreditSuite(address creditManager) internal { - IContractsRegister(contractsRegister).registerCreditSuite(creditManager); + function initializeCreditSuite(address creditManager) external { + _ensureRegisteredCreditSuite(creditManager); + if (_creditFactories[creditManager] != address(0)) revert CreditSuiteAlreadyInitializedException(creditManager); - address factory = _getLatestCreditFactory(version); + address factory = _getLatestCreditFactory(3_10); _creditFactories[creditManager] = factory; - address creditConfigurator = ICreditManagerV3(creditManager).creditConfigurator(); - _authorizeFactory(factory, creditManager, creditConfigurator); - _authorizeFactory(factory, creditManager, ICreditManagerV3(creditManager).creditFacade()); - address[] memory adapters = ICreditConfiguratorV3(creditConfigurator).allowedAdapters(); - uint256 numAdapters = adapters.length; - for (uint256 k; k < numAdapters; ++k) { - _authorizeFactory(factory, creditManager, adapters[k]); - } + + // NOTE: authorizing credit factory for legacy configurator is required since it's used to update to the new one; + // legacy facade and adapters are left unauthorized since they're not gonna be used after the migration + _authorizeFactory(factory, creditManager, ICreditManagerV3(creditManager).creditConfigurator()); emit CreateCreditSuite(creditManager, factory); } @@ -200,10 +235,28 @@ contract MarketConfiguratorLegacy is MarketConfigurator { // ------------- // function finalizeMigration() external onlyCrossChainGovernanceProxy { + address[] memory pools = IContractsRegister(contractsRegister).getPools(); + uint256 numPools = pools.length; + for (uint256 i; i < numPools; ++i) { + if (_marketFactories[pools[i]].poolFactory == address(0)) { + revert MarketIsNotInitializedException(pools[i]); + } + } + address[] memory creditManagers = IContractsRegister(contractsRegister).getCreditManagers(); + uint256 numCreditManagers = creditManagers.length; + for (uint256 i; i < numCreditManagers; ++i) { + if (_creditFactories[creditManagers[i]] == address(0)) { + revert CreditSuiteIsNotInitializedException(creditManagers[i]); + } + } + // NOTE: on some chains, legacy ACL implements a 2-step ownership transfer - try IACLLegacy(aclLegacy).pendingOwner() { + try IACLLegacy(aclLegacy).pendingOwner() returns (address pendingOwner) { + if (pendingOwner != address(this)) revert ACLOwnershipNotTransferredException(); IACLLegacy(aclLegacy).claimOwnership(); - } catch {} + } catch { + if (IACLLegacy(aclLegacy).owner() != address(this)) revert ACLOwnershipNotTransferredException(); + } IACLLegacy(aclLegacy).addPausableAdmin(address(this)); IACLLegacy(aclLegacy).addUnpausableAdmin(address(this)); @@ -213,10 +266,20 @@ contract MarketConfiguratorLegacy is MarketConfigurator { gearStakingLegacy.functionCall(data); } + function removeLegacyPeripheryContract(bytes32 domain, address peripheryContract) external onlyAdmin { + if (_peripheryContracts[domain].remove(peripheryContract)) { + emit RemovePeripheryContract(domain, peripheryContract); + } + } + // --------- // // INTERNALS // // --------- // + function _ensureCallerIsCrossChainGovernanceProxy() internal view { + if (msg.sender != crossChainGovernanceProxy) revert CallerIsNotCrossChainGovernanceProxyException(msg.sender); + } + function _grantRole(bytes32 role, address account) internal override { super._grantRole(role, account); if (role == ROLE_PAUSABLE_ADMIN) IACLLegacy(aclLegacy).addPausableAdmin(account); @@ -253,8 +316,4 @@ contract MarketConfiguratorLegacy is MarketConfigurator { return false; } } - - function _priceOracle(address creditManager) internal view returns (address) { - return ICreditManagerV3(creditManager).priceOracle(); - } } diff --git a/contracts/test/Governor.unit.t.sol b/contracts/test/Governor.unit.t.sol index 19dc627..d95714e 100644 --- a/contracts/test/Governor.unit.t.sol +++ b/contracts/test/Governor.unit.t.sol @@ -537,6 +537,15 @@ contract GovernorUnitTest is Test { assertTrue(governor.isPermissionlessExecutionAllowed(), "Permissionless execution is forbidden"); } + function test_GOV_20_renounceOwnership_reverts() public { + vm.expectRevert(IGovernor.CannotRenounceOwnershipException.selector); + vm.prank(owner); + governor.renounceOwnership(); + + vm.expectRevert(IGovernor.CannotRenounceOwnershipException.selector); + governor.renounceOwnership(); + } + // ----- // // UTILS // // ----- // diff --git a/contracts/test/configuration/ConfigurationTestHelper.sol b/contracts/test/configuration/ConfigurationTestHelper.sol new file mode 100644 index 0000000..a2b68fb --- /dev/null +++ b/contracts/test/configuration/ConfigurationTestHelper.sol @@ -0,0 +1,214 @@ +// 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 {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"; +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); + _submitBatchAndSign("Activate instance", calls); + + _setupPriceFeedStore(); + + _addMockLossPolicy(); + + 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 _addMockLossPolicy() internal { + CrossChainCall[] memory calls = new CrossChainCall[](1); + + bytes32 bytecodeHash = _uploadByteCodeAndSign(type(MockLossPolicy).creationCode, "LOSS_POLICY::MOCK", 3_10); + + calls[0] = _generateAllowPublicContractCall(bytecodeHash); + + _submitBatchAndSign("Allow public contracts", calls); + } + + function _deployTestPool() internal returns (address) { + IERC20(WETH).transfer(address(marketConfigurator), 1e18); + + address _pool = 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: "MOCK", salt: 0, constructorParams: abi.encode(_pool, addressProvider)}); + + vm.prank(admin); + _pool = 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.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..314dfed --- /dev/null +++ b/contracts/test/configuration/CreditSuiteConfiguration.unit.t.sol @@ -0,0 +1,587 @@ +// 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 {IMarketConfigurator} from "../../interfaces/IMarketConfigurator.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; + address creditFactory; + + function setUp() public override { + super.setUp(); + + target = address(new GeneralMock()); + creditFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_CREDIT_FACTORY, 3_10); + } + + function _uploadCreditConfiguratorPatch() internal { + CrossChainCall[] memory calls = new CrossChainCall[](1); + + bytes32 bytecodeHash = + _uploadByteCodeAndSign(type(MockCreditConfiguratorPatch).creationCode, AP_CREDIT_CONFIGURATOR, 3_11); + + calls[0] = _generateAllowSystemContractCall(bytecodeHash); + + _submitBatchAndSign("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 expectedAdapter = IBytecodeRepository(bytecodeRepository).computeAddress( + "ADAPTER::BALANCER_VAULT", + 3_10, + params.constructorParams, + keccak256(abi.encode(0, address(marketConfigurator))), + 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)) + ); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + 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" + ); + + // Now deploy second adapter with different salt + DeployParams memory params2 = DeployParams({ + postfix: "BALANCER_VAULT", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(address(creditManager), target) + }); + + address expectedAdapter2 = IBytecodeRepository(bytecodeRepository).computeAddress( + "ADAPTER::BALANCER_VAULT", + 3_10, + params2.constructorParams, + keccak256(abi.encode(uint256(1), address(marketConfigurator))), + creditFactory + ); + + // Expect first adapter to be unauthorized + vm.expectCall( + address(marketConfigurator), + abi.encodeCall( + IMarketConfigurator.unauthorizeFactory, (creditFactory, address(creditManager), expectedAdapter) + ) + ); + vm.expectCall( + address(marketConfigurator), + abi.encodeCall( + IMarketConfigurator.authorizeFactory, (creditFactory, address(creditManager), expectedAdapter2) + ) + ); + vm.expectCall( + address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.allowAdapter, (expectedAdapter2)) + ); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.allowAdapter, (params2)) + ); + + // Verify first adapter is forbidden and factory is unauthorized + assertEq( + ICreditManagerV3(creditManager).adapterToContract(expectedAdapter), + address(0), + "First adapter must be forbidden" + ); + assertTrue( + IMarketConfigurator(marketConfigurator).getAuthorizedFactory(expectedAdapter) == address(0), + "Factory must be unauthorized for first adapter" + ); + + // Verify second adapter is allowed and factory is authorized + assertEq( + ICreditManagerV3(creditManager).adapterToContract(expectedAdapter2), + target, + "Second adapter must be allowed" + ); + assertTrue( + IMarketConfigurator(marketConfigurator).getAuthorizedFactory(expectedAdapter2) == creditFactory, + "Factory must be authorized for second adapter" + ); + } + + function test_CS_02_forbidAdapter() public { + DeployParams memory params = DeployParams({ + postfix: "BALANCER_VAULT", + salt: 0, + constructorParams: abi.encode(address(creditManager), target) + }); + + // 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)) + ); + + // 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 { + 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); + + // Compute expected new configurator address + address expectedNewConfigurator = IBytecodeRepository(bytecodeRepository).computeAddress( + "CREDIT_CONFIGURATOR", + 3_11, + abi.encode(address(creditManager)), + bytes32(bytes20(address(marketConfigurator))), + creditFactory + ); + + // 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)) + ); + vm.expectCall(address(expectedNewConfigurator), abi.encodeCall(ICreditConfiguratorV3.makeAllTokensQuoted, ())); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.upgradeCreditConfigurator, ()) + ); + + // 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); + + 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( + "CREDIT_FACADE", + 3_10, + abi.encode( + addressProvider, + address(creditManager), + lossPolicy, + ICreditFacadeV3(oldFacade).botList(), + WETH, + params.degenNFT, + params.expirable + ), + bytes32(bytes20(address(marketConfigurator))), + creditFactory + ); + + // 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)) + ); + + vm.prank(admin); + marketConfigurator.configureCreditSuite( + address(creditManager), abi.encodeCall(ICreditConfigureActions.upgradeCreditFacade, (params)) + ); + + // 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 { + _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"); + + // 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 { + 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/InterestRateModelConfiguration.unit.t.sol b/contracts/test/configuration/InterestRateModelConfiguration.unit.t.sol new file mode 100644 index 0000000..1f49127 --- /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] = _generateAllowPublicContractCall(bytecodeHash); + _submitBatchAndSign("Allow public 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..c38616a --- /dev/null +++ b/contracts/test/configuration/LossPolicyConfiguration.unit.t.sol @@ -0,0 +1,68 @@ +// 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.setAccessMode, (ILossPolicy.AccessMode.Permissioned))); + + vm.prank(admin); + marketConfigurator.configureLossPolicy( + address(pool), abi.encodeCall(ILossPolicy.setAccessMode, (ILossPolicy.AccessMode.Permissioned)) + ); + + assertEq( + uint8(DefaultLossPolicy(_lossPolicy).accessMode()), + uint8(ILossPolicy.AccessMode.Permissioned), + "Access mode must be PERMISSIONED" + ); + + vm.expectCall(_lossPolicy, abi.encodeCall(ILossPolicy.setChecksEnabled, (true))); + + vm.prank(admin); + marketConfigurator.configureLossPolicy(address(pool), abi.encodeCall(ILossPolicy.setChecksEnabled, (true))); + + assertTrue(DefaultLossPolicy(_lossPolicy).checksEnabled(), "Checks must be enabled"); + } + + /// EMERGENCY CONFIGURATION TESTS /// + + function test_LP_02_emergency_configure() public { + vm.expectCall(_lossPolicy, abi.encodeCall(ILossPolicy.setAccessMode, (ILossPolicy.AccessMode.Forbidden))); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigureLossPolicy( + address(pool), abi.encodeCall(ILossPolicy.setAccessMode, (ILossPolicy.AccessMode.Forbidden)) + ); + + assertEq( + uint8(DefaultLossPolicy(_lossPolicy).accessMode()), + uint8(ILossPolicy.AccessMode.Forbidden), + "Access mode must be FORBIDDEN" + ); + + vm.expectCall(_lossPolicy, abi.encodeCall(ILossPolicy.setChecksEnabled, (false))); + + vm.prank(emergencyAdmin); + marketConfigurator.emergencyConfigureLossPolicy( + address(pool), abi.encodeCall(ILossPolicy.setChecksEnabled, (false)) + ); + + assertFalse(DefaultLossPolicy(_lossPolicy).checksEnabled(), "Checks must be disabled"); + } +} diff --git a/contracts/test/configuration/MarketConfigurator.unit.t.sol b/contracts/test/configuration/MarketConfigurator.unit.t.sol new file mode 100644 index 0000000..da6c7d9 --- /dev/null +++ b/contracts/test/configuration/MarketConfigurator.unit.t.sol @@ -0,0 +1,1799 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ConfigurationTestHelper} from "./ConfigurationTestHelper.sol"; +import {MarketConfigurator} from "../../market/MarketConfigurator.sol"; +import {IMarketConfigurator, DeployParams, MarketFactories} 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 {PoolFactory} from "../../factories/PoolFactory.sol"; +import {IMarketFactory} from "../../interfaces/factories/IMarketFactory.sol"; +import {IPriceOracleV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol"; +import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol"; +import {IPoolQuotaKeeperV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolQuotaKeeperV3.sol"; +import {IGaugeV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IGaugeV3.sol"; +import {RateKeeperFactory} from "../../factories/RateKeeperFactory.sol"; +import {IRateKeeper} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IRateKeeper.sol"; +import {IAccountFactory} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IAccountFactory.sol"; +import {CreditManagerParams, CreditFacadeParams} from "../../interfaces/factories/ICreditConfigureActions.sol"; +import {CreditFactory} from "../../factories/CreditFactory.sol"; +import {ICreditConfiguratorV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditConfiguratorV3.sol"; +import {ICreditManagerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol"; +import {IPriceOracleFactory} from "../../interfaces/factories/IPriceOracleFactory.sol"; +import {IPriceFeedStore} from "../../interfaces/IPriceFeedStore.sol"; +import {MockPriceOraclePatch} from "../mocks/MockPriceOraclePatch.sol"; +import {CrossChainCall} from "../helpers/GlobalSetup.sol"; + +import { + AP_ACL, + AP_CONTRACTS_REGISTER, + AP_GOVERNOR, + AP_MARKET_CONFIGURATOR, + AP_TREASURY, + AP_TREASURY_SPLITTER, + AP_BYTECODE_REPOSITORY, + AP_MARKET_CONFIGURATOR_FACTORY, + AP_PRICE_ORACLE_FACTORY, + AP_POOL_FACTORY, + AP_CREDIT_FACTORY, + AP_INTEREST_RATE_MODEL_FACTORY, + AP_RATE_KEEPER_FACTORY, + AP_LOSS_POLICY_FACTORY, + AP_PRICE_ORACLE, + 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"; + + address lossPolicy; + address priceOracle; + address poolFactory; + address creditFactory; + address priceOracleFactory; + address interestRateModelFactory; + address rateKeeperFactory; + address lossPolicyFactory; + address gearStaking; + + function setUp() public override { + super.setUp(); + mcf = IAddressProvider(addressProvider).getAddressOrRevert(AP_MARKET_CONFIGURATOR_FACTORY, NO_VERSION_CONTROL); + poolFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_POOL_FACTORY, 3_10); + creditFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_CREDIT_FACTORY, 3_10); + priceOracleFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_PRICE_ORACLE_FACTORY, 3_10); + interestRateModelFactory = + IAddressProvider(addressProvider).getAddressOrRevert(AP_INTEREST_RATE_MODEL_FACTORY, 3_10); + rateKeeperFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_RATE_KEEPER_FACTORY, 3_10); + lossPolicyFactory = IAddressProvider(addressProvider).getAddressOrRevert(AP_LOSS_POLICY_FACTORY, 3_10); + gearStaking = RateKeeperFactory(rateKeeperFactory).gearStaking(); + vm.mockCall(gearStaking, abi.encodeWithSignature("getCurrentEpoch()"), abi.encode(62)); + priceOracle = IContractsRegister(marketConfigurator.contractsRegister()).getPriceOracle(address(pool)); + lossPolicy = IContractsRegister(marketConfigurator.contractsRegister()).getLossPolicy(address(pool)); + } + + /// @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("isDeployedFromRepository(address)", peripheryContract), + abi.encode(true) + ); + + // 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("isDeployedFromRepository(address)", peripheryContract), + abi.encode(false) + ); + + 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("isDeployedFromRepository(address)", peripheryContract), + abi.encode(true) + ); + 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 no-op authorizing already authorized target + vm.prank(address(marketConfigurator)); + marketConfigurator.authorizeFactory(factory, suite, target); + + // Test authorizing already authorized target by wrong factory + address wrongFactory = makeAddr("WRONG_FACTORY"); + vm.prank(address(marketConfigurator)); + vm.expectRevert( + abi.encodeWithSelector(IMarketConfigurator.UnauthorizedFactoryException.selector, wrongFactory, target) + ); + marketConfigurator.authorizeFactory(wrongFactory, 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 unauthorizing target 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); + + // 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 no-op unauthorizing already unauthorized target + vm.prank(address(marketConfigurator)); + marketConfigurator.unauthorizeFactory(factory, 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("POOL_FACTORY", 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("POOL_FACTORY", 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("CREDIT_FACTORY", 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("CREDIT_FACTORY", 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"); + + 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("PRICE_ORACLE_FACTORY", 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("PRICE_ORACLE_FACTORY", 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("INTEREST_RATE_MODEL_FACTORY", 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("INTEREST_RATE_MODEL_FACTORY", 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("RATE_KEEPER_FACTORY", 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("RATE_KEEPER_FACTORY", 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"); + + 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("LOSS_POLICY_FACTORY", 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("LOSS_POLICY_FACTORY", patchFactory, true); + + vm.prank(admin); + marketConfigurator.upgradeLossPolicyFactory(address(pool)); + + assertEq( + marketConfigurator.getMarketFactories(address(pool)).lossPolicyFactory, + patchFactory, + "Loss policy factory not upgraded" + ); + } + + /// @notice Tests market creation + function test_MC_17_createMarket() public { + IERC20(USDC).transfer(address(marketConfigurator), 1e6); + + // Test that only admin can create markets + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.createMarket( + 3_10, + USDC, + "TEST", + "TEST", + DeployParams({ + postfix: "LINEAR", + salt: bytes32(0), + constructorParams: abi.encode(100, 200, 100, 100, 200, 300, false) + }), + DeployParams({postfix: "TUMBLER", salt: bytes32(0), constructorParams: abi.encode(address(0), 7 days)}), + DeployParams({postfix: "MOCK", salt: bytes32(0), constructorParams: abi.encode(address(0), addressProvider)}), + CHAINLINK_USDC_USD + ); + + // Compute expected addresses + address expectedPool = IBytecodeRepository(bytecodeRepository).computeAddress( + "POOL", + 3_10, + abi.encode( + marketConfigurator.acl(), + marketConfigurator.contractsRegister(), + USDC, + marketConfigurator.treasury(), + PoolFactory(poolFactory).defaultInterestRateModel(), + type(uint256).max, + "TEST", + "TEST" + ), + bytes32(bytes20(address(marketConfigurator))), + poolFactory + ); + + address expectedQuotaKeeper = IBytecodeRepository(bytecodeRepository).computeAddress( + "POOL_QUOTA_KEEPER", + 3_10, + abi.encode(expectedPool), + bytes32(bytes20(address(marketConfigurator))), + poolFactory + ); + + address expectedPriceOracle = IBytecodeRepository(bytecodeRepository).computeAddress( + "PRICE_ORACLE", + 3_10, + abi.encode(marketConfigurator.acl()), + bytes32(bytes20(expectedPool)), + priceOracleFactory + ); + + address expectedIRM = IBytecodeRepository(bytecodeRepository).computeAddress( + "IRM::LINEAR", + 3_10, + abi.encode(100, 200, 100, 100, 200, 300, false), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)), + interestRateModelFactory + ); + + address expectedRateKeeper = IBytecodeRepository(bytecodeRepository).computeAddress( + "RATE_KEEPER::GAUGE", + 3_10, + abi.encode(expectedPool, gearStaking), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)), + rateKeeperFactory + ); + + address expectedLossPolicy = IBytecodeRepository(bytecodeRepository).computeAddress( + "LOSS_POLICY::MOCK", + 3_10, + abi.encode(expectedPool, addressProvider), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)), + lossPolicyFactory + ); + { + bytes memory poolParams = abi.encode( + marketConfigurator.acl(), + marketConfigurator.contractsRegister(), + USDC, + marketConfigurator.treasury(), + PoolFactory(poolFactory).defaultInterestRateModel(), + type(uint256).max, + "TEST", + "TEST" + ); + + address acl = marketConfigurator.acl(); + + // Expect contract deployments + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ("POOL", 3_10, poolParams, bytes32(bytes20(address(marketConfigurator)))) + ) + ); + + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ("POOL_QUOTA_KEEPER", 3_10, abi.encode(expectedPool), bytes32(bytes20(address(marketConfigurator)))) + ) + ); + + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, ("PRICE_ORACLE", 3_10, abi.encode(acl), bytes32(bytes20(expectedPool))) + ) + ); + } + + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "IRM::LINEAR", + 3_10, + abi.encode(100, 200, 100, 100, 200, 300, false), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)) + ) + ) + ); + + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "RATE_KEEPER::GAUGE", + 3_10, + abi.encode(expectedPool, gearStaking), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)) + ) + ) + ); + + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "LOSS_POLICY::MOCK", + 3_10, + abi.encode(expectedPool, addressProvider), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)) + ) + ) + ); + + // Expect factory hooks + vm.expectCall( + poolFactory, + abi.encodeCall( + IMarketFactory.onCreateMarket, + ( + expectedPool, + expectedPriceOracle, + expectedIRM, + expectedRateKeeper, + expectedLossPolicy, + CHAINLINK_USDC_USD + ) + ) + ); + + // Expect hook calls from PoolFactory + vm.expectCall(expectedPool, abi.encodeCall(IPoolV3.setInterestRateModel, (expectedIRM))); + vm.expectCall(expectedQuotaKeeper, abi.encodeCall(IPoolQuotaKeeperV3.setGauge, (expectedRateKeeper))); + + // Expect hook calls from PriceOracleFactory + vm.expectCall( + expectedPriceOracle, abi.encodeCall(IPriceOracleV3.setPriceFeed, (USDC, CHAINLINK_USDC_USD, 1 days)) + ); + + vm.expectCall(expectedRateKeeper, abi.encodeCall(IGaugeV3.setFrozenEpoch, (false))); + + vm.expectCall( + priceOracleFactory, + abi.encodeCall( + IMarketFactory.onCreateMarket, + ( + expectedPool, + expectedPriceOracle, + expectedIRM, + expectedRateKeeper, + expectedLossPolicy, + CHAINLINK_USDC_USD + ) + ) + ); + vm.expectCall( + interestRateModelFactory, + abi.encodeCall( + IMarketFactory.onCreateMarket, + ( + expectedPool, + expectedPriceOracle, + expectedIRM, + expectedRateKeeper, + expectedLossPolicy, + CHAINLINK_USDC_USD + ) + ) + ); + vm.expectCall( + rateKeeperFactory, + abi.encodeCall( + IMarketFactory.onCreateMarket, + ( + expectedPool, + expectedPriceOracle, + expectedIRM, + expectedRateKeeper, + expectedLossPolicy, + CHAINLINK_USDC_USD + ) + ) + ); + vm.expectCall( + lossPolicyFactory, + abi.encodeCall( + IMarketFactory.onCreateMarket, + ( + expectedPool, + expectedPriceOracle, + expectedIRM, + expectedRateKeeper, + expectedLossPolicy, + CHAINLINK_USDC_USD + ) + ) + ); + + // Expect factory authorizations + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(poolFactory, expectedPool, expectedPool); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(poolFactory, expectedPool, expectedQuotaKeeper); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(priceOracleFactory, expectedPool, expectedPriceOracle); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(interestRateModelFactory, expectedPool, expectedIRM); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(rateKeeperFactory, expectedPool, expectedRateKeeper); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(lossPolicyFactory, expectedPool, expectedLossPolicy); + + // Create market + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.CreateMarket( + expectedPool, + expectedPriceOracle, + expectedIRM, + expectedRateKeeper, + expectedLossPolicy, + MarketFactories({ + poolFactory: poolFactory, + priceOracleFactory: priceOracleFactory, + interestRateModelFactory: interestRateModelFactory, + rateKeeperFactory: rateKeeperFactory, + lossPolicyFactory: lossPolicyFactory + }) + ); + address newPool = marketConfigurator.createMarket( + 3_10, + USDC, + "TEST", + "TEST", + DeployParams({ + postfix: "LINEAR", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(100, 200, 100, 100, 200, 300, false) + }), + DeployParams({ + postfix: "GAUGE", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(expectedPool, gearStaking) + }), + DeployParams({ + postfix: "MOCK", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(expectedPool, addressProvider) + }), + CHAINLINK_USDC_USD + ); + + assertEq(newPool, expectedPool, "Incorrect pool address"); + } + + /// @notice Tests market shutdown + function test_MC_18_shutdownMarket() public { + IERC20(USDC).transfer(address(marketConfigurator), 1e6); + + address expectedPool = IBytecodeRepository(bytecodeRepository).computeAddress( + "POOL", + 3_10, + abi.encode( + marketConfigurator.acl(), + marketConfigurator.contractsRegister(), + USDC, + marketConfigurator.treasury(), + PoolFactory(poolFactory).defaultInterestRateModel(), + type(uint256).max, + "TEST", + "TEST" + ), + bytes32(bytes20(address(marketConfigurator))), + poolFactory + ); + + vm.prank(admin); + address newPool = marketConfigurator.createMarket( + 3_10, + USDC, + "TEST", + "TEST", + DeployParams({ + postfix: "LINEAR", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(100, 200, 100, 100, 200, 300, false) + }), + DeployParams({ + postfix: "GAUGE", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(expectedPool, gearStaking) + }), + DeployParams({ + postfix: "MOCK", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(expectedPool, addressProvider) + }), + CHAINLINK_USDC_USD + ); + + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.shutdownMarket(address(newPool)); + + vm.expectCall(poolFactory, abi.encodeCall(IMarketFactory.onShutdownMarket, (address(newPool)))); + + vm.expectCall(priceOracleFactory, abi.encodeCall(IMarketFactory.onShutdownMarket, (address(newPool)))); + + vm.expectCall(interestRateModelFactory, abi.encodeCall(IMarketFactory.onShutdownMarket, (address(newPool)))); + + vm.expectCall(rateKeeperFactory, abi.encodeCall(IMarketFactory.onShutdownMarket, (address(newPool)))); + + vm.expectCall(lossPolicyFactory, abi.encodeCall(IMarketFactory.onShutdownMarket, (address(newPool)))); + + vm.expectCall( + marketConfigurator.contractsRegister(), + abi.encodeCall(IContractsRegister.shutdownMarket, (address(newPool))) + ); + + address expectedRateKeeper = IPoolQuotaKeeperV3(IPoolV3(newPool).poolQuotaKeeper()).gauge(); + + vm.expectCall(newPool, abi.encodeCall(IPoolV3.setTotalDebtLimit, (0))); + vm.expectCall(expectedRateKeeper, abi.encodeCall(IGaugeV3.setFrozenEpoch, (true))); + + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.ShutdownMarket(address(newPool)); + marketConfigurator.shutdownMarket(address(newPool)); + } + + /// @notice Tests adding token to market + function test_MC_19_addToken() public { + // Test that only admin can add tokens + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.addToken(address(pool), USDC, CHAINLINK_USDC_USD); + + address rateKeeper = IPoolQuotaKeeperV3(IPoolV3(pool).poolQuotaKeeper()).gauge(); + + // Expect hook calls from PriceOracleFactory + vm.expectCall(priceOracle, abi.encodeCall(IPriceOracleV3.setPriceFeed, (USDC, CHAINLINK_USDC_USD, 1 days))); + + // Expect hook calls from RateKeeperFactory + vm.expectCall(rateKeeper, abi.encodeCall(IRateKeeper.addToken, (USDC))); + + // Expect factory hooks + vm.expectCall(poolFactory, abi.encodeCall(IMarketFactory.onAddToken, (address(pool), USDC, CHAINLINK_USDC_USD))); + + vm.expectCall( + priceOracleFactory, abi.encodeCall(IMarketFactory.onAddToken, (address(pool), USDC, CHAINLINK_USDC_USD)) + ); + vm.expectCall( + interestRateModelFactory, + abi.encodeCall(IMarketFactory.onAddToken, (address(pool), USDC, CHAINLINK_USDC_USD)) + ); + vm.expectCall( + rateKeeperFactory, abi.encodeCall(IMarketFactory.onAddToken, (address(pool), USDC, CHAINLINK_USDC_USD)) + ); + vm.expectCall( + lossPolicyFactory, abi.encodeCall(IMarketFactory.onAddToken, (address(pool), USDC, CHAINLINK_USDC_USD)) + ); + + // Add token + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AddToken(address(pool), USDC); + marketConfigurator.addToken(address(pool), USDC, CHAINLINK_USDC_USD); + } + + // Expected addresses for credit suite creation + address poolQuotaKeeper; + address expectedAccountFactory; + address expectedCreditManager; + address expectedCreditConfigurator; + address expectedCreditFacade; + address expectedMasterCreditAccount; + address botList; + + /// @notice Tests credit suite deployment + function test_MC_20_deployCreditSuite() public { + botList = CreditFactory(creditFactory).botList(); + lossPolicy = IContractsRegister(marketConfigurator.contractsRegister()).getLossPolicy(address(pool)); + poolQuotaKeeper = IPoolV3(pool).poolQuotaKeeper(); + // Prepare credit suite params + DeployParams memory accountFactoryParams = DeployParams({ + postfix: "DEFAULT", + salt: bytes32(uint256(1)), + 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); + + // Test that only admin can deploy credit suites + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.createCreditSuite(3_10, address(pool), creditSuiteParams); + + expectedAccountFactory = IBytecodeRepository(bytecodeRepository).computeAddress( + "ACCOUNT_FACTORY::DEFAULT", + 3_10, + abi.encode(addressProvider), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)), + creditFactory + ); + + // Compute expected addresses + expectedCreditManager = IBytecodeRepository(bytecodeRepository).computeAddress( + "CREDIT_MANAGER", + 3_10, + abi.encode( + address(pool), + expectedAccountFactory, + priceOracle, + creditManagerParams.maxEnabledTokens, + creditManagerParams.feeInterest, + creditManagerParams.feeLiquidation, + creditManagerParams.liquidationPremium, + creditManagerParams.feeLiquidationExpired, + creditManagerParams.liquidationPremiumExpired, + creditManagerParams.name + ), + bytes32(bytes20(address(marketConfigurator))), + creditFactory + ); + + expectedCreditConfigurator = IBytecodeRepository(bytecodeRepository).computeAddress( + "CREDIT_CONFIGURATOR", + 3_10, + abi.encode(expectedCreditManager), + bytes32(bytes20(address(marketConfigurator))), + creditFactory + ); + + expectedCreditFacade = IBytecodeRepository(bytecodeRepository).computeAddress( + "CREDIT_FACADE", + 3_10, + abi.encode( + addressProvider, + expectedCreditManager, + lossPolicy, + botList, + WETH, + facadeParams.degenNFT, + facadeParams.expirable + ), + bytes32(bytes20(address(marketConfigurator))), + creditFactory + ); + + // Expect contract deployments + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "CREDIT_MANAGER", + 3_10, + abi.encode( + address(pool), + expectedAccountFactory, + priceOracle, + creditManagerParams.maxEnabledTokens, + creditManagerParams.feeInterest, + creditManagerParams.feeLiquidation, + creditManagerParams.liquidationPremium, + creditManagerParams.feeLiquidationExpired, + creditManagerParams.liquidationPremiumExpired, + creditManagerParams.name + ), + bytes32(bytes20(address(marketConfigurator))) + ) + ) + ); + + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "CREDIT_CONFIGURATOR", + 3_10, + abi.encode(expectedCreditManager), + bytes32(bytes20(address(marketConfigurator))) + ) + ) + ); + + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "CREDIT_FACADE", + 3_10, + abi.encode( + addressProvider, + expectedCreditManager, + lossPolicy, + botList, + WETH, + facadeParams.degenNFT, + facadeParams.expirable + ), + bytes32(bytes20(address(marketConfigurator))) + ) + ) + ); + + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "ACCOUNT_FACTORY::DEFAULT", + 3_10, + abi.encode(addressProvider), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)) + ) + ) + ); + + vm.expectCall(expectedAccountFactory, abi.encodeCall(IAccountFactory.addCreditManager, (expectedCreditManager))); + vm.expectCall( + expectedCreditManager, abi.encodeCall(ICreditManagerV3.setCreditConfigurator, (expectedCreditConfigurator)) + ); + + // Expect factory hooks + vm.expectCall(poolFactory, abi.encodeCall(IMarketFactory.onCreateCreditSuite, (expectedCreditManager))); + vm.expectCall(priceOracleFactory, abi.encodeCall(IMarketFactory.onCreateCreditSuite, (expectedCreditManager))); + vm.expectCall( + interestRateModelFactory, abi.encodeCall(IMarketFactory.onCreateCreditSuite, (expectedCreditManager)) + ); + vm.expectCall(rateKeeperFactory, abi.encodeCall(IMarketFactory.onCreateCreditSuite, (expectedCreditManager))); + vm.expectCall(lossPolicyFactory, abi.encodeCall(IMarketFactory.onCreateCreditSuite, (expectedCreditManager))); + + vm.expectCall( + expectedCreditConfigurator, + abi.encodeCall(ICreditConfiguratorV3.setCreditFacade, (expectedCreditFacade, false)) + ); + + vm.expectCall( + expectedCreditConfigurator, + abi.encodeCall( + ICreditConfiguratorV3.setDebtLimits, (creditManagerParams.minDebt, creditManagerParams.maxDebt) + ) + ); + + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.setCreditManagerDebtLimit, (expectedCreditManager, 0))); + + vm.expectCall(poolQuotaKeeper, abi.encodeCall(IPoolQuotaKeeperV3.addCreditManager, (expectedCreditManager))); + + // Expect factory authorizations + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(creditFactory, expectedCreditManager, expectedCreditConfigurator); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(creditFactory, expectedCreditManager, expectedCreditFacade); + + // Deploy credit suite + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.CreateCreditSuite(expectedCreditManager, creditFactory); + address newCreditManager = marketConfigurator.createCreditSuite(3_10, address(pool), creditSuiteParams); + + assertEq(newCreditManager, expectedCreditManager, "Incorrect credit manager address"); + } + + /// @notice Tests credit suite shutdown + function test_MC_21_shutdownCreditSuite() public { + address contractsRegister = marketConfigurator.contractsRegister(); + + // Test that only admin can shutdown credit suites + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.shutdownCreditSuite(address(creditManager)); + + // Test that credit suite with non-zero debt cannot be shutdown + vm.mockCall( + address(pool), abi.encodeCall(IPoolV3.creditManagerBorrowed, (address(creditManager))), abi.encode(1) + ); + vm.prank(admin); + vm.expectRevert( + abi.encodeWithSelector( + PoolFactory.CantShutdownCreditSuiteWithNonZeroDebtException.selector, address(creditManager) + ) + ); + marketConfigurator.shutdownCreditSuite(address(creditManager)); + vm.clearMockedCalls(); + + // Expect factory hooks + vm.expectCall(poolFactory, abi.encodeCall(IMarketFactory.onShutdownCreditSuite, (address(creditManager)))); + vm.expectCall( + priceOracleFactory, abi.encodeCall(IMarketFactory.onShutdownCreditSuite, (address(creditManager))) + ); + vm.expectCall( + interestRateModelFactory, abi.encodeCall(IMarketFactory.onShutdownCreditSuite, (address(creditManager))) + ); + vm.expectCall(rateKeeperFactory, abi.encodeCall(IMarketFactory.onShutdownCreditSuite, (address(creditManager)))); + vm.expectCall(lossPolicyFactory, abi.encodeCall(IMarketFactory.onShutdownCreditSuite, (address(creditManager)))); + + // Expect hook calls from PoolFactory + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.setCreditManagerDebtLimit, (address(creditManager), 0))); + + // Expect call to ContractsRegister + vm.expectCall( + contractsRegister, abi.encodeCall(IContractsRegister.shutdownCreditSuite, (address(creditManager))) + ); + + // Shutdown credit suite + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.ShutdownCreditSuite(address(creditManager)); + marketConfigurator.shutdownCreditSuite(address(creditManager)); + } + + function _uploadPriceOraclePatch() internal { + CrossChainCall[] memory calls = new CrossChainCall[](1); + + bytes32 bytecodeHash = _uploadByteCodeAndSign(type(MockPriceOraclePatch).creationCode, AP_PRICE_ORACLE, 3_11); + + calls[0] = _generateAllowSystemContractCall(bytecodeHash); + + _submitBatchAndSign("Allow system contracts", calls); + } + + /// @notice Tests price oracle update + function test_MC_22_updatePriceOracle() public { + _uploadPriceOraclePatch(); + + // Add USDC to the pool to have multiple tokens with price feeds + _addUSDC(); + + // Test that only admin can update price oracle + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.updatePriceOracle(address(pool)); + + // Deploy new price oracle + address newPriceOracle = IBytecodeRepository(bytecodeRepository).computeAddress( + "PRICE_ORACLE", + 3_11, + abi.encode(marketConfigurator.acl()), + bytes32(bytes20(address(pool))), + priceOracleFactory + ); + + // Expect contract deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ("PRICE_ORACLE", 3_11, abi.encode(marketConfigurator.acl()), bytes32(bytes20(address(pool)))) + ) + ); + + // Expect factory hooks + vm.expectCall( + priceOracleFactory, + abi.encodeCall(IMarketFactory.onUpdatePriceOracle, (address(pool), newPriceOracle, priceOracle)) + ); + vm.expectCall( + creditFactory, + abi.encodeCall(IMarketFactory.onUpdatePriceOracle, (address(creditManager), newPriceOracle, priceOracle)) + ); + + // Expect price feed transfers + vm.expectCall(newPriceOracle, abi.encodeCall(IPriceOracleV3.setPriceFeed, (WETH, CHAINLINK_ETH_USD, 1 days))); + vm.expectCall(newPriceOracle, abi.encodeCall(IPriceOracleV3.setPriceFeed, (USDC, CHAINLINK_USDC_USD, 1 days))); + + // Expect credit manager update + vm.expectCall( + address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.setPriceOracle, (newPriceOracle)) + ); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(priceOracleFactory, address(pool), newPriceOracle); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(priceOracleFactory, address(pool), priceOracle); + + // Update price oracle + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpdatePriceOracle(address(pool), newPriceOracle); + marketConfigurator.updatePriceOracle(address(pool)); + } + + /// @notice Tests interest rate model update + function test_MC_23_updateInterestRateModel() public { + // Test that only admin can update interest rate model + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.updateInterestRateModel( + address(pool), + DeployParams({ + postfix: "LINEAR", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(100, 200, 100, 100, 200, 300, false) + }) + ); + + address oldInterestRateModel = IPoolV3(pool).interestRateModel(); + + // Deploy new interest rate model + address newInterestRateModel = IBytecodeRepository(bytecodeRepository).computeAddress( + "IRM::LINEAR", + 3_10, + abi.encode(100, 200, 100, 100, 200, 300, false), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)), + interestRateModelFactory + ); + + // Expect contract deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "IRM::LINEAR", + 3_10, + abi.encode(100, 200, 100, 100, 200, 300, false), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)) + ) + ) + ); + + // Expect factory hooks + vm.expectCall( + poolFactory, + abi.encodeCall( + IMarketFactory.onUpdateInterestRateModel, (address(pool), newInterestRateModel, oldInterestRateModel) + ) + ); + vm.expectCall( + interestRateModelFactory, + abi.encodeCall( + IMarketFactory.onUpdateInterestRateModel, (address(pool), newInterestRateModel, oldInterestRateModel) + ) + ); + + // Expect hook calls from PoolFactory + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.setInterestRateModel, (newInterestRateModel))); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(interestRateModelFactory, address(pool), newInterestRateModel); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(interestRateModelFactory, address(pool), oldInterestRateModel); + + // Update interest rate model + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpdateInterestRateModel(address(pool), newInterestRateModel); + marketConfigurator.updateInterestRateModel( + address(pool), + DeployParams({ + postfix: "LINEAR", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(100, 200, 100, 100, 200, 300, false) + }) + ); + } + + /// @notice Tests rate keeper update + function test_MC_24_updateRateKeeper() public { + // Test that only admin can update rate keeper + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.updateRateKeeper( + address(pool), + DeployParams({ + postfix: "GAUGE", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(address(pool), gearStaking) + }) + ); + + address quotaKeeper = IPoolV3(pool).poolQuotaKeeper(); + address oldRateKeeper = IPoolQuotaKeeperV3(quotaKeeper).gauge(); + + // Deploy new rate keeper + address newRateKeeper = IBytecodeRepository(bytecodeRepository).computeAddress( + "RATE_KEEPER::GAUGE", + 3_10, + abi.encode(address(pool), gearStaking), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)), + rateKeeperFactory + ); + + // Expect contract deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "RATE_KEEPER::GAUGE", + 3_10, + abi.encode(address(pool), gearStaking), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)) + ) + ) + ); + + // Expect factory hooks + vm.expectCall( + poolFactory, + abi.encodeCall(IMarketFactory.onUpdateRateKeeper, (address(pool), newRateKeeper, oldRateKeeper)) + ); + vm.expectCall( + rateKeeperFactory, + abi.encodeCall(IMarketFactory.onUpdateRateKeeper, (address(pool), newRateKeeper, oldRateKeeper)) + ); + + // Expect hook calls from PoolFactory + vm.expectCall(quotaKeeper, abi.encodeCall(IPoolQuotaKeeperV3.setGauge, (newRateKeeper))); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(rateKeeperFactory, address(pool), newRateKeeper); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(rateKeeperFactory, address(pool), oldRateKeeper); + + // Update rate keeper + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpdateRateKeeper(address(pool), newRateKeeper); + marketConfigurator.updateRateKeeper( + address(pool), + DeployParams({ + postfix: "GAUGE", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(address(pool), gearStaking) + }) + ); + } + + /// @notice Tests loss policy update + function test_MC_25_updateLossPolicy() public { + address contractsRegister = marketConfigurator.contractsRegister(); + + // Test that only admin can update loss policy + vm.expectRevert(abi.encodeWithSelector(IMarketConfigurator.CallerIsNotAdminException.selector, address(this))); + marketConfigurator.updateLossPolicy( + address(pool), + DeployParams({ + postfix: "MOCK", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(address(pool), addressProvider) + }) + ); + + address oldLossPolicy = IContractsRegister(marketConfigurator.contractsRegister()).getLossPolicy(address(pool)); + + // Deploy new loss policy + address newLossPolicy = IBytecodeRepository(bytecodeRepository).computeAddress( + "LOSS_POLICY::MOCK", + 3_10, + abi.encode(address(pool), addressProvider), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)), + lossPolicyFactory + ); + + // Expect contract deployment + vm.expectCall( + bytecodeRepository, + abi.encodeCall( + IBytecodeRepository.deploy, + ( + "LOSS_POLICY::MOCK", + 3_10, + abi.encode(address(pool), addressProvider), + keccak256(abi.encode(bytes32(uint256(1)), marketConfigurator)) + ) + ) + ); + + // Expect factory hooks + vm.expectCall( + poolFactory, + abi.encodeCall(IMarketFactory.onUpdateLossPolicy, (address(pool), newLossPolicy, oldLossPolicy)) + ); + vm.expectCall( + lossPolicyFactory, + abi.encodeCall(IMarketFactory.onUpdateLossPolicy, (address(pool), newLossPolicy, oldLossPolicy)) + ); + vm.expectCall( + creditFactory, + abi.encodeCall(IMarketFactory.onUpdateLossPolicy, (address(creditManager), newLossPolicy, oldLossPolicy)) + ); + + // Expect hook calls from CreditFactory + vm.expectCall(address(creditConfigurator), abi.encodeCall(ICreditConfiguratorV3.setLossPolicy, (newLossPolicy))); + + vm.expectCall( + contractsRegister, abi.encodeCall(IContractsRegister.setLossPolicy, (address(pool), newLossPolicy)) + ); + + // Expect factory authorization changes + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.AuthorizeFactory(lossPolicyFactory, address(pool), newLossPolicy); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UnauthorizeFactory(lossPolicyFactory, address(pool), oldLossPolicy); + + // Update loss policy + vm.prank(admin); + vm.expectEmit(true, true, true, true); + emit IMarketConfigurator.UpdateLossPolicy(address(pool), newLossPolicy); + marketConfigurator.updateLossPolicy( + address(pool), + DeployParams({ + postfix: "MOCK", + salt: bytes32(uint256(1)), + constructorParams: abi.encode(address(pool), addressProvider) + }) + ); + } +} diff --git a/contracts/test/configuration/PoolConfiguration.unit.t.sol b/contracts/test/configuration/PoolConfiguration.unit.t.sol new file mode 100644 index 0000000..9825890 --- /dev/null +++ b/contracts/test/configuration/PoolConfiguration.unit.t.sol @@ -0,0 +1,165 @@ +// 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 {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; + address private _quotaKeeper; + + function setUp() public override { + super.setUp(); + + _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"); + + // 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 { + _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..20f065c --- /dev/null +++ b/contracts/test/configuration/PriceOracleConfiguration.unit.t.sol @@ -0,0 +1,224 @@ +// 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 {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; + 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; + + // 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.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"); + + // 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 { + address token = USDC; + 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.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"); + + // 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 /// + + function test_PO_03_emergency_setPriceFeed() public { + address token = USDC; + 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)), + 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"); + + // 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)) + ); + } +} 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/global/AddressProvider.unit.t.sol b/contracts/test/global/AddressProvider.unit.t.sol new file mode 100644 index 0000000..5ea1c08 --- /dev/null +++ b/contracts/test/global/AddressProvider.unit.t.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {AddressProvider} from "../../instance/AddressProvider.sol"; +import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; +import {AddressProviderEntry} from "../../interfaces/Types.sol"; +import {IImmutableOwnableTrait} from "../../interfaces/base/IImmutableOwnableTrait.sol"; +import {AP_ADDRESS_PROVIDER, NO_VERSION_CONTROL} from "../../libraries/ContractLiterals.sol"; + +contract AddressProviderTest is Test { + AddressProvider public provider; + address public owner; + + function setUp() public { + owner = makeAddr("owner"); + provider = new AddressProvider(owner); + } + + /// @notice Test constructor sets up initial state correctly + function test_U_AP_01_constructor_sets_initial_state() public view { + assertEq(provider.owner(), owner); + assertEq(provider.getKeys().length, 0); + } + + /// @notice Test address setting functionality + function test_U_AP_02_setAddress_works() public { + bytes32 key = "TEST"; + address value = makeAddr("test"); + + // Test it reverts if not owner + address notOwner = makeAddr("notOwner"); + vm.prank(notOwner); + vm.expectRevert(abi.encodeWithSelector(IImmutableOwnableTrait.CallerIsNotOwnerException.selector, notOwner)); + provider.setAddress(key, value, false); + + // Test it reverts if zero address + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(IAddressProvider.ZeroAddressException.selector, key)); + provider.setAddress(key, address(0), false); + + // Test successful address setting without version + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit IAddressProvider.SetAddress(key, NO_VERSION_CONTROL, value); + provider.setAddress(key, value, false); + + assertEq(provider.getAddress(key, NO_VERSION_CONTROL), value); + assertEq(provider.getAddressOrRevert(key, NO_VERSION_CONTROL), value); + + // Test successful address setting with version + address versionedValue = makeAddr("versioned"); + uint256 version = 310; + vm.mockCall(versionedValue, abi.encodeWithSignature("version()"), abi.encode(version)); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit IAddressProvider.SetAddress(key, version, versionedValue); + provider.setAddress(key, versionedValue, true); + + assertEq(provider.getAddress(key, version), versionedValue); + assertEq(provider.getAddressOrRevert(key, version), versionedValue); + + // Test setting same address twice doesn't emit event + vm.recordLogs(); + vm.prank(owner); + provider.setAddress(key, versionedValue, true); + + // Verify no SetAddress event was emitted + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(logs.length, 0); + + // Test it reverts if version is less than 100 + address invalidVersionValue = makeAddr("invalidVersion"); + vm.mockCall(invalidVersionValue, abi.encodeWithSignature("version()"), abi.encode(99)); + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(IAddressProvider.InvalidVersionException.selector, key, 99)); + provider.setAddress(key, invalidVersionValue, true); + } + + /// @notice Test version tracking functionality + function test_U_AP_03_version_tracking_works() public { + bytes32 key = "TEST"; + uint256[] memory versions = new uint256[](6); + versions[0] = 310; // 3.1.0 + versions[1] = 311; // 3.1.1 + versions[2] = 320; // 3.2.0 + versions[3] = 321; // 3.2.1 + versions[4] = 400; // 4.0.0 + versions[5] = 401; // 4.0.1 + + // Add addresses with different versions + for (uint256 i = 0; i < versions.length; i++) { + address value = makeAddr(string(abi.encode(versions[i]))); + vm.mockCall(value, abi.encodeWithSignature("version()"), abi.encode(versions[i])); + vm.prank(owner); + provider.setAddress(key, value, true); + } + + // Test latest version tracking + assertEq(provider.getLatestVersion(key), 401); + + // Test latest minor version tracking + assertEq(provider.getLatestMinorVersion(key, 300), 321); // Latest 3.x.x is 3.2.1 + assertEq(provider.getLatestMinorVersion(key, 400), 401); // Latest 4.x.x is 4.0.1 + + // Test latest patch version tracking + assertEq(provider.getLatestPatchVersion(key, 310), 311); // Latest 3.1.x is 3.1.1 + assertEq(provider.getLatestPatchVersion(key, 320), 321); // Latest 3.2.x is 3.2.1 + assertEq(provider.getLatestPatchVersion(key, 400), 401); // Latest 4.0.x is 4.0.1 + + // Test version not found cases + bytes32 unknownKey = "UNKNOWN"; + vm.expectRevert(abi.encodeWithSelector(IAddressProvider.VersionNotFoundException.selector, unknownKey)); + provider.getLatestVersion(unknownKey); + + vm.expectRevert(abi.encodeWithSelector(IAddressProvider.VersionNotFoundException.selector, key)); + provider.getLatestMinorVersion(key, 500); // No 5.x.x versions + + vm.expectRevert(abi.encodeWithSelector(IAddressProvider.VersionNotFoundException.selector, key)); + provider.getLatestPatchVersion(key, 330); // No 3.3.x versions + + // Test invalid version cases + vm.expectRevert(abi.encodeWithSelector(IAddressProvider.InvalidVersionException.selector, key, 99)); + provider.getLatestMinorVersion(key, 99); // Version < 100 + + vm.expectRevert(abi.encodeWithSelector(IAddressProvider.InvalidVersionException.selector, key, 99)); + provider.getLatestPatchVersion(key, 99); // Version < 100 + } + + /// @notice Test getters functionality + function test_U_AP_04_getters_work() public { + bytes32 key = "TEST"; + uint256 version = 310; + address value = makeAddr("test"); + vm.mockCall(value, abi.encodeWithSignature("version()"), abi.encode(version)); + address noVersionValue = makeAddr("noVersion"); + + // Test getAddress returns zero for non-existent entry + assertEq(provider.getAddress(key, version), address(0)); + + // Test getAddressOrRevert reverts for non-existent entry + vm.expectRevert(abi.encodeWithSelector(IAddressProvider.AddressNotFoundException.selector, key, version)); + provider.getAddressOrRevert(key, version); + + // Add some entries + vm.startPrank(owner); + provider.setAddress(key, value, true); + provider.setAddress(key, noVersionValue, false); + vm.stopPrank(); + + // Test getKeys + bytes32[] memory keys = provider.getKeys(); + assertEq(keys.length, 1); + assertEq(keys[0], key); + + // Test getVersions + uint256[] memory versions = provider.getVersions(key); + assertEq(versions.length, 2); // 310 + NO_VERSION_CONTROL + assertTrue( + (versions[0] == version && versions[1] == NO_VERSION_CONTROL) + || (versions[0] == NO_VERSION_CONTROL && versions[1] == version) + ); + + // Test getAllEntries + AddressProviderEntry[] memory entries = provider.getAllEntries(); + assertEq(entries.length, 2); // 310 + NO_VERSION_CONTROL + + // Find and verify versioned entry + bool foundVersioned = false; + bool foundNoVersion = false; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].ver == version) { + assertEq(entries[i].key, key); + assertEq(entries[i].value, value); + foundVersioned = true; + } else if (entries[i].ver == NO_VERSION_CONTROL) { + assertEq(entries[i].key, key); + assertEq(entries[i].value, noVersionValue); + foundNoVersion = true; + } + } + assertTrue(foundVersioned, "Versioned entry not found"); + assertTrue(foundNoVersion, "Non-versioned entry not found"); + } +} diff --git a/contracts/test/global/BytecodeRepository.unit.t.sol b/contracts/test/global/BytecodeRepository.unit.t.sol index b85747b..0fdd2f6 100644 --- a/contracts/test/global/BytecodeRepository.unit.t.sol +++ b/contracts/test/global/BytecodeRepository.unit.t.sol @@ -4,14 +4,15 @@ pragma solidity ^0.8.23; import {Test} from "forge-std/Test.sol"; import {BytecodeRepository} from "../../global/BytecodeRepository.sol"; import {IBytecodeRepository} from "../../interfaces/IBytecodeRepository.sol"; -import {Bytecode, AuditorSignature} from "../../interfaces/Types.sol"; +import {AuditReport, Bytecode} from "../../interfaces/Types.sol"; import {LibString} from "@solady/utils/LibString.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; import {MockedVersionContract} from "../mocks/MockedVersionContract.sol"; +import {SignatureHelper} from "../helpers/SignatureHelper.sol"; -contract BytecodeRepositoryTest is Test { +contract BytecodeRepositoryTest is Test, SignatureHelper { using LibString for bytes32; using ECDSA for bytes32; @@ -20,9 +21,8 @@ contract BytecodeRepositoryTest is Test { address public auditor; address public author; - uint256 public auditorPK = vm.randomUint(); - - uint256 public authorPK = vm.randomUint(); + uint256 public auditorPK = _generatePrivateKey("AUDITOR"); + uint256 public authorPK = _generatePrivateKey("AUTHOR"); bytes32 private constant _TEST_CONTRACT = "TEST_CONTRACT"; uint256 private constant _TEST_VERSION = 310; @@ -93,7 +93,7 @@ contract BytecodeRepositoryTest is Test { // Verify bytecode was stored assertTrue(repository.isBytecodeUploaded(bytecodeHash)); - Bytecode memory storedBc = repository.bytecodeByHash(bytecodeHash); + Bytecode memory storedBc = repository.getBytecode(bytecodeHash); assertEq(storedBc.contractType, _TEST_CONTRACT); assertEq(storedBc.version, _TEST_VERSION); assertEq(storedBc.author, author); @@ -120,9 +120,6 @@ contract BytecodeRepositoryTest is Test { vm.startPrank(author); repository.uploadBytecode(bc); - - vm.expectRevert(IBytecodeRepository.BytecodeAlreadyExistsException.selector); - repository.uploadBytecode(bc); vm.stopPrank(); } @@ -145,40 +142,46 @@ contract BytecodeRepositoryTest is Test { /// AUDITOR SIGNATURE TESTS - function test_BCR_04_signBytecodeHash_works() public { + function test_BCR_04_submitAuditReport_works() public { // First upload bytecode bytes32 bytecodeHash = _uploadTestBytecode(); // Now sign as auditor string memory reportUrl = "https://audit.report"; bytes32 signatureHash = repository.domainSeparatorV4().toTypedDataHash( - keccak256(abi.encode(repository.AUDITOR_SIGNATURE_TYPEHASH(), bytecodeHash, keccak256(bytes(reportUrl)))) + keccak256( + abi.encode(repository.AUDIT_REPORT_TYPEHASH(), bytecodeHash, auditor, keccak256(bytes(reportUrl))) + ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(auditorPK, signatureHash); bytes memory signature = abi.encodePacked(r, s, v); + AuditReport memory auditReport = AuditReport({auditor: auditor, reportUrl: reportUrl, signature: signature}); + vm.prank(auditor); - repository.signBytecodeHash(bytecodeHash, reportUrl, signature); + repository.submitAuditReport(bytecodeHash, auditReport); // Verify signature was stored - assertTrue(repository.isAuditBytecode(bytecodeHash)); + assertTrue(repository.isBytecodeAudited(bytecodeHash)); - AuditorSignature[] memory sigs = repository.auditorSignaturesByHash(bytecodeHash); - assertEq(sigs.length, 1); - assertEq(sigs[0].auditor, auditor); - assertEq(sigs[0].reportUrl, reportUrl); - assertEq(sigs[0].signature, signature); + AuditReport[] memory reports = repository.getAuditReports(bytecodeHash); + assertEq(reports.length, 1); + assertEq(reports[0].auditor, auditor); + assertEq(reports[0].reportUrl, reportUrl); + assertEq(reports[0].signature, signature); } - function test_BCR_05_signBytecodeHash_reverts_if_not_auditor() public { + function test_BCR_05_submitAuditReport_reverts_if_not_auditor() public { // First upload bytecode bytes32 bytecodeHash = _uploadTestBytecode(); // Now sign as auditor string memory reportUrl = "https://audit.report"; bytes32 signatureHash = repository.domainSeparatorV4().toTypedDataHash( - keccak256(abi.encode(repository.AUDITOR_SIGNATURE_TYPEHASH(), bytecodeHash, keccak256(bytes(reportUrl)))) + keccak256( + abi.encode(repository.AUDIT_REPORT_TYPEHASH(), bytecodeHash, auditor, keccak256(bytes(reportUrl))) + ) ); uint256 notAuditorPK = vm.randomUint(); @@ -187,9 +190,11 @@ contract BytecodeRepositoryTest is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(notAuditorPK, signatureHash); bytes memory signature = abi.encodePacked(r, s, v); + AuditReport memory auditReport = AuditReport({auditor: notAuditor, reportUrl: reportUrl, signature: signature}); + vm.prank(notAuditor); - vm.expectRevert(abi.encodeWithSelector(IBytecodeRepository.SignerIsNotAuditorException.selector, notAuditor)); - repository.signBytecodeHash(bytecodeHash, reportUrl, signature); + vm.expectRevert(abi.encodeWithSelector(IBytecodeRepository.AuditorIsNotApprovedException.selector, notAuditor)); + repository.submitAuditReport(bytecodeHash, auditReport); } /// DEPLOYMENT TESTS @@ -201,14 +206,18 @@ contract BytecodeRepositoryTest is Test { // Now sign as auditor string memory reportUrl = "https://audit.report"; bytes32 signatureHash = repository.domainSeparatorV4().toTypedDataHash( - keccak256(abi.encode(repository.AUDITOR_SIGNATURE_TYPEHASH(), bytecodeHash, keccak256(bytes(reportUrl)))) + keccak256( + abi.encode(repository.AUDIT_REPORT_TYPEHASH(), bytecodeHash, auditor, keccak256(bytes(reportUrl))) + ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(auditorPK, signatureHash); bytes memory signature = abi.encodePacked(r, s, v); + AuditReport memory auditReport = AuditReport({auditor: auditor, reportUrl: reportUrl, signature: signature}); + vm.prank(auditor); - repository.signBytecodeHash(bytecodeHash, reportUrl, signature); + repository.submitAuditReport(bytecodeHash, auditReport); // Mark as system contract to auto-approve vm.prank(owner); @@ -221,7 +230,8 @@ contract BytecodeRepositoryTest is Test { // Verify deployment assertTrue(deployed.code.length > 0); - assertEq(repository.deployedContracts(deployed), bytecodeHash); + assertTrue(repository.isDeployedFromRepository(deployed)); + assertEq(repository.getDeployedContractBytecodeHash(deployed), bytecodeHash); IVersion version = IVersion(deployed); assertEq(version.contractType(), _TEST_CONTRACT); @@ -231,41 +241,7 @@ contract BytecodeRepositoryTest is Test { function test_BCR_07_deploy_reverts_if_not_approved() public { vm.expectRevert( abi.encodeWithSelector( - IBytecodeRepository.BytecodeIsNotApprovedException.selector, _TEST_CONTRACT, _TEST_VERSION - ) - ); - repository.deploy(_TEST_CONTRACT, _TEST_VERSION, "", _TEST_SALT); - } - - function test_BCR_08_deploy_reverts_if_not_audited() public { - // Upload but don't audit - bytes memory bytecode = _getMockBytecode(_TEST_CONTRACT, _TEST_VERSION); - - Bytecode memory bc = Bytecode({ - contractType: _TEST_CONTRACT, - version: _TEST_VERSION, - initCode: bytecode, - author: author, - source: _TEST_SOURCE, - authorSignature: bytes("") - }); - - bytes32 bytecodeHash = repository.computeBytecodeHash(bc); - - (uint8 v, bytes32 r, bytes32 s) = - vm.sign(authorPK, repository.domainSeparatorV4().toTypedDataHash(bytecodeHash)); - bc.authorSignature = abi.encodePacked(r, s, v); - - vm.prank(author); - repository.uploadBytecode(bc); - - // Mark as system contract to auto-approve - vm.prank(owner); - repository.allowSystemContract(bytecodeHash); - - vm.expectRevert( - abi.encodeWithSelector( - IBytecodeRepository.BytecodeIsNotApprovedException.selector, _TEST_CONTRACT, _TEST_VERSION + IBytecodeRepository.BytecodeIsNotAllowedException.selector, _TEST_CONTRACT, _TEST_VERSION ) ); repository.deploy(_TEST_CONTRACT, _TEST_VERSION, "", _TEST_SALT); diff --git a/contracts/test/global/CrossChainMultisig.unit.t.sol b/contracts/test/global/CrossChainMultisig.unit.t.sol index d15ce38..5bd85ba 100644 --- a/contracts/test/global/CrossChainMultisig.unit.t.sol +++ b/contracts/test/global/CrossChainMultisig.unit.t.sol @@ -3,11 +3,12 @@ pragma solidity ^0.8.23; import {Test} from "forge-std/Test.sol"; import {CrossChainMultisigHarness} from "./CrossChainMultisigHarness.sol"; -import {CrossChainCall, SignedProposal} from "../../interfaces/ICrossChainMultisig.sol"; +import {CrossChainCall, SignedBatch, SignedRecoveryModeMessage} from "../../interfaces/ICrossChainMultisig.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {ICrossChainMultisig} from "../../interfaces/ICrossChainMultisig.sol"; import {console} from "forge-std/console.sol"; import {SignatureHelper} from "../helpers/SignatureHelper.sol"; +import {GeneralMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/GeneralMock.sol"; contract CrossChainMultisigTest is Test, SignatureHelper { CrossChainMultisigHarness multisig; @@ -18,7 +19,8 @@ contract CrossChainMultisigTest is Test, SignatureHelper { uint8 constant THRESHOLD = 2; address owner; - bytes32 PROPOSAL_TYPEHASH = keccak256("Proposal(string name,bytes32 proposalHash,bytes32 prevHash)"); + bytes32 COMPACT_BATCH_TYPEHASH = keccak256("CompactBatch(string name,bytes32 batchHash,bytes32 prevHash)"); + bytes32 RECOVERY_MODE_TYPEHASH = keccak256("RecoveryMode(uint256 chainId,bytes32 startingBatchHash)"); function setUp() public { // Setup initial signers @@ -38,26 +40,26 @@ contract CrossChainMultisigTest is Test, SignatureHelper { return ECDSA.toTypedDataHash(domainSeparator, structHash); } - function _signProposal(uint256 privateKey, CrossChainCall[] memory calls, bytes32 prevHash) + function _signBatch(uint256 privateKey, CrossChainCall[] memory calls, bytes32 prevHash) internal view returns (bytes memory) { - bytes32 proposalHash = multisig.hashProposal("test", calls, prevHash); - bytes32 structHash = _getDigest(proposalHash); + bytes32 batchHash = multisig.computeBatchHash("test", calls, prevHash); + bytes32 structHash = _getDigest(batchHash); (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, structHash); return abi.encodePacked(r, s, v); } - function _signProposalHash(uint256 privateKey, bytes32 structHash) internal view returns (bytes memory) { + function _signBatchHash(uint256 privateKey, bytes32 structHash) internal view returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _getDigest(structHash)); return abi.encodePacked(r, s, v); } - /// @dev U:[SM-1]: Initial state is correct - function test_CCG_01_InitialState() public { + /// @notice U:[CCM-1]: Initial state is correct + function test_U_CCM_01_InitialState() public { assertEq(multisig.confirmationThreshold(), THRESHOLD); - assertEq(multisig.lastProposalHash(), bytes32(0)); + assertEq(multisig.lastBatchHash(), bytes32(0)); assertEq(multisig.owner(), owner); // Verify all signers were added @@ -82,62 +84,78 @@ contract CrossChainMultisigTest is Test, SignatureHelper { new CrossChainMultisigHarness(signers, THRESHOLD, owner); } - /// @dev U:[SM-2]: Access modifiers work correctly - function test_CCG_02_AccessModifiers() public { + /// @notice U:[CCM-2]: Access modifiers work correctly + function test_U_CCM_02_AccessModifiers() public { // Test onlyOnMainnet modifier vm.chainId(5); // Set to non-mainnet chain vm.startPrank(owner); CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); vm.expectRevert(ICrossChainMultisig.CantBeExecutedOnCurrentChainException.selector); - multisig.submitProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); vm.expectRevert(ICrossChainMultisig.CantBeExecutedOnCurrentChainException.selector); - multisig.signProposal(bytes32(0), new bytes(65)); + multisig.signBatch(bytes32(0), new bytes(65)); vm.stopPrank(); // Test onlyOnNotMainnet modifier vm.chainId(1); vm.expectRevert(ICrossChainMultisig.CantBeExecutedOnCurrentChainException.selector); - multisig.executeProposal( - SignedProposal({name: "test", calls: calls, prevHash: bytes32(0), signatures: new bytes[](0)}) + multisig.executeBatch( + SignedBatch({name: "test", calls: calls, prevHash: bytes32(0), signatures: new bytes[](0)}) ); // Test onlySelf modifier - vm.expectRevert(ICrossChainMultisig.OnlySelfException.selector); + vm.expectRevert(abi.encodeWithSelector(ICrossChainMultisig.CallerIsNotSelfException.selector, address(this))); multisig.addSigner(address(0x123)); - vm.expectRevert(ICrossChainMultisig.OnlySelfException.selector); + vm.expectRevert(abi.encodeWithSelector(ICrossChainMultisig.CallerIsNotSelfException.selector, address(this))); multisig.removeSigner(signers[0]); - vm.expectRevert(ICrossChainMultisig.OnlySelfException.selector); + vm.expectRevert(abi.encodeWithSelector(ICrossChainMultisig.CallerIsNotSelfException.selector, address(this))); multisig.setConfirmationThreshold(3); + vm.expectRevert(abi.encodeWithSelector(ICrossChainMultisig.CallerIsNotSelfException.selector, address(this))); + multisig.disableRecoveryMode(0); + // Test onlyOwner modifier vm.prank(makeAddr("notOwner")); vm.expectRevert("Ownable: caller is not the owner"); - multisig.submitProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); } - /// @dev U:[SM-3]: Submit proposal works correctly - function test_CCG_03_SubmitProposal() public { - vm.startPrank(owner); + /// @notice U:[CCM-3]: Submit batch works correctly + function test_U_CCM_03_SubmitBatch() public { vm.chainId(1); // Set to mainnet CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); - multisig.submitProposal("test", calls, bytes32(0)); + bytes32 expectedBatchHash = multisig.computeBatchHash("test", calls, bytes32(0)); + + vm.expectEmit(true, true, true, true); + emit ICrossChainMultisig.SubmitBatch(expectedBatchHash); + + vm.prank(owner); + multisig.submitBatch("test", calls, bytes32(0)); + + SignedBatch memory batch = multisig.getBatch(expectedBatchHash); + assertEq(batch.calls.length, 1); + assertEq(batch.prevHash, bytes32(0)); + assertEq(batch.signatures.length, 0); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); - SignedProposal memory proposal = multisig.getSignedProposal(proposalHash); + // submit the same batch again doesn't change calls + vm.prank(owner); + multisig.submitBatch("test", calls, bytes32(0)); - assertEq(proposal.calls.length, 1); - assertEq(proposal.prevHash, bytes32(0)); - assertEq(proposal.signatures.length, 0); + batch = multisig.getBatch(expectedBatchHash); + assertEq(batch.calls.length, 1); + assertEq(batch.prevHash, bytes32(0)); + assertEq(batch.signatures.length, 0); } - function test_CCG_04_RevertOnInvalidPrevHash() public { + /// @notice U:[CCM-4]: Reverts when submitting batch with invalid prev hash + function test_U_CCM_04_revert_on_invalid_prev_hash() public { vm.chainId(1); vm.startPrank(owner); @@ -145,130 +163,113 @@ contract CrossChainMultisigTest is Test, SignatureHelper { calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); vm.expectRevert(ICrossChainMultisig.InvalidPrevHashException.selector); - multisig.submitProposal("test", calls, bytes32(uint256(1))); // Invalid prevHash + multisig.submitBatch("test", calls, bytes32(uint256(1))); // Invalid prevHash } - function test_CCG_05_RevertOnEmptyCalls() public { + /// @notice U:[CCM-5]: Reverts when submitting empty batch + function test_U_CCM_05_revert_on_empty_calls() public { vm.chainId(1); vm.startPrank(owner); CrossChainCall[] memory calls = new CrossChainCall[](0); - vm.expectRevert(ICrossChainMultisig.NoCallsInProposalException.selector); - multisig.submitProposal("test", calls, bytes32(0)); + vm.expectRevert(ICrossChainMultisig.InvalidBatchException.selector); + multisig.submitBatch("test", calls, bytes32(0)); } - /// @dev U:[SM-6]: Sign proposal works correctly with single signature - function test_CCG_06_SignProposal() public { + /// @notice U:[CCM-6]: Sign batch works correctly with single signature + function test_U_CCM_06_SignBatch() public { vm.chainId(1); // Set to mainnet - // Submit proposal + // Submit batch CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); vm.prank(owner); - multisig.submitProposal("test", calls, bytes32(0)); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); console.log(signers[0]); - console.logBytes32(proposalHash); + console.logBytes32(batchHash); bytes32 structHash = - keccak256(abi.encode(PROPOSAL_TYPEHASH, keccak256(bytes("test")), proposalHash, bytes32(0))); + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, bytes32(0))); // Generate EIP-712 signature - bytes memory signature = _signProposalHash(signer0PrivateKey, structHash); + bytes memory signature = _signBatchHash(signer0PrivateKey, structHash); // Sign with first signer - multisig.signProposal(proposalHash, signature); + multisig.signBatch(batchHash, signature); - // Verify proposal state after signing - SignedProposal memory proposal = multisig.getSignedProposal(proposalHash); - assertEq(proposal.signatures.length, 1); - assertEq(proposal.signatures[0], signature); + // Verify batch state after signing + SignedBatch memory batch = multisig.getBatch(batchHash); + assertEq(batch.signatures.length, 1); + assertEq(batch.signatures[0], signature); - // Verify proposal was not executed since threshold not met - assertEq(multisig.lastProposalHash(), bytes32(0)); + // Verify batch was not executed since threshold not met + assertEq(multisig.lastBatchHash(), bytes32(0)); } - /// @dev U:[SM-7]: Sign proposal reverts when signing with invalid signature - function test_CCG_07_SignProposalInvalidSignature() public { + /// @notice U:[CCM-7]: Sign batch reverts when signing with invalid signature + function test_U_CCM_07_SignBatchInvalidSignature() public { vm.chainId(1); - // Submit proposal + // Submit batch CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); vm.prank(owner); - multisig.submitProposal("test", calls, bytes32(0)); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); // Try to sign with invalid signature bytes memory invalidSig = hex"1234"; vm.expectRevert("ECDSA: invalid signature length"); - multisig.signProposal(proposalHash, invalidSig); + multisig.signBatch(batchHash, invalidSig); } - /// @dev U:[SM-8]: Sign proposal reverts when signing non-existent proposal - function test_CCG_08_SignProposalNonExistentProposal() public { + /// @notice U:[CCM-8]: Sign batch reverts when signing non-existent batch + function test_U_CCM_08_SignBatchNonExistentBatch() public { vm.chainId(1); - // Set last proposal hash to 1, to avoid ambiguity that default value of 0 is a valid proposal hash - multisig.setLastProposalHash(bytes32(uint256(1))); + // Set last batch hash to 1, to avoid ambiguity that default value of 0 is a valid batch hash + multisig.setLastBatchHash(bytes32(uint256(1))); bytes32 nonExistentHash = keccak256("non-existent"); - bytes memory signature = _signProposalHash(signer0PrivateKey, nonExistentHash); + bytes memory signature = _signBatchHash(signer0PrivateKey, nonExistentHash); - vm.expectRevert(ICrossChainMultisig.InvalidPrevHashException.selector); - multisig.signProposal(nonExistentHash, signature); + vm.expectRevert( + abi.encodeWithSelector(ICrossChainMultisig.BatchIsNotSubmittedException.selector, nonExistentHash) + ); + multisig.signBatch(nonExistentHash, signature); } - /// @dev U:[SM-9]: Sign proposal reverts when same signer signs twice - function test_CCG_09_SignProposalDuplicateSigner() public { + /// @notice U:[CCM-9]: Sign batch reverts when same signer signs twice + function test_U_CCM_09_SignBatchDuplicateSigner() public { vm.chainId(1); - // Submit proposal + // Submit batch CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); vm.prank(owner); - multisig.submitProposal("test", calls, bytes32(0)); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); bytes32 structHash = - keccak256(abi.encode(PROPOSAL_TYPEHASH, keccak256(bytes("test")), proposalHash, bytes32(0))); + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, bytes32(0))); // Sign first time - bytes memory signature = _signProposalHash(signer0PrivateKey, structHash); - multisig.signProposal(proposalHash, signature); + bytes memory signature = _signBatchHash(signer0PrivateKey, structHash); + multisig.signBatch(batchHash, signature); // Try to sign again with same signer - vm.expectRevert(ICrossChainMultisig.AlreadySignedException.selector); - multisig.signProposal(proposalHash, signature); - } - - /// @dev U:[SM-10]: Sign proposal reverts when non-signer tries to sign - function test_CCG_10_SignProposalNonSigner() public { - vm.chainId(1); - - // Submit proposal - CrossChainCall[] memory calls = new CrossChainCall[](1); - calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); - - vm.prank(owner); - multisig.submitProposal("test", calls, bytes32(0)); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); - - // Try to sign with non-signer private key - uint256 nonSignerKey = 999; - bytes memory signature = _signProposalHash(nonSignerKey, proposalHash); - - vm.expectRevert(ICrossChainMultisig.SignerDoesNotExistException.selector); - multisig.signProposal(proposalHash, signature); + vm.expectRevert(abi.encodeWithSelector(ICrossChainMultisig.DuplicateSignatureException.selector, signers[0])); + multisig.signBatch(batchHash, signature); } - /// @dev U:[SM-11]: Sign and execute proposal works correctly - function test_CCG_11_SignAndExecuteProposal() public { + /// @notice U:[CCM-10]: Sign and execute proposal works correctly + function test_U_CCM_10_SignAndExecuteProposal() public { vm.chainId(1); // Set to mainnet // Submit proposal @@ -280,65 +281,73 @@ contract CrossChainMultisigTest is Test, SignatureHelper { }); vm.prank(owner); - multisig.submitProposal("test", calls, bytes32(0)); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); bytes32 structHash = - keccak256(abi.encode(PROPOSAL_TYPEHASH, keccak256(bytes("test")), proposalHash, bytes32(0))); + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, bytes32(0))); // Sign with first signer - bytes memory sig0 = _signProposalHash(signer0PrivateKey, structHash); - multisig.signProposal(proposalHash, sig0); + bytes memory sig0 = _signBatchHash(signer0PrivateKey, structHash); + multisig.signBatch(batchHash, sig0); // Sign with second signer which should trigger execution // Check events emitted during execution vm.expectEmit(true, true, true, true); - emit ICrossChainMultisig.SignProposal(proposalHash, vm.addr(signer1PrivateKey)); + emit ICrossChainMultisig.SignBatch(batchHash, vm.addr(signer1PrivateKey)); vm.expectEmit(true, true, true, true); - emit ICrossChainMultisig.ExecuteProposal(proposalHash); - bytes memory sig1 = _signProposalHash(signer1PrivateKey, structHash); - multisig.signProposal(proposalHash, sig1); + emit ICrossChainMultisig.ExecuteBatch(batchHash); + bytes memory sig1 = _signBatchHash(signer1PrivateKey, structHash); + multisig.signBatch(batchHash, sig1); - // Verify proposal was executed - assertEq(multisig.lastProposalHash(), proposalHash, "lastProposalHash"); - assertEq(multisig.getExecutedProposalHashes()[0], proposalHash, "executedProposalHashes"); + // Verify batch was executed + assertEq(multisig.lastBatchHash(), batchHash, "lastBatchHash"); + assertEq(multisig.getExecutedBatchHashes()[0], batchHash, "executedBatchHashes"); } - /// @dev U:[SM-12]: _verifyProposal reverts if prevHash doesn't match lastProposalHash - function test_CCG_12_VerifyProposalInvalidPrevHash() public { - CrossChainCall[] memory calls = new CrossChainCall[](1); - calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); + /// @notice U:[CCM-11]: Batch validation works correctly + function test_U_CCM_11_BatchValidation() public { + vm.chainId(5); // Set to non-mainnet chain - bytes32 invalidPrevHash = keccak256("invalid"); - vm.expectRevert(ICrossChainMultisig.InvalidPrevHashException.selector); - multisig.exposed_verifyProposal(calls, invalidPrevHash); - } + // Test empty batch + CrossChainCall[] memory emptyCalls = new CrossChainCall[](0); + vm.expectRevert(ICrossChainMultisig.InvalidBatchException.selector); + multisig.executeBatch( + SignedBatch({name: "test", calls: emptyCalls, prevHash: bytes32(0), signatures: new bytes[](0)}) + ); - /// @dev U:[SM-13]: _verifyProposal reverts if calls array is empty - function test_CCG_13_VerifyProposalEmptyCalls() public { - CrossChainCall[] memory calls = new CrossChainCall[](0); + // Test invalid prev hash + CrossChainCall[] memory calls = new CrossChainCall[](1); + calls[0] = CrossChainCall({chainId: 5, target: makeAddr("target"), callData: hex"1234"}); + vm.expectRevert(ICrossChainMultisig.InvalidPrevHashException.selector); + multisig.executeBatch( + SignedBatch({name: "test", calls: calls, prevHash: bytes32(uint256(1)), signatures: new bytes[](0)}) + ); - vm.expectRevert(ICrossChainMultisig.NoCallsInProposalException.selector); - multisig.exposed_verifyProposal(calls, bytes32(0)); - } + // Test local self-call + calls[0] = CrossChainCall({chainId: 5, target: address(multisig), callData: hex"1234"}); + vm.expectRevert(ICrossChainMultisig.InvalidBatchException.selector); + multisig.executeBatch( + SignedBatch({name: "test", calls: calls, prevHash: bytes32(0), signatures: new bytes[](0)}) + ); - /// @dev U:[SM-14]: _verifyProposal reverts if trying to call self on other chain - function test_CCG_14_VerifyProposalSelfCallOtherChain() public { - CrossChainCall[] memory calls = new CrossChainCall[](1); - // Try to call the multisig contract itself on another chain - calls[0] = CrossChainCall({ - chainId: 5, // Goerli chain ID + // Test disableRecoveryMode not being the only call + CrossChainCall[] memory mixedCalls = new CrossChainCall[](2); + mixedCalls[0] = CrossChainCall({ + chainId: 0, target: address(multisig), - callData: hex"1234" + callData: abi.encodeWithSelector(ICrossChainMultisig.disableRecoveryMode.selector, block.chainid) }); - - vm.expectRevert(ICrossChainMultisig.InconsistentSelfCallOnOtherChainException.selector); - multisig.exposed_verifyProposal(calls, bytes32(0)); + mixedCalls[1] = CrossChainCall({chainId: 5, target: makeAddr("target"), callData: hex"1234"}); + vm.expectRevert(ICrossChainMultisig.InvalidBatchException.selector); + multisig.executeBatch( + SignedBatch({name: "test", calls: mixedCalls, prevHash: bytes32(0), signatures: new bytes[](0)}) + ); } - /// @dev U:[SM-15]: _verifyProposal succeeds with valid calls - function test_CCG_15_VerifyProposalValidCalls() public view { + /// @notice U:[CCM-12]: _verifyBatch succeeds with valid calls + function test_U_CCM_12_VerifyBatchValidCalls() public view { CrossChainCall[] memory calls = new CrossChainCall[](3); // Valid call on same chain @@ -351,121 +360,319 @@ contract CrossChainMultisigTest is Test, SignatureHelper { calls[2] = CrossChainCall({chainId: 0, target: address(0x456), callData: hex"9abc"}); // Should not revert - multisig.exposed_verifyProposal(calls, bytes32(0)); + multisig.exposed_verifyBatch(calls, bytes32(0)); } - /// @dev U:[SM-16]: _verifySignatures returns 0 for empty signatures array - function test_CCG_16_VerifySignaturesEmptyArray() public view { + /// @notice U:[CCM-13]: _verifySignatures returns 0 for empty signatures array + function test_U_CCM_13_VerifySignaturesEmptyArray() public view { bytes[] memory signatures = new bytes[](0); - bytes32 proposalHash = keccak256("test"); + bytes32 batchHash = keccak256("test"); - uint256 validCount = multisig.exposed_verifySignatures(signatures, proposalHash); + uint256 validCount = multisig.exposed_verifySignatures(signatures, batchHash); assertEq(validCount, 0); } - /// @dev U:[SM-17]: _verifySignatures correctly counts valid signatures - function test_CCG_17_VerifySignaturesValidSignatures() public { + /// @notice U:[CCM-14]: _verifySignatures correctly counts valid signatures + function test_U_CCM_14_VerifySignaturesValidSignatures() public { vm.chainId(1); // Set to mainnet CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); vm.prank(owner); - multisig.submitProposal("test", calls, bytes32(0)); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); bytes32 structHash = - keccak256(abi.encode(PROPOSAL_TYPEHASH, keccak256(bytes("test")), proposalHash, bytes32(0))); + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, bytes32(0))); // Create array with 2 valid signatures bytes[] memory signatures = new bytes[](2); - signatures[0] = _signProposalHash(signer0PrivateKey, structHash); - signatures[1] = _signProposalHash(signer1PrivateKey, structHash); + signatures[0] = _signBatchHash(signer0PrivateKey, structHash); + signatures[1] = _signBatchHash(signer1PrivateKey, structHash); uint256 validCount = multisig.exposed_verifySignatures(signatures, _getDigest(structHash)); assertEq(validCount, 2); } - /// @dev U:[SM-18]: _verifySignatures ignores invalid signatures - function test_CCG_18_VerifySignaturesInvalidSignatures() public { + /// @notice U:[CCM-15]: _verifySignatures ignores invalid signatures + function test_U_CCM_15_VerifySignaturesInvalidSignatures() public { vm.chainId(1); // Set to mainnet CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); vm.prank(owner); - multisig.submitProposal("test", calls, bytes32(0)); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); bytes32 structHash = - keccak256(abi.encode(PROPOSAL_TYPEHASH, keccak256(bytes("test")), proposalHash, bytes32(0))); + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, bytes32(0))); // Create array with 1 valid and 1 invalid signature bytes[] memory signatures = new bytes[](2); - signatures[0] = _signProposalHash(signer0PrivateKey, structHash); + signatures[0] = _signBatchHash(signer0PrivateKey, structHash); // Create invalid signature by signing different hash - signatures[1] = _signProposalHash(signer1PrivateKey, keccak256("wrong hash")); + signatures[1] = _signBatchHash(signer1PrivateKey, keccak256("wrong hash")); uint256 validCount = multisig.exposed_verifySignatures(signatures, _getDigest(structHash)); assertEq(validCount, 1); } - /// @dev U:[SM-19]: _verifySignatures reverts with AlreadySignedException on duplicate signatures from same signer - function test_CCG_19_VerifySignaturesDuplicateSigner() public { + /// @notice U:[CCM-16]: _verifySignatures reverts with DuplicateSignatureException on duplicate signatures from same signer + function test_U_CCM_16_VerifySignaturesDuplicateSigner() public { vm.chainId(1); // Set to mainnet CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); vm.prank(owner); - multisig.submitProposal("test", calls, bytes32(0)); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); bytes32 structHash = - keccak256(abi.encode(PROPOSAL_TYPEHASH, keccak256(bytes("test")), proposalHash, bytes32(0))); + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, bytes32(0))); // Create array with 2 signatures from same signer bytes[] memory signatures = new bytes[](2); - signatures[0] = _signProposalHash(signer0PrivateKey, structHash); - signatures[1] = _signProposalHash(signer0PrivateKey, structHash); + signatures[0] = _signBatchHash(signer0PrivateKey, structHash); + signatures[1] = _signBatchHash(signer0PrivateKey, structHash); bytes32 digest = _getDigest(structHash); - vm.expectRevert(ICrossChainMultisig.AlreadySignedException.selector); + vm.expectRevert(abi.encodeWithSelector(ICrossChainMultisig.DuplicateSignatureException.selector, signers[0])); multisig.exposed_verifySignatures(signatures, digest); } - /// @dev U:[SM-20]: _verifySignatures ignores signatures from non-signers - function test_CCG_20_VerifySignaturesNonSigner() public { + /// @notice U:[CCM-17]: _verifySignatures ignores signatures from non-signers + function test_U_CCM_17_VerifySignaturesNonSigner() public { vm.chainId(1); // Set to mainnet CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = CrossChainCall({chainId: 1, target: address(0x123), callData: hex"1234"}); vm.prank(owner); - multisig.submitProposal("test", calls, bytes32(0)); - bytes32 proposalHash = multisig.hashProposal("test", calls, bytes32(0)); + multisig.submitBatch("test", calls, bytes32(0)); + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); bytes32 structHash = - keccak256(abi.encode(PROPOSAL_TYPEHASH, keccak256(bytes("test")), proposalHash, bytes32(0))); + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, bytes32(0))); // Create random non-signer private key uint256 nonSignerKey = uint256(keccak256("non-signer")); bytes[] memory signatures = new bytes[](2); - signatures[0] = _signProposalHash(signer0PrivateKey, structHash); // Valid signer - signatures[1] = _signProposalHash(nonSignerKey, structHash); // Non-signer + signatures[0] = _signBatchHash(signer0PrivateKey, structHash); // Valid signer + signatures[1] = _signBatchHash(nonSignerKey, structHash); // Non-signer uint256 validCount = multisig.exposed_verifySignatures(signatures, _getDigest(structHash)); assertEq(validCount, 1); } - /// @dev U:[SM-21]: _verifySignatures reverts on malformed signatures - function test_CCG_21_VerifySignaturesMalformedSignature() public { - bytes32 proposalHash = keccak256("test"); + /// @notice U:[CCM-18]: _verifySignatures reverts on malformed signatures + function test_U_CCM_18_VerifySignaturesMalformedSignature() public { + bytes32 batchHash = keccak256("test"); bytes[] memory signatures = new bytes[](2); - signatures[0] = _signProposalHash(signer0PrivateKey, proposalHash); // Valid signature + signatures[0] = _signBatchHash(signer0PrivateKey, batchHash); // Valid signature signatures[1] = hex"1234"; // Malformed signature vm.expectRevert("ECDSA: invalid signature length"); - multisig.exposed_verifySignatures(signatures, proposalHash); + multisig.exposed_verifySignatures(signatures, batchHash); + } + + /// @notice U:[CCM-19]: Cannot reduce signers below threshold + function test_U_CCM_19_CannotReduceSignersBelowThreshold() public { + vm.prank(address(multisig)); + multisig.removeSigner(signers[0]); + + vm.expectRevert(ICrossChainMultisig.InvalidConfirmationThresholdException.selector); + vm.prank(address(multisig)); + multisig.removeSigner(signers[1]); + } + + /// @notice U:[CCM-20]: Recovery mode can be enabled with valid signatures + function test_U_CCM_20_EnableRecoveryMode() public { + vm.chainId(5); // Set to non-mainnet chain + + address target = makeAddr("target"); + vm.etch(target, hex"ff"); // Put some code there to make call possible + vm.mockCall(target, hex"1234", ""); + + // First execute a batch to have non-zero lastBatchHash + CrossChainCall[] memory calls = new CrossChainCall[](1); + calls[0] = CrossChainCall({chainId: 5, target: target, callData: hex"1234"}); + + SignedBatch memory batch = + SignedBatch({name: "test", calls: calls, prevHash: bytes32(0), signatures: new bytes[](2)}); + + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); + bytes32 structHash = + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, bytes32(0))); + + batch.signatures[0] = _signBatchHash(signer0PrivateKey, structHash); + batch.signatures[1] = _signBatchHash(signer1PrivateKey, structHash); + + multisig.executeBatch(batch); + + // Now enable recovery mode + bytes32 recoveryHash = keccak256(abi.encode(RECOVERY_MODE_TYPEHASH, block.chainid, batchHash)); + + bytes[] memory signatures = new bytes[](2); + signatures[0] = _signBatchHash(signer0PrivateKey, recoveryHash); + signatures[1] = _signBatchHash(signer1PrivateKey, recoveryHash); + + vm.expectEmit(true, false, false, false); + emit ICrossChainMultisig.EnableRecoveryMode(batchHash); + + multisig.enableRecoveryMode( + SignedRecoveryModeMessage({chainId: block.chainid, startingBatchHash: batchHash, signatures: signatures}) + ); + + assertTrue(multisig.isRecoveryModeEnabled()); + } + + /// @notice U:[CCM-21]: Recovery mode skips non-self calls during execution + function test_U_CCM_21_RecoveryModeSkipsExecution() public { + vm.chainId(5); // Set to non-mainnet chain + + // Setup and enable recovery mode first + bytes32 lastHash = _setupRecoveryMode(); + assertTrue(multisig.isRecoveryModeEnabled()); + + address target = makeAddr("target"); + vm.mockCallRevert(target, hex"1234", ""); // This call should be skipped + + // Create batch with both self and external calls + CrossChainCall[] memory calls = new CrossChainCall[](2); + calls[0] = CrossChainCall({ + chainId: 5, + target: target, + callData: hex"1234" // This should be skipped + }); + calls[1] = CrossChainCall({ + chainId: 0, + target: address(multisig), + callData: abi.encodeWithSelector(ICrossChainMultisig.setConfirmationThreshold.selector, 3) + }); + + SignedBatch memory batch = + SignedBatch({name: "test", calls: calls, prevHash: lastHash, signatures: new bytes[](2)}); + + bytes32 batchHash = multisig.computeBatchHash("test", calls, lastHash); + bytes32 structHash = + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, lastHash)); + + batch.signatures[0] = _signBatchHash(signer0PrivateKey, structHash); + batch.signatures[1] = _signBatchHash(signer1PrivateKey, structHash); + + // Execute batch and verify only self-call was executed + multisig.executeBatch(batch); + assertEq(multisig.confirmationThreshold(), 3); // Self-call executed + } + + /// @notice U:[CCM-22]: Recovery mode can be disabled through dedicated batch + function test_U_CCM_22_DisableRecoveryMode() public { + vm.chainId(5); // Set to non-mainnet chain + + // Setup and enable recovery mode first + bytes32 lastHash = _setupRecoveryMode(); + assertTrue(multisig.isRecoveryModeEnabled()); + + // Create batch with single disableRecoveryMode call + CrossChainCall[] memory calls = new CrossChainCall[](1); + calls[0] = CrossChainCall({ + chainId: 0, + target: address(multisig), + callData: abi.encodeWithSelector(ICrossChainMultisig.disableRecoveryMode.selector, block.chainid) + }); + + SignedBatch memory batch = + SignedBatch({name: "test", calls: calls, prevHash: lastHash, signatures: new bytes[](2)}); + + bytes32 batchHash = multisig.computeBatchHash("test", calls, lastHash); + bytes32 structHash = + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, lastHash)); + + batch.signatures[0] = _signBatchHash(signer0PrivateKey, structHash); + batch.signatures[1] = _signBatchHash(signer1PrivateKey, structHash); + + vm.expectEmit(false, false, false, true); + emit ICrossChainMultisig.DisableRecoveryMode(); + + multisig.executeBatch(batch); + assertFalse(multisig.isRecoveryModeEnabled()); + } + + /// @notice U:[CCM-23]: Recovery mode cannot be enabled on mainnet + function test_U_CCM_23_EnableRecoveryModeOnMainnet() public { + vm.chainId(1); // Set to mainnet + + bytes32 batchHash = bytes32(uint256(1)); + bytes32 recoveryHash = keccak256(abi.encode(RECOVERY_MODE_TYPEHASH, block.chainid, batchHash)); + + bytes[] memory signatures = new bytes[](2); + signatures[0] = _signBatchHash(signer0PrivateKey, recoveryHash); + signatures[1] = _signBatchHash(signer1PrivateKey, recoveryHash); + + vm.expectRevert(ICrossChainMultisig.CantBeExecutedOnCurrentChainException.selector); + multisig.enableRecoveryMode( + SignedRecoveryModeMessage({chainId: block.chainid, startingBatchHash: batchHash, signatures: signatures}) + ); + } + + /// @notice U:[CCM-24]: Recovery mode message must match current chain + function test_U_CCM_24_EnableRecoveryModeWrongChain() public { + vm.chainId(5); // Set to non-mainnet chain + + bytes32 batchHash = bytes32(uint256(1)); + // Create recovery message for wrong chain + bytes32 recoveryHash = keccak256(abi.encode(RECOVERY_MODE_TYPEHASH, uint256(137), batchHash)); + + bytes[] memory signatures = new bytes[](2); + signatures[0] = _signBatchHash(signer0PrivateKey, recoveryHash); + signatures[1] = _signBatchHash(signer1PrivateKey, recoveryHash); + + // Should silently return without enabling recovery mode + multisig.enableRecoveryMode( + SignedRecoveryModeMessage({ + chainId: 137, // Different chain + startingBatchHash: batchHash, + signatures: signatures + }) + ); + assertFalse(multisig.isRecoveryModeEnabled()); + } + + /// Helper function to setup recovery mode + function _setupRecoveryMode() internal returns (bytes32) { + address target = makeAddr("target"); + vm.etch(target, hex"ff"); // Put some code there to make call possible + vm.mockCall(target, hex"1234", ""); + + // First execute a batch to have non-zero lastBatchHash + CrossChainCall[] memory calls = new CrossChainCall[](1); + calls[0] = CrossChainCall({chainId: 5, target: target, callData: hex"1234"}); + + SignedBatch memory batch = + SignedBatch({name: "test", calls: calls, prevHash: bytes32(0), signatures: new bytes[](2)}); + + bytes32 batchHash = multisig.computeBatchHash("test", calls, bytes32(0)); + bytes32 structHash = + keccak256(abi.encode(COMPACT_BATCH_TYPEHASH, keccak256(bytes("test")), batchHash, bytes32(0))); + + batch.signatures[0] = _signBatchHash(signer0PrivateKey, structHash); + batch.signatures[1] = _signBatchHash(signer1PrivateKey, structHash); + + multisig.executeBatch(batch); + + // Enable recovery mode + bytes32 recoveryHash = keccak256(abi.encode(RECOVERY_MODE_TYPEHASH, block.chainid, batchHash)); + bytes[] memory signatures = new bytes[](2); + signatures[0] = _signBatchHash(signer0PrivateKey, recoveryHash); + signatures[1] = _signBatchHash(signer1PrivateKey, recoveryHash); + + multisig.enableRecoveryMode( + SignedRecoveryModeMessage({chainId: block.chainid, startingBatchHash: batchHash, signatures: signatures}) + ); + + return batchHash; } } diff --git a/contracts/test/global/CrossChainMultisigHarness.sol b/contracts/test/global/CrossChainMultisigHarness.sol index 13e28e0..0be4cda 100644 --- a/contracts/test/global/CrossChainMultisigHarness.sol +++ b/contracts/test/global/CrossChainMultisigHarness.sol @@ -18,20 +18,20 @@ contract CrossChainMultisigHarness is CrossChainMultisig { _setConfirmationThreshold(newConfirmationThreshold); } - function exposed_verifyProposal(CrossChainCall[] memory calls, bytes32 prevHash) external view { - _verifyProposal(calls, prevHash); + function exposed_verifyBatch(CrossChainCall[] memory calls, bytes32 prevHash) external view { + _verifyBatch(calls, prevHash); } function exposed_verifySignatures(bytes[] memory signatures, bytes32 structHash) external view returns (uint256) { return _verifySignatures(signatures, structHash); } - function exposed_executeProposal(CrossChainCall[] memory calls, bytes32 proposalHash) external { - _executeProposal(calls, proposalHash); + function exposed_executeBatch(CrossChainCall[] memory calls, bytes32 batchHash) external { + _executeBatch(calls, batchHash); } - // Add setter for lastProposalHash - function setLastProposalHash(bytes32 newHash) external { - lastProposalHash = newHash; + // Add setter for lastBatchHash + function setLastBatchHash(bytes32 newHash) external { + _executedBatchHashes.push(newHash); } } diff --git a/contracts/test/global/InstanceManager.unit.t.sol b/contracts/test/global/InstanceManager.unit.t.sol index aec7aca..352450f 100644 --- a/contracts/test/global/InstanceManager.unit.t.sol +++ b/contracts/test/global/InstanceManager.unit.t.sol @@ -2,20 +2,23 @@ pragma solidity ^0.8.23; import {Test} from "forge-std/Test.sol"; -import {InstanceManager} from "../../instance/InstanceManager.sol"; -import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; + import {BytecodeRepository} from "../../global/BytecodeRepository.sol"; import {ProxyCall} from "../../helpers/ProxyCall.sol"; +import {InstanceManager} from "../../instance/InstanceManager.sol"; +import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; +import {IInstanceManager} from "../../interfaces/IInstanceManager.sol"; import { - AP_INSTANCE_MANAGER, - AP_CROSS_CHAIN_GOVERNANCE, - AP_TREASURY, - AP_BYTECODE_REPOSITORY, AP_ADDRESS_PROVIDER, - AP_INSTANCE_MANAGER_PROXY, + AP_BYTECODE_REPOSITORY, + AP_CROSS_CHAIN_GOVERNANCE, AP_CROSS_CHAIN_GOVERNANCE_PROXY, - AP_TREASURY_PROXY, + AP_GEAR_STAKING, AP_GEAR_TOKEN, + AP_INSTANCE_MANAGER, + AP_INSTANCE_MANAGER_PROXY, + AP_TREASURY, + AP_TREASURY_PROXY, AP_WETH_TOKEN, NO_VERSION_CONTROL } from "../../libraries/ContractLiterals.sol"; @@ -23,122 +26,331 @@ import { contract InstanceManagerTest is Test { InstanceManager public manager; address public owner; - address public treasury; address public crossChainGovernance; + address public treasury; address public weth; address public gear; IAddressProvider public addressProvider; function setUp() public { owner = makeAddr("owner"); + crossChainGovernance = makeAddr("crossChainGovernance"); treasury = makeAddr("treasury"); weth = makeAddr("weth"); gear = makeAddr("gear"); - manager = new InstanceManager(owner); + // Deploy with cross-chain governance as initial owner + manager = new InstanceManager(crossChainGovernance); addressProvider = IAddressProvider(manager.addressProvider()); + + // Activate instance and transfer ownership to instance owner + vm.prank(crossChainGovernance); + manager.activate(owner, treasury, weth, gear); } /// @notice Test constructor sets up initial state correctly - function test_IM_01_constructor_sets_initial_state() public view { + function test_U_IM_01_constructor_sets_initial_state() public { + // Create new non-activated instance + InstanceManager newManager = new InstanceManager(crossChainGovernance); + IAddressProvider newAddressProvider = IAddressProvider(newManager.addressProvider()); + // Verify proxies were created - assertTrue(manager.instanceManagerProxy() != address(0)); - assertTrue(manager.treasuryProxy() != address(0)); - assertTrue(manager.crossChainGovernanceProxy() != address(0)); + assertTrue(newManager.instanceManagerProxy() != address(0)); + assertTrue(newManager.treasuryProxy() != address(0)); + assertTrue(newManager.crossChainGovernanceProxy() != address(0)); // Verify initial addresses were set assertEq( - addressProvider.getAddressOrRevert(AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL), manager.bytecodeRepository() + newAddressProvider.getAddressOrRevert(AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL), + newManager.bytecodeRepository() ); - assertEq(addressProvider.getAddressOrRevert(AP_CROSS_CHAIN_GOVERNANCE, NO_VERSION_CONTROL), owner); assertEq( - addressProvider.getAddressOrRevert(AP_INSTANCE_MANAGER_PROXY, NO_VERSION_CONTROL), - manager.instanceManagerProxy() + newAddressProvider.getAddressOrRevert(AP_CROSS_CHAIN_GOVERNANCE, NO_VERSION_CONTROL), crossChainGovernance ); - assertEq(addressProvider.getAddressOrRevert(AP_TREASURY_PROXY, NO_VERSION_CONTROL), manager.treasuryProxy()); assertEq( - addressProvider.getAddressOrRevert(AP_CROSS_CHAIN_GOVERNANCE_PROXY, NO_VERSION_CONTROL), - manager.crossChainGovernanceProxy() + newAddressProvider.getAddressOrRevert(AP_INSTANCE_MANAGER_PROXY, NO_VERSION_CONTROL), + newManager.instanceManagerProxy() ); + assertEq( + newAddressProvider.getAddressOrRevert(AP_TREASURY_PROXY, NO_VERSION_CONTROL), newManager.treasuryProxy() + ); + assertEq( + newAddressProvider.getAddressOrRevert(AP_CROSS_CHAIN_GOVERNANCE_PROXY, NO_VERSION_CONTROL), + newManager.crossChainGovernanceProxy() + ); + assertEq(newAddressProvider.getAddressOrRevert(AP_INSTANCE_MANAGER, NO_VERSION_CONTROL), address(newManager)); // Verify ownership - assertEq(manager.owner(), owner); - } + assertEq(newManager.owner(), crossChainGovernance); - /// @notice Test activation sets up remaining addresses - function test_IM_02_activate_sets_remaining_addresses() public { - vm.prank(owner); - manager.activate(owner, treasury, weth, gear); - - assertTrue(manager.isActivated()); - assertEq(addressProvider.getAddressOrRevert(AP_INSTANCE_MANAGER, NO_VERSION_CONTROL), address(manager)); - assertEq(addressProvider.getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL), treasury); - assertEq(addressProvider.getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL), weth); - assertEq(addressProvider.getAddressOrRevert(AP_GEAR_TOKEN, NO_VERSION_CONTROL), gear); + // Verify not activated + assertFalse(newManager.isActivated()); } - /// @notice Test only owner can activate - function test_IM_03_activate_only_owner() public { - vm.prank(makeAddr("notOwner")); + /// @notice Test activation functionality + function test_U_IM_02_activate_works() public { + // Create new non-activated instance + InstanceManager newManager = new InstanceManager(crossChainGovernance); + IAddressProvider newAddressProvider = IAddressProvider(newManager.addressProvider()); + + // Test it reverts if not governance + vm.prank(makeAddr("notGovernance")); vm.expectRevert("Ownable: caller is not the owner"); - manager.activate(owner, treasury, weth, gear); - } + newManager.activate(owner, treasury, weth, gear); - /// @notice Test can't activate twice - function test_IM_04_cant_activate_twice() public { - vm.startPrank(owner); + // Test successful activation + vm.prank(crossChainGovernance); + newManager.activate(owner, treasury, weth, gear); - manager.activate(owner, treasury, weth, gear); + assertTrue(newManager.isActivated()); + assertEq(newManager.owner(), owner); + assertEq(newAddressProvider.getAddressOrRevert(AP_INSTANCE_MANAGER, NO_VERSION_CONTROL), address(newManager)); + assertEq(newAddressProvider.getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL), treasury); + assertEq(newAddressProvider.getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL), weth); + assertEq(newAddressProvider.getAddressOrRevert(AP_GEAR_TOKEN, NO_VERSION_CONTROL), gear); - // Second activation should not change state + // Test it can't be activated twice address newTreasury = makeAddr("newTreasury"); - manager.activate(owner, newTreasury, weth, gear); - - assertEq(addressProvider.getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL), treasury); - vm.stopPrank(); + vm.prank(owner); + newManager.activate(owner, newTreasury, weth, gear); + assertEq(newAddressProvider.getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL), treasury); } - /// @notice Test setting global address requires correct prefix - function test_IM_05_setGlobalAddress_requires_prefix() public { - vm.mockCall( - addressProvider.getAddressOrRevert(AP_CROSS_CHAIN_GOVERNANCE, NO_VERSION_CONTROL), - abi.encodeWithSignature("getAddress(bytes32)"), - abi.encode(msg.sender) + /// @notice Test address setting functionality + function test_U_IM_03_setAddress_functions_work() public { + bytes32 globalKey = "GLOBAL::TEST"; + bytes32 localKey = "LOCAL::TEST"; + address testAddr = makeAddr("testAddr"); + + // Test setGlobalAddress + // Test it reverts if not governance + address notGovernance = makeAddr("notGovernance"); + vm.prank(notGovernance); + vm.expectRevert( + abi.encodeWithSelector(IInstanceManager.CallerIsNotCrossChainGovernanceException.selector, notGovernance) ); + manager.setGlobalAddress(globalKey, testAddr, false); + + // Test it reverts if key doesn't have correct prefix + vm.prank(crossChainGovernance); + vm.expectRevert(abi.encodeWithSelector(IInstanceManager.InvalidKeyException.selector, bytes32("INVALID"))); + manager.setGlobalAddress("INVALID", testAddr, false); + + // Test successful global address setting + vm.prank(crossChainGovernance); + manager.setGlobalAddress(globalKey, testAddr, false); + assertEq(addressProvider.getAddressOrRevert(globalKey, NO_VERSION_CONTROL), testAddr); + + // Test setLocalAddress + // Test it reverts if not governance + vm.prank(makeAddr("notOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + manager.setLocalAddress(localKey, testAddr, false); + // Test it reverts if key doesn't have correct prefix vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(InstanceManager.InvalidKeyException.selector, "INVALID")); - manager.setGlobalAddress("INVALID", address(0), false); + vm.expectRevert(abi.encodeWithSelector(IInstanceManager.InvalidKeyException.selector, bytes32("INVALID"))); + manager.setLocalAddress("INVALID", testAddr, false); + + // Test successful local address setting + vm.prank(owner); + manager.setLocalAddress(localKey, testAddr, false); + assertEq(addressProvider.getAddressOrRevert(localKey, NO_VERSION_CONTROL), testAddr); } - /// @notice Test setting local address requires correct prefix - function test_IM_06_setLocalAddress_requires_prefix() public { + /// @notice Test configuration functionality + function test_U_IM_04_configure_functions_work() public { + address target = makeAddr("target"); + bytes memory data = "test"; + vm.mockCall(target, data, ""); + + // Test configureGlobal + // Test it reverts if not governance + address notGovernance = makeAddr("notGovernance"); + vm.expectRevert( + abi.encodeWithSelector(IInstanceManager.CallerIsNotCrossChainGovernanceException.selector, notGovernance) + ); + vm.prank(notGovernance); + manager.configureGlobal(target, data); + + // Test successful global configuration + vm.expectCall(manager.crossChainGovernanceProxy(), abi.encodeCall(ProxyCall.proxyCall, (target, data))); + vm.prank(crossChainGovernance); + manager.configureGlobal(target, data); + + // Test configureLocal + // Test it reverts if not governance + vm.prank(makeAddr("notOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + manager.configureLocal(target, data); + + // Test successful local configuration + vm.expectCall(manager.instanceManagerProxy(), abi.encodeCall(ProxyCall.proxyCall, (target, data))); vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(InstanceManager.InvalidKeyException.selector, "INVALID")); - manager.setLocalAddress("INVALID", address(0), false); + manager.configureLocal(target, data); + + // Test configureTreasury + // Test it reverts if not treasury + address notTreasury = makeAddr("notTreasury"); + vm.expectRevert(abi.encodeWithSelector(IInstanceManager.CallerIsNotTreasuryException.selector, notTreasury)); + vm.prank(notTreasury); + manager.configureTreasury(target, data); + + // Test successful treasury configuration + vm.expectCall(manager.treasuryProxy(), abi.encodeCall(ProxyCall.proxyCall, (target, data))); + vm.prank(treasury); + manager.configureTreasury(target, data); } - /// @notice Test only cross chain governance can configure global - function test_IM_07_configureGlobal_access_control() public { + /// @notice Test governance transfer works + function test_U_IM_05_governance_transfer_works() public { + address newGovernance = makeAddr("newGovernance"); + + // Test only current governance can set pending vm.prank(makeAddr("notGovernance")); - vm.expectRevert("Only cross chain governance can call this function"); - manager.configureGlobal(address(0), ""); + vm.expectRevert( + abi.encodeWithSelector( + IInstanceManager.CallerIsNotCrossChainGovernanceException.selector, makeAddr("notGovernance") + ) + ); + manager.setPendingGovernance(newGovernance); + + // Test setting pending governance + vm.prank(crossChainGovernance); + vm.expectEmit(true, false, false, true); + emit IInstanceManager.SetPendingGovernance(newGovernance); + manager.setPendingGovernance(newGovernance); + assertEq(manager.pendingGovernance(), newGovernance); + + // Test only pending governance can accept + vm.prank(makeAddr("notPending")); + vm.expectRevert( + abi.encodeWithSelector( + IInstanceManager.CallerIsNotPendingGovernanceException.selector, makeAddr("notPending") + ) + ); + manager.acceptGovernance(); + + // Test accepting governance + vm.prank(newGovernance); + vm.expectEmit(true, false, false, true); + emit IInstanceManager.AcceptGovernance(newGovernance); + manager.acceptGovernance(); + + assertEq(manager.pendingGovernance(), address(0)); + assertEq(addressProvider.getAddressOrRevert(AP_CROSS_CHAIN_GOVERNANCE, NO_VERSION_CONTROL), newGovernance); } - /// @notice Test only owner can configure local - function test_IM_08_configureLocal_access_control() public { - vm.prank(makeAddr("notOwner")); - vm.expectRevert("Ownable: caller is not the owner"); - manager.configureLocal(address(0), ""); + /// @notice Test system contract deployment + function test_U_IM_06_deploy_system_contract() public { + bytes32 contractType = "TYPE::TEST"; + uint256 version = 3_10; + address newContract = makeAddr("newContract"); + + // Test it reverts if not governance + vm.prank(makeAddr("notGovernance")); + vm.expectRevert( + abi.encodeWithSelector( + IInstanceManager.CallerIsNotCrossChainGovernanceException.selector, makeAddr("notGovernance") + ) + ); + manager.deploySystemContract(contractType, version, false); + + // Mock successful deployment + vm.mockCall( + address(manager.bytecodeRepository()), + abi.encodeCall( + BytecodeRepository.deploy, (contractType, version, abi.encode(manager.addressProvider()), bytes32(0)) + ), + abi.encode(newContract) + ); + + // Test successful deployment + vm.prank(crossChainGovernance); + manager.deploySystemContract(contractType, version, false); + assertEq(addressProvider.getAddressOrRevert(contractType, NO_VERSION_CONTROL), newContract); + + // Test deployment with version saving + address newVersionedContract = makeAddr("newVersionedContract"); + vm.mockCall(newVersionedContract, abi.encodeWithSignature("version()"), abi.encode(version)); + vm.mockCall( + address(manager.bytecodeRepository()), + abi.encodeCall( + BytecodeRepository.deploy, (contractType, version, abi.encode(manager.addressProvider()), bytes32(0)) + ), + abi.encode(newVersionedContract) + ); + + vm.prank(crossChainGovernance); + manager.deploySystemContract(contractType, version, true); + assertEq(addressProvider.getAddressOrRevert(contractType, version), newVersionedContract); + + // Test deployment failure + vm.mockCallRevert( + address(manager.bytecodeRepository()), + abi.encodeCall( + BytecodeRepository.deploy, (contractType, version, abi.encode(manager.addressProvider()), bytes32(0)) + ), + "DEPLOYMENT_FAILED" + ); + + vm.expectRevert("DEPLOYMENT_FAILED"); + vm.prank(crossChainGovernance); + manager.deploySystemContract(contractType, version, false); } - /// @notice Test only treasury can configure treasury - function test_IM_09_configureTreasury_access_control() public { - vm.prank(owner); - manager.activate(owner, treasury, weth, gear); + /// @notice Test legacy GEAR staking deployment + function test_U_IM_07_deploy_legacy_gear_staking() public { + // Test on legacy chains + uint256[] memory legacyChains = new uint256[](4); + legacyChains[0] = 1; // Mainnet + legacyChains[1] = 10; // Optimism + legacyChains[2] = 146; // Sonic + legacyChains[3] = 42161; // Arbitrum + + address[] memory expectedAddresses = new address[](4); + expectedAddresses[0] = 0x2fcbD02d5B1D52FC78d4c02890D7f4f47a459c33; + expectedAddresses[1] = 0x8D2622f1CA3B42b637e2ff6753E6b69D3ab9Adfd; + expectedAddresses[2] = 0xe88846b6C85AA67688e453c7eaeeeb40F51e1F0a; + expectedAddresses[3] = 0xf3599BEfe8E79169Afd5f0b7eb0A1aA322F193D9; + + for (uint256 i = 0; i < legacyChains.length; i++) { + vm.chainId(legacyChains[i]); + + // Test legacy address is used for version 3.10 + vm.prank(crossChainGovernance); + manager.deploySystemContract(AP_GEAR_STAKING, 3_10, false); + assertEq(addressProvider.getAddressOrRevert(AP_GEAR_STAKING, NO_VERSION_CONTROL), expectedAddresses[i]); + + // Test normal deployment for other versions + address newStaking = makeAddr("newStaking"); + vm.mockCall( + address(manager.bytecodeRepository()), + abi.encodeCall( + BytecodeRepository.deploy, + (AP_GEAR_STAKING, 3_11, abi.encode(manager.addressProvider()), bytes32(0)) + ), + abi.encode(newStaking) + ); + + vm.prank(crossChainGovernance); + manager.deploySystemContract(AP_GEAR_STAKING, 3_11, false); + assertEq(addressProvider.getAddressOrRevert(AP_GEAR_STAKING, NO_VERSION_CONTROL), newStaking); + } + + // Test on non-legacy chain + vm.chainId(137); // Polygon + address nonLegacyStaking = makeAddr("nonLegacyStaking"); + vm.mockCall( + address(manager.bytecodeRepository()), + abi.encodeCall( + BytecodeRepository.deploy, (AP_GEAR_STAKING, 3_10, abi.encode(manager.addressProvider()), bytes32(0)) + ), + abi.encode(nonLegacyStaking) + ); - vm.prank(makeAddr("notTreasury")); - vm.expectRevert("Only financial multisig can call this function"); - manager.configureTreasury(address(0), ""); + vm.prank(crossChainGovernance); + manager.deploySystemContract(AP_GEAR_STAKING, 3_10, false); + assertEq(addressProvider.getAddressOrRevert(AP_GEAR_STAKING, NO_VERSION_CONTROL), nonLegacyStaking); } } diff --git a/contracts/test/global/MarketConfiguratorFactory.unit.t.sol b/contracts/test/global/MarketConfiguratorFactory.unit.t.sol new file mode 100644 index 0000000..6e24389 --- /dev/null +++ b/contracts/test/global/MarketConfiguratorFactory.unit.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; + +import {MarketConfiguratorFactory} from "../../instance/MarketConfiguratorFactory.sol"; +import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; +import {IMarketConfiguratorFactory} from "../../interfaces/IMarketConfiguratorFactory.sol"; +import {IBytecodeRepository} from "../../interfaces/IBytecodeRepository.sol"; +import {Call} from "../../interfaces/Types.sol"; +import {IImmutableOwnableTrait} from "../../interfaces/base/IImmutableOwnableTrait.sol"; + +import { + AP_BYTECODE_REPOSITORY, + AP_CROSS_CHAIN_GOVERNANCE, + AP_MARKET_CONFIGURATOR, + NO_VERSION_CONTROL +} from "../../libraries/ContractLiterals.sol"; + +import {MarketConfiguratorFactoryHarness} from "./MarketConfiguratorFactoryHarness.sol"; + +contract MarketConfiguratorFactoryTest is Test { + MarketConfiguratorFactoryHarness public factory; + address public governance; + address public configurator; + IAddressProvider public addressProvider; + IBytecodeRepository public bytecodeRepository; + + function setUp() public { + governance = makeAddr("governance"); + configurator = makeAddr("configurator"); + addressProvider = IAddressProvider(makeAddr("addressProvider")); + bytecodeRepository = IBytecodeRepository(makeAddr("bytecodeRepository")); + + vm.mockCall(configurator, abi.encodeWithSignature("curatorName()"), abi.encode("Test Curator")); + + vm.mockCall( + address(addressProvider), + abi.encodeWithSignature( + "getAddressOrRevert(bytes32,uint256)", AP_CROSS_CHAIN_GOVERNANCE, NO_VERSION_CONTROL + ), + abi.encode(governance) + ); + + vm.mockCall( + address(addressProvider), + abi.encodeWithSignature("getAddressOrRevert(bytes32,uint256)", AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL), + abi.encode(address(bytecodeRepository)) + ); + + factory = new MarketConfiguratorFactoryHarness(address(addressProvider)); + } + + function test_U_MCF_01_create_market_configurator_works() public { + address emergencyAdmin = makeAddr("emergencyAdmin"); + address adminFeeTreasury = makeAddr("treasury"); + string memory curatorName = "Test Curator"; + + // Mock deployment + vm.mockCall( + address(bytecodeRepository), + abi.encodeCall(IBytecodeRepository.getLatestPatchVersion, (AP_MARKET_CONFIGURATOR, 3_10)), + abi.encode(3_11) + ); + vm.mockCall( + address(bytecodeRepository), + abi.encodeCall( + IBytecodeRepository.deploy, + ( + AP_MARKET_CONFIGURATOR, + 3_11, + abi.encode( + address(addressProvider), address(this), emergencyAdmin, adminFeeTreasury, curatorName, false + ), + bytes32(bytes20(address(this))) + ) + ), + abi.encode(configurator) + ); + + // Expect CreateMarketConfigurator event + vm.expectEmit(true, true, true, true); + emit IMarketConfiguratorFactory.CreateMarketConfigurator(configurator, curatorName); + + factory.createMarketConfigurator(emergencyAdmin, adminFeeTreasury, curatorName, false); + + // Verify configurator was registered + assertTrue(factory.isMarketConfigurator(configurator)); + assertEq(factory.getMarketConfigurators()[0], configurator); + assertEq(factory.getNumMarketConfigurators(), 1); + } + + function test_U_MCF_02_shutdown_market_configurator_works() public { + address contractsRegister = makeAddr("contractsRegister"); + vm.mockCall(configurator, abi.encodeWithSignature("admin()"), abi.encode(address(this))); + + // Test it reverts if caller is not admin + address notAdmin = makeAddr("notAdmin"); + vm.prank(notAdmin); + vm.expectRevert( + abi.encodeWithSelector( + IMarketConfiguratorFactory.CallerIsNotMarketConfiguratorAdminException.selector, notAdmin + ) + ); + factory.shutdownMarketConfigurator(configurator); + + // Test it reverts if already shutdown + vm.mockCall(configurator, abi.encodeWithSignature("admin()"), abi.encode(address(this))); + factory.exposed_addShutdownConfigurator(configurator); + vm.expectRevert( + abi.encodeWithSelector( + IMarketConfiguratorFactory.MarketConfiguratorIsAlreadyShutdownException.selector, configurator + ) + ); + factory.shutdownMarketConfigurator(configurator); + factory.exposed_removeShutdownConfigurator(configurator); + + // Test it reverts if configurator is not registered + vm.expectRevert( + abi.encodeWithSelector( + IMarketConfiguratorFactory.MarketConfiguratorIsNotRegisteredException.selector, configurator + ) + ); + factory.shutdownMarketConfigurator(configurator); + + // Test it reverts if configurator has pools + factory.exposed_addRegisteredConfigurator(configurator); + vm.mockCall(configurator, abi.encodeWithSignature("contractsRegister()"), abi.encode(contractsRegister)); + address[] memory pools = new address[](1); + pools[0] = makeAddr("pool"); + vm.mockCall(contractsRegister, abi.encodeWithSignature("getPools()"), abi.encode(pools)); + vm.expectRevert( + abi.encodeWithSelector( + IMarketConfiguratorFactory.CantShutdownMarketConfiguratorException.selector, configurator + ) + ); + factory.shutdownMarketConfigurator(configurator); + + // Test it is shutdown successfully + vm.mockCall(contractsRegister, abi.encodeWithSignature("getPools()"), abi.encode(new address[](0))); + vm.expectEmit(true, false, false, false); + emit IMarketConfiguratorFactory.ShutdownMarketConfigurator(configurator); + factory.shutdownMarketConfigurator(configurator); + + // Verify final state + assertFalse(factory.isMarketConfigurator(configurator)); + assertEq(factory.getNumMarketConfigurators(), 0); + assertEq(factory.getShutdownMarketConfigurators()[0], configurator); + } + + function test_U_MCF_03_add_market_configurator_works() public { + // Test it reverts if not governance + address notGovernance = makeAddr("notGovernance"); + vm.prank(notGovernance); + vm.expectRevert( + abi.encodeWithSelector( + IMarketConfiguratorFactory.CallerIsNotCrossChainGovernanceException.selector, notGovernance + ) + ); + factory.addMarketConfigurator(configurator); + + // Test successful addition + vm.prank(governance); + vm.expectEmit(true, true, true, true); + emit IMarketConfiguratorFactory.CreateMarketConfigurator(configurator, "Test Curator"); + factory.addMarketConfigurator(configurator); + assertTrue(factory.isMarketConfigurator(configurator)); + assertEq(factory.getMarketConfigurators()[0], configurator); + assertEq(factory.getNumMarketConfigurators(), 1); + + // Test it reverts on duplicate addition + vm.prank(governance); + vm.expectRevert( + abi.encodeWithSelector( + IMarketConfiguratorFactory.MarketConfiguratorIsAlreadyAddedException.selector, configurator + ) + ); + factory.addMarketConfigurator(configurator); + + // Test it reverts if configurator is shutdown + address shutdownConfigurator = makeAddr("shutdownConfigurator"); + vm.mockCall(shutdownConfigurator, abi.encodeWithSignature("curatorName()"), abi.encode("Shutdown Curator")); + + factory.exposed_addShutdownConfigurator(shutdownConfigurator); + + vm.prank(governance); + vm.expectRevert( + abi.encodeWithSelector( + IMarketConfiguratorFactory.MarketConfiguratorIsAlreadyShutdownException.selector, shutdownConfigurator + ) + ); + factory.addMarketConfigurator(shutdownConfigurator); + } +} diff --git a/contracts/test/global/MarketConfiguratorFactoryHarness.sol b/contracts/test/global/MarketConfiguratorFactoryHarness.sol new file mode 100644 index 0000000..556e9c2 --- /dev/null +++ b/contracts/test/global/MarketConfiguratorFactoryHarness.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {MarketConfiguratorFactory} from "../../instance/MarketConfiguratorFactory.sol"; + +/// @title Market configurator factory harness +/// @notice Exposes internal state manipulation functions for testing +contract MarketConfiguratorFactoryHarness is MarketConfiguratorFactory { + using EnumerableSet for EnumerableSet.AddressSet; + + constructor(address addressProvider_) MarketConfiguratorFactory(addressProvider_) {} + + function exposed_addRegisteredConfigurator(address configurator) external { + _registeredMarketConfiguratorsSet.add(configurator); + } + + function exposed_removeRegisteredConfigurator(address configurator) external { + _registeredMarketConfiguratorsSet.remove(configurator); + } + + function exposed_addShutdownConfigurator(address configurator) external { + _shutdownMarketConfiguratorsSet.add(configurator); + } + + function exposed_removeShutdownConfigurator(address configurator) external { + _shutdownMarketConfiguratorsSet.remove(configurator); + } +} diff --git a/contracts/test/global/PriceFeedStore.unit.t.sol b/contracts/test/global/PriceFeedStore.unit.t.sol index 07c3f5e..af00ac3 100644 --- a/contracts/test/global/PriceFeedStore.unit.t.sol +++ b/contracts/test/global/PriceFeedStore.unit.t.sol @@ -1,15 +1,26 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.23; -import {PriceFeedInfo} from "../../interfaces/Types.sol"; +import {Call, PriceFeedInfo} from "../../interfaces/Types.sol"; import {Test} from "forge-std/Test.sol"; import {PriceFeedStore} from "../../instance/PriceFeedStore.sol"; -import {IPriceFeedStore} from "../../interfaces/IPriceFeedStore.sol"; +import {IBytecodeRepository} from "../../interfaces/IBytecodeRepository.sol"; +import {IPriceFeedStore, PriceUpdate} from "../../interfaces/IPriceFeedStore.sol"; import {IAddressProvider} from "../../interfaces/IAddressProvider.sol"; -import {MockPriceFeed} from "../mocks/MockPriceFeed.sol"; -import {AP_INSTANCE_MANAGER_PROXY, NO_VERSION_CONTROL} from "../../libraries/ContractLiterals.sol"; -import {ImmutableOwnableTrait} from "../../traits/ImmutableOwnableTrait.sol"; +import {IImmutableOwnableTrait} from "../../interfaces/base/IImmutableOwnableTrait.sol"; +import { + MockPriceFeed, + MockSingleUnderlyingPriceFeed, + MockMultipleUnderlyingPriceFeed, + MockUpdatablePriceFeed +} from "../mocks/MockPriceFeed.sol"; +import { + AP_BYTECODE_REPOSITORY, + AP_INSTANCE_MANAGER_PROXY, + AP_ZERO_PRICE_FEED, + NO_VERSION_CONTROL +} from "../../libraries/ContractLiterals.sol"; import { ZeroAddressException, StalePriceException, @@ -21,13 +32,18 @@ contract PriceFeedStoreTest is Test { PriceFeedStore public store; address public owner; address public token; + address public zeroPriceFeed; MockPriceFeed public priceFeed; IAddressProvider public addressProvider; + IBytecodeRepository public bytecodeRepository; function setUp() public { owner = makeAddr("owner"); token = makeAddr("token"); + zeroPriceFeed = makeAddr("zeroPriceFeed"); priceFeed = new MockPriceFeed(); + addressProvider = IAddressProvider(makeAddr("addressProvider")); + bytecodeRepository = IBytecodeRepository(makeAddr("bytecodeRepository")); vm.mockCall( address(addressProvider), @@ -37,28 +53,55 @@ contract PriceFeedStoreTest is Test { abi.encode(owner) ); + vm.mockCall( + address(addressProvider), + abi.encodeWithSignature("getAddressOrRevert(bytes32,uint256)", AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL), + abi.encode(address(bytecodeRepository)) + ); + + vm.mockCall( + address(bytecodeRepository), + abi.encodeWithSignature("deploy(bytes32,uint256,bytes,bytes32)", AP_ZERO_PRICE_FEED, 3_10, "", bytes32(0)), + abi.encode(zeroPriceFeed) + ); + + vm.mockCall( + address(bytecodeRepository), abi.encodeWithSignature("isDeployedFromRepository(address)"), abi.encode(false) + ); + store = new PriceFeedStore(address(addressProvider)); + _markAsInternalPriceFeed(address(priceFeed)); + } + + function _markAsInternalPriceFeed(address target) internal { + vm.mockCall( + address(bytecodeRepository), + abi.encodeWithSignature("isDeployedFromRepository(address)", target), + abi.encode(true) + ); + + vm.mockCall(address(target), abi.encodeWithSignature("owner()"), abi.encode(address(store))); } /// @notice Test basic price feed addition flow function test_PFS_01_addPriceFeed_works() public { uint32 stalenessPeriod = 3600; + string memory name = "ETH/USD"; vm.prank(owner); - store.addPriceFeed(address(priceFeed), stalenessPeriod); + store.addPriceFeed(address(priceFeed), stalenessPeriod, name); // Verify price feed was added correctly assertEq(store.getStalenessPeriod(address(priceFeed)), stalenessPeriod); // Get price feed info PriceFeedInfo memory priceFeedInfo = store.priceFeedInfo(address(priceFeed)); - store.priceFeedInfo(address(priceFeed)); // Verify all parameters were set correctly - assertEq(priceFeedInfo.author, owner); - assertEq(priceFeedInfo.priceFeedType, "MOCK_PRICE_FEED"); + assertEq(priceFeedInfo.priceFeedType, "PRICE_FEED::MOCK"); assertEq(priceFeedInfo.stalenessPeriod, stalenessPeriod); assertEq(priceFeedInfo.version, 1); + assertEq(priceFeedInfo.name, name); // Verify price feed is in known list address[] memory knownPriceFeeds = store.getKnownPriceFeeds(); @@ -71,25 +114,25 @@ contract PriceFeedStoreTest is Test { address notOwner = makeAddr("notOwner"); vm.prank(notOwner); vm.expectRevert(abi.encodeWithSignature("CallerIsNotOwnerException(address)", notOwner)); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); } /// @notice Test that zero address price feed cannot be added function test_PFS_03_addPriceFeed_reverts_on_zero_address() public { vm.prank(owner); vm.expectRevert(ZeroAddressException.selector); - store.addPriceFeed(address(0), 3600); + store.addPriceFeed(address(0), 3600, "ETH/USD"); } /// @notice Test duplicate price feed addition is prevented function test_PFS_04_addPriceFeed_reverts_on_duplicate() public { vm.startPrank(owner); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); vm.expectRevert( - abi.encodeWithSelector(IPriceFeedStore.PriceFeedAlreadyAddedException.selector, address(priceFeed)) + abi.encodeWithSelector(IPriceFeedStore.PriceFeedIsAlreadyAddedException.selector, address(priceFeed)) ); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); vm.stopPrank(); } @@ -102,13 +145,13 @@ contract PriceFeedStoreTest is Test { vm.prank(owner); vm.expectRevert(StalePriceException.selector); - store.addPriceFeed(address(stalePriceFeed), 3600); + store.addPriceFeed(address(stalePriceFeed), 3600, "ETH/USD"); } /// @notice Test price feed allowance for tokens function test_PFS_06_allowPriceFeed_works() public { vm.startPrank(owner); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); store.allowPriceFeed(token, address(priceFeed)); vm.stopPrank(); @@ -118,7 +161,7 @@ contract PriceFeedStoreTest is Test { /// @notice Test only owner can allow price feeds function test_PFS_07_allowPriceFeed_reverts_if_not_owner() public { vm.prank(owner); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); address notOwner = makeAddr("notOwner"); vm.prank(notOwner); @@ -129,14 +172,16 @@ contract PriceFeedStoreTest is Test { /// @notice Test unknown price feeds cannot be allowed function test_PFS_08_allowPriceFeed_reverts_on_unknown_feed() public { vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(IPriceFeedStore.PriceFeedNotKnownException.selector, address(priceFeed))); + vm.expectRevert( + abi.encodeWithSelector(IPriceFeedStore.PriceFeedIsNotKnownException.selector, address(priceFeed)) + ); store.allowPriceFeed(token, address(priceFeed)); } /// @notice Test price feed forbidding function test_PFS_09_forbidPriceFeed_works() public { vm.startPrank(owner); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); store.allowPriceFeed(token, address(priceFeed)); store.forbidPriceFeed(token, address(priceFeed)); vm.stopPrank(); @@ -147,7 +192,7 @@ contract PriceFeedStoreTest is Test { /// @notice Test only owner can forbid price feeds function test_PFS_10_forbidPriceFeed_reverts_if_not_owner() public { vm.startPrank(owner); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); store.allowPriceFeed(token, address(priceFeed)); vm.stopPrank(); @@ -160,7 +205,7 @@ contract PriceFeedStoreTest is Test { /// @notice Test staleness period updates function test_PFS_11_setStalenessPeriod_works() public { vm.startPrank(owner); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); store.setStalenessPeriod(address(priceFeed), 7200); vm.stopPrank(); @@ -170,7 +215,7 @@ contract PriceFeedStoreTest is Test { /// @notice Test staleness period validation on update function test_PFS_12_setStalenessPeriod_validates_staleness() public { vm.startPrank(owner); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); priceFeed.setLastUpdateTime(block.timestamp); vm.warp(block.timestamp + 7200); @@ -183,7 +228,7 @@ contract PriceFeedStoreTest is Test { /// @notice Test token list management function test_PFS_13_maintains_token_list() public { vm.startPrank(owner); - store.addPriceFeed(address(priceFeed), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); store.allowPriceFeed(token, address(priceFeed)); vm.stopPrank(); @@ -195,10 +240,11 @@ contract PriceFeedStoreTest is Test { /// @notice Test multiple price feeds per token function test_PFS_14_allows_multiple_feeds_per_token() public { MockPriceFeed priceFeed2 = new MockPriceFeed(); + _markAsInternalPriceFeed(address(priceFeed2)); vm.startPrank(owner); - store.addPriceFeed(address(priceFeed), 3600); - store.addPriceFeed(address(priceFeed2), 3600); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD Primary"); + store.addPriceFeed(address(priceFeed2), 3600, "ETH/USD Secondary"); store.allowPriceFeed(token, address(priceFeed)); store.allowPriceFeed(token, address(priceFeed2)); vm.stopPrank(); @@ -208,4 +254,376 @@ contract PriceFeedStoreTest is Test { assertTrue(store.isAllowedPriceFeed(token, address(priceFeed))); assertTrue(store.isAllowedPriceFeed(token, address(priceFeed2))); } + + /// @notice Test handles updatable price feeds + function test_PFS_15_handles_updatable_price_feeds() public { + MockUpdatablePriceFeed updatableFeed = new MockUpdatablePriceFeed(); + _markAsInternalPriceFeed(address(updatableFeed)); + + vm.startPrank(owner); + + // Expect AddUpdatablePriceFeed event + vm.expectEmit(true, false, false, false); + emit IPriceFeedStore.AddUpdatablePriceFeed(address(updatableFeed)); + + store.addPriceFeed(address(updatableFeed), 3600, "ETH/USD Updatable"); + + // Check it was added to updatable feeds list + address[] memory updatableFeeds = store.getUpdatablePriceFeeds(); + assertEq(updatableFeeds.length, 1); + assertEq(updatableFeeds[0], address(updatableFeed)); + + // Test price updates + bytes memory updateData = abi.encode(1234); + PriceUpdate[] memory updates = new PriceUpdate[](1); + updates[0] = PriceUpdate({priceFeed: address(updatableFeed), data: updateData}); + store.updatePrices(updates); + + // Test non-updatable feed + MockPriceFeed nonUpdatableFeed = new MockPriceFeed(); + _markAsInternalPriceFeed(address(nonUpdatableFeed)); + store.addPriceFeed(address(nonUpdatableFeed), 3600, "ETH/USD Non-updatable"); + + // Verify only updatable feed is in the list + updatableFeeds = store.getUpdatablePriceFeeds(); + assertEq(updatableFeeds.length, 1); + assertEq(updatableFeeds[0], address(updatableFeed)); + + // Try to update non-updatable feed + updates[0] = PriceUpdate({priceFeed: address(nonUpdatableFeed), data: updateData}); + vm.expectRevert( + abi.encodeWithSelector(IPriceFeedStore.PriceFeedIsNotUpdatableException.selector, address(nonUpdatableFeed)) + ); + store.updatePrices(updates); + + // Verify update was processed + assertEq(updatableFeed.lastUpdateData(), updateData); + vm.stopPrank(); + } + + function test_PFS_16_validates_nested_price_feeds() public { + // Create a tree of price feeds: + // root (multiple underlying) + // ├── child1 (single underlying) + // │ └── leaf1 + // └── child2 (multiple underlying) + // ├── leaf2 + // └── leaf3 (updatable) + + MockPriceFeed leaf1 = new MockPriceFeed(); + MockPriceFeed leaf2 = new MockPriceFeed(); + MockUpdatablePriceFeed leaf3 = new MockUpdatablePriceFeed(); + + // Mock all as internal feeds + _markAsInternalPriceFeed(address(leaf1)); + _markAsInternalPriceFeed(address(leaf2)); + _markAsInternalPriceFeed(address(leaf3)); + + MockSingleUnderlyingPriceFeed child1 = new MockSingleUnderlyingPriceFeed(address(leaf1)); + address[] memory child2Feeds = new address[](2); + child2Feeds[0] = address(leaf2); + child2Feeds[1] = address(leaf3); + MockMultipleUnderlyingPriceFeed child2 = new MockMultipleUnderlyingPriceFeed(child2Feeds); + + _markAsInternalPriceFeed(address(child1)); + _markAsInternalPriceFeed(address(child2)); + + address[] memory rootFeeds = new address[](2); + rootFeeds[0] = address(child1); + rootFeeds[1] = address(child2); + MockMultipleUnderlyingPriceFeed root = new MockMultipleUnderlyingPriceFeed(rootFeeds); + + _markAsInternalPriceFeed(address(root)); + + vm.startPrank(owner); + + // Expect AddUpdatablePriceFeed event for leaf3 + vm.expectEmit(true, false, false, false); + emit IPriceFeedStore.AddUpdatablePriceFeed(address(leaf3)); + + store.addPriceFeed(address(root), 3600, "Root Feed"); + + // Verify updatable feeds were detected + address[] memory updatableFeeds = store.getUpdatablePriceFeeds(); + assertEq(updatableFeeds.length, 1, "Wrong number of updatable feeds"); + assertEq(updatableFeeds[0], address(leaf3), "Wrong updatable feed"); + + vm.stopPrank(); + } + + function test_PFS_17_handles_external_feeds_in_tree() public { + // Create a tree with mix of internal and external feeds: + // root (multiple underlying) + // ├── external1 + // └── child (single underlying) + // └── external2 + + address external1 = makeAddr("external1"); + address external2 = makeAddr("external2"); + + MockSingleUnderlyingPriceFeed child = new MockSingleUnderlyingPriceFeed(external2); + _markAsInternalPriceFeed(address(child)); + + address[] memory rootFeeds = new address[](2); + rootFeeds[0] = external1; + rootFeeds[1] = address(child); + MockMultipleUnderlyingPriceFeed root = new MockMultipleUnderlyingPriceFeed(rootFeeds); + _markAsInternalPriceFeed(address(root)); + + // These calls should never happen + vm.expectCall(address(external1), abi.encodeWithSignature("owner()"), 0); + vm.expectCall(address(external2), abi.encodeWithSignature("owner()"), 0); + + vm.startPrank(owner); + store.addPriceFeed(address(root), 3600, "Root Feed"); + vm.stopPrank(); + } + + function test_PFS_18_handles_complex_nesting() public { + // Create a complex tree: + // root (multiple underlying) + // ├── branch1 (multiple underlying) + // │ ├── leaf1 (updatable) + // │ └── branch2 (single underlying) + // │ └── leaf2 + // └── branch3 (single underlying) + // └── leaf3 (updatable) + + // Create leaf nodes + MockUpdatablePriceFeed leaf1 = new MockUpdatablePriceFeed(); + MockPriceFeed leaf2 = new MockPriceFeed(); + MockUpdatablePriceFeed leaf3 = new MockUpdatablePriceFeed(); + + // Mock all as internal feeds + _markAsInternalPriceFeed(address(leaf1)); + _markAsInternalPriceFeed(address(leaf2)); + _markAsInternalPriceFeed(address(leaf3)); + + // Create branch2 (single underlying -> leaf2) + MockSingleUnderlyingPriceFeed branch2 = new MockSingleUnderlyingPriceFeed(address(leaf2)); + _markAsInternalPriceFeed(address(branch2)); + + // Create branch1 (multiple underlying -> [leaf1, branch2]) + address[] memory branch1Feeds = new address[](2); + branch1Feeds[0] = address(leaf1); + branch1Feeds[1] = address(branch2); + MockMultipleUnderlyingPriceFeed branch1 = new MockMultipleUnderlyingPriceFeed(branch1Feeds); + _markAsInternalPriceFeed(address(branch1)); + + // Create branch3 (single underlying -> leaf3) + MockSingleUnderlyingPriceFeed branch3 = new MockSingleUnderlyingPriceFeed(address(leaf3)); + _markAsInternalPriceFeed(address(branch3)); + + // Create root (multiple underlying -> [branch1, branch3]) + address[] memory rootFeeds = new address[](2); + rootFeeds[0] = address(branch1); + rootFeeds[1] = address(branch3); + MockMultipleUnderlyingPriceFeed root = new MockMultipleUnderlyingPriceFeed(rootFeeds); + _markAsInternalPriceFeed(address(root)); + + vm.startPrank(owner); + + // Expect AddUpdatablePriceFeed events + vm.expectEmit(true, false, false, false); + emit IPriceFeedStore.AddUpdatablePriceFeed(address(leaf1)); + vm.expectEmit(true, false, false, false); + emit IPriceFeedStore.AddUpdatablePriceFeed(address(leaf3)); + + store.addPriceFeed(address(root), 3600, "Complex Root"); + + // Verify updatable feeds were detected + address[] memory updatableFeeds = store.getUpdatablePriceFeeds(); + assertEq(updatableFeeds.length, 2, "Wrong number of updatable feeds"); + assertTrue( + (updatableFeeds[0] == address(leaf1) && updatableFeeds[1] == address(leaf3)) + || (updatableFeeds[0] == address(leaf3) && updatableFeeds[1] == address(leaf1)), + "Wrong updatable feeds" + ); + + // Test updating both feeds + bytes memory updateData1 = abi.encode(1234); + bytes memory updateData3 = abi.encode(5678); + PriceUpdate[] memory updates = new PriceUpdate[](2); + updates[0] = PriceUpdate({priceFeed: address(leaf1), data: updateData1}); + updates[1] = PriceUpdate({priceFeed: address(leaf3), data: updateData3}); + store.updatePrices(updates); + + // Verify updates were processed + assertEq(leaf1.lastUpdateData(), updateData1, "Leaf1 not updated"); + assertEq(leaf3.lastUpdateData(), updateData3, "Leaf3 not updated"); + vm.stopPrank(); + } + + function test_PFS_19_handles_external_feed() public { + MockPriceFeed externalFeed = new MockPriceFeed(); + + vm.startPrank(owner); + store.addPriceFeed(address(externalFeed), 3600, "External Feed"); + + // Verify no ownership check was performed + vm.expectCall(address(externalFeed), abi.encodeWithSignature("owner()"), 0); + + // Verify feed was added with correct metadata + PriceFeedInfo memory info = store.priceFeedInfo(address(externalFeed)); + assertEq(info.priceFeedType, "PRICE_FEED::EXTERNAL"); + assertEq(info.version, 0); + assertEq(info.stalenessPeriod, 3600); + assertEq(info.name, "External Feed"); + vm.stopPrank(); + } + + function test_PFS_20_validates_different_ownership_types() public { + // Test non-ownable feed (should pass) + MockPriceFeed nonOwnableFeed = new MockPriceFeed(); + vm.mockCall( + address(bytecodeRepository), + abi.encodeWithSignature("isDeployedFromRepository(address)", address(nonOwnableFeed)), + abi.encode(true) + ); + + // Test Ownable feed owned by store (should pass) + MockPriceFeed ownableFeed = new MockPriceFeed(); + vm.mockCall( + address(bytecodeRepository), + abi.encodeWithSignature("isDeployedFromRepository(address)", address(ownableFeed)), + abi.encode(true) + ); + vm.mockCall(address(ownableFeed), abi.encodeWithSignature("acceptOwnership()"), ""); + vm.mockCall(address(ownableFeed), abi.encodeWithSignature("owner()"), abi.encode(address(store))); + + // Test Ownable feed owned by other (should fail) + MockPriceFeed wrongOwnerFeed = new MockPriceFeed(); + vm.mockCall( + address(bytecodeRepository), + abi.encodeWithSignature("isDeployedFromRepository(address)", address(wrongOwnerFeed)), + abi.encode(true) + ); + vm.mockCall(address(wrongOwnerFeed), abi.encodeWithSignature("acceptOwnership()"), ""); + vm.mockCall(address(wrongOwnerFeed), abi.encodeWithSignature("owner()"), abi.encode(makeAddr("other"))); + + vm.startPrank(owner); + + // Non-ownable should pass + store.addPriceFeed(address(nonOwnableFeed), 3600, "Non-ownable Feed"); + + // Correctly owned should pass + store.addPriceFeed(address(ownableFeed), 3600, "Ownable Feed"); + + // Wrong owner should fail + vm.expectRevert( + abi.encodeWithSelector(IPriceFeedStore.PriceFeedIsNotOwnedByStore.selector, address(wrongOwnerFeed)) + ); + store.addPriceFeed(address(wrongOwnerFeed), 3600, "Wrong Owner Feed"); + + vm.stopPrank(); + } + + function test_PFS_21_configurePriceFeeds_works() public { + vm.startPrank(owner); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); + + // Test allowed configuration call + bytes memory callData = abi.encodeWithSignature("setPrice(int256)", 1234); + Call[] memory calls = new Call[](1); + calls[0] = Call(address(priceFeed), callData); + store.configurePriceFeeds(calls); + + // Verify call was executed + (, int256 answer,,,) = priceFeed.latestRoundData(); + assertEq(answer, 1234); + vm.stopPrank(); + } + + function test_PFS_22_configurePriceFeeds_reverts_if_not_owner() public { + vm.prank(owner); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); + + Call[] memory calls = new Call[](1); + calls[0] = Call(address(priceFeed), abi.encodeWithSignature("setPrice(int256)", 1234)); + + address notOwner = makeAddr("notOwner"); + vm.prank(notOwner); + vm.expectRevert(abi.encodeWithSignature("CallerIsNotOwnerException(address)", notOwner)); + store.configurePriceFeeds(calls); + } + + function test_PFS_23_configurePriceFeeds_reverts_on_unknown_feed() public { + Call[] memory calls = new Call[](1); + calls[0] = Call(address(priceFeed), abi.encodeWithSignature("setPrice(int256)", 1234)); + + vm.prank(owner); + vm.expectRevert( + abi.encodeWithSelector(IPriceFeedStore.PriceFeedIsNotKnownException.selector, address(priceFeed)) + ); + store.configurePriceFeeds(calls); + } + + function test_PFS_24_configurePriceFeeds_reverts_on_renounceOwnership() public { + vm.prank(owner); + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); + + Call[] memory renounceCall = new Call[](1); + renounceCall[0] = Call(address(priceFeed), abi.encodeWithSignature("renounceOwnership()")); + + vm.expectRevert( + abi.encodeWithSelector( + IPriceFeedStore.ForbiddenConfigurationMethodException.selector, bytes4(renounceCall[0].callData) + ) + ); + vm.prank(owner); + store.configurePriceFeeds(renounceCall); + } + + function test_PFS_25_removePriceFeed_works() public { + vm.startPrank(owner); + + // Test it reverts on unknown feed + vm.expectRevert( + abi.encodeWithSelector(IPriceFeedStore.PriceFeedIsNotKnownException.selector, address(priceFeed)) + ); + store.removePriceFeed(address(priceFeed)); + + // Add price feed and allow it for some tokens + store.addPriceFeed(address(priceFeed), 3600, "ETH/USD"); + store.allowPriceFeed(token, address(priceFeed)); + address token2 = makeAddr("token2"); + store.allowPriceFeed(token2, address(priceFeed)); + vm.stopPrank(); + + // Test it reverts if not owner + address notOwner = makeAddr("notOwner"); + vm.prank(notOwner); + vm.expectRevert(abi.encodeWithSelector(IImmutableOwnableTrait.CallerIsNotOwnerException.selector, notOwner)); + store.removePriceFeed(address(priceFeed)); + + // Test it successfully removes feed + vm.prank(owner); + + // Expect ForbidPriceFeed events for each token + vm.expectEmit(true, true, false, false); + emit IPriceFeedStore.ForbidPriceFeed(token, address(priceFeed)); + vm.expectEmit(true, true, false, false); + emit IPriceFeedStore.ForbidPriceFeed(token2, address(priceFeed)); + + // Expect RemovePriceFeed event + vm.expectEmit(true, false, false, false); + emit IPriceFeedStore.RemovePriceFeed(address(priceFeed)); + + store.removePriceFeed(address(priceFeed)); + + // Verify feed was removed + assertFalse(store.isKnownPriceFeed(address(priceFeed)), "Feed should not be known"); + assertFalse(store.isAllowedPriceFeed(token, address(priceFeed)), "Feed should not be allowed for token1"); + assertFalse(store.isAllowedPriceFeed(token2, address(priceFeed)), "Feed should not be allowed for token2"); + + vm.expectRevert( + abi.encodeWithSelector(IPriceFeedStore.PriceFeedIsNotAllowedException.selector, token, address(priceFeed)) + ); + store.getAllowanceTimestamp(token, address(priceFeed)); + + vm.expectRevert( + abi.encodeWithSelector(IPriceFeedStore.PriceFeedIsNotAllowedException.selector, token2, address(priceFeed)) + ); + store.getAllowanceTimestamp(token2, address(priceFeed)); + } } diff --git a/contracts/test/helpers/BCRHelpers.sol b/contracts/test/helpers/BCRHelpers.sol index 1dff1a4..fb31682 100644 --- a/contracts/test/helpers/BCRHelpers.sol +++ b/contracts/test/helpers/BCRHelpers.sol @@ -4,13 +4,14 @@ pragma solidity ^0.8.23; import {SignatureHelper} from "./SignatureHelper.sol"; -import {Bytecode} from "../../interfaces/Types.sol"; +import {AuditReport, Bytecode} from "../../interfaces/Types.sol"; import {IBytecodeRepository} from "../../interfaces/IBytecodeRepository.sol"; import {console} from "forge-std/console.sol"; import {LibString} from "@solady/utils/LibString.sol"; contract BCRHelpers is SignatureHelper { using LibString for bytes32; + using LibString for uint256; address internal bytecodeRepository; uint256 internal auditorKey; @@ -25,9 +26,18 @@ contract BCRHelpers is SignatureHelper { authorKey = _generatePrivateKey("AUTHOR"); author = vm.rememberKey(authorKey); + // Print debug info + + if (!_isTestMode()) { + console.log("BCR setup:"); + console.log("Auditor:", auditor, "Key:", auditorKey.toHexString()); + console.log("Author:", author, "Key:", authorKey.toHexString()); + } } - function _setUpBCR() internal {} + function _isTestMode() internal pure virtual returns (bool) { + return false; + } function _uploadByteCode(bytes memory _initCode, bytes32 _contractType, uint256 _version) internal @@ -75,8 +85,9 @@ contract BCRHelpers is SignatureHelper { // Build auditor signature bytes32 signatureHash = keccak256( abi.encode( - keccak256("SignBytecodeHash(bytes32 bytecodeHash,string reportUrl)"), + keccak256("AuditReport(bytes32 bytecodeHash,address auditor,string reportUrl)"), bytecodeHash, + auditor, keccak256(bytes(reportUrl)) ) ); @@ -85,8 +96,10 @@ contract BCRHelpers is SignatureHelper { bytes memory signature = _sign(auditorKey, keccak256(abi.encodePacked("\x19\x01", _bytecodeDomainSeparator(), signatureHash))); - // Call signBytecodeHash with signature - IBytecodeRepository(bytecodeRepository).signBytecodeHash(bytecodeHash, reportUrl, signature); + AuditReport memory auditReport = AuditReport({auditor: auditor, reportUrl: reportUrl, signature: signature}); + + // Call submitAuditReport with signature + IBytecodeRepository(bytecodeRepository).submitAuditReport(bytecodeHash, auditReport); } function _bytecodeDomainSeparator() internal view returns (bytes32) { diff --git a/contracts/test/helpers/CCGHelper.sol b/contracts/test/helpers/CCGHelper.sol index 8d7fd58..3f1fbcd 100644 --- a/contracts/test/helpers/CCGHelper.sol +++ b/contracts/test/helpers/CCGHelper.sol @@ -5,18 +5,21 @@ pragma solidity ^0.8.23; import {SignatureHelper} from "./SignatureHelper.sol"; import {CrossChainMultisig} from "../../../contracts/global/CrossChainMultisig.sol"; -import {CrossChainCall, SignedProposal} from "../../../contracts/interfaces/ICrossChainMultisig.sol"; +import {CrossChainCall, SignedBatch} from "../../../contracts/interfaces/ICrossChainMultisig.sol"; import {console} from "forge-std/console.sol"; import {LibString} from "@solady/utils/LibString.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +import {console2} from "forge-std/console2.sol"; contract CCGHelper is SignatureHelper { using LibString for bytes; using LibString for uint256; // Core contracts - bytes32 constant PROPOSAL_TYPEHASH = keccak256("Proposal(string name,bytes32 proposalHash,bytes32 prevHash)"); + bytes32 constant COMPACT_BATCH_TYPEHASH = keccak256("CompactBatch(string name,bytes32 batchHash,bytes32 prevHash)"); CrossChainMultisig internal multisig; @@ -28,15 +31,26 @@ contract CCGHelper is SignatureHelper { address internal dao; - bytes32 prevProposalHash; + bytes32 prevBatchHash; constructor() { signer1Key = _generatePrivateKey("SIGNER_1"); signer2Key = _generatePrivateKey("SIGNER_2"); signer1 = vm.rememberKey(signer1Key); signer2 = vm.rememberKey(signer2Key); - dao = vm.rememberKey(_generatePrivateKey("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 { @@ -45,8 +59,6 @@ contract CCGHelper is SignatureHelper { initialSigners[0] = signer1; initialSigners[1] = signer2; - // EACH NETWORK SETUP - // Deploy CrossChainMultisig with 2 signers and threshold of 2 multisig = new CrossChainMultisig{salt: "SALT"}( initialSigners, @@ -54,57 +66,81 @@ contract CCGHelper is SignatureHelper { dao ); - prevProposalHash = 0; + prevBatchHash = 0; + } + + function _attachCCG() internal { + address ccg = computeCCGAddress(); + + if (ccg.code.length == 0) { + revert("CCG not deployed"); + } + multisig = CrossChainMultisig(ccg); + + prevBatchHash = multisig.lastBatchHash(); + } + + function computeCCGAddress() internal view returns (address) { + address[] memory initialSigners = new address[](2); + initialSigners[0] = signer1; + initialSigners[1] = signer2; + + bytes memory creationCode = + abi.encodePacked(type(CrossChainMultisig).creationCode, abi.encode(initialSigners, 2, dao)); + + return Create2.computeAddress( + bytes32("SALT"), keccak256(creationCode), address(0x4e59b44847b379578588920cA78FbF26c0B4956C) + ); } - function _submitProposal(string memory name, CrossChainCall[] memory calls) internal { + function _submitBatch(string memory name, CrossChainCall[] memory calls) internal { _startPrankOrBroadcast(dao); - multisig.submitProposal(name, calls, prevProposalHash); + multisig.submitBatch(name, calls, prevBatchHash); _stopPrankOrBroadcast(); } - function _signCurrentProposal() internal { - bytes32[] memory currentProposalHashes = multisig.getCurrentProposalHashes(); + function _signCurrentBatch() internal { + bytes32[] memory currentBatchHashes = multisig.getCurrentBatchHashes(); - SignedProposal memory currentProposal = multisig.getSignedProposal(currentProposalHashes[0]); + SignedBatch memory currentBatch = multisig.getBatch(currentBatchHashes[0]); - bytes32 proposalHash = - multisig.hashProposal(currentProposal.name, currentProposal.calls, currentProposal.prevHash); + bytes32 batchHash = multisig.computeBatchHash(currentBatch.name, currentBatch.calls, currentBatch.prevHash); - bytes32 structHash = keccak256( - abi.encode( - PROPOSAL_TYPEHASH, keccak256(bytes(currentProposal.name)), proposalHash, currentProposal.prevHash - ) - ); + bytes32 structHash = multisig.computeCompactBatchHash(currentBatch.name, batchHash, currentBatch.prevHash); - 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()); + multisig.signBatch(batchHash, signature1); + if (!_isTestMode()) { + console.log("== SIGNER 1 =="); + console.log("name", currentBatch.name); + console.log("batchHash"); + console.logBytes32(batchHash); + console.log("prevHash"); + console.logBytes32(currentBatch.prevHash); + console.log(signature1.toHexString()); + } bytes memory signature2 = _sign(signer2Key, ECDSA.toTypedDataHash(_ccmDomainSeparator(), structHash)); - multisig.signProposal(proposalHash, signature2); + multisig.signBatch(batchHash, signature2); - console.log("== SIGNER 2=="); - console.log("name", currentProposal.name); - console.log(signature2.toHexString()); + if (!_isTestMode()) { + console.log("== SIGNER 2=="); + console.log("name", currentBatch.name); + console.log(signature2.toHexString()); + } - prevProposalHash = proposalHash; + prevBatchHash = batchHash; } - function _submitProposalAndSign(string memory name, CrossChainCall[] memory calls) internal { - _submitProposal(name, calls); - _signCurrentProposal(); + function _submitBatchAndSign(string memory name, CrossChainCall[] memory calls) internal { + _submitBatch(name, calls); + _signCurrentBatch(); } function _ccmDomainSeparator() internal view returns (bytes32) { 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); + } +} diff --git a/contracts/test/helpers/GlobalSetup.sol b/contracts/test/helpers/GlobalSetup.sol index 4bc93c2..37813fb 100644 --- a/contracts/test/helpers/GlobalSetup.sol +++ b/contracts/test/helpers/GlobalSetup.sol @@ -10,6 +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 {Domain} from "../../libraries/Domain.sol"; import {IWETH} from "@gearbox-protocol/core-v3/contracts/interfaces/external/IWETH.sol"; import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; @@ -36,12 +37,21 @@ import { AP_INTEREST_RATE_MODEL_LINEAR, AP_RATE_KEEPER_TUMBLER, AP_RATE_KEEPER_GAUGE, + AP_LOSS_POLICY_ALIASED, AP_LOSS_POLICY_DEFAULT, AP_CREDIT_MANAGER, AP_CREDIT_FACADE, - AP_CREDIT_CONFIGURATOR + AP_CREDIT_CONFIGURATOR, + DOMAIN_ADAPTER, + DOMAIN_BOT, + DOMAIN_DEGEN_NFT, + DOMAIN_IRM, + DOMAIN_LOSS_POLICY, + DOMAIN_PRICE_FEED, + DOMAIN_RATE_KEEPER, + DOMAIN_ZAPPER } from "../../libraries/ContractLiterals.sol"; -import {SignedProposal, Bytecode} from "../../interfaces/Types.sol"; +import {SignedBatch, Bytecode} from "../../interfaces/Types.sol"; import {CreditFactory} from "../../factories/CreditFactory.sol"; import {InterestRateModelFactory} from "../../factories/InterestRateModelFactory.sol"; @@ -59,6 +69,7 @@ import {TreasurySplitter} from "../../market/TreasurySplitter.sol"; // Core contracts import {BotListV3} from "@gearbox-protocol/core-v3/contracts/core/BotListV3.sol"; +import {AliasedLossPolicyV3} from "@gearbox-protocol/core-v3/contracts/core/AliasedLossPolicyV3.sol"; import {GearStakingV3} from "@gearbox-protocol/core-v3/contracts/core/GearStakingV3.sol"; import {PoolV3} from "@gearbox-protocol/core-v3/contracts/pool/PoolV3.sol"; import {PoolQuotaKeeperV3} from "@gearbox-protocol/core-v3/contracts/pool/PoolQuotaKeeperV3.sol"; @@ -67,7 +78,6 @@ import {PriceOracleV3} from "@gearbox-protocol/core-v3/contracts/core/PriceOracl import {LinearInterestRateModelV3} from "@gearbox-protocol/core-v3/contracts/pool/LinearInterestRateModelV3.sol"; import {TumblerV3} from "@gearbox-protocol/core-v3/contracts/pool/TumblerV3.sol"; import {GaugeV3} from "@gearbox-protocol/core-v3/contracts/pool/GaugeV3.sol"; -import {DefaultLossPolicy} from "../../helpers/DefaultLossPolicy.sol"; import {CreditManagerV3} from "@gearbox-protocol/core-v3/contracts/credit/CreditManagerV3.sol"; import {CreditFacadeV3} from "@gearbox-protocol/core-v3/contracts/credit/CreditFacadeV3.sol"; import {CreditConfiguratorV3} from "@gearbox-protocol/core-v3/contracts/credit/CreditConfiguratorV3.sol"; @@ -120,8 +130,6 @@ import {CurveCryptoLPPriceFeed} from "@gearbox-protocol/oracles-v3/contracts/ora import {CurveStableLPPriceFeed} from "@gearbox-protocol/oracles-v3/contracts/oracles/curve/CurveStableLPPriceFeed.sol"; import {ERC4626PriceFeed} from "@gearbox-protocol/oracles-v3/contracts/oracles/erc4626/ERC4626PriceFeed.sol"; -import {console} from "forge-std/console.sol"; - struct UploadableContract { bytes initCode; bytes32 contractType; @@ -141,7 +149,10 @@ contract GlobalSetup is Test, InstanceManagerHelper { constructor() { _setCoreContracts(); _setAdapters(); + _setInterestRateModels(); + _setLossPolicies(); _setPriceFeeds(); + _setRateKeepers(); } function _setUpGlobalContracts() internal { @@ -149,21 +160,40 @@ contract GlobalSetup is Test, InstanceManagerHelper { CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = _generateAddAuditorCall(auditor, "Initial Auditor"); - - _submitProposalAndSign("Add Auditor", calls); + _submitBatchAndSign("Add auditor", calls); + + bytes32[8] memory publicDomains = [ + DOMAIN_ADAPTER, + DOMAIN_BOT, + DOMAIN_DEGEN_NFT, + DOMAIN_IRM, + DOMAIN_LOSS_POLICY, + DOMAIN_PRICE_FEED, + DOMAIN_RATE_KEEPER, + DOMAIN_ZAPPER + ]; + calls = new CrossChainCall[](publicDomains.length); + for (uint256 i = 0; i < publicDomains.length; ++i) { + calls[i] = _generateAddPublicDomainCall(publicDomains[i]); + } + _submitBatchAndSign("Add public domains", calls); uint256 len = contractsToUpload.length; - calls = new CrossChainCall[](len); - for (uint256 i = 0; i < len; ++i) { bytes32 bytecodeHash = _uploadByteCodeAndSign( contractsToUpload[i].initCode, contractsToUpload[i].contractType, contractsToUpload[i].version ); - calls[i] = _generateAllowSystemContractCall(bytecodeHash); - } - _submitProposalAndSign("Allow system contracts", calls); + bool isPublicContract = IBytecodeRepository(bytecodeRepository).isPublicDomain( + Domain.extractDomain(contractsToUpload[i].contractType) + ); + // NOTE: allowing public contracts doesn't require CCG permissions but it's convenient to execute in batch + calls[i] = isPublicContract + ? _generateAllowPublicContractCall(bytecodeHash) + : _generateAllowSystemContractCall(bytecodeHash); + } + _submitBatchAndSign("Allow contracts", calls); DeploySystemContractCall[10] memory deployCalls = [ DeploySystemContractCall({contractType: AP_BOT_LIST, version: 3_10, saveVersion: false}), @@ -177,17 +207,35 @@ contract GlobalSetup is Test, InstanceManagerHelper { DeploySystemContractCall({contractType: AP_RATE_KEEPER_FACTORY, version: 3_10, saveVersion: true}), DeploySystemContractCall({contractType: AP_LOSS_POLICY_FACTORY, version: 3_10, saveVersion: true}) ]; - len = deployCalls.length; - calls = new CrossChainCall[](len); for (uint256 i = 0; i < len; ++i) { calls[i] = _generateDeploySystemContractCall( deployCalls[i].contractType, deployCalls[i].version, deployCalls[i].saveVersion ); } + _submitBatchAndSign("Deploy system contracts", calls); + } + + function _attachGlobalContracts() internal { + _attachInstanceManager(); + } + + function _fundActors() internal { + address[6] memory actors = [instanceOwner, author, dao, auditor, signer1, signer2]; + for (uint256 i = 0; i < actors.length; ++i) { + payable(actors[i]).transfer(10 ether); + } + } + + function _exportJson() internal { + // Store address manager state as JSON + string memory json = vm.serializeAddress("addresses", "instanceManager", address(instanceManager)); + json = vm.serializeAddress("addresses", "bytecodeRepository", address(bytecodeRepository)); + json = vm.serializeAddress("addresses", "multisig", address(multisig)); + json = vm.serializeAddress("addresses", "addressProvider", address(instanceManager.addressProvider())); - _submitProposalAndSign("System contracts", calls); + vm.writeJson(json, "./addresses.json"); } function _setCoreContracts() internal { @@ -267,26 +315,6 @@ contract GlobalSetup is Test, InstanceManagerHelper { }) ); - contractsToUpload.push( - UploadableContract({ - initCode: type(LinearInterestRateModelV3).creationCode, - contractType: AP_INTEREST_RATE_MODEL_LINEAR, - version: 3_10 - }) - ); - - contractsToUpload.push( - UploadableContract({ - initCode: type(TumblerV3).creationCode, - contractType: AP_RATE_KEEPER_TUMBLER, - version: 3_10 - }) - ); - - contractsToUpload.push( - UploadableContract({initCode: type(GaugeV3).creationCode, contractType: AP_RATE_KEEPER_GAUGE, version: 3_10}) - ); - contractsToUpload.push( UploadableContract({initCode: type(BotListV3).creationCode, contractType: AP_BOT_LIST, version: 3_10}) ); @@ -315,14 +343,6 @@ contract GlobalSetup is Test, InstanceManagerHelper { }) ); - contractsToUpload.push( - UploadableContract({ - initCode: type(DefaultLossPolicy).creationCode, - contractType: AP_LOSS_POLICY_DEFAULT, - version: 3_10 - }) - ); - contractsToUpload.push( UploadableContract({ initCode: type(MarketConfigurator).creationCode, @@ -377,7 +397,6 @@ contract GlobalSetup is Test, InstanceManagerHelper { } function _setAdapters() internal { - // TODO: set adapters contractsToUpload.push( UploadableContract({ initCode: type(EqualizerRouterAdapter).creationCode, @@ -571,15 +590,34 @@ contract GlobalSetup is Test, InstanceManagerHelper { ); } + function _setInterestRateModels() internal { + contractsToUpload.push( + UploadableContract({ + initCode: type(LinearInterestRateModelV3).creationCode, + contractType: "IRM::LINEAR", + version: 3_10 + }) + ); + } + + function _setLossPolicies() internal { + contractsToUpload.push( + UploadableContract({ + initCode: type(AliasedLossPolicyV3).creationCode, + contractType: "LOSS_POLICY::ALIASED", + version: 3_10 + }) + ); + } + function _setPriceFeeds() internal { - // TODO: set price feeds - // contractsToUpload.push( - // UploadableContract({ - // initCode: type(BPTWeightedPriceFeed).creationCode, - // contractType: "PRICE_FEED::BALANCER_WEIGHTED", - // version: 3_10 - // }) - // ); + contractsToUpload.push( + UploadableContract({ + initCode: type(BPTWeightedPriceFeed).creationCode, + contractType: "PRICE_FEED::BALANCER_WEIGHTED", + version: 3_10 + }) + ); contractsToUpload.push( UploadableContract({ @@ -694,6 +732,20 @@ contract GlobalSetup is Test, InstanceManagerHelper { ); } + function _setRateKeepers() internal { + contractsToUpload.push( + UploadableContract({initCode: type(GaugeV3).creationCode, contractType: "RATE_KEEPER::GAUGE", version: 3_10}) + ); + + contractsToUpload.push( + UploadableContract({ + initCode: type(TumblerV3).creationCode, + contractType: "RATE_KEEPER::TUMBLER", + version: 3_10 + }) + ); + } + // function _setupPriceFeedStore() internal { // // _addPriceFeed(CHAINLINK_ETH_USD, 1 days); // // _addPriceFeed(CHAINLINK_USDC_USD, 1 days); diff --git a/contracts/test/helpers/InstanceManagerHelper.sol b/contracts/test/helpers/InstanceManagerHelper.sol index 3401710..27b5950 100644 --- a/contracts/test/helpers/InstanceManagerHelper.sol +++ b/contracts/test/helpers/InstanceManagerHelper.sol @@ -18,27 +18,63 @@ import {CrossChainCall} from "../../../contracts/interfaces/ICrossChainMultisig. import {IBytecodeRepository} from "../../../contracts/interfaces/IBytecodeRepository.sol"; import {IPriceFeedStore} from "../../../contracts/interfaces/IPriceFeedStore.sol"; import {IAddressProvider} from "../../../contracts/interfaces/IAddressProvider.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {console} from "forge-std/console.sol"; +import {LibString} from "@solady/utils/LibString.sol"; contract InstanceManagerHelper is BCRHelpers, CCGHelper { + using LibString for bytes32; + using LibString for uint256; // Core contracts - address internal instanceOwner; + + uint256 instanceOwnerKey; + address instanceOwner; InstanceManager internal instanceManager; constructor() { - instanceOwner = vm.rememberKey(_generatePrivateKey("INSTANCE_OWNER")); + instanceOwnerKey = _generatePrivateKey("INSTANCE_OWNER"); + instanceOwner = vm.rememberKey(instanceOwnerKey); + + 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 { _setUpCCG(); - _setUpBCR(); // Generate random private keys and derive addresses // Deploy InstanceManager owned by multisig - instanceManager = new InstanceManager(address(multisig)); + instanceManager = new InstanceManager{salt: bytes32("SALT")}(address(multisig)); + bytecodeRepository = instanceManager.bytecodeRepository(); + } + + function _attachInstanceManager() internal { + _attachCCG(); + address instanceManagerAddress = computeInstanceManagerAddress(); + + if (instanceManagerAddress.code.length == 0) { + revert("InstanceManager not deployed"); + } + + instanceManager = InstanceManager(instanceManagerAddress); bytecodeRepository = instanceManager.bytecodeRepository(); } + function computeInstanceManagerAddress() internal view returns (address) { + bytes memory creationCode = abi.encodePacked(type(InstanceManager).creationCode, abi.encode(multisig)); + + return Create2.computeAddress( + bytes32("SALT"), keccak256(creationCode), address(0x4e59b44847b379578588920cA78FbF26c0B4956C) + ); + } + function _generateAddAuditorCall(address _auditor, string memory _name) internal view @@ -55,6 +91,12 @@ contract InstanceManagerHelper is BCRHelpers, CCGHelper { ); } + function _generateAllowPublicContractCall(bytes32 _bytecodeHash) internal view returns (CrossChainCall memory) { + return _buildCrossChainCallDAO( + bytecodeRepository, abi.encodeCall(IBytecodeRepository.allowPublicContract, (_bytecodeHash)) + ); + } + function _generateDeploySystemContractCall(bytes32 _contractName, uint256 _version, bool _saveVersion) internal view @@ -67,6 +109,11 @@ contract InstanceManagerHelper is BCRHelpers, CCGHelper { }); } + function _generateAddPublicDomainCall(bytes32 domain) internal view returns (CrossChainCall memory) { + return + _buildCrossChainCallDAO(bytecodeRepository, abi.encodeCall(IBytecodeRepository.addPublicDomain, (domain))); + } + function _generateActivateCall( uint256 _chainId, address _instanceOwner, @@ -103,12 +150,12 @@ contract InstanceManagerHelper is BCRHelpers, CCGHelper { _stopPrankOrBroadcast(); } - function _addPriceFeed(address _priceFeed, uint32 _stalenessPeriod) internal { + function _addPriceFeed(address _priceFeed, uint32 _stalenessPeriod, string memory _name) internal { address ap = instanceManager.addressProvider(); address priceFeedStore = IAddressProvider(ap).getAddressOrRevert(AP_PRICE_FEED_STORE, NO_VERSION_CONTROL); _startPrankOrBroadcast(instanceOwner); instanceManager.configureLocal( - priceFeedStore, abi.encodeCall(IPriceFeedStore.addPriceFeed, (_priceFeed, _stalenessPeriod)) + priceFeedStore, abi.encodeCall(IPriceFeedStore.addPriceFeed, (_priceFeed, _stalenessPeriod, _name)) ); _stopPrankOrBroadcast(); } diff --git a/contracts/test/libraries/Domain.unit.t.sol b/contracts/test/libraries/Domain.unit.t.sol index 76a5350..3b89290 100644 --- a/contracts/test/libraries/Domain.unit.t.sol +++ b/contracts/test/libraries/Domain.unit.t.sol @@ -7,31 +7,84 @@ import {Test} from "forge-std/Test.sol"; import {Domain} from "../../libraries/Domain.sol"; contract DomainUnitTest is Test { - /// @notice Test extracting domain from string with domain separator - function test_Domain_01_extracts_domain_with_separator() public pure { - string memory input = "test::name"; - string memory domain = Domain.extractDomain(input); - assertEq(domain, "test"); + /// @notice Tests extracting domain from various inputs + function test_U_DOM_01_extracts_domain() public pure { + assertEq(Domain.extractDomain("test::name"), "test"); // With separator + assertEq(Domain.extractDomain("test"), "test"); // Without separator + assertEq(Domain.extractDomain(""), ""); // Empty string + assertEq(Domain.extractDomain("test::sub::name"), "test"); // Multiple separators } - /// @notice Test extracting domain from string without domain separator - function test_Domain_02_extracts_full_domain_without_separator() public pure { - string memory input = "test"; - string memory domain = Domain.extractDomain(input); - assertEq(domain, "test"); + /// @notice Tests extracting postfix from various inputs + function test_U_DOM_02_extracts_postfix() public pure { + assertEq(Domain.extractPostfix("test::name"), "name"); // With separator + assertEq(Domain.extractPostfix("test"), bytes32(0)); // Without separator + assertEq(Domain.extractDomain(""), ""); // Empty string + assertEq(Domain.extractPostfix("test::sub::name"), "sub::name"); // Multiple separators } - /// @notice Test extracting domain from empty string - function test_Domain_03_extracts_empty_domain_from_empty_string() public pure { - string memory input = ""; - string memory domain = Domain.extractDomain(input); - assertEq(domain, ""); + /// @notice Tests getting contract type with various inputs + function test_U_DOM_03_gets_contract_type() public pure { + assertEq(Domain.getContractType("test", "name"), "test::name"); // With domain and postfix + assertEq(Domain.getContractType("test", bytes32(0)), "test"); // With domain only + assertEq(Domain.getContractType(bytes32(0), "name"), "::name"); // With postfix only } - /// @notice Test extracting domain with multiple separators - function test_Domain_04_extracts_first_domain_with_multiple_separators() public pure { - string memory input = "test::sub::name"; - string memory domain = Domain.extractDomain(input); - assertEq(domain, "test"); + /// @notice Tests domain validation + function test_U_DOM_04_validates_domain() public pure { + // Valid domains + assertTrue(Domain.isValidDomain("test")); + assertTrue(Domain.isValidDomain("test123")); + assertTrue(Domain.isValidDomain("test_type")); + assertTrue(Domain.isValidDomain("testType")); + assertTrue(Domain.isValidDomain("123")); + assertTrue(Domain.isValidDomain("_")); + + // Invalid domains + assertFalse(Domain.isValidDomain("")); // empty + assertFalse(Domain.isValidDomain("test space")); // spaces + assertFalse(Domain.isValidDomain("test$")); // symbols + assertFalse(Domain.isValidDomain(unicode"тест")); // unicode + } + + /// @notice Tests postfix validation + function test_U_DOM_05_validates_postfix() public pure { + // Valid postfixes + assertTrue(Domain.isValidPostfix("name")); + assertTrue(Domain.isValidPostfix("name456")); + assertTrue(Domain.isValidPostfix("name_test")); + assertTrue(Domain.isValidPostfix("nameTest")); + assertTrue(Domain.isValidPostfix("456")); + assertTrue(Domain.isValidPostfix("_")); + assertTrue(Domain.isValidPostfix(bytes32(0))); // empty postfix is valid + + // Invalid postfixes + assertFalse(Domain.isValidPostfix("name space")); // spaces + assertFalse(Domain.isValidPostfix("name#")); // symbols + assertFalse(Domain.isValidPostfix(unicode"имя")); // unicode + } + + /// @notice Tests contract type validation + function test_U_DOM_06_validates_contract_type() public pure { + // Valid contract types + assertTrue(Domain.isValidContractType("test::name")); // with postfix + assertTrue(Domain.isValidContractType("test")); // without postfix + assertTrue(Domain.isValidContractType("test123::name456")); // with numbers + assertTrue(Domain.isValidContractType("test_type::name_test")); // with underscores + assertTrue(Domain.isValidContractType("testType::nameTest")); // mixed case + assertTrue(Domain.isValidContractType("123::456")); // only numbers + assertTrue(Domain.isValidContractType("_::_")); // only underscores + assertTrue(Domain.isValidContractType("a::b")); // single chars + + // Invalid contract types + assertFalse(Domain.isValidContractType("")); // empty + assertFalse(Domain.isValidContractType("test::sub::name")); // double nested + assertFalse(Domain.isValidContractType("test::")); // empty postfix after separator + assertFalse(Domain.isValidContractType("test space::name")); // invalid domain + assertFalse(Domain.isValidContractType("test::name space")); // invalid postfix + assertFalse(Domain.isValidContractType("test$::name")); // invalid domain symbol + assertFalse(Domain.isValidContractType("test::name#")); // invalid postfix symbol + assertFalse(Domain.isValidContractType(unicode"тест::name")); // invalid domain unicode + assertFalse(Domain.isValidContractType(unicode"test::имя")); // invalid postfix unicode } } diff --git a/contracts/test/libraries/NestedPriceFeeds.unit.t.sol b/contracts/test/libraries/NestedPriceFeeds.unit.t.sol new file mode 100644 index 0000000..cc03462 --- /dev/null +++ b/contracts/test/libraries/NestedPriceFeeds.unit.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol"; +import {NestedPriceFeeds} from "../../libraries/NestedPriceFeeds.sol"; +import { + MockPriceFeed, + MockSingleUnderlyingPriceFeed, + MockMultipleUnderlyingPriceFeed, + MockFallbackPriceFeed +} from "../mocks/MockPriceFeed.sol"; + +contract NestedPriceFeedsUnitTest is Test { + function test_U_NPF_01_getNestingType_works_as_expected() public { + // Test simple feed with no nesting + MockPriceFeed simpleFeed = new MockPriceFeed(); + assertEq( + uint256(NestedPriceFeeds.getNestingType(simpleFeed)), + uint256(NestedPriceFeeds.NestingType.NO_NESTING), + "Wrong nesting type for simple feed" + ); + + // Test single underlying + MockSingleUnderlyingPriceFeed singleFeed = new MockSingleUnderlyingPriceFeed(address(1)); + assertEq( + uint256(NestedPriceFeeds.getNestingType(singleFeed)), + uint256(NestedPriceFeeds.NestingType.SINGLE_UNDERLYING), + "Wrong nesting type for single underlying" + ); + + // Test multiple underlying + address[] memory underlyings = new address[](3); + underlyings[0] = makeAddr("feed0"); + underlyings[1] = makeAddr("feed1"); + underlyings[2] = makeAddr("feed2"); + MockMultipleUnderlyingPriceFeed multiFeed = new MockMultipleUnderlyingPriceFeed(underlyings); + assertEq( + uint256(NestedPriceFeeds.getNestingType(multiFeed)), + uint256(NestedPriceFeeds.NestingType.MULTIPLE_UNDERLYING), + "Wrong nesting type for multiple underlying" + ); + + // Test feed with fallback but no actual implementation + MockFallbackPriceFeed fallbackFeed = new MockFallbackPriceFeed(); + assertEq( + uint256(NestedPriceFeeds.getNestingType(fallbackFeed)), + uint256(NestedPriceFeeds.NestingType.NO_NESTING), + "Wrong nesting type for fallback feed" + ); + } + + function test_U_NPF_02_getUnderlyingFeeds_works_as_expected_with_no_nesting() public { + MockPriceFeed feed = new MockPriceFeed(); + address[] memory feeds = NestedPriceFeeds.getUnderlyingFeeds(feed); + assertEq(feeds.length, 0, "Should return empty array for non-nested feed"); + } + + function test_U_NPF_03_getUnderlyingFeeds_works_as_expected_with_single_underlying() public { + address underlying = makeAddr("underlying"); + MockSingleUnderlyingPriceFeed feed = new MockSingleUnderlyingPriceFeed(underlying); + + address[] memory feeds = NestedPriceFeeds.getUnderlyingFeeds(feed); + assertEq(feeds.length, 1, "Wrong number of feeds"); + assertEq(feeds[0], underlying, "Wrong underlying feed"); + } + + function test_U_NPF_04_getUnderlyingFeeds_works_as_expected_with_multiple_underlyings() public { + // Test regular case with 3 feeds + address[] memory underlyings = new address[](3); + for (uint256 i = 0; i < 3; ++i) { + underlyings[i] = makeAddr(string.concat("feed", vm.toString(i))); + } + + MockMultipleUnderlyingPriceFeed feed = new MockMultipleUnderlyingPriceFeed(underlyings); + address[] memory feeds = NestedPriceFeeds.getUnderlyingFeeds(feed); + assertEq(feeds.length, 3, "Wrong number of feeds"); + for (uint256 i = 0; i < 3; ++i) { + assertEq(feeds[i], underlyings[i], "Wrong underlying feed"); + } + + // Test max length case + underlyings = new address[](NestedPriceFeeds.MAX_UNDERLYING_PRICE_FEEDS); + for (uint256 i = 0; i < NestedPriceFeeds.MAX_UNDERLYING_PRICE_FEEDS; ++i) { + underlyings[i] = makeAddr(string.concat("feed", vm.toString(i))); + } + + feed = new MockMultipleUnderlyingPriceFeed(underlyings); + feeds = NestedPriceFeeds.getUnderlyingFeeds(feed); + assertEq(feeds.length, NestedPriceFeeds.MAX_UNDERLYING_PRICE_FEEDS, "Wrong number of feeds for max length"); + for (uint256 i = 0; i < NestedPriceFeeds.MAX_UNDERLYING_PRICE_FEEDS; ++i) { + assertEq(feeds[i], underlyings[i], "Wrong underlying feed for max length"); + } + + // Test array with zeros at the end + underlyings = new address[](NestedPriceFeeds.MAX_UNDERLYING_PRICE_FEEDS); + for (uint256 i = 0; i < 3; ++i) { + underlyings[i] = makeAddr(string.concat("feed", vm.toString(i))); + } + // rest are address(0) + + feed = new MockMultipleUnderlyingPriceFeed(underlyings); + feeds = NestedPriceFeeds.getUnderlyingFeeds(feed); + assertEq(feeds.length, 3, "Wrong number of feeds for partial array"); + for (uint256 i = 0; i < 3; ++i) { + assertEq(feeds[i], underlyings[i], "Wrong underlying feed for partial array"); + } + + // Test smaller array with zeros at the end + underlyings = new address[](5); + for (uint256 i = 0; i < 2; ++i) { + underlyings[i] = makeAddr(string.concat("feed", vm.toString(i))); + } + // rest are address(0) + + feed = new MockMultipleUnderlyingPriceFeed(underlyings); + feeds = NestedPriceFeeds.getUnderlyingFeeds(feed); + assertEq(feeds.length, 2, "Wrong number of feeds for smaller array"); + for (uint256 i = 0; i < 2; ++i) { + assertEq(feeds[i], underlyings[i], "Wrong underlying feed for smaller array"); + } + } +} diff --git a/contracts/test/mocks/MockCreditConfiguratorPatch.sol b/contracts/test/mocks/MockCreditConfiguratorPatch.sol new file mode 100644 index 0000000..eb155d1 --- /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 _creditManager) CreditConfiguratorV3(_creditManager) { + vm.mockCall(address(this), abi.encodeCall(IVersion.version, ()), abi.encode(311)); + } +} 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_; + } +} diff --git a/contracts/test/mocks/MockLossPolicy.sol b/contracts/test/mocks/MockLossPolicy.sol new file mode 100644 index 0000000..3dab24a --- /dev/null +++ b/contracts/test/mocks/MockLossPolicy.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +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"; + + AccessMode public override accessMode = AccessMode.Permissionless; + bool public override checksEnabled = true; + + constructor(address pool, address addressProvider) {} + + function serialize() external pure override returns (bytes memory) {} + + function isLiquidatableWithLoss(address, address, Params calldata) external view override returns (bool) { + return !checksEnabled && accessMode == AccessMode.Permissionless; + } + + function setAccessMode(AccessMode mode) external override { + accessMode = mode; + } + + function setChecksEnabled(bool enabled) external override { + checksEnabled = enabled; + } +} diff --git a/contracts/test/mocks/MockPriceFeed.sol b/contracts/test/mocks/MockPriceFeed.sol index 97e6b26..cb849dd 100644 --- a/contracts/test/mocks/MockPriceFeed.sol +++ b/contracts/test/mocks/MockPriceFeed.sol @@ -6,7 +6,7 @@ import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IP contract MockPriceFeed is IPriceFeed { uint256 private _lastUpdateTime; int256 private _price; - bytes32 private constant _CONTRACT_TYPE = "MOCK_PRICE_FEED"; + bytes32 private constant _CONTRACT_TYPE = "PRICE_FEED::MOCK"; uint256 private constant _VERSION = 1; constructor() { @@ -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; } @@ -55,3 +57,77 @@ contract MockPriceFeed is IPriceFeed { return false; } } + +contract MockFallbackPriceFeed is MockPriceFeed { + fallback() external {} +} + +contract MockUpdatablePriceFeed is MockPriceFeed { + bytes public lastUpdateData; + bool public constant updatable = true; + + function updatePrice(bytes calldata data) external { + lastUpdateData = data; + } +} + +contract MockSingleUnderlyingPriceFeed is MockPriceFeed { + address public immutable priceFeed; + + constructor(address _priceFeed) { + priceFeed = _priceFeed; + } +} + +contract MockMultipleUnderlyingPriceFeed is MockPriceFeed { + address[] public priceFeeds; + + constructor(address[] memory priceFeeds_) { + require(priceFeeds_.length <= 8, "Too many feeds"); + + bool seenZero; + for (uint256 i = 0; i < priceFeeds_.length; ++i) { + if (priceFeeds_[i] == address(0)) seenZero = true; + else require(!seenZero, "Non-zero feed after zero"); + } + + priceFeeds = priceFeeds_; + } + + function _getPriceFeed(uint256 index) internal view returns (address) { + if (index >= priceFeeds.length) revert("Not implemented"); + return priceFeeds[index]; + } + + function priceFeed0() external view returns (address) { + return _getPriceFeed(0); + } + + function priceFeed1() external view returns (address) { + return _getPriceFeed(1); + } + + function priceFeed2() external view returns (address) { + return _getPriceFeed(2); + } + + function priceFeed3() external view returns (address) { + return _getPriceFeed(3); + } + + function priceFeed4() external view returns (address) { + return _getPriceFeed(4); + } + + function priceFeed5() external view returns (address) { + return _getPriceFeed(5); + } + + function priceFeed6() external view returns (address) { + return _getPriceFeed(6); + } + + function priceFeed7() external view returns (address) { + return _getPriceFeed(7); + } +} diff --git a/contracts/test/mocks/MockPriceOraclePatch.sol b/contracts/test/mocks/MockPriceOraclePatch.sol new file mode 100644 index 0000000..a929b53 --- /dev/null +++ b/contracts/test/mocks/MockPriceOraclePatch.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {PriceOracleV3} from "@gearbox-protocol/core-v3/contracts/core/PriceOracleV3.sol"; +import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; + +contract MockPriceOraclePatch is PriceOracleV3, Test { + constructor(address _acl) PriceOracleV3(_acl) { + vm.mockCall(address(this), abi.encodeCall(IVersion.version, ()), abi.encode(311)); + } +} diff --git a/contracts/test/suite/NewChainDeploySuite.sol b/contracts/test/suite/NewChainDeploySuite.sol index 4e59420..3ca3e9e 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 {IConfigureActions} 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"; @@ -40,7 +40,7 @@ import { AP_CREDIT_CONFIGURATOR, NO_VERSION_CONTROL } from "../../libraries/ContractLiterals.sol"; -import {SignedProposal, Bytecode} from "../../interfaces/Types.sol"; +import {SignedBatch, Bytecode} from "../../interfaces/Types.sol"; import {CreditFactory} from "../../factories/CreditFactory.sol"; import {InterestRateModelFactory} from "../../factories/InterestRateModelFactory.sol"; @@ -67,9 +67,9 @@ 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"; contract NewChainDeploySuite is Test, GlobalSetup { address internal riskCurator; @@ -95,16 +95,28 @@ contract NewChainDeploySuite is Test, GlobalSetup { // activate instance CrossChainCall[] memory calls = new CrossChainCall[](1); calls[0] = _generateActivateCall(1, instanceOwner, TREASURY, WETH, GEAR); - _submitProposalAndSign("Activate instance", calls); + _submitBatchAndSign("Activate instance", calls); // 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] = _generateAllowPublicContractCall(bytecodeHash); + + _submitBatchAndSign("Allow public contracts", calls); } function _setupPriceFeedStore() internal { - _addPriceFeed(CHAINLINK_ETH_USD, 1 days); - _addPriceFeed(CHAINLINK_USDC_USD, 1 days); + _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); @@ -115,22 +127,18 @@ contract NewChainDeploySuite is Test, GlobalSetup { address mcf = IAddressProvider(ap).getAddressOrRevert(AP_MARKET_CONFIGURATOR_FACTORY, NO_VERSION_CONTROL); - address poolFactory = IAddressProvider(ap).getAddressOrRevert(AP_POOL_FACTORY, 3_10); - - IWETH(WETH).deposit{value: 1e18}(); - IERC20(WETH).transfer(poolFactory, 1e18); - uint256 gasBefore = gasleft(); vm.startPrank(riskCurator); address mc = MarketConfiguratorFactory(mcf).createMarketConfigurator( - riskCurator, riskCurator, riskCurator, "Test Risk Curator", false + riskCurator, riskCurator, "Test Risk Curator", false ); uint256 gasAfter = gasleft(); uint256 used = gasBefore - gasAfter; console.log("createMarketConfigurator gasUsed", used); + deal(WETH, mc, 1e5); address pool = MarketConfigurator(mc).previewCreateMarket(3_10, WETH, name, symbol); DeployParams memory interestRateModelParams = DeployParams({ @@ -141,7 +149,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(); @@ -182,14 +190,19 @@ contract NewChainDeploySuite is Test, GlobalSetup { bytes memory creditSuiteParams = abi.encode(creditManagerParams, facadeParams); + address cmExpected = + MarketConfigurator(mc).previewCreateCreditSuite(3_10, 3_10, WETH, name, symbol, creditSuiteParams); + address cm = MarketConfigurator(mc).createCreditSuite(3_10, pool, creditSuiteParams); + assertEq(cm, cmExpected); + address balancerVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; MarketConfigurator(mc).configureCreditSuite( cm, abi.encodeCall( - IConfigureActions.allowAdapter, (DeployParams("BALANCER_VAULT", 0, abi.encode(cm, balancerVault))) + ICreditConfigureActions.allowAdapter, (DeployParams("BALANCER_VAULT", 0, abi.encode(cm, balancerVault))) ) ); diff --git a/contracts/traits/DeployerTrait.sol b/contracts/traits/DeployerTrait.sol index 12576b2..7853cff 100644 --- a/contracts/traits/DeployerTrait.sol +++ b/contracts/traits/DeployerTrait.sol @@ -10,15 +10,13 @@ import {IBytecodeRepository} from "../interfaces/IBytecodeRepository.sol"; import {IDeployerTrait} from "../interfaces/base/IDeployerTrait.sol"; import {AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL} from "../libraries/ContractLiterals.sol"; +import {Domain} from "../libraries/Domain.sol"; abstract contract DeployerTrait is IDeployerTrait { using LibString for string; using LibString for bytes32; - /// @notice Address of the address provider address public immutable override addressProvider; - - /// @notice Address of the bytecode repository address public immutable override bytecodeRepository; constructor(address addressProvider_) { @@ -26,21 +24,16 @@ abstract contract DeployerTrait is IDeployerTrait { bytecodeRepository = _getAddressOrRevert(AP_BYTECODE_REPOSITORY, NO_VERSION_CONTROL); } - function _getAddressOrRevert(bytes32 key, uint256 version) internal view returns (address) { - return IAddressProvider(addressProvider).getAddressOrRevert(key, version); + function _getAddress(bytes32 key, uint256 version) internal view returns (address) { + return IAddressProvider(addressProvider).getAddress(key, version); } - function _tryGetAddress(bytes32 key, uint256 version) internal view returns (address) { - try IAddressProvider(addressProvider).getAddressOrRevert(key, version) returns (address result) { - return result; - } catch { - return address(0); - } + function _getAddressOrRevert(bytes32 key, uint256 version) internal view returns (address) { + return IAddressProvider(addressProvider).getAddressOrRevert(key, version); } function _getContractType(bytes32 domain, bytes32 postfix) internal pure returns (bytes32) { - if (postfix == 0) return domain; - return string.concat(domain.fromSmallString(), "::", postfix.fromSmallString()).toSmallString(); + return Domain.getContractType(domain, postfix); } function _deploy(bytes32 contractType, uint256 version, bytes memory constructorParams, bytes32 salt) diff --git a/contracts/traits/ImmutableOwnableTrait.sol b/contracts/traits/ImmutableOwnableTrait.sol index 139305a..234017d 100644 --- a/contracts/traits/ImmutableOwnableTrait.sol +++ b/contracts/traits/ImmutableOwnableTrait.sol @@ -5,20 +5,14 @@ pragma solidity ^0.8.23; import {IImmutableOwnableTrait} from "../interfaces/base/IImmutableOwnableTrait.sol"; -/// @title Immutable ownable trait -/// @notice Contract that adds immutable owner functionality when inherited abstract contract ImmutableOwnableTrait is IImmutableOwnableTrait { - /// @notice The immutable owner address address public immutable override owner; - /// @notice Modifier to restrict access to owner only modifier onlyOwner() { if (msg.sender != owner) revert CallerIsNotOwnerException(msg.sender); _; } - /// @notice Constructor - /// @param owner_ Immutable owner address constructor(address owner_) { owner = owner_; } diff --git a/foundry.toml b/foundry.toml index 705c314..9eaae3d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,5 +4,15 @@ libs = ["lib"] optimizer_runs = 1000 solc_version = '0.8.23' -evm_version = 'shanghai' + +block_base_fee_per_gas = 100000000 +block_number = 120000 +block_timestamp = 1640000000 +evm_version = 'shanghai' fs_permissions = [{access = "read-write", path = "./"}] +gas_limit = 9223372036854775807 # the gas limit in tests +bytecode_hash="none" + +[fuzz] +max_test_rejects = 1000000 +runs = 256 \ No newline at end of file diff --git a/generated.ts b/generated.ts deleted file mode 100644 index 5ee4657..0000000 --- a/generated.ts +++ /dev/null @@ -1,1633 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// AddressProvider -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -export const addressProviderAbi = [ - { - type: 'constructor', - inputs: [{ name: '_owner', internalType: 'address', type: 'address' }], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: '', internalType: 'string', type: 'string' }, - { name: '', internalType: 'uint256', type: 'uint256' }, - ], - name: 'addresses', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'contractType', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'key', internalType: 'string', type: 'string' }, - { name: '_version', internalType: 'uint256', type: 'uint256' }, - ], - name: 'getAddressOrRevert', - outputs: [{ name: 'result', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'key', internalType: 'bytes32', type: 'bytes32' }, - { name: '_version', internalType: 'uint256', type: 'uint256' }, - ], - name: 'getAddressOrRevert', - outputs: [{ name: 'result', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'getAllSavedContracts', - outputs: [ - { - name: '', - internalType: 'struct ContractValue[]', - type: 'tuple[]', - components: [ - { name: 'key', internalType: 'string', type: 'string' }, - { name: 'value', internalType: 'address', type: 'address' }, - { name: 'version', internalType: 'uint256', type: 'uint256' }, - ], - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'key', internalType: 'string', type: 'string' }, - { name: 'majorVersion', internalType: 'uint256', type: 'uint256' }, - ], - name: 'getLatestMinorVersion', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'key', internalType: 'string', type: 'string' }, - { name: 'minorVersion', internalType: 'uint256', type: 'uint256' }, - ], - name: 'getLatestPatchVersion', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'key', internalType: 'string', type: 'string' }], - name: 'getLatestVersion', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: '', internalType: 'string', type: 'string' }, - { name: '', internalType: 'uint256', type: 'uint256' }, - ], - name: 'latestMinorVersions', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: '', internalType: 'string', type: 'string' }, - { name: '', internalType: 'uint256', type: 'uint256' }, - ], - name: 'latestPatchVersions', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: '', internalType: 'string', type: 'string' }], - name: 'latestVersions', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'owner', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'key', internalType: 'string', type: 'string' }, - { name: 'value', internalType: 'address', type: 'address' }, - { name: 'saveVersion', internalType: 'bool', type: 'bool' }, - ], - name: 'setAddress', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'addr', internalType: 'address', type: 'address' }, - { name: 'saveVersion', internalType: 'bool', type: 'bool' }, - ], - name: 'setAddress', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'key', internalType: 'bytes32', type: 'bytes32' }, - { name: 'value', internalType: 'address', type: 'address' }, - { name: 'saveVersion', internalType: 'bool', type: 'bool' }, - ], - name: 'setAddress', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'version', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { name: 'key', internalType: 'string', type: 'string', indexed: true }, - { - name: 'version', - internalType: 'uint256', - type: 'uint256', - indexed: true, - }, - { - name: 'value', - internalType: 'address', - type: 'address', - indexed: true, - }, - ], - name: 'SetAddress', - }, - { type: 'error', inputs: [], name: 'AddressNotFoundException' }, - { - type: 'error', - inputs: [{ name: 'caller', internalType: 'address', type: 'address' }], - name: 'CallerIsNotOwnerException', - }, - { type: 'error', inputs: [], name: 'VersionNotFoundException' }, -] - -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// IBytecodeRepository -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -export const iBytecodeRepositoryAbi = [ - { - type: 'function', - inputs: [], - name: 'BYTECODE_TYPEHASH', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'auditor', internalType: 'address', type: 'address' }, - { name: 'name', internalType: 'string', type: 'string' }, - ], - name: 'addAuditor', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [{ name: 'domain', internalType: 'bytes32', type: 'bytes32' }], - name: 'addPublicDomain', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'allowSystemContract', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'allowedSystemContracts', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - { name: 'version', internalType: 'uint256', type: 'uint256' }, - ], - name: 'approvedBytecodeHash', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'auditor', internalType: 'address', type: 'address' }], - name: 'auditorName', - outputs: [{ name: '', internalType: 'string', type: 'string' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - { name: 'index', internalType: 'uint256', type: 'uint256' }, - ], - name: 'auditorSignaturesByHash', - outputs: [ - { - name: '', - internalType: 'struct AuditorSignature', - type: 'tuple', - components: [ - { name: 'reportUrl', internalType: 'string', type: 'string' }, - { name: 'auditor', internalType: 'address', type: 'address' }, - { name: 'signature', internalType: 'bytes', type: 'bytes' }, - ], - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'auditorSignaturesByHash', - outputs: [ - { - name: '', - internalType: 'struct AuditorSignature[]', - type: 'tuple[]', - components: [ - { name: 'reportUrl', internalType: 'string', type: 'string' }, - { name: 'auditor', internalType: 'address', type: 'address' }, - { name: 'signature', internalType: 'bytes', type: 'bytes' }, - ], - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'hash', internalType: 'bytes32', type: 'bytes32' }], - name: 'bytecodeByHash', - outputs: [ - { - name: '', - internalType: 'struct Bytecode', - type: 'tuple', - components: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - { name: 'version', internalType: 'uint256', type: 'uint256' }, - { name: 'initCode', internalType: 'bytes', type: 'bytes' }, - { name: 'author', internalType: 'address', type: 'address' }, - { name: 'source', internalType: 'string', type: 'string' }, - { name: 'authorSignature', internalType: 'bytes', type: 'bytes' }, - ], - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'type_', internalType: 'bytes32', type: 'bytes32' }, - { name: 'version_', internalType: 'uint256', type: 'uint256' }, - { name: 'constructorParams', internalType: 'bytes', type: 'bytes' }, - { name: 'salt', internalType: 'bytes32', type: 'bytes32' }, - { name: 'deployer', internalType: 'address', type: 'address' }, - ], - name: 'computeAddress', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { - name: 'bytecode', - internalType: 'struct Bytecode', - type: 'tuple', - components: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - { name: 'version', internalType: 'uint256', type: 'uint256' }, - { name: 'initCode', internalType: 'bytes', type: 'bytes' }, - { name: 'author', internalType: 'address', type: 'address' }, - { name: 'source', internalType: 'string', type: 'string' }, - { name: 'authorSignature', internalType: 'bytes', type: 'bytes' }, - ], - }, - ], - name: 'computeBytecodeHash', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'pure', - }, - { - type: 'function', - inputs: [], - name: 'contractType', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'contractTypeOwner', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'type_', internalType: 'bytes32', type: 'bytes32' }, - { name: 'version_', internalType: 'uint256', type: 'uint256' }, - { name: 'constructorParams', internalType: 'bytes', type: 'bytes' }, - { name: 'salt', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'deploy', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'contractAddress', internalType: 'address', type: 'address' }, - ], - name: 'deployedContracts', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'initCodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'forbidInitCode', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'initCodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'forbiddenInitCode', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'getAuditors', - outputs: [{ name: '', internalType: 'address[]', type: 'address[]' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'type_', internalType: 'bytes32', type: 'bytes32' }, - { name: 'majorVersion', internalType: 'uint256', type: 'uint256' }, - ], - name: 'getLatestMinorVersion', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'type_', internalType: 'bytes32', type: 'bytes32' }, - { name: 'minorVersion', internalType: 'uint256', type: 'uint256' }, - ], - name: 'getLatestPatchVersion', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'type_', internalType: 'bytes32', type: 'bytes32' }], - name: 'getLatestVersion', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'token', internalType: 'address', type: 'address' }], - name: 'getTokenSpecificPostfix', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'isAuditBytecode', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'auditor', internalType: 'address', type: 'address' }], - name: 'isAuditor', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'isBytecodeUploaded', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'isInPublicDomain', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'domain', internalType: 'bytes32', type: 'bytes32' }], - name: 'isPublicDomain', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'listPublicDomains', - outputs: [{ name: '', internalType: 'bytes32[]', type: 'bytes32[]' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'owner', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'auditor', internalType: 'address', type: 'address' }], - name: 'removeAuditor', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'removeContractTypeOwner', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [{ name: 'domain', internalType: 'bytes32', type: 'bytes32' }], - name: 'removePublicDomain', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [{ name: 'initCode', internalType: 'bytes', type: 'bytes' }], - name: 'revertIfInitCodeForbidden', - outputs: [], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - { name: 'version', internalType: 'uint256', type: 'uint256' }, - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'revokeApproval', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'token', internalType: 'address', type: 'address' }, - { name: 'postfix', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'setTokenSpecificPostfix', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - { name: 'reportUrl', internalType: 'string', type: 'string' }, - { name: 'signature', internalType: 'bytes', type: 'bytes' }, - ], - name: 'signBytecodeHash', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { - name: 'bytecode', - internalType: 'struct Bytecode', - type: 'tuple', - components: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - { name: 'version', internalType: 'uint256', type: 'uint256' }, - { name: 'initCode', internalType: 'bytes', type: 'bytes' }, - { name: 'author', internalType: 'address', type: 'address' }, - { name: 'source', internalType: 'string', type: 'string' }, - { name: 'authorSignature', internalType: 'bytes', type: 'bytes' }, - ], - }, - ], - name: 'uploadBytecode', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'version', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'auditor', - internalType: 'address', - type: 'address', - indexed: true, - }, - { name: 'name', internalType: 'string', type: 'string', indexed: false }, - ], - name: 'AddAuditor', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'domain', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - ], - name: 'AddPublicDomain', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'bytecodeHash', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - { - name: 'contractType', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - { - name: 'version', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - ], - name: 'ApproveContract', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'bytecodeHash', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - { - name: 'auditor', - internalType: 'address', - type: 'address', - indexed: true, - }, - { - name: 'reportUrl', - internalType: 'string', - type: 'string', - indexed: false, - }, - { - name: 'signature', - internalType: 'bytes', - type: 'bytes', - indexed: false, - }, - ], - name: 'AuditBytecode', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { name: 'addr', internalType: 'address', type: 'address', indexed: true }, - { - name: 'bytecodeHash', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - { - name: 'contractType', - internalType: 'string', - type: 'string', - indexed: false, - }, - { - name: 'version', - internalType: 'uint256', - type: 'uint256', - indexed: true, - }, - ], - name: 'DeployContact', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'bytecodeHash', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - ], - name: 'ForbidBytecode', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'auditor', - internalType: 'address', - type: 'address', - indexed: true, - }, - ], - name: 'RemoveAuditor', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'contractType', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - ], - name: 'RemoveContractTypeOwner', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'domain', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - ], - name: 'RemovePublicDomain', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'bytecodeHash', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - { - name: 'contractType', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - { - name: 'version', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - ], - name: 'RevokeApproval', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'token', - internalType: 'address', - type: 'address', - indexed: true, - }, - { - name: 'postfix', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - ], - name: 'SetTokenSpecificPostfix', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'bytecodeHash', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - { - name: 'contractType', - internalType: 'string', - type: 'string', - indexed: false, - }, - { - name: 'version', - internalType: 'uint256', - type: 'uint256', - indexed: true, - }, - { - name: 'author', - internalType: 'address', - type: 'address', - indexed: true, - }, - { - name: 'source', - internalType: 'string', - type: 'string', - indexed: false, - }, - ], - name: 'UploadBytecode', - }, - { type: 'error', inputs: [], name: 'AuditorAlreadyAddedException' }, - { type: 'error', inputs: [], name: 'AuditorAlreadySignedException' }, - { type: 'error', inputs: [], name: 'AuditorNotFoundException' }, - { - type: 'error', - inputs: [{ name: '', internalType: 'address', type: 'address' }], - name: 'BytecodeAlreadyExistsAtAddressException', - }, - { type: 'error', inputs: [], name: 'BytecodeAlreadyExistsException' }, - { - type: 'error', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'BytecodeForbiddenException', - }, - { - type: 'error', - inputs: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - { name: 'version', internalType: 'uint256', type: 'uint256' }, - ], - name: 'BytecodeIsNotApprovedException', - }, - { type: 'error', inputs: [], name: 'BytecodeIsNotAuditedException' }, - { - type: 'error', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'BytecodeIsNotUploadedException', - }, - { - type: 'error', - inputs: [{ name: 'caller', internalType: 'address', type: 'address' }], - name: 'CallerIsNotOwnerException', - }, - { type: 'error', inputs: [], name: 'ContractIsNotAuditedException' }, - { - type: 'error', - inputs: [], - name: 'ContractTypeVersionAlreadyExistsException', - }, - { type: 'error', inputs: [], name: 'EmptyBytecodeException' }, - { - type: 'error', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'IncorrectBytecodeException', - }, - { type: 'error', inputs: [], name: 'InvalidAuthorSignatureException' }, - { type: 'error', inputs: [], name: 'NoValidAuditorPermissionsAException' }, - { type: 'error', inputs: [], name: 'NoValidAuditorSignatureException' }, - { - type: 'error', - inputs: [ - { name: 'bytecodeHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'NotAllowedSystemContractException', - }, - { type: 'error', inputs: [], name: 'NotDeployerException' }, - { type: 'error', inputs: [], name: 'NotDomainOwnerException' }, - { type: 'error', inputs: [], name: 'OnlyAuthorCanSyncException' }, - { - type: 'error', - inputs: [{ name: 'signer', internalType: 'address', type: 'address' }], - name: 'SignerIsNotAuditorException', - }, - { - type: 'error', - inputs: [{ name: '', internalType: 'string', type: 'string' }], - name: 'TooLongContractTypeException', - }, -] - -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// ICrossChainMultisig -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -export const iCrossChainMultisigAbi = [ - { - type: 'function', - inputs: [{ name: 'signer', internalType: 'address', type: 'address' }], - name: 'addSigner', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'confirmationThreshold', - outputs: [{ name: '', internalType: 'uint8', type: 'uint8' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'contractType', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'domainSeparatorV4', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { - name: 'proposal', - internalType: 'struct SignedProposal', - type: 'tuple', - components: [ - { name: 'name', internalType: 'string', type: 'string' }, - { name: 'prevHash', internalType: 'bytes32', type: 'bytes32' }, - { - name: 'calls', - internalType: 'struct CrossChainCall[]', - type: 'tuple[]', - components: [ - { name: 'chainId', internalType: 'uint256', type: 'uint256' }, - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'callData', internalType: 'bytes', type: 'bytes' }, - ], - }, - { name: 'signatures', internalType: 'bytes[]', type: 'bytes[]' }, - ], - }, - ], - name: 'executeProposal', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'getCurrentProposals', - outputs: [ - { - name: '', - internalType: 'struct SignedProposal[]', - type: 'tuple[]', - components: [ - { name: 'name', internalType: 'string', type: 'string' }, - { name: 'prevHash', internalType: 'bytes32', type: 'bytes32' }, - { - name: 'calls', - internalType: 'struct CrossChainCall[]', - type: 'tuple[]', - components: [ - { name: 'chainId', internalType: 'uint256', type: 'uint256' }, - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'callData', internalType: 'bytes', type: 'bytes' }, - ], - }, - { name: 'signatures', internalType: 'bytes[]', type: 'bytes[]' }, - ], - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'getExecutedProposalHashes', - outputs: [{ name: '', internalType: 'bytes32[]', type: 'bytes32[]' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'getExecutedProposals', - outputs: [ - { - name: '', - internalType: 'struct SignedProposal[]', - type: 'tuple[]', - components: [ - { name: 'name', internalType: 'string', type: 'string' }, - { name: 'prevHash', internalType: 'bytes32', type: 'bytes32' }, - { - name: 'calls', - internalType: 'struct CrossChainCall[]', - type: 'tuple[]', - components: [ - { name: 'chainId', internalType: 'uint256', type: 'uint256' }, - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'callData', internalType: 'bytes', type: 'bytes' }, - ], - }, - { name: 'signatures', internalType: 'bytes[]', type: 'bytes[]' }, - ], - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'proposalHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'getProposal', - outputs: [ - { - name: '', - internalType: 'struct SignedProposal', - type: 'tuple', - components: [ - { name: 'name', internalType: 'string', type: 'string' }, - { name: 'prevHash', internalType: 'bytes32', type: 'bytes32' }, - { - name: 'calls', - internalType: 'struct CrossChainCall[]', - type: 'tuple[]', - components: [ - { name: 'chainId', internalType: 'uint256', type: 'uint256' }, - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'callData', internalType: 'bytes', type: 'bytes' }, - ], - }, - { name: 'signatures', internalType: 'bytes[]', type: 'bytes[]' }, - ], - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'proposalHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'getSignedProposal', - outputs: [ - { - name: '', - internalType: 'struct SignedProposal', - type: 'tuple', - components: [ - { name: 'name', internalType: 'string', type: 'string' }, - { name: 'prevHash', internalType: 'bytes32', type: 'bytes32' }, - { - name: 'calls', - internalType: 'struct CrossChainCall[]', - type: 'tuple[]', - components: [ - { name: 'chainId', internalType: 'uint256', type: 'uint256' }, - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'callData', internalType: 'bytes', type: 'bytes' }, - ], - }, - { name: 'signatures', internalType: 'bytes[]', type: 'bytes[]' }, - ], - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'getSigners', - outputs: [{ name: '', internalType: 'address[]', type: 'address[]' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'name', internalType: 'string', type: 'string' }, - { - name: 'calls', - internalType: 'struct CrossChainCall[]', - type: 'tuple[]', - components: [ - { name: 'chainId', internalType: 'uint256', type: 'uint256' }, - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'callData', internalType: 'bytes', type: 'bytes' }, - ], - }, - { name: 'prevHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'hashProposal', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'account', internalType: 'address', type: 'address' }], - name: 'isSigner', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'lastProposalHash', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'signer', internalType: 'address', type: 'address' }], - name: 'removeSigner', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [{ name: 'newThreshold', internalType: 'uint8', type: 'uint8' }], - name: 'setConfirmationThreshold', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'proposalHash', internalType: 'bytes32', type: 'bytes32' }, - { name: 'signature', internalType: 'bytes', type: 'bytes' }, - ], - name: 'signProposal', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'name', internalType: 'string', type: 'string' }, - { - name: 'calls', - internalType: 'struct CrossChainCall[]', - type: 'tuple[]', - components: [ - { name: 'chainId', internalType: 'uint256', type: 'uint256' }, - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'callData', internalType: 'bytes', type: 'bytes' }, - ], - }, - { name: 'prevHash', internalType: 'bytes32', type: 'bytes32' }, - ], - name: 'submitProposal', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'version', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'signer', - internalType: 'address', - type: 'address', - indexed: true, - }, - ], - name: 'AddSigner', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'proposalHash', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - ], - name: 'ExecuteProposal', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'signer', - internalType: 'address', - type: 'address', - indexed: true, - }, - ], - name: 'RemoveSigner', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'newconfirmationThreshold', - internalType: 'uint8', - type: 'uint8', - indexed: false, - }, - ], - name: 'SetConfirmationThreshold', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'proposalHash', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - { - name: 'signer', - internalType: 'address', - type: 'address', - indexed: true, - }, - ], - name: 'SignProposal', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'proposalHash', - internalType: 'bytes32', - type: 'bytes32', - indexed: true, - }, - ], - name: 'SubmitProposal', - }, - { type: 'error', inputs: [], name: 'AlreadySignedException' }, - { type: 'error', inputs: [], name: 'CantBeExecutedOnCurrentChainException' }, - { - type: 'error', - inputs: [], - name: 'InconsistentSelfCallOnOtherChainException', - }, - { - type: 'error', - inputs: [], - name: 'InvalidConfirmationThresholdValueException', - }, - { type: 'error', inputs: [], name: 'InvalidPrevHashException' }, - { type: 'error', inputs: [], name: 'InvalidconfirmationThresholdException' }, - { type: 'error', inputs: [], name: 'NoCallsInProposalException' }, - { type: 'error', inputs: [], name: 'NotEnoughSignaturesException' }, - { type: 'error', inputs: [], name: 'OnlySelfException' }, - { type: 'error', inputs: [], name: 'ProposalDoesNotExistException' }, - { type: 'error', inputs: [], name: 'SignerAlreadyExistsException' }, - { type: 'error', inputs: [], name: 'SignerDoesNotExistException' }, -] - -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// IInstanceManager -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -export const iInstanceManagerAbi = [ - { - type: 'function', - inputs: [ - { name: 'instanceOwner', internalType: 'address', type: 'address' }, - { name: 'treasury', internalType: 'address', type: 'address' }, - { name: 'weth', internalType: 'address', type: 'address' }, - { name: 'gear', internalType: 'address', type: 'address' }, - ], - name: 'activate', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'addressProvider', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'bytecodeRepository', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'data', internalType: 'bytes', type: 'bytes' }, - ], - name: 'configureGlobal', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'data', internalType: 'bytes', type: 'bytes' }, - ], - name: 'configureLocal', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'target', internalType: 'address', type: 'address' }, - { name: 'data', internalType: 'bytes', type: 'bytes' }, - ], - name: 'configureTreasury', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'contractType', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'crossChainGovernanceProxy', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'contractType', internalType: 'bytes32', type: 'bytes32' }, - { name: 'version', internalType: 'uint256', type: 'uint256' }, - { name: 'saveVersion', internalType: 'bool', type: 'bool' }, - ], - name: 'deploySystemContract', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'instanceManagerProxy', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'isActivated', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'key', internalType: 'string', type: 'string' }, - { name: 'addr', internalType: 'address', type: 'address' }, - { name: 'saveVersion', internalType: 'bool', type: 'bool' }, - ], - name: 'setGlobalAddress', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'key', internalType: 'string', type: 'string' }, - { name: 'addr', internalType: 'address', type: 'address' }, - { name: 'saveVersion', internalType: 'bool', type: 'bool' }, - ], - name: 'setLocalAddress', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'treasuryProxy', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'version', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, -] - -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// IPriceFeedStore -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -export const iPriceFeedStoreAbi = [ - { - type: 'function', - inputs: [ - { name: 'priceFeed', internalType: 'address', type: 'address' }, - { name: 'stalenessPeriod', internalType: 'uint32', type: 'uint32' }, - ], - name: 'addPriceFeed', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'token', internalType: 'address', type: 'address' }, - { name: 'priceFeed', internalType: 'address', type: 'address' }, - ], - name: 'allowPriceFeed', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'contractType', - outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'token', internalType: 'address', type: 'address' }, - { name: 'priceFeed', internalType: 'address', type: 'address' }, - ], - name: 'forbidPriceFeed', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'token', internalType: 'address', type: 'address' }, - { name: 'priceFeed', internalType: 'address', type: 'address' }, - ], - name: 'getAllowanceTimestamp', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'token', internalType: 'address', type: 'address' }], - name: 'getPriceFeeds', - outputs: [{ name: '', internalType: 'address[]', type: 'address[]' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [{ name: 'priceFeed', internalType: 'address', type: 'address' }], - name: 'getStalenessPeriod', - outputs: [{ name: '', internalType: 'uint32', type: 'uint32' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'token', internalType: 'address', type: 'address' }, - { name: 'priceFeed', internalType: 'address', type: 'address' }, - ], - name: 'isAllowedPriceFeed', - outputs: [{ name: '', internalType: 'bool', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'owner', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [ - { name: 'priceFeed', internalType: 'address', type: 'address' }, - { name: 'stalenessPeriod', internalType: 'uint32', type: 'uint32' }, - ], - name: 'setStalenessPeriod', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [], - name: 'version', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'priceFeed', - internalType: 'address', - type: 'address', - indexed: false, - }, - { - name: 'stalenessPeriod', - internalType: 'uint32', - type: 'uint32', - indexed: false, - }, - ], - name: 'AddPriceFeed', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'token', - internalType: 'address', - type: 'address', - indexed: false, - }, - { - name: 'priceFeed', - internalType: 'address', - type: 'address', - indexed: false, - }, - ], - name: 'AllowPriceFeed', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'token', - internalType: 'address', - type: 'address', - indexed: false, - }, - { - name: 'priceFeed', - internalType: 'address', - type: 'address', - indexed: false, - }, - ], - name: 'ForbidPriceFeed', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'priceFeed', - internalType: 'address', - type: 'address', - indexed: false, - }, - { - name: 'stalenessPeriod', - internalType: 'uint32', - type: 'uint32', - indexed: false, - }, - ], - name: 'SetStalenessPeriod', - }, - { - type: 'error', - inputs: [{ name: 'caller', internalType: 'address', type: 'address' }], - name: 'CallerIsNotOwnerException', - }, - { - type: 'error', - inputs: [{ name: 'priceFeed', internalType: 'address', type: 'address' }], - name: 'PriceFeedAlreadyAddedException', - }, - { - type: 'error', - inputs: [ - { name: 'token', internalType: 'address', type: 'address' }, - { name: 'priceFeed', internalType: 'address', type: 'address' }, - ], - name: 'PriceFeedIsNotAllowedException', - }, - { - type: 'error', - inputs: [{ name: 'priceFeed', internalType: 'address', type: 'address' }], - name: 'PriceFeedNotKnownException', - }, -] diff --git a/lib/@gearbox-protocol/core-v3 b/lib/@gearbox-protocol/core-v3 index 66f9b8e..a94cb84 160000 --- a/lib/@gearbox-protocol/core-v3 +++ b/lib/@gearbox-protocol/core-v3 @@ -1 +1 @@ -Subproject commit 66f9b8e7be833964c935cd43d1e8002b9f08d9ea +Subproject commit a94cb842d221594fff4fac92253d316c24bcad7e 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 diff --git a/package.json b/package.json index e0493ff..2c3ac03 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "@gearbox-protocol/governance", "version": "1.0.0", "description": "Gearbox Protocol governance contracts", + "type": "module", "files": [ "contracts" ], @@ -15,6 +16,8 @@ "@wagmi/cli": "^2.1.22" }, "dependencies": { - "@redstone-finance/evm-connector": "^0.2.5" - } + "@redstone-finance/evm-connector": "^0.2.5", + "tsx": "^4.19.2" + }, + "packageManager": "pnpm@9.15.4" } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4040ec7..5da78fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@redstone-finance/evm-connector': specifier: ^0.2.5 version: 0.2.6 + tsx: + specifier: ^4.19.2 + version: 4.19.2 devDependencies: '@chainlink/contracts': specifier: 0.4.0 @@ -42,138 +45,282 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.19.12': resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.19.12': resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.19.12': resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.19.12': resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.19.12': resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.19.12': resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.19.12': resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.19.12': resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.19.12': resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.19.12': resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.19.12': resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.19.12': resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.19.12': resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.19.12': resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.19.12': resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.19.12': resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-x64@0.19.12': resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.19.12': resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.19.12': resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.19.12': resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.19.12': resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.19.12': resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eth-optimism/contracts@0.5.40': resolution: {integrity: sha512-MrzV0nvsymfO/fursTB7m/KunkPsCndltVgfdHaT1Aj5Vi6R/doKIGGkOofHX+8B6VMZpuZosKCMQ5lQuqjt8w==} peerDependencies: @@ -444,6 +591,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -482,9 +634,17 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -579,12 +739,20 @@ packages: peerDependencies: ethers: 5.x.x + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + scrypt-js@3.0.1: resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==} tslib@2.4.0: resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + type-detect@4.1.0: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} @@ -661,72 +829,144 @@ snapshots: '@esbuild/aix-ppc64@0.19.12': optional: true + '@esbuild/aix-ppc64@0.23.1': + optional: true + '@esbuild/android-arm64@0.19.12': optional: true + '@esbuild/android-arm64@0.23.1': + optional: true + '@esbuild/android-arm@0.19.12': optional: true + '@esbuild/android-arm@0.23.1': + optional: true + '@esbuild/android-x64@0.19.12': optional: true + '@esbuild/android-x64@0.23.1': + optional: true + '@esbuild/darwin-arm64@0.19.12': optional: true + '@esbuild/darwin-arm64@0.23.1': + optional: true + '@esbuild/darwin-x64@0.19.12': optional: true + '@esbuild/darwin-x64@0.23.1': + optional: true + '@esbuild/freebsd-arm64@0.19.12': optional: true + '@esbuild/freebsd-arm64@0.23.1': + optional: true + '@esbuild/freebsd-x64@0.19.12': optional: true + '@esbuild/freebsd-x64@0.23.1': + optional: true + '@esbuild/linux-arm64@0.19.12': optional: true + '@esbuild/linux-arm64@0.23.1': + optional: true + '@esbuild/linux-arm@0.19.12': optional: true + '@esbuild/linux-arm@0.23.1': + optional: true + '@esbuild/linux-ia32@0.19.12': optional: true + '@esbuild/linux-ia32@0.23.1': + optional: true + '@esbuild/linux-loong64@0.19.12': optional: true + '@esbuild/linux-loong64@0.23.1': + optional: true + '@esbuild/linux-mips64el@0.19.12': optional: true + '@esbuild/linux-mips64el@0.23.1': + optional: true + '@esbuild/linux-ppc64@0.19.12': optional: true + '@esbuild/linux-ppc64@0.23.1': + optional: true + '@esbuild/linux-riscv64@0.19.12': optional: true + '@esbuild/linux-riscv64@0.23.1': + optional: true + '@esbuild/linux-s390x@0.19.12': optional: true + '@esbuild/linux-s390x@0.23.1': + optional: true + '@esbuild/linux-x64@0.19.12': optional: true + '@esbuild/linux-x64@0.23.1': + optional: true + '@esbuild/netbsd-x64@0.19.12': optional: true + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + '@esbuild/openbsd-x64@0.19.12': optional: true + '@esbuild/openbsd-x64@0.23.1': + optional: true + '@esbuild/sunos-x64@0.19.12': optional: true + '@esbuild/sunos-x64@0.23.1': + optional: true + '@esbuild/win32-arm64@0.19.12': optional: true + '@esbuild/win32-arm64@0.23.1': + optional: true + '@esbuild/win32-ia32@0.19.12': optional: true + '@esbuild/win32-ia32@0.23.1': + optional: true + '@esbuild/win32-x64@0.19.12': optional: true + '@esbuild/win32-x64@0.23.1': + optional: true + '@eth-optimism/contracts@0.5.40(ethers@5.7.2)': dependencies: '@eth-optimism/core-utils': 0.12.0 @@ -1219,6 +1459,33 @@ snapshots: '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.2.0: {} ethereum-multicall@2.26.0: @@ -1292,8 +1559,15 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + fsevents@2.3.3: + optional: true + get-func-name@2.0.2: {} + get-tsconfig@4.10.0: + dependencies: + resolve-pkg-maps: 1.0.0 + hash.js@1.1.7: dependencies: inherits: 2.0.4 @@ -1393,10 +1667,19 @@ snapshots: - debug - utf-8-validate + resolve-pkg-maps@1.0.0: {} + scrypt-js@3.0.1: {} tslib@2.4.0: {} + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.10.0 + optionalDependencies: + fsevents: 2.3.3 + type-detect@4.1.0: {} viem@2.22.8(zod@3.24.1): diff --git a/script/CCGTest.s.sol b/script/CCGTest.s.sol new file mode 100644 index 0000000..1d3c2c8 --- /dev/null +++ b/script/CCGTest.s.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {Script, console2} from "forge-std/Script.sol"; + +import {CCGHelper} from "../contracts/test/helpers/CCGHelper.sol"; +import {GlobalSetup} from "../contracts/test/helpers/GlobalSetup.sol"; + +import {AP_POOL_FACTORY} from "../contracts/libraries/ContractLiterals.sol"; +import {CrossChainCall} from "../contracts/interfaces/Types.sol"; + +contract CCGTestScript is Script, GlobalSetup { + function run() public { + vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY")); + + _attachGlobalContracts(); + + CrossChainCall[] memory calls = new CrossChainCall[](1); + calls[0] = _generateDeploySystemContractCall(AP_POOL_FACTORY, 3_10, true); + + _submitBatch("Testtt", calls); + } +} diff --git a/script/Permissionless.s.sol b/script/Permissionless.s.sol index b2e3f5c..5b337eb 100644 --- a/script/Permissionless.s.sol +++ b/script/Permissionless.s.sol @@ -7,30 +7,14 @@ import {CCGHelper} from "../contracts/test/helpers/CCGHelper.sol"; import {GlobalSetup} from "../contracts/test/helpers/GlobalSetup.sol"; contract PermissionlessScript is Script, GlobalSetup { - function setUp() public {} - function run() public { vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY")); _fundActors(); _setUpGlobalContracts(); - console2.log("DONE"); vm.stopBroadcast(); - // Store address manager state as JSON - string memory json = vm.serializeAddress("addresses", "instanceManager", address(instanceManager)); - json = vm.serializeAddress("addresses", "bytecodeRepository", address(bytecodeRepository)); - json = vm.serializeAddress("addresses", "multisig", address(multisig)); - json = vm.serializeAddress("addresses", "addressProvider", address(instanceManager.addressProvider())); - - vm.writeJson(json, "./addresses.json"); - } - - function _fundActors() internal { - address[6] memory actors = [instanceOwner, author, dao, auditor, signer1, signer2]; - for (uint256 i = 0; i < actors.length; ++i) { - payable(actors[i]).transfer(10 ether); - } + _exportJson(); } } diff --git a/wagmi.config.js b/wagmi.config.js index 82b3de0..69b65e5 100644 --- a/wagmi.config.js +++ b/wagmi.config.js @@ -10,8 +10,13 @@ export default defineConfig({ "AddressProvider.sol/AddressProvider.json", "IBytecodeRepository.sol/**.json", "ICrossChainMultisig.sol/**.json", - "IPriceFeedStore.sol/**.json", + "PriceFeedStore.sol/**.json", "IInstanceManager.sol/**.json", + "IMarketConfiguratorFactory.sol/**.json", + "IMarketConfigurator.sol/**.json", + "IPoolConfigureActions.sol/**.json", + "IPriceOracleConfigureActions.sol/**.json", + "ICreditConfigureActions.sol/**.json", ], }), ], diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index ffdb8a3..0000000 --- a/yarn.lock +++ /dev/null @@ -1,1193 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@adraffy/ens-normalize@1.10.1": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" - integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== - -"@adraffy/ens-normalize@^1.10.1": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" - integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== - -"@chainlink/contracts@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@chainlink/contracts/-/contracts-0.4.0.tgz#4bcfd486d02cdc73047f27904119d0f8cb7291f4" - integrity sha512-yZGeCBd7d+qxfw9r/JxtPzsW2kCc6MorPRZ/tDKnaJI98H99j5P2Fosfehmcwk6wVZlz+0Bp4kS1y480nw3Zow== - -"@chainlink/contracts@^0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@chainlink/contracts/-/contracts-0.6.1.tgz#8842b57e755793cbdbcbc45277fb5d179c993e19" - integrity sha512-EuwijGexttw0UjfrW+HygwhQIrGAbqpf1ue28R55HhWMHBzphEH0PhWm8DQmFfj5OZNy8Io66N4L0nStkZ3QKQ== - dependencies: - "@eth-optimism/contracts" "^0.5.21" - "@openzeppelin/contracts" "~4.3.3" - "@openzeppelin/contracts-upgradeable" "^4.7.3" - "@openzeppelin/contracts-v0.7" "npm:@openzeppelin/contracts@v3.4.2" - -"@esbuild/aix-ppc64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" - integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== - -"@esbuild/android-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" - integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== - -"@esbuild/android-arm@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" - integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== - -"@esbuild/android-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" - integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== - -"@esbuild/darwin-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" - integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== - -"@esbuild/darwin-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" - integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== - -"@esbuild/freebsd-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" - integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== - -"@esbuild/freebsd-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" - integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== - -"@esbuild/linux-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" - integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== - -"@esbuild/linux-arm@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" - integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== - -"@esbuild/linux-ia32@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" - integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== - -"@esbuild/linux-loong64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" - integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== - -"@esbuild/linux-mips64el@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" - integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== - -"@esbuild/linux-ppc64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" - integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== - -"@esbuild/linux-riscv64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" - integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== - -"@esbuild/linux-s390x@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" - integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== - -"@esbuild/linux-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" - integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== - -"@esbuild/netbsd-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" - integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== - -"@esbuild/openbsd-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" - integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== - -"@esbuild/sunos-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" - integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== - -"@esbuild/win32-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" - integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== - -"@esbuild/win32-ia32@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" - integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== - -"@esbuild/win32-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" - integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== - -"@eth-optimism/contracts@^0.5.21": - version "0.5.40" - resolved "https://registry.yarnpkg.com/@eth-optimism/contracts/-/contracts-0.5.40.tgz#d13a04a15ea947a69055e6fc74d87e215d4c936a" - integrity sha512-MrzV0nvsymfO/fursTB7m/KunkPsCndltVgfdHaT1Aj5Vi6R/doKIGGkOofHX+8B6VMZpuZosKCMQ5lQuqjt8w== - dependencies: - "@eth-optimism/core-utils" "0.12.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/abstract-signer" "^5.7.0" - -"@eth-optimism/core-utils@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.12.0.tgz#6337e4599a34de23f8eceb20378de2a2de82b0ea" - integrity sha512-qW+7LZYCz7i8dRa7SRlUKIo1VBU8lvN0HeXCxJR+z+xtMzMQpPds20XJNCMclszxYQHkXY00fOT6GvFw9ZL6nw== - dependencies: - "@ethersproject/abi" "^5.7.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/contracts" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/providers" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/web" "^5.7.0" - bufio "^1.0.7" - chai "^4.3.4" - -"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" - integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== - dependencies: - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" - integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/networks" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/web" "^5.7.0" - -"@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" - integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== - dependencies: - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - -"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" - integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - -"@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" - integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== - dependencies: - "@ethersproject/bytes" "^5.7.0" - -"@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" - integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - -"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" - integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - bn.js "^5.2.1" - -"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" - integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== - dependencies: - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" - integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - -"@ethersproject/contracts@5.7.0", "@ethersproject/contracts@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" - integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== - dependencies: - "@ethersproject/abi" "^5.7.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - -"@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" - integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== - dependencies: - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/base64" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf" - integrity sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg== - dependencies: - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/basex" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/pbkdf2" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/sha2" "^5.7.0" - "@ethersproject/signing-key" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/wordlists" "^5.7.0" - -"@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360" - integrity sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g== - dependencies: - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/hdnode" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/pbkdf2" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/random" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - aes-js "3.0.0" - scrypt-js "3.0.1" - -"@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" - integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== - dependencies: - "@ethersproject/bytes" "^5.7.0" - js-sha3 "0.8.0" - -"@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" - integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== - -"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" - integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== - dependencies: - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102" - integrity sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/sha2" "^5.7.0" - -"@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" - integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== - dependencies: - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.0.10", "@ethersproject/providers@^5.7.0": - version "5.7.2" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" - integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== - dependencies: - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/base64" "^5.7.0" - "@ethersproject/basex" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/networks" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/random" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/sha2" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/web" "^5.7.0" - bech32 "1.1.4" - ws "7.4.6" - -"@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" - integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" - integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" - integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - hash.js "1.1.7" - -"@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" - integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - bn.js "^5.2.1" - elliptic "6.5.4" - hash.js "1.1.7" - -"@ethersproject/solidity@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" - integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/sha2" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" - integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" - integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== - dependencies: - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/signing-key" "^5.7.0" - -"@ethersproject/units@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1" - integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/wallet@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d" - integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA== - dependencies: - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/hdnode" "^5.7.0" - "@ethersproject/json-wallets" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/random" "^5.7.0" - "@ethersproject/signing-key" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/wordlists" "^5.7.0" - -"@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" - integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== - dependencies: - "@ethersproject/base64" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5" - integrity sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@gearbox-protocol/sdk-gov@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@gearbox-protocol/sdk-gov/-/sdk-gov-2.1.0.tgz#0fa3526198095d30158c30b49ab87616242703d6" - integrity sha512-2jWPEdYqvMBM+Z3Ue0KQ9ioF55CvNWVlb5fMBiogxp3W103pXxJuTdBsh4LdDunnGgUaLScDItd8YHbNkmBhiw== - dependencies: - "@types/yaml" "^1.9.7" - add "^2.0.6" - deep-object-diff "^1.1.9" - ethers "6.12.1" - humanize-duration-ts "^2.1.1" - yaml "^2.3.2" - yarn "^1.22.19" - zod "^3.22.2" - -"@noble/curves@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" - integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== - dependencies: - "@noble/hashes" "1.3.2" - -"@noble/curves@1.7.0", "@noble/curves@~1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" - integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== - dependencies: - "@noble/hashes" "1.6.0" - -"@noble/curves@^1.6.0", "@noble/curves@~1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.0.tgz#fe035a23959e6aeadf695851b51a87465b5ba8f7" - integrity sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ== - dependencies: - "@noble/hashes" "1.7.0" - -"@noble/hashes@1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" - integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== - -"@noble/hashes@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" - integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== - -"@noble/hashes@1.6.1", "@noble/hashes@~1.6.0": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" - integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== - -"@noble/hashes@1.7.0", "@noble/hashes@^1.5.0", "@noble/hashes@~1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.0.tgz#5d9e33af2c7d04fee35de1519b80c958b2e35e39" - integrity sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w== - -"@openzeppelin/contracts-upgradeable@^4.7.3": - version "4.9.6" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.6.tgz#38b21708a719da647de4bb0e4802ee235a0d24df" - integrity sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA== - -"@openzeppelin/contracts-v0.7@npm:@openzeppelin/contracts@v3.4.2": - version "3.4.2" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" - integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== - -"@openzeppelin/contracts@^4.7.3": - version "4.9.6" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677" - integrity sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA== - -"@openzeppelin/contracts@~4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.3.3.tgz#ff6ee919fc2a1abaf72b22814bfb72ed129ec137" - integrity sha512-tDBopO1c98Yk7Cv/PZlHqrvtVjlgK5R4J6jxLwoO7qxK4xqOiZG+zSkIvGFpPZ0ikc3QOED3plgdqjgNTnBc7g== - -"@redstone-finance/evm-connector@^0.2.5": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@redstone-finance/evm-connector/-/evm-connector-0.2.6.tgz#17ec603c2c4954bf096742b030573e8f005c43e9" - integrity sha512-TUU2kIaZ2RUjQapQUDcKMClal3M/S/SVpTBok6dAwSHtxCnyG4wpSaBrfH2zs5trMHb/ZBsdwZAOBEFdiqZvbw== - dependencies: - "@chainlink/contracts" "^0.6.1" - "@openzeppelin/contracts" "^4.7.3" - axios "^1.1.3" - ethers "^5.6.8" - redstone-protocol "^1.0.5" - redstone-sdk "^1.8.2" - -"@scure/base@~1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" - integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== - -"@scure/bip32@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.6.0.tgz#6dbc6b4af7c9101b351f41231a879d8da47e0891" - integrity sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA== - dependencies: - "@noble/curves" "~1.7.0" - "@noble/hashes" "~1.6.0" - "@scure/base" "~1.2.1" - -"@scure/bip32@^1.5.0": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.6.1.tgz#848eca1bc96f6b5ce6aa750798853fb142dace05" - integrity sha512-jSO+5Ud1E588Y+LFo8TaB8JVPNAZw/lGGao+1SepHDeTs2dFLurdNIAgUuDlwezqEjRjElkCJajVrtrZaBxvaQ== - dependencies: - "@noble/curves" "~1.8.0" - "@noble/hashes" "~1.7.0" - "@scure/base" "~1.2.1" - -"@scure/bip39@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.5.0.tgz#c8f9533dbd787641b047984356531d84485f19be" - integrity sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A== - dependencies: - "@noble/hashes" "~1.6.0" - "@scure/base" "~1.2.1" - -"@scure/bip39@^1.4.0": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.5.1.tgz#a056868d672c7203a6035c808893742a79e151f6" - integrity sha512-GnlufVSP9UdAo/H2Patfv22VTtpNTyfi+I3qCKpvuB5l1KWzEYx+l2TNpBy9Ksh4xTs3Rn06tBlpWCi/1Vz8gw== - dependencies: - "@noble/hashes" "~1.7.0" - "@scure/base" "~1.2.1" - -"@types/node@18.15.13": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== - -"@types/yaml@^1.9.7": - version "1.9.7" - resolved "https://registry.yarnpkg.com/@types/yaml/-/yaml-1.9.7.tgz#2331f36e0aac91311a63d33eb026c21687729679" - integrity sha512-8WMXRDD1D+wCohjfslHDgICd2JtMATZU8CkhH8LVJqcJs6dyYj5TGptzP8wApbmEullGBSsCEzzap73DQ1HJaA== - dependencies: - yaml "*" - -"@wagmi/cli@^2.1.22": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@wagmi/cli/-/cli-2.2.0.tgz#050c01a5f419accea208fb51f89b89f89d1e334a" - integrity sha512-24U9wgmeKjs+lbnswYcWjic6leuKV/JduK2T8hGXO1fxUWzcoZ3tDtb7KQq+DmgbnJm49uaa7iKcB4K7SxN4Ag== - dependencies: - abitype "^1.0.4" - bundle-require "^4.0.2" - cac "^6.7.14" - change-case "^5.4.4" - chokidar "4.0.1" - dedent "^0.7.0" - dotenv "^16.3.1" - dotenv-expand "^10.0.0" - esbuild "^0.19.0" - escalade "3.2.0" - fdir "^6.1.1" - nanospinner "1.2.2" - pathe "^1.1.2" - picocolors "^1.0.0" - picomatch "^3.0.0" - prettier "^3.0.3" - viem "2.x" - zod "^3.22.2" - -abitype@1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.7.tgz#876a0005d211e1c9132825d45bcee7b46416b284" - integrity sha512-ZfYYSktDQUwc2eduYu8C4wOs+RDPmnRYMh7zNfzeMtGGgb0U+6tLGjixUic6mXf5xKKCcgT5Qp6cv39tOARVFw== - -abitype@^1.0.4, abitype@^1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.8.tgz#3554f28b2e9d6e9f35eb59878193eabd1b9f46ba" - integrity sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg== - -add@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235" - integrity sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q== - -aes-js@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" - integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== - -aes-js@4.0.0-beta.5: - version "4.0.0-beta.5" - resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" - integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== - -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -axios@^0.27.2: - version "0.27.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== - dependencies: - follow-redirects "^1.14.9" - form-data "^4.0.0" - -axios@^1.1.3, axios@^1.5.0: - version "1.7.9" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" - integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - -bech32@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" - integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== - -bn.js@^4.11.9: - version "4.12.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.1.tgz#215741fe3c9dba2d7e12c001d0cfdbae43975ba7" - integrity sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg== - -bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - -brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - -bufio@^1.0.7: - version "1.2.2" - resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.2.2.tgz#60a1b21e176cc9d432d4a6c52a01312e735d1753" - integrity sha512-sTsA0ka7sjge/bGUfjk00O/8kNfyeAvJjXXeyvgbXefIrf5GTp99W71qfmCP6FGHWbr4A0IjjM7dFj6bHXVMlw== - -bundle-require@^4.0.2: - version "4.2.1" - resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-4.2.1.tgz#4c450a5807381d20ade987bde8ac391544257919" - integrity sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA== - dependencies: - load-tsconfig "^0.2.3" - -cac@^6.7.14: - version "6.7.14" - resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" - integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== - -chai@^4.3.4: - version "4.5.0" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" - integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.3" - deep-eql "^4.1.3" - get-func-name "^2.0.2" - loupe "^2.3.6" - pathval "^1.1.1" - type-detect "^4.1.0" - -change-case@^5.4.4: - version "5.4.4" - resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" - integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== - -check-error@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" - integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== - dependencies: - get-func-name "^2.0.2" - -chokidar@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" - integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== - dependencies: - readdirp "^4.0.1" - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -decimal.js@^10.4.3: - version "10.4.3" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" - integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== - -dedent@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" - integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== - -deep-eql@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" - integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== - dependencies: - type-detect "^4.0.0" - -deep-object-diff@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.9.tgz#6df7ef035ad6a0caa44479c536ed7b02570f4595" - integrity sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA== - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -dotenv-expand@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" - integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== - -dotenv@^16.3.1: - version "16.4.7" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" - integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== - -elliptic@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -esbuild@^0.19.0: - version "0.19.12" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" - integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg== - optionalDependencies: - "@esbuild/aix-ppc64" "0.19.12" - "@esbuild/android-arm" "0.19.12" - "@esbuild/android-arm64" "0.19.12" - "@esbuild/android-x64" "0.19.12" - "@esbuild/darwin-arm64" "0.19.12" - "@esbuild/darwin-x64" "0.19.12" - "@esbuild/freebsd-arm64" "0.19.12" - "@esbuild/freebsd-x64" "0.19.12" - "@esbuild/linux-arm" "0.19.12" - "@esbuild/linux-arm64" "0.19.12" - "@esbuild/linux-ia32" "0.19.12" - "@esbuild/linux-loong64" "0.19.12" - "@esbuild/linux-mips64el" "0.19.12" - "@esbuild/linux-ppc64" "0.19.12" - "@esbuild/linux-riscv64" "0.19.12" - "@esbuild/linux-s390x" "0.19.12" - "@esbuild/linux-x64" "0.19.12" - "@esbuild/netbsd-x64" "0.19.12" - "@esbuild/openbsd-x64" "0.19.12" - "@esbuild/sunos-x64" "0.19.12" - "@esbuild/win32-arm64" "0.19.12" - "@esbuild/win32-ia32" "0.19.12" - "@esbuild/win32-x64" "0.19.12" - -escalade@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -ethereum-multicall@^2.19.0: - version "2.26.0" - resolved "https://registry.yarnpkg.com/ethereum-multicall/-/ethereum-multicall-2.26.0.tgz#92fe9efb6ed1cbccc2baa757a91e4d2287d56ccb" - integrity sha512-7PslfFiHPUrA0zQpMgZdftUBNyVWuHVBwnRtDOr6Eam8O/OwucqP+OHZZDE7qdrWzi4Jrji8IMw2ZUFv/wbs8Q== - dependencies: - "@ethersproject/providers" "^5.0.10" - ethers "^5.0.15" - -ethers@6.12.1: - version "6.12.1" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.12.1.tgz#517ff6d66d4fd5433e38e903051da3e57c87ff37" - integrity sha512-j6wcVoZf06nqEcBbDWkKg8Fp895SS96dSnTCjiXT+8vt2o02raTn4Lo9ERUuIVU5bAjoPYeA+7ytQFexFmLuVw== - dependencies: - "@adraffy/ens-normalize" "1.10.1" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@types/node" "18.15.13" - aes-js "4.0.0-beta.5" - tslib "2.4.0" - ws "8.5.0" - -ethers@^5.0.15, ethers@^5.6.8, ethers@^5.7.2: - version "5.7.2" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" - integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== - dependencies: - "@ethersproject/abi" "5.7.0" - "@ethersproject/abstract-provider" "5.7.0" - "@ethersproject/abstract-signer" "5.7.0" - "@ethersproject/address" "5.7.0" - "@ethersproject/base64" "5.7.0" - "@ethersproject/basex" "5.7.0" - "@ethersproject/bignumber" "5.7.0" - "@ethersproject/bytes" "5.7.0" - "@ethersproject/constants" "5.7.0" - "@ethersproject/contracts" "5.7.0" - "@ethersproject/hash" "5.7.0" - "@ethersproject/hdnode" "5.7.0" - "@ethersproject/json-wallets" "5.7.0" - "@ethersproject/keccak256" "5.7.0" - "@ethersproject/logger" "5.7.0" - "@ethersproject/networks" "5.7.1" - "@ethersproject/pbkdf2" "5.7.0" - "@ethersproject/properties" "5.7.0" - "@ethersproject/providers" "5.7.2" - "@ethersproject/random" "5.7.0" - "@ethersproject/rlp" "5.7.0" - "@ethersproject/sha2" "5.7.0" - "@ethersproject/signing-key" "5.7.0" - "@ethersproject/solidity" "5.7.0" - "@ethersproject/strings" "5.7.0" - "@ethersproject/transactions" "5.7.0" - "@ethersproject/units" "5.7.0" - "@ethersproject/wallet" "5.7.0" - "@ethersproject/web" "5.7.1" - "@ethersproject/wordlists" "5.7.0" - -eventemitter3@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== - -fdir@^6.1.1: - version "6.4.2" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" - integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== - -follow-redirects@^1.14.9, follow-redirects@^1.15.6: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== - -form-data@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" - integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -get-func-name@^2.0.1, get-func-name@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" - integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== - -hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -humanize-duration-ts@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/humanize-duration-ts/-/humanize-duration-ts-2.1.1.tgz#5382b2789f851005a67229eaf031931d71f37ee9" - integrity sha512-TibNF2/fkypjAfHdGpWL/dmWUS0G6Qi+3mKyiB6LDCowbMy+PtzbgPTnFMNTOVAJXDau01jYrJ3tFoz5AJSqhA== - -inherits@^2.0.3, inherits@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -isows@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" - integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== - -js-sha3@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== - -load-tsconfig@^0.2.3: - version "0.2.5" - resolved "https://registry.yarnpkg.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz#453b8cd8961bfb912dea77eb6c168fe8cca3d3a1" - integrity sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg== - -loupe@^2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" - integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== - dependencies: - get-func-name "^2.0.1" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - -nanospinner@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/nanospinner/-/nanospinner-1.2.2.tgz#5a38f4410b5bf7a41585964bee74d32eab3e040b" - integrity sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA== - dependencies: - picocolors "^1.1.1" - -ox@0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/ox/-/ox-0.6.5.tgz#e6506a589bd6af9b5fecfcb2c641b63c9882edb6" - integrity sha512-vmnH8KvMDwFZDbNY1mq2CBRBWIgSliZB/dFV0xKp+DfF/dJkTENt6nmA+DzHSSAgL/GO2ydjkXWvlndJgSY4KQ== - dependencies: - "@adraffy/ens-normalize" "^1.10.1" - "@noble/curves" "^1.6.0" - "@noble/hashes" "^1.5.0" - "@scure/bip32" "^1.5.0" - "@scure/bip39" "^1.4.0" - abitype "^1.0.6" - eventemitter3 "5.0.1" - -pathe@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" - integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== - -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== - -picocolors@^1.0.0, picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-3.0.1.tgz#817033161def55ec9638567a2f3bbc876b3e7516" - integrity sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag== - -prettier@^3.0.3: - version "3.4.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" - integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -readdirp@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.1.tgz#bd115327129672dc47f87408f05df9bd9ca3ef55" - integrity sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw== - -redstone-oracles-smartweave-contracts@*: - version "1.0.6" - resolved "https://registry.yarnpkg.com/redstone-oracles-smartweave-contracts/-/redstone-oracles-smartweave-contracts-1.0.6.tgz#aa7b6ec453133be240df6cb4fe52a11582cb65b7" - integrity sha512-QTGDhkSJyVxggFVaET9EiuwTjdiyNjqWmVNE/jyd8OAESvVtrclLuhQL9bIVlJhVXq6IroyWf0rs8eh/OEG/4Q== - -redstone-protocol@*, redstone-protocol@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/redstone-protocol/-/redstone-protocol-1.0.5.tgz#c8555e9a9e3ee49d11b6586bf7090e59404e3395" - integrity sha512-62qHSb9jF5875a/gc8MbDcHH1n4dZsBXxlIxpk3vx/MeykAMB6nLd1174Qukvupkr69cneeRQiVqqALkQpmRlg== - dependencies: - ethers "^5.6.8" - -redstone-sdk@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/redstone-sdk/-/redstone-sdk-1.8.2.tgz#39abf332b0bd2b3e935fd58f385f8e2c70d2e2e5" - integrity sha512-kcNt7HwcKLBI3X5EMnB8V1HODiLNVsFEqoVfwToBRFtgTPV8yTcPG7XfUkYiybE8MYSzoM49HNePBwp1liqDbg== - dependencies: - axios "^0.27.2" - ethers "^5.7.2" - redstone-oracles-smartweave-contracts "*" - redstone-protocol "*" - redstone-utils "^0.1.0" - -redstone-utils@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/redstone-utils/-/redstone-utils-0.1.0.tgz#5af17f5371f940978b5dd0d1776cf48822e75355" - integrity sha512-p0MJV5W3RbGX5xDchgQ1Aik7VQ1q0FStDo5TgCl3midng/V8RhLI0IjhKXjC037DwgKbMFOo//rSuQGlJqhZ4A== - dependencies: - axios "^1.5.0" - decimal.js "^10.4.3" - ethereum-multicall "^2.19.0" - -scrypt-js@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" - integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== - -tslib@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== - -type-detect@^4.0.0, type-detect@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" - integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== - -viem@2.x: - version "2.22.9" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.22.9.tgz#14ddb7f1ccf900784e347e1aa157e8e58043b0c2" - integrity sha512-2yy46qYhcdo8GZggQ3Zoq9QCahI0goddzpVI/vSnTpcClQBSDxYRCuAqRzzLqjvJ7hS0UYgplC7eRkM2sYgflw== - dependencies: - "@noble/curves" "1.7.0" - "@noble/hashes" "1.6.1" - "@scure/bip32" "1.6.0" - "@scure/bip39" "1.5.0" - abitype "1.0.7" - isows "1.0.6" - ox "0.6.5" - ws "8.18.0" - -ws@7.4.6: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== - -ws@8.18.0: - version "8.18.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" - integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== - -ws@8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" - integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== - -yaml@*, yaml@^2.3.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362" - integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA== - -yarn@^1.22.19: - version "1.22.22" - resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.22.tgz#ac34549e6aa8e7ead463a7407e1c7390f61a6610" - integrity sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg== - -zod@^3.22.2: - version "3.23.8" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" - integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==