From 825e6d9621620b4f4df89f3608e88ed6972247b2 Mon Sep 17 00:00:00 2001 From: Jayesh Yadav Date: Sat, 30 May 2026 11:42:35 +0530 Subject: [PATCH] feat(allocator): quarantine to isolate failing strategies --- README.md | 17 ++ src/Vault.sol | 3 + src/facets/AllocatorFacet.sol | 50 ++++- src/facets/HarvestFacet.sol | 1 + src/libraries/LibAllocator.sol | 5 + test/mocks/MockStrategyFacet.sol | 9 + test/unit/Quarantine.t.sol | 374 +++++++++++++++++++++++++++++++ 7 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 test/unit/Quarantine.t.sol diff --git a/README.md b/README.md index e134246..90871a9 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,23 @@ New strategies are added as facets and registered through the curator, no vault - Management fee accrues linearly (capped at 10% annually) - Slippage protection on strategy deposits - NAV circuit breaker — bounds how far the share price may move between checkpoints +- Strategy quarantine — isolate a failing strategy so it can't brick the vault + +### Strategy quarantine + +The vault prices itself by summing each strategy's reported position. If one +strategy's read reverts (a failing, exploited, or stuck protocol), it would +otherwise brick `totalAssets` — and with it every deposit, withdrawal, and fee +accrual vault-wide. The owner can `quarantineStrategy(id)` to isolate it: + +- excluded from `totalAssets` (valued at zero — conservative over trusting a + stale or manipulable reading), +- skipped by the rebalancer and `harvestAll`, and its target allocation zeroed, + +so the rest of the vault keeps operating. Funds already in the strategy stay put, +untouched, until `releaseStrategy(id)` lifts the quarantine once it is healthy +again. Failures are loud by default — a non-quarantined strategy that reverts +still halts the vault, so isolation is always a deliberate, audited owner action. ### NAV circuit breaker diff --git a/src/Vault.sol b/src/Vault.sol index 5e5cd59..76d6062 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -56,6 +56,9 @@ contract Vault is Diamond, ERC4626 { bytes32 id = $.strategyIds[i]; LibAllocator.StrategyConfig storage configs = $.configs[id]; if (!configs.active) continue; + // Isolated strategy: excluded from NAV so a single failing protocol + // cannot brick deposits, withdrawals, or fee accrual vault-wide. + if ($.quarantined[id]) continue; (bool ok, bytes memory data) = address(this).staticcall(abi.encodeWithSelector(configs.totalAssetsSelector)); if (!ok) revert StrategyTotalAssetsCallFailed(id); total += abi.decode(data, (uint256)); diff --git a/src/facets/AllocatorFacet.sol b/src/facets/AllocatorFacet.sol index 73056f4..a2d0ad6 100644 --- a/src/facets/AllocatorFacet.sol +++ b/src/facets/AllocatorFacet.sol @@ -26,6 +26,9 @@ contract AllocatorFacet { error StrategyTotalAssetsCallFailed(bytes32 strategyId); error StrategyCallFailed(bytes32 strategyId, bytes4 selector); error RebalanceTooSoon(uint256 lastBlock, uint256 currentBlock); + error StrategyAlreadyQuarantined(bytes32 strategyId); + error StrategyNotQuarantined(bytes32 strategyId); + error AllocationToQuarantined(bytes32 strategyId); event StrategyRegistered(bytes32 indexed strategyId, LibAllocator.StrategyConfig config); event StrategyRemoved(bytes32 indexed strategyId); @@ -34,6 +37,8 @@ contract AllocatorFacet { event StrategyCapSet(bytes32 indexed strategyId, uint16 capBps); event GlobalStrategyCapSet(uint16 capBps); event Rebalanced(uint256 totalAssets, uint256 idleAfter); + event StrategyQuarantined(bytes32 indexed strategyId); + event StrategyReleased(bytes32 indexed strategyId); // ----------------------------------------------------------------------- // Owner-gated governance / risk bounds @@ -103,6 +108,7 @@ contract AllocatorFacet { bytes32 id = strategyIds[i]; uint16 b = bps[i]; if (!s.configs[id].active) revert StrategyNotRegistered(id); + if (s.quarantined[id] && b > 0) revert AllocationToQuarantined(id); if (b > LibAllocator.BPS_DENOMINATOR) revert InvalidBps(b); uint16 cap = _effectiveCap(s, id); if (b > cap) revert AllocationExceedsCap(id, cap, b); @@ -137,6 +143,38 @@ contract AllocatorFacet { emit GlobalStrategyCapSet(capBps); } + /// @notice Isolate a strategy whose accounting can no longer be trusted (a + /// failing, exploited, or stuck protocol). A quarantined strategy is + /// excluded from `totalAssets` and skipped by the rebalancer and + /// harvester, so its failure can never brick deposits, withdrawals, + /// or fee accrual for the rest of the vault. + /// @dev Owner-only — it changes how vault NAV is computed. The strategy's + /// target is zeroed so the rebalancer stops funding it. Funds already in + /// the strategy stay there (untouched and unvalued) until it is released; + /// valuing them at zero is the conservative choice over trusting a stale + /// or manipulable reading. + function quarantineStrategy(bytes32 strategyId) external { + LibDiamond.enforceIsContractOwner(); + LibAllocator.AllocatorStorage storage s = LibAllocator.allocatorStorage(); + if (!s.configs[strategyId].active) revert StrategyNotRegistered(strategyId); + if (s.quarantined[strategyId]) revert StrategyAlreadyQuarantined(strategyId); + s.quarantined[strategyId] = true; + s.targetBps[strategyId] = 0; + emit StrategyQuarantined(strategyId); + } + + /// @notice Lift quarantine once a strategy is healthy again; its position is + /// counted in NAV and it becomes rebalanceable once more. + /// @dev Owner-only. Re-funding it requires a fresh `setAllocation`, since the + /// target was zeroed on quarantine. + function releaseStrategy(bytes32 strategyId) external { + LibDiamond.enforceIsContractOwner(); + LibAllocator.AllocatorStorage storage s = LibAllocator.allocatorStorage(); + if (!s.quarantined[strategyId]) revert StrategyNotQuarantined(strategyId); + s.quarantined[strategyId] = false; + emit StrategyReleased(strategyId); + } + // ----------------------------------------------------------------------- // Rebalance // ----------------------------------------------------------------------- @@ -158,7 +196,11 @@ contract AllocatorFacet { uint256[] memory currentAssets = new uint256[](n); uint256 totalCached = _idleAssetsInternal(); for (uint256 i; i < n; i++) { - uint256 cur = _strategyTotalAssetsInternal(s.configs[s.strategyIds[i]], s.strategyIds[i]); + bytes32 id = s.strategyIds[i]; + // Isolated: never read (the read may revert) or fund a quarantined + // strategy. currentAssets[i] stays 0, so both passes skip it too. + if (s.quarantined[id]) continue; + uint256 cur = _strategyTotalAssetsInternal(s.configs[id], id); currentAssets[i] = cur; totalCached += cur; } @@ -166,6 +208,7 @@ contract AllocatorFacet { // Pass 1: withdraw from over-target strategies. for (uint256 i; i < n; i++) { bytes32 id = s.strategyIds[i]; + if (s.quarantined[id]) continue; uint256 target = (totalCached * uint256(s.targetBps[id])) / LibAllocator.BPS_DENOMINATOR; if (currentAssets[i] > target) { uint256 delta = currentAssets[i] - target; @@ -176,6 +219,7 @@ contract AllocatorFacet { // Pass 2: deposit into under-target strategies. for (uint256 i; i < n; i++) { bytes32 id = s.strategyIds[i]; + if (s.quarantined[id]) continue; uint256 target = (totalCached * uint256(s.targetBps[id])) / LibAllocator.BPS_DENOMINATOR; if (currentAssets[i] < target) { uint256 delta = target - currentAssets[i]; @@ -233,6 +277,10 @@ contract AllocatorFacet { return LibAllocator.allocatorStorage().lastRebalanceBlock; } + function isQuarantined(bytes32 strategyId) external view returns (bool) { + return LibAllocator.allocatorStorage().quarantined[strategyId]; + } + // ----------------------------------------------------------------------- // Internals // ----------------------------------------------------------------------- diff --git a/src/facets/HarvestFacet.sol b/src/facets/HarvestFacet.sol index 4f533a9..35b518c 100644 --- a/src/facets/HarvestFacet.sol +++ b/src/facets/HarvestFacet.sol @@ -49,6 +49,7 @@ contract HarvestFacet { bytes32 id = s.strategyIds[i]; LibAllocator.StrategyConfig memory cfg = s.configs[id]; if (!cfg.active) continue; + if (s.quarantined[id]) continue; // isolated: don't let a failing strategy break harvestAll if (cfg.harvestSelector != bytes4(0)) { (bool ok, bytes memory ret) = address(this).call(abi.encodeWithSelector(cfg.harvestSelector)); if (!ok) { diff --git a/src/libraries/LibAllocator.sol b/src/libraries/LibAllocator.sol index dfe4c9c..0aa202c 100644 --- a/src/libraries/LibAllocator.sol +++ b/src/libraries/LibAllocator.sol @@ -34,6 +34,11 @@ library LibAllocator { uint16 idleReserveBps; uint16 globalMaxStrategyCapBps; uint64 lastRebalanceBlock; + /// @dev Strategies flagged here are isolated: excluded from NAV and + /// skipped by the rebalancer/harvester, so a single failing protocol + /// cannot brick the whole vault. Owner-controlled risk state, kept + /// out of StrategyConfig so it is dynamic rather than static config. + mapping(bytes32 => bool) quarantined; } function allocatorStorage() internal pure returns (AllocatorStorage storage s) { diff --git a/test/mocks/MockStrategyFacet.sol b/test/mocks/MockStrategyFacet.sol index 389d683..57880f4 100644 --- a/test/mocks/MockStrategyFacet.sol +++ b/test/mocks/MockStrategyFacet.sol @@ -17,6 +17,7 @@ contract MockStrategyFacet { struct MockStorage { MockProtocol protocol; uint256 harvestCount; + bool reverting; } function _ms() internal pure returns (MockStorage storage s) { @@ -34,7 +35,14 @@ contract MockStrategyFacet { return _ms().protocol; } + /// @notice Test-only — when set, the strategy's totalAssets and harvest revert, + /// simulating a failing/exploited/stuck underlying protocol. + function mockSetReverting(bool v) external { + _ms().reverting = v; + } + function mockTotalAssets() external view returns (uint256) { + if (_ms().reverting) revert("mock: totalAssets reverted"); MockProtocol p = _ms().protocol; if (address(p) == address(0)) return 0; return p.balanceOf(address(this)); @@ -52,6 +60,7 @@ contract MockStrategyFacet { } function mockHarvest() external { + if (_ms().reverting) revert("mock: harvest reverted"); _ms().harvestCount += 1; } diff --git a/test/unit/Quarantine.t.sol b/test/unit/Quarantine.t.sol new file mode 100644 index 0000000..aa614a7 --- /dev/null +++ b/test/unit/Quarantine.t.sol @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { Test } from "forge-std/Test.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Vault } from "../../src/Vault.sol"; +import { IDiamond } from "../../src/interfaces/IDiamond.sol"; +import { IDiamondCut } from "../../src/interfaces/IDiamondCut.sol"; +import { IDiamondLoupe } from "../../src/interfaces/IDiamondLoupe.sol"; +import { IERC173 } from "../../src/interfaces/IERC173.sol"; +import { DiamondCutFacet } from "../../src/facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "../../src/facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "../../src/facets/OwnershipFacet.sol"; +import { IdleStrategyFacet } from "../../src/facets/strategies/IdleStrategyFacet.sol"; +import { AllocatorFacet } from "../../src/facets/AllocatorFacet.sol"; +import { HarvestFacet } from "../../src/facets/HarvestFacet.sol"; +import { LibAllocator } from "../../src/libraries/LibAllocator.sol"; +import { LibDiamond } from "../../src/libraries/LibDiamond.sol"; + +import { MockProtocol } from "../mocks/MockProtocol.sol"; +import { MockStrategyFacet } from "../mocks/MockStrategyFacet.sol"; + +contract MockUSDC is ERC20 { + constructor() ERC20("USD Coin", "USDC") { } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function decimals() public pure override returns (uint8) { + return 6; + } +} + +/// @notice Proves that a single failing strategy can no longer brick the vault: +/// once quarantined it is excluded from NAV and skipped by the rebalancer +/// and harvester, so deposits/withdrawals/fees keep working. +contract QuarantineTest is Test { + MockUSDC internal usdc; + Vault internal vault; + MockProtocol internal mockProtocol; + + address internal owner = makeAddr("owner"); + address internal alice = makeAddr("alice"); + + bytes32 internal constant MOCK_ID = bytes32("mock"); + + function setUp() public { + usdc = new MockUSDC(); + mockProtocol = new MockProtocol(IERC20(address(usdc))); + vault = _deployVault(); + + vm.prank(owner); + MockStrategyFacet(address(vault)).mockSetProtocol(mockProtocol); + vm.prank(owner); + AllocatorFacet(address(vault)).registerStrategy(MOCK_ID, _mockStrategyConfig(0)); + + // 1000 in: 50% to the mock, 50% idle. A mock failure then maps to half NAV. + _depositToVault(alice, 1000 * 1e6); + _setSingleAllocation(MOCK_ID, 5000); + vm.roll(block.number + 1); + vm.prank(owner); + AllocatorFacet(address(vault)).rebalance(); + } + + // ----------------------------------------------------------------------- + // the bug: a failing strategy bricks the whole vault + // ----------------------------------------------------------------------- + + function test_FailingStrategy_BricksTotalAssets() public { + MockStrategyFacet(address(vault)).mockSetReverting(true); + vm.expectRevert(abi.encodeWithSelector(Vault.StrategyTotalAssetsCallFailed.selector, MOCK_ID)); + vault.totalAssets(); + } + + function test_FailingStrategy_BricksDeposit() public { + MockStrategyFacet(address(vault)).mockSetReverting(true); + usdc.mint(alice, 100 * 1e6); + vm.startPrank(alice); + usdc.approve(address(vault), 100 * 1e6); + vm.expectRevert(abi.encodeWithSelector(Vault.StrategyTotalAssetsCallFailed.selector, MOCK_ID)); + vault.deposit(100 * 1e6, alice); + vm.stopPrank(); + } + + function test_FailingStrategy_BricksWithdraw() public { + MockStrategyFacet(address(vault)).mockSetReverting(true); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(Vault.StrategyTotalAssetsCallFailed.selector, MOCK_ID)); + vault.withdraw(10 * 1e6, alice, alice); + } + + // ----------------------------------------------------------------------- + // the fix: quarantine isolates the failing strategy + // ----------------------------------------------------------------------- + + function test_Quarantine_ExcludesFromNav() public { + assertEq(vault.totalAssets(), 1000 * 1e6, "healthy NAV before"); + MockStrategyFacet(address(vault)).mockSetReverting(true); + + vm.prank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + + assertTrue(AllocatorFacet(address(vault)).isQuarantined(MOCK_ID)); + assertEq(vault.totalAssets(), 500 * 1e6, "NAV = idle only; mock excluded"); + } + + function test_Quarantine_RestoresDepositAndWithdraw() public { + MockStrategyFacet(address(vault)).mockSetReverting(true); + vm.prank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + + // Both flows work again now that the failing strategy is isolated. + _depositToVault(alice, 100 * 1e6); + vm.prank(alice); + vault.withdraw(10 * 1e6, alice, alice); + } + + function test_Quarantine_ZeroesTarget() public { + vm.prank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + assertEq(AllocatorFacet(address(vault)).targetAllocation(MOCK_ID), 0); + } + + function test_Rebalance_SkipsQuarantined() public { + MockStrategyFacet(address(vault)).mockSetReverting(true); + vm.prank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + + // Rebalance must not revert even though the mock's read would fail. + vm.roll(block.number + 1); + vm.prank(owner); + AllocatorFacet(address(vault)).rebalance(); + } + + // ----------------------------------------------------------------------- + // harvestAll isolation + // ----------------------------------------------------------------------- + + function test_HarvestAll_RevertsOnFailingStrategy_WhenNotQuarantined() public { + MockStrategyFacet(address(vault)).mockSetReverting(true); + vm.prank(owner); + vm.expectRevert(bytes("mock: harvest reverted")); + HarvestFacet(address(vault)).harvestAll(); + } + + function test_HarvestAll_SkipsQuarantined() public { + MockStrategyFacet(address(vault)).mockSetReverting(true); + vm.prank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + + vm.prank(owner); + HarvestFacet(address(vault)).harvestAll(); // must not revert + } + + // ----------------------------------------------------------------------- + // release + // ----------------------------------------------------------------------- + + function test_Release_RestoresAccounting() public { + MockStrategyFacet(address(vault)).mockSetReverting(true); + vm.prank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + assertEq(vault.totalAssets(), 500 * 1e6); + + // Protocol recovers; owner lifts the quarantine. + MockStrategyFacet(address(vault)).mockSetReverting(false); + vm.prank(owner); + AllocatorFacet(address(vault)).releaseStrategy(MOCK_ID); + + assertFalse(AllocatorFacet(address(vault)).isQuarantined(MOCK_ID)); + assertEq(vault.totalAssets(), 1000 * 1e6, "mock position counted again"); + } + + // ----------------------------------------------------------------------- + // access control & guards + // ----------------------------------------------------------------------- + + function test_Quarantine_OwnerOnly() public { + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(LibDiamond.NotContractOwner.selector, alice, owner)); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + } + + function test_Quarantine_RevertsIfNotRegistered() public { + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(AllocatorFacet.StrategyNotRegistered.selector, bytes32("nope"))); + AllocatorFacet(address(vault)).quarantineStrategy(bytes32("nope")); + } + + function test_Quarantine_RevertsIfAlreadyQuarantined() public { + vm.startPrank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + vm.expectRevert(abi.encodeWithSelector(AllocatorFacet.StrategyAlreadyQuarantined.selector, MOCK_ID)); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + vm.stopPrank(); + } + + function test_Release_OwnerOnly() public { + vm.prank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(LibDiamond.NotContractOwner.selector, alice, owner)); + AllocatorFacet(address(vault)).releaseStrategy(MOCK_ID); + } + + function test_Release_RevertsIfNotQuarantined() public { + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(AllocatorFacet.StrategyNotQuarantined.selector, MOCK_ID)); + AllocatorFacet(address(vault)).releaseStrategy(MOCK_ID); + } + + function test_SetAllocation_RejectsQuarantined() public { + vm.prank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + + bytes32[] memory ids = new bytes32[](1); + uint16[] memory bps = new uint16[](1); + ids[0] = MOCK_ID; + bps[0] = 3000; + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(AllocatorFacet.AllocationToQuarantined.selector, MOCK_ID)); + AllocatorFacet(address(vault)).setAllocation(ids, bps); + } + + function test_SetAllocation_AllowsZeroToQuarantined() public { + vm.prank(owner); + AllocatorFacet(address(vault)).quarantineStrategy(MOCK_ID); + + bytes32[] memory ids = new bytes32[](1); + uint16[] memory bps = new uint16[](1); + ids[0] = MOCK_ID; + bps[0] = 0; + vm.prank(owner); + AllocatorFacet(address(vault)).setAllocation(ids, bps); // no revert + } + + // ----------------------------------------------------------------------- + // helpers + // ----------------------------------------------------------------------- + + function _deployVault() internal returns (Vault) { + DiamondCutFacet cut = new DiamondCutFacet(); + DiamondLoupeFacet loupe = new DiamondLoupeFacet(); + OwnershipFacet ownership = new OwnershipFacet(); + IdleStrategyFacet idle = new IdleStrategyFacet(); + AllocatorFacet allocator = new AllocatorFacet(); + HarvestFacet harvest = new HarvestFacet(); + MockStrategyFacet mock = new MockStrategyFacet(); + + IDiamond.FacetCut[] memory cuts = new IDiamond.FacetCut[](7); + cuts[0] = IDiamond.FacetCut({ + facetAddress: address(cut), action: IDiamond.FacetCutAction.Add, functionSelectors: _diamondCutSelectors() + }); + cuts[1] = IDiamond.FacetCut({ + facetAddress: address(loupe), + action: IDiamond.FacetCutAction.Add, + functionSelectors: _diamondLoupeSelectors() + }); + cuts[2] = IDiamond.FacetCut({ + facetAddress: address(ownership), + action: IDiamond.FacetCutAction.Add, + functionSelectors: _ownershipSelectors() + }); + cuts[3] = IDiamond.FacetCut({ + facetAddress: address(idle), action: IDiamond.FacetCutAction.Add, functionSelectors: _idleSelectors() + }); + cuts[4] = IDiamond.FacetCut({ + facetAddress: address(allocator), + action: IDiamond.FacetCutAction.Add, + functionSelectors: _allocatorSelectors() + }); + cuts[5] = IDiamond.FacetCut({ + facetAddress: address(harvest), action: IDiamond.FacetCutAction.Add, functionSelectors: _harvestSelectors() + }); + cuts[6] = IDiamond.FacetCut({ + facetAddress: address(mock), action: IDiamond.FacetCutAction.Add, functionSelectors: _mockSelectors() + }); + + return new Vault(IERC20(address(usdc)), "Vault Router", "vUSDC", owner, cuts, address(0), ""); + } + + function _depositToVault(address from, uint256 amount) internal { + usdc.mint(from, amount); + vm.startPrank(from); + usdc.approve(address(vault), amount); + vault.deposit(amount, from); + vm.stopPrank(); + } + + function _setSingleAllocation(bytes32 id, uint16 bps) internal { + bytes32[] memory ids = new bytes32[](1); + uint16[] memory b = new uint16[](1); + ids[0] = id; + b[0] = bps; + vm.prank(owner); + AllocatorFacet(address(vault)).setAllocation(ids, b); + } + + function _mockStrategyConfig(uint16 capBps) internal pure returns (LibAllocator.StrategyConfig memory) { + return LibAllocator.StrategyConfig({ + totalAssetsSelector: MockStrategyFacet.mockTotalAssets.selector, + depositSelector: MockStrategyFacet.mockDeposit.selector, + withdrawSelector: MockStrategyFacet.mockWithdraw.selector, + harvestSelector: MockStrategyFacet.mockHarvest.selector, + capBps: capBps, + active: false + }); + } + + function _diamondCutSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](1); + s[0] = IDiamondCut.diamondCut.selector; + } + + function _diamondLoupeSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](4); + s[0] = IDiamondLoupe.facets.selector; + s[1] = IDiamondLoupe.facetFunctionSelectors.selector; + s[2] = IDiamondLoupe.facetAddresses.selector; + s[3] = IDiamondLoupe.facetAddress.selector; + } + + function _ownershipSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](2); + s[0] = IERC173.owner.selector; + s[1] = IERC173.transferOwnership.selector; + } + + function _idleSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](1); + s[0] = IdleStrategyFacet.idleTotalAssets.selector; + } + + function _allocatorSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](16); + s[0] = AllocatorFacet.registerStrategy.selector; + s[1] = AllocatorFacet.removeStrategy.selector; + s[2] = AllocatorFacet.setAllocation.selector; + s[3] = AllocatorFacet.setIdleReserve.selector; + s[4] = AllocatorFacet.setStrategyCap.selector; + s[5] = AllocatorFacet.setGlobalStrategyCap.selector; + s[6] = AllocatorFacet.rebalance.selector; + s[7] = AllocatorFacet.strategies.selector; + s[8] = AllocatorFacet.strategyConfig.selector; + s[9] = AllocatorFacet.targetAllocation.selector; + s[10] = AllocatorFacet.idleReserveBps.selector; + s[11] = AllocatorFacet.strategyTotalAssets.selector; + s[12] = AllocatorFacet.idleAssets.selector; + s[13] = AllocatorFacet.quarantineStrategy.selector; + s[14] = AllocatorFacet.releaseStrategy.selector; + s[15] = AllocatorFacet.isQuarantined.selector; + } + + function _harvestSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](2); + s[0] = HarvestFacet.harvest.selector; + s[1] = HarvestFacet.harvestAll.selector; + } + + function _mockSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](7); + s[0] = MockStrategyFacet.mockSetProtocol.selector; + s[1] = MockStrategyFacet.mockProtocol.selector; + s[2] = MockStrategyFacet.mockTotalAssets.selector; + s[3] = MockStrategyFacet.mockDeposit.selector; + s[4] = MockStrategyFacet.mockWithdraw.selector; + s[5] = MockStrategyFacet.mockHarvest.selector; + s[6] = MockStrategyFacet.mockSetReverting.selector; + } +}