From 9a90f11905a3ac4e8f8bb140612b630185b2b181 Mon Sep 17 00:00:00 2001 From: Jayesh Yadav Date: Sun, 31 May 2026 17:54:05 +0530 Subject: [PATCH] feat(vault): async withdrawal queue for illiquid exits --- src/Vault.sol | 97 ++++++++ src/facets/WithdrawQueueFacet.sol | 27 +++ src/libraries/LibWithdrawQueue.sol | 50 ++++ test/unit/WithdrawQueue.t.sol | 353 +++++++++++++++++++++++++++++ 4 files changed, 527 insertions(+) create mode 100644 src/facets/WithdrawQueueFacet.sol create mode 100644 src/libraries/LibWithdrawQueue.sol create mode 100644 test/unit/WithdrawQueue.t.sol diff --git a/src/Vault.sol b/src/Vault.sol index 4f64c6a..73b45be 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -13,6 +14,8 @@ import { LibAllocator } from "./libraries/LibAllocator.sol"; import { LibFees } from "./libraries/LibFees.sol"; import { LibGuard } from "./libraries/LibGuard.sol"; import { LibLock } from "./libraries/LibLock.sol"; +import { LibRoles } from "./libraries/LibRoles.sol"; +import { LibWithdrawQueue } from "./libraries/LibWithdrawQueue.sol"; /// @title Vault Router is a modular ERC-4626 vault on the EIP-2535 Diamond pattern. /// @notice Vault owns the ERC-4626 surface (deposit/withdraw/totalAssets) plus the @@ -23,8 +26,20 @@ import { LibLock } from "./libraries/LibLock.sol"; /// virtual shares. The ERC-4626 surface is native (non-facet) and therefore /// non-upgradeable, so it cannot be altered by a later diamondCut. contract Vault is Diamond, ERC4626, ReentrancyGuard { + using SafeERC20 for IERC20; + error StrategyTotalAssetsCallFailed(bytes32 strategyId); + error WithdrawQueueZeroShares(); + error WithdrawToZeroAddress(); + error WithdrawRequestNotFound(uint256 id); + error NotRequestOwner(uint256 id, address caller); + error InsufficientIdleLiquidity(uint256 needed, uint256 available); + + event WithdrawRequested(uint256 indexed id, address indexed owner, address indexed receiver, uint256 shares); + event WithdrawCancelled(uint256 indexed id, address indexed owner, uint256 shares); + event WithdrawFulfilled(uint256 indexed id, address indexed receiver, uint256 shares, uint256 assets); + constructor( IERC20 asset_, string memory name_, @@ -186,4 +201,86 @@ contract Vault is Diamond, ERC4626, ReentrancyGuard { f.lastFeeAccrual = nowTs; if (feeShares > 0) _mint(f.feeRecipient, feeShares); } + + // ----------------------------------------------------------------------- + // Async withdrawal queue + // ----------------------------------------------------------------------- + // Synchronous `withdraw`/`redeem` pay from idle balance and revert when the + // vault is short — which happens when capital sits in an illiquid strategy + // (notably Pendle PT before maturity). The queue gives those exits a path: + // the requester escrows shares, a curator frees liquidity via `rebalance`, + // then fulfils the claim at the live share price. These live on the native + // surface because only it can `_transfer`/`_burn` shares; the readers are on + // `WithdrawQueueFacet`. + + /// @notice Escrow `shares` for a later asynchronous exit to `receiver`. + /// @dev The shares are transferred to the vault (not burned), so the request + /// stays NAV-neutral and the requester keeps full exposure until + /// fulfillment — the claim is priced at fulfil time, not now. Subject to + /// the share lock: locked shares cannot be queued. + /// @param shares Amount of vault shares to escrow. + /// @param receiver Address that will receive the underlying on fulfillment. + /// @return id The request id, used to fulfil or cancel. + function requestWithdraw(uint256 shares, address receiver) external nonReentrant returns (uint256 id) { + if (shares == 0) revert WithdrawQueueZeroShares(); + if (receiver == address(0)) revert WithdrawToZeroAddress(); + + // Escrow the shares (reverts on insufficient balance or active lock). + _transfer(msg.sender, address(this), shares); + + LibWithdrawQueue.QueueStorage storage q = LibWithdrawQueue.queueStorage(); + id = q.nextRequestId++; + q.requests[id] = LibWithdrawQueue.WithdrawRequest({ owner: msg.sender, receiver: receiver, shares: shares }); + q.totalPendingShares += shares; + + emit WithdrawRequested(id, msg.sender, receiver, shares); + } + + /// @notice Cancel a pending request and return the escrowed shares to their owner. + /// @dev Only the request's owner may cancel, and only while it is unfulfilled. + /// @param id The request id returned by `requestWithdraw`. + function cancelWithdraw(uint256 id) external nonReentrant { + LibWithdrawQueue.QueueStorage storage q = LibWithdrawQueue.queueStorage(); + LibWithdrawQueue.WithdrawRequest memory req = q.requests[id]; + if (req.shares == 0) revert WithdrawRequestNotFound(id); + if (req.owner != msg.sender) revert NotRequestOwner(id, msg.sender); + + q.totalPendingShares -= req.shares; + delete q.requests[id]; + + _transfer(address(this), req.owner, req.shares); + + emit WithdrawCancelled(id, req.owner, req.shares); + } + + /// @notice Fulfil a pending request: burn the escrowed shares and pay the + /// underlying to the recorded receiver. + /// @dev Curator-gated (the keeper seat). Honors the circuit breaker and + /// accrues fees first, so the payout reflects the live, post-fee share + /// price. Reverts if idle liquidity is insufficient — the curator must + /// `rebalance` enough out of strategies first. Effects precede the + /// transfer (CEI) and the function is `nonReentrant`. + /// @param id The request id to fulfil. + function fulfillWithdraw(uint256 id) external nonReentrant { + LibRoles.enforceIsCurator(); + + LibWithdrawQueue.QueueStorage storage q = LibWithdrawQueue.queueStorage(); + LibWithdrawQueue.WithdrawRequest memory req = q.requests[id]; + if (req.shares == 0) revert WithdrawRequestNotFound(id); + + _guard(); + _accrueFees(); + + uint256 assets = convertToAssets(req.shares); + uint256 idle = IERC20(asset()).balanceOf(address(this)); + if (idle < assets) revert InsufficientIdleLiquidity(assets, idle); + + q.totalPendingShares -= req.shares; + delete q.requests[id]; + + _burn(address(this), req.shares); + IERC20(asset()).safeTransfer(req.receiver, assets); + + emit WithdrawFulfilled(id, req.receiver, req.shares, assets); + } } diff --git a/src/facets/WithdrawQueueFacet.sol b/src/facets/WithdrawQueueFacet.sol new file mode 100644 index 0000000..8122d42 --- /dev/null +++ b/src/facets/WithdrawQueueFacet.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { LibWithdrawQueue } from "../libraries/LibWithdrawQueue.sol"; + +/// @title WithdrawQueueFacet +/// @notice Public readers for the asynchronous withdrawal queue. The +/// share-moving entry points (`requestWithdraw`, `cancelWithdraw`, +/// `fulfillWithdraw`) live on the native `Vault` surface because only it +/// can `_transfer`/`_burn` shares; this facet exposes the queue state. +contract WithdrawQueueFacet { + /// @notice The request that will be assigned to the next `requestWithdraw`. + function nextWithdrawRequestId() external view returns (uint256) { + return LibWithdrawQueue.queueStorage().nextRequestId; + } + + /// @notice Total escrowed shares across all unfulfilled requests. + function pendingWithdrawShares() external view returns (uint256) { + return LibWithdrawQueue.queueStorage().totalPendingShares; + } + + /// @notice The stored request for `id`. A `shares == 0` result means the slot + /// is empty — never created, already fulfilled, or cancelled. + function withdrawRequest(uint256 id) external view returns (LibWithdrawQueue.WithdrawRequest memory) { + return LibWithdrawQueue.queueStorage().requests[id]; + } +} diff --git a/src/libraries/LibWithdrawQueue.sol b/src/libraries/LibWithdrawQueue.sol new file mode 100644 index 0000000..2a5efda --- /dev/null +++ b/src/libraries/LibWithdrawQueue.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title LibWithdrawQueue +/// @notice Namespaced storage for the asynchronous withdrawal queue. Lets users +/// exit when the vault lacks idle liquidity to satisfy a synchronous +/// ERC-4626 `withdraw`/`redeem` — most importantly when capital sits in +/// an illiquid strategy (e.g. Pendle PT before maturity). The user's +/// shares are escrowed on request and a curator/keeper fulfills them +/// once a rebalance has freed enough idle balance. +/// @dev Escrow model: shares are transferred to the diamond (not burned) on +/// request, so they remain in `totalSupply` and the request stays +/// NAV-neutral for the remaining holders. The claim is converted to assets +/// at the *live* share price on fulfillment — the requester keeps full +/// exposure (yield and loss) until they are actually paid out, so the queue +/// cannot be used to lock in a stale price at the expense of stayers. +/// +/// The share-moving entry points (request/cancel/fulfill) live on Vault.sol +/// because only the native ERC-4626 surface can `_transfer`/`_burn` shares; +/// this library holds the queue state and `WithdrawQueueFacet` the readers. +/// Storage location: +/// keccak256(abi.encode(uint256(keccak256("vaultrouter.storage.withdrawqueue")) - 1)) & ~bytes32(uint256(0xff)) +library LibWithdrawQueue { + /// @dev erc7201:vaultrouter.storage.withdrawqueue + bytes32 internal constant WITHDRAW_QUEUE_STORAGE_SLOT = + 0x3d5a7857d3d4e9dbe18f39f41bd8fd54a510e284a7d9a4464e9cc2159e9f9100; + + /// @notice A single pending exit. `shares == 0` marks the slot as + /// empty/settled (fulfilled or cancelled), so ids are never reused. + struct WithdrawRequest { + address owner; // who requested — receives the shares back on cancel + address receiver; // who receives the underlying on fulfillment + uint256 shares; // escrowed shares awaiting fulfillment + } + + /// @custom:storage-location erc7201:vaultrouter.storage.withdrawqueue + struct QueueStorage { + uint256 nextRequestId; + mapping(uint256 => WithdrawRequest) requests; + /// @dev Sum of all escrowed shares currently awaiting fulfillment. + uint256 totalPendingShares; + } + + function queueStorage() internal pure returns (QueueStorage storage s) { + bytes32 slot = WITHDRAW_QUEUE_STORAGE_SLOT; + assembly { + s.slot := slot + } + } +} diff --git a/test/unit/WithdrawQueue.t.sol b/test/unit/WithdrawQueue.t.sol new file mode 100644 index 0000000..93b0d4b --- /dev/null +++ b/test/unit/WithdrawQueue.t.sol @@ -0,0 +1,353 @@ +// 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 { AllocatorFacet } from "../../src/facets/AllocatorFacet.sol"; +import { WithdrawQueueFacet } from "../../src/facets/WithdrawQueueFacet.sol"; +import { LibAllocator } from "../../src/libraries/LibAllocator.sol"; +import { LibWithdrawQueue } from "../../src/libraries/LibWithdrawQueue.sol"; +import { LibRoles } from "../../src/libraries/LibRoles.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; + } +} + +/// @title WithdrawQueueTest +/// @notice Unit coverage for the asynchronous withdrawal queue (Tier 3): escrow +/// on request, return on cancel, burn-and-pay on fulfillment at the live +/// share price, and the headline illiquid-exit flow where capital parked +/// in a strategy is freed by a rebalance before the claim is paid. +contract WithdrawQueueTest is Test { + MockUSDC internal usdc; + MockProtocol internal mockProtocol; + Vault internal vault; + + address internal owner = makeAddr("owner"); + address internal alice = makeAddr("alice"); + address internal bob = makeAddr("bob"); + + 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()); + + // Alice funds the vault and holds all the shares. + usdc.mint(alice, 1000 * 1e6); + vm.startPrank(alice); + usdc.approve(address(vault), type(uint256).max); + vault.deposit(1000 * 1e6, alice); + vm.stopPrank(); + } + + // ----------------------------------------------------------------------- + // requestWithdraw — escrow + // ----------------------------------------------------------------------- + + function test_Request_EscrowsSharesAndRecordsRequest() public { + uint256 shares = vault.balanceOf(alice); + + vm.expectEmit(true, true, true, true, address(vault)); + emit Vault.WithdrawRequested(0, alice, alice, shares); + vm.prank(alice); + uint256 id = vault.requestWithdraw(shares, alice); + + assertEq(id, 0, "first request id"); + assertEq(vault.balanceOf(alice), 0, "alice's shares escrowed"); + assertEq(vault.balanceOf(address(vault)), shares, "shares held in escrow by the vault"); + assertEq(WithdrawQueueFacet(address(vault)).pendingWithdrawShares(), shares, "pending tracked"); + + LibWithdrawQueue.WithdrawRequest memory req = WithdrawQueueFacet(address(vault)).withdrawRequest(id); + assertEq(req.owner, alice, "owner recorded"); + assertEq(req.receiver, alice, "receiver recorded"); + assertEq(req.shares, shares, "shares recorded"); + } + + function test_Request_RevertsOnZeroShares() public { + vm.prank(alice); + vm.expectRevert(Vault.WithdrawQueueZeroShares.selector); + vault.requestWithdraw(0, alice); + } + + function test_Request_RevertsOnZeroReceiver() public { + uint256 shares = vault.balanceOf(alice); + vm.prank(alice); + vm.expectRevert(Vault.WithdrawToZeroAddress.selector); + vault.requestWithdraw(shares, address(0)); + } + + function test_Request_RevertsWhenSharesExceedBalance() public { + uint256 tooMany = vault.balanceOf(alice) + 1; + vm.prank(alice); + vm.expectRevert(); // ERC20 insufficient balance on the escrow transfer + vault.requestWithdraw(tooMany, alice); + } + + // ----------------------------------------------------------------------- + // cancelWithdraw + // ----------------------------------------------------------------------- + + function test_Cancel_ReturnsSharesToOwner() public { + uint256 shares = vault.balanceOf(alice); + vm.prank(alice); + uint256 id = vault.requestWithdraw(shares, alice); + + vm.expectEmit(true, true, false, true, address(vault)); + emit Vault.WithdrawCancelled(id, alice, shares); + vm.prank(alice); + vault.cancelWithdraw(id); + + assertEq(vault.balanceOf(alice), shares, "shares returned"); + assertEq(vault.balanceOf(address(vault)), 0, "escrow emptied"); + assertEq(WithdrawQueueFacet(address(vault)).pendingWithdrawShares(), 0, "pending cleared"); + assertEq(WithdrawQueueFacet(address(vault)).withdrawRequest(id).shares, 0, "request slot cleared"); + } + + function test_Cancel_RevertsForNonOwner() public { + uint256 shares = vault.balanceOf(alice); + vm.prank(alice); + uint256 id = vault.requestWithdraw(shares, alice); + + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(Vault.NotRequestOwner.selector, id, bob)); + vault.cancelWithdraw(id); + } + + function test_Cancel_RevertsWhenNotFound() public { + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(Vault.WithdrawRequestNotFound.selector, 99)); + vault.cancelWithdraw(99); + } + + // ----------------------------------------------------------------------- + // fulfillWithdraw + // ----------------------------------------------------------------------- + + function test_Fulfill_BurnsEscrowAndPaysReceiver() public { + uint256 shares = vault.balanceOf(alice); + vm.prank(alice); + uint256 id = vault.requestWithdraw(shares, bob); // pay a different receiver + + uint256 supplyBefore = vault.totalSupply(); + uint256 expectedAssets = vault.convertToAssets(shares); // no fee recipient -> price stable + + vm.expectEmit(true, true, false, true, address(vault)); + emit Vault.WithdrawFulfilled(id, bob, shares, expectedAssets); + vm.prank(owner); // owner is implicitly a curator + vault.fulfillWithdraw(id); + + assertEq(usdc.balanceOf(bob), expectedAssets, "receiver paid the live-priced assets"); + assertEq(vault.balanceOf(address(vault)), 0, "escrowed shares burned"); + assertEq(vault.totalSupply(), supplyBefore - shares, "supply reduced by burned shares"); + assertEq(WithdrawQueueFacet(address(vault)).pendingWithdrawShares(), 0, "pending cleared"); + } + + function test_Fulfill_RevertsForNonCurator() public { + uint256 shares = vault.balanceOf(alice); + vm.prank(alice); + uint256 id = vault.requestWithdraw(shares, alice); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(LibRoles.NotCurator.selector, alice)); + vault.fulfillWithdraw(id); + } + + function test_Fulfill_RevertsWhenNotFound() public { + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(Vault.WithdrawRequestNotFound.selector, 7)); + vault.fulfillWithdraw(7); + } + + function test_Fulfill_PaysAtLiveSharePrice() public { + uint256 shares = vault.balanceOf(alice); + vm.prank(alice); + uint256 id = vault.requestWithdraw(shares, alice); + + // Yield accrues to the vault after the request was queued. Because the + // claim is priced at fulfil time (not request time), the requester + // captures it. + uint256 assetsAtRequest = vault.convertToAssets(shares); + usdc.mint(address(vault), 100 * 1e6); // +10% donated as idle yield + uint256 assetsAtFulfil = vault.convertToAssets(shares); + assertGt(assetsAtFulfil, assetsAtRequest, "claim repriced upward by yield"); + + vm.prank(owner); + vault.fulfillWithdraw(id); + assertEq(usdc.balanceOf(alice), assetsAtFulfil, "paid at the live, post-yield price"); + } + + // ----------------------------------------------------------------------- + // Illiquid exit — the reason the queue exists + // ----------------------------------------------------------------------- + + function test_IlliquidExit_FreedByRebalanceThenFulfilled() public { + // Deploy 100% of the vault into the strategy, draining idle to zero. + _setSingleAllocation(MOCK_ID, 10_000); + vm.roll(block.number + 1); + vm.prank(owner); + AllocatorFacet(address(vault)).rebalance(); + assertEq(usdc.balanceOf(address(vault)), 0, "no idle after full deployment"); + + // Alice queues a full exit. + uint256 shares = vault.balanceOf(alice); + vm.prank(alice); + uint256 id = vault.requestWithdraw(shares, alice); + + // Nothing idle yet -> fulfilment reverts until liquidity is freed. + uint256 owed = vault.convertToAssets(shares); + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(Vault.InsufficientIdleLiquidity.selector, owed, 0)); + vault.fulfillWithdraw(id); + + // Curator frees liquidity by pulling the strategy allocation back to 0. + _setSingleAllocation(MOCK_ID, 0); + vm.roll(block.number + 1); + vm.prank(owner); + AllocatorFacet(address(vault)).rebalance(); + + // Now the claim can be paid. + vm.prank(owner); + vault.fulfillWithdraw(id); + + assertGt(usdc.balanceOf(alice), 0, "alice exited an illiquid position via the queue"); + assertEq(vault.balanceOf(address(vault)), 0, "escrow burned"); + assertEq(WithdrawQueueFacet(address(vault)).pendingWithdrawShares(), 0, "queue drained"); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + 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() 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: 0, + active: false // overwritten in registerStrategy + }); + } + + function _deployVault() internal returns (Vault) { + DiamondCutFacet cut = new DiamondCutFacet(); + DiamondLoupeFacet loupe = new DiamondLoupeFacet(); + OwnershipFacet ownership = new OwnershipFacet(); + AllocatorFacet allocator = new AllocatorFacet(); + MockStrategyFacet mock = new MockStrategyFacet(); + WithdrawQueueFacet queue = new WithdrawQueueFacet(); + + IDiamond.FacetCut[] memory cuts = new IDiamond.FacetCut[](6); + 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(allocator), + action: IDiamond.FacetCutAction.Add, + functionSelectors: _allocatorSelectors() + }); + cuts[4] = IDiamond.FacetCut({ + facetAddress: address(mock), action: IDiamond.FacetCutAction.Add, functionSelectors: _mockSelectors() + }); + cuts[5] = IDiamond.FacetCut({ + facetAddress: address(queue), + action: IDiamond.FacetCutAction.Add, + functionSelectors: _withdrawQueueSelectors() + }); + + return new Vault(IERC20(address(usdc)), "Vault Router", "vUSDC", owner, cuts, address(0), ""); + } + + 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 _allocatorSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](6); + s[0] = AllocatorFacet.registerStrategy.selector; + s[1] = AllocatorFacet.setAllocation.selector; + s[2] = AllocatorFacet.rebalance.selector; + s[3] = AllocatorFacet.setIdleReserve.selector; + s[4] = AllocatorFacet.strategyTotalAssets.selector; + s[5] = AllocatorFacet.idleAssets.selector; + } + + function _mockSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](6); + 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; + } + + function _withdrawQueueSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](3); + s[0] = WithdrawQueueFacet.nextWithdrawRequestId.selector; + s[1] = WithdrawQueueFacet.pendingWithdrawShares.selector; + s[2] = WithdrawQueueFacet.withdrawRequest.selector; + } +}