From 5ed8ee82aef73704dab6e42b62a9fe0a394af4b8 Mon Sep 17 00:00:00 2001 From: Cursor Bot Date: Tue, 13 Jan 2026 16:15:09 -0500 Subject: [PATCH 1/3] feature: arbitrator for early withdrawals --- .../interfaces/IDurationVaultStrategy.sol | 22 +++++++++ .../strategies/DurationVaultStrategy.sol | 28 ++++++++++++ .../DurationVaultStrategyStorage.sol | 9 ++-- .../tests/DurationVaultIntegration.t.sol | 45 +++++++++++++++++++ src/test/unit/DurationVaultStrategyUnit.t.sol | 23 ++++++++++ src/test/unit/StrategyFactoryUnit.t.sol | 2 + .../unit/StrategyManagerDurationUnit.t.sol | 1 + 7 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index 3adc8ed043..f4ecb8d45f 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -17,6 +17,10 @@ interface IDurationVaultStrategyErrors { error InvalidDuration(); /// @dev Thrown when attempting to mutate configuration from a non-admin. error OnlyVaultAdmin(); + /// @dev Thrown when attempting to call arbitrator-only functionality from a non-arbitrator. + error OnlyArbitrator(); + /// @dev Thrown when attempting to configure a zero-address arbitrator. + error InvalidArbitrator(); /// @dev Thrown when attempting to lock an already locked vault. error VaultAlreadyLocked(); /// @dev Thrown when attempting to deposit after the vault has been locked. @@ -27,6 +31,10 @@ interface IDurationVaultStrategyErrors { error MustBeDelegatedToVaultOperator(); /// @dev Thrown when attempting to mark the vault as matured before duration elapses. error DurationNotElapsed(); + /// @dev Thrown when attempting to use the arbitrator early-advance after the duration has elapsed. + error DurationAlreadyElapsed(); + /// @dev Thrown when attempting to use the arbitrator early-advance before the vault is locked. + error VaultNotLocked(); /// @dev Thrown when operator integration inputs are missing or invalid. error OperatorIntegrationInvalid(); /// @dev Thrown when attempting to deposit into a vault whose underlying token is blacklisted. @@ -61,6 +69,7 @@ interface IDurationVaultStrategyTypes { struct VaultConfig { IERC20 underlyingToken; address vaultAdmin; + address arbitrator; uint32 duration; uint256 maxPerDeposit; uint256 stakeCap; @@ -99,6 +108,11 @@ interface IDurationVaultStrategyEvents { /// @param maturedAt Timestamp when the vault matured. event VaultMatured(uint32 maturedAt); + /// @notice Emitted when the vault is advanced to WITHDRAWALS early by the arbitrator. + /// @param arbitrator The arbitrator that performed the early advance. + /// @param maturedAt Timestamp when the vault transitioned to WITHDRAWALS. + event VaultAdvancedToWithdrawals(address indexed arbitrator, uint32 maturedAt); + /// @notice Emitted when the vault metadata URI is updated. /// @param newMetadataURI The new metadata URI. event MetadataURIUpdated(string newMetadataURI); @@ -138,6 +152,11 @@ interface IDurationVaultStrategy is /// the duration has elapsed. function markMatured() external; + /// @notice Advances the vault to WITHDRAWALS early, after lock but before duration elapses. + /// @dev Transitions state from ALLOCATIONS to WITHDRAWALS, and triggers the same best-effort operator cleanup + /// as `markMatured()`. Only callable by the configured arbitrator. + function advanceToWithdrawals() external; + /// @notice Updates the vault metadata URI. /// @param newMetadataURI The new metadata URI to set. /// @dev Only callable by the vault admin. @@ -157,6 +176,9 @@ interface IDurationVaultStrategy is /// @notice Returns the vault administrator address. function vaultAdmin() external view returns (address); + /// @notice Returns the arbitrator address. + function arbitrator() external view returns (address); + /// @notice Returns the configured lock duration in seconds. function duration() external view returns (uint32); diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index 708fa1b09a..5051bba937 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -43,6 +43,12 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase { _; } + /// @dev Restricts function access to the vault arbitrator. + modifier onlyArbitrator() { + require(msg.sender == arbitrator, OnlyArbitrator()); + _; + } + /// @param _strategyManager The StrategyManager contract. /// @param _pauserRegistry The PauserRegistry contract. /// @param _delegationManager The DelegationManager contract for operator registration. @@ -75,11 +81,13 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase { VaultConfig memory config ) public initializer { require(config.vaultAdmin != address(0), InvalidVaultAdmin()); + require(config.arbitrator != address(0), InvalidArbitrator()); require(config.duration != 0 && config.duration <= MAX_DURATION, InvalidDuration()); _setTVLLimits(config.maxPerDeposit, config.stakeCap); _initializeStrategyBase(config.underlyingToken); vaultAdmin = config.vaultAdmin; + arbitrator = config.arbitrator; duration = config.duration; metadataURI = config.metadataURI; @@ -127,6 +135,26 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase { _deregisterFromOperatorSet(); } + /// @notice Advances the vault to withdrawals early, after lock but before duration elapses. + /// @dev Only callable by the configured arbitrator. + function advanceToWithdrawals() external override onlyArbitrator { + if (_state == VaultState.WITHDRAWALS) { + // already recorded; noop + return; + } + require(_state == VaultState.ALLOCATIONS, VaultNotLocked()); + require(block.timestamp < unlockAt, DurationAlreadyElapsed()); + + _state = VaultState.WITHDRAWALS; + maturedAt = uint32(block.timestamp); + + emit VaultMatured(maturedAt); + emit VaultAdvancedToWithdrawals(msg.sender, maturedAt); + + _deallocateAll(); + _deregisterFromOperatorSet(); + } + /// @notice Updates the metadata URI describing the vault. function updateMetadataURI( string calldata newMetadataURI diff --git a/src/contracts/strategies/DurationVaultStrategyStorage.sol b/src/contracts/strategies/DurationVaultStrategyStorage.sol index 41d7594c0d..fa08ce0f44 100644 --- a/src/contracts/strategies/DurationVaultStrategyStorage.sol +++ b/src/contracts/strategies/DurationVaultStrategyStorage.sol @@ -17,6 +17,9 @@ abstract contract DurationVaultStrategyStorage is IDurationVaultStrategy { /// @notice Address empowered to configure and lock the vault. address public vaultAdmin; + /// @notice Address empowered to advance the vault to withdrawals early (after lock, before duration elapses). + address public arbitrator; + /// @notice The enforced lock duration once `lock` is called. uint32 public duration; @@ -46,8 +49,8 @@ abstract contract DurationVaultStrategyStorage is IDurationVaultStrategy { /// @dev This empty reserved space is put in place to allow future versions to add new /// variables without shifting down storage in the inheritance chain. - /// Storage slots used: vaultAdmin (1) + duration/lockedAt/unlockAt/maturedAt/_state (packed, 1) + + /// Storage slots used: vaultAdmin (1) + arbitrator (1) + duration/lockedAt/unlockAt/maturedAt/_state (packed, 1) + /// metadataURI (1) + _operatorSet (1) + maxPerDeposit (1) + maxTotalDeposits (1) = 6. - /// Gap: 50 - 6 = 44. - uint256[44] private __gap; + /// Gap: 50 - 7 = 43. + uint256[43] private __gap; } diff --git a/src/test/integration/tests/DurationVaultIntegration.t.sol b/src/test/integration/tests/DurationVaultIntegration.t.sol index 59ecf711d3..e860005013 100644 --- a/src/test/integration/tests/DurationVaultIntegration.t.sol +++ b/src/test/integration/tests/DurationVaultIntegration.t.sol @@ -201,6 +201,50 @@ contract Integration_DurationVault is IntegrationCheckUtils { assertEq(ctx.asset.balanceOf(address(staker)), depositAmount, "staker should recover deposit"); } + function test_durationVault_arbitrator_can_advance_to_withdrawals_early() public { + DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); + User staker = new User("duration-arbitrator-staker"); + + uint depositAmount = 50 ether; + ctx.asset.transfer(address(staker), depositAmount); + IStrategy[] memory strategies = _durationStrategyArray(ctx.vault); + uint[] memory tokenBalances = _singleAmountArray(depositAmount); + _delegateToVault(staker, ctx.vault); + staker.depositIntoEigenlayer(strategies, tokenBalances); + + // Arbitrator cannot advance before lock. + cheats.expectRevert(IDurationVaultStrategyErrors.VaultNotLocked.selector); + ctx.vault.advanceToWithdrawals(); + + ctx.vault.lock(); + assertTrue(ctx.vault.allocationsActive(), "allocations should be active after lock"); + assertFalse(ctx.vault.withdrawalsOpen(), "withdrawals should be closed while locked"); + + // Non-arbitrator cannot advance. + cheats.prank(address(0x1234)); + cheats.expectRevert(IDurationVaultStrategyErrors.OnlyArbitrator.selector); + ctx.vault.advanceToWithdrawals(); + + // Arbitrator can advance after lock but before duration elapses. + cheats.warp(block.timestamp + 1); + ctx.vault.advanceToWithdrawals(); + + assertTrue(ctx.vault.withdrawalsOpen(), "withdrawals should open after arbitrator advance"); + assertFalse(ctx.vault.allocationsActive(), "allocations should be inactive after arbitrator advance"); + assertTrue(ctx.vault.isMatured(), "vault should be matured after arbitrator advance"); + + // Withdrawals should actually be possible in this early-advance path. + uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies); + Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, withdrawableShares); + _rollBlocksForCompleteWithdrawals(withdrawals); + IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[0]); + assertEq(address(tokens[0]), address(ctx.asset), "unexpected token returned"); + assertEq(ctx.asset.balanceOf(address(staker)), depositAmount, "staker should recover deposit after arbitrator advance"); + + // markMatured should be a noop after the state has already transitioned. + ctx.vault.markMatured(); + } + function test_durationVault_operatorIntegrationAndMetadataUpdate() public { DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); @@ -396,6 +440,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { IDurationVaultStrategyTypes.VaultConfig memory config; config.underlyingToken = asset; config.vaultAdmin = address(this); + config.arbitrator = address(this); config.duration = DEFAULT_DURATION; config.maxPerDeposit = VAULT_MAX_PER_DEPOSIT; config.stakeCap = VAULT_STAKE_CAP; diff --git a/src/test/unit/DurationVaultStrategyUnit.t.sol b/src/test/unit/DurationVaultStrategyUnit.t.sol index ccc6c245f6..acea433c48 100644 --- a/src/test/unit/DurationVaultStrategyUnit.t.sol +++ b/src/test/unit/DurationVaultStrategyUnit.t.sol @@ -57,6 +57,7 @@ contract DurationVaultStrategyUnitTests is StrategyBaseUnitTests { IDurationVaultStrategyTypes.VaultConfig memory config = IDurationVaultStrategyTypes.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), + arbitrator: address(this), duration: defaultDuration, maxPerDeposit: maxPerDeposit, stakeCap: maxTotalDeposits, @@ -168,6 +169,28 @@ contract DurationVaultStrategyUnitTests is StrategyBaseUnitTests { assertEq(allocationManagerMock.deregisterFromOperatorSetsCallCount(), 0, "unexpected deregister count"); } + function testAdvanceToWithdrawals_onlyArbitrator_and_onlyBeforeUnlock() public { + // Cannot advance before lock (even as arbitrator). + cheats.expectRevert(IDurationVaultStrategyErrors.VaultNotLocked.selector); + durationVault.advanceToWithdrawals(); + + durationVault.lock(); + + // Non-arbitrator cannot advance. + cheats.prank(address(0xBEEF)); + cheats.expectRevert(IDurationVaultStrategyErrors.OnlyArbitrator.selector); + durationVault.advanceToWithdrawals(); + + // After unlockAt, arbitrator advance is not allowed. + cheats.warp(block.timestamp + defaultDuration + 1); + cheats.expectRevert(IDurationVaultStrategyErrors.DurationAlreadyElapsed.selector); + durationVault.advanceToWithdrawals(); + + // markMatured works once duration has elapsed. + durationVault.markMatured(); + assertTrue(durationVault.withdrawalsOpen(), "withdrawals should open after maturity"); + } + // ===================== VAULT STATE TESTS ===================== function testDepositsBlockedAfterLock() public { diff --git a/src/test/unit/StrategyFactoryUnit.t.sol b/src/test/unit/StrategyFactoryUnit.t.sol index 973157c64d..73291277d2 100644 --- a/src/test/unit/StrategyFactoryUnit.t.sol +++ b/src/test/unit/StrategyFactoryUnit.t.sol @@ -151,6 +151,7 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { IDurationVaultStrategyTypes.VaultConfig memory config = IDurationVaultStrategyTypes.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), + arbitrator: address(this), duration: uint32(30 days), maxPerDeposit: 10 ether, stakeCap: 100 ether, @@ -176,6 +177,7 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { IDurationVaultStrategyTypes.VaultConfig memory config = IDurationVaultStrategyTypes.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), + arbitrator: address(this), duration: uint32(7 days), maxPerDeposit: 5 ether, stakeCap: 50 ether, diff --git a/src/test/unit/StrategyManagerDurationUnit.t.sol b/src/test/unit/StrategyManagerDurationUnit.t.sol index 932e5f9cef..80b119559b 100644 --- a/src/test/unit/StrategyManagerDurationUnit.t.sol +++ b/src/test/unit/StrategyManagerDurationUnit.t.sol @@ -73,6 +73,7 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM IDurationVaultStrategyTypes.VaultConfig memory cfg = IDurationVaultStrategyTypes.VaultConfig({ underlyingToken: IERC20(address(underlyingToken)), vaultAdmin: address(this), + arbitrator: address(this), duration: uint32(30 days), maxPerDeposit: 1_000_000 ether, stakeCap: 10_000_000 ether, From c86c6667b5f5a2e5306c0dcd0cc753bffefce6ab Mon Sep 17 00:00:00 2001 From: Cursor Bot Date: Wed, 14 Jan 2026 09:46:14 -0500 Subject: [PATCH 2/3] chore: add arbitrator to event --- src/contracts/interfaces/IDurationVaultStrategy.sol | 2 ++ src/contracts/strategies/DurationVaultStrategy.sol | 1 + 2 files changed, 3 insertions(+) diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index f4ecb8d45f..af7debc073 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -85,6 +85,7 @@ interface IDurationVaultStrategyTypes { interface IDurationVaultStrategyEvents { /// @notice Emitted when a vault is initialized with its configuration. /// @param vaultAdmin The address of the vault administrator. + /// @param arbitrator The address of the vault arbitrator. /// @param underlyingToken The ERC20 token used for deposits. /// @param duration The lock duration in seconds. /// @param maxPerDeposit Maximum deposit amount per transaction. @@ -92,6 +93,7 @@ interface IDurationVaultStrategyEvents { /// @param metadataURI URI pointing to vault metadata. event VaultInitialized( address indexed vaultAdmin, + address indexed arbitrator, IERC20 indexed underlyingToken, uint32 duration, uint256 maxPerDeposit, diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index 5051bba937..2730c6c5ab 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -96,6 +96,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase { emit VaultInitialized( vaultAdmin, + arbitrator, config.underlyingToken, duration, config.maxPerDeposit, From 28a93121cbe1349f341ebfe3b8499828623fb93c Mon Sep 17 00:00:00 2001 From: Cursor Bot Date: Wed, 14 Jan 2026 09:59:42 -0500 Subject: [PATCH 3/3] docs: update docs: --- docs/core/DurationVaultStrategy.md | 39 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/docs/core/DurationVaultStrategy.md b/docs/core/DurationVaultStrategy.md index aca3b9d039..35d0f1b4ac 100644 --- a/docs/core/DurationVaultStrategy.md +++ b/docs/core/DurationVaultStrategy.md @@ -31,7 +31,9 @@ The `DurationVaultStrategy` is a **time-bound, single-use EigenLayer strategy** 3. **Stakers deposit** during the open window, subject to per-deposit and total caps 4. **Admin locks the vault** — deposits/withdrawal queuing blocked; full magnitude allocated to operator set 5. **AVS submits rewards** — stakers can claim rewards via normal EigenLayer reward flow -6. **Duration elapses** — anyone calls `markMatured()` to deallocate and enable withdrawals +6. **Vault exits lock**: + - **Normal exit**: duration elapses and anyone calls `markMatured()` to deallocate and enable withdrawals + - **Early exit**: arbitrator calls `advanceToWithdrawals()` after lock but before duration elapses 7. **Stakers withdraw** — receive principal minus any slashing that occurred > **Note**: Duration vaults are **single-use**. Once matured, a vault cannot be re-locked. Deploy a new vault for new duration commitments. @@ -46,7 +48,7 @@ The `DurationVaultStrategy` is a **time-bound, single-use EigenLayer strategy** | [Configuration](#configuration) | [`updateTVLLimits`](#updatetvllimits), [`setTVLLimits`](#settvllimits), [`updateMetadataURI`](#updatemetadatauri) | | [State: DEPOSITS](#state-deposits) | [`beforeAddShares`](#beforeaddshares), [`beforeRemoveShares`](#beforeremoveshares) | | [State: ALLOCATIONS](#state-allocations) | [`lock`](#lock) | -| [State: WITHDRAWALS](#state-withdrawals) | [`markMatured`](#markmatured) | +| [State: WITHDRAWALS](#state-withdrawals) | [`markMatured`](#markmatured), [`advanceToWithdrawals`](#advancetowithdrawals) | --- @@ -117,6 +119,7 @@ function deployDurationVaultStrategy( struct VaultConfig { IERC20 underlyingToken; // Token stakers deposit address vaultAdmin; // Address that can lock the vault + address arbitrator; // Address that can advance to withdrawals early (after lock, pre-duration) uint32 duration; // Lock duration in seconds uint256 maxPerDeposit; // Max deposit per transaction uint256 stakeCap; // Max total deposits (TVL cap) @@ -139,6 +142,7 @@ struct VaultConfig { * Pause status MUST NOT be set: `PAUSED_NEW_STRATEGIES` * Token MUST NOT be blacklisted * `vaultAdmin` MUST NOT be zero address +* `arbitrator` MUST NOT be zero address * `duration` MUST be non-zero and <= `MAX_DURATION` (2 years for now) * `maxPerDeposit` MUST be <= `stakeCap` * `operatorSet.avs` MUST NOT be zero address @@ -220,6 +224,7 @@ stateDiagram-v2 DEPOSITS --> ALLOCATIONS: lock()
vaultAdmin only ALLOCATIONS --> WITHDRAWALS: markMatured()
anyone, after duration + ALLOCATIONS --> WITHDRAWALS: advanceToWithdrawals()
arbitrator only, before duration note right of DEPOSITS ✓ Deposits @@ -248,7 +253,7 @@ _* Slashable after allocation delay passes. ** Slashable until deallocation dela |-------|:--------:|:-----------------:|:---------:|---------|-----| | `DEPOSITS` | ✓ | ✓ | ✗ | — | — | | `ALLOCATIONS` | ✗ | ✗ | ✓* | `lock()` | `vaultAdmin` | -| `WITHDRAWALS` | ✗ | ✓ | ✓** | `markMatured()` | Anyone | +| `WITHDRAWALS` | ✗ | ✓ | ✓** | `markMatured()` / `advanceToWithdrawals()` | Anyone / `arbitrator` | --- @@ -354,6 +359,26 @@ Transitions the vault from `ALLOCATIONS` to `WITHDRAWALS`. Callable by anyone on > **NOTE**: Even after `markMatured()`, the vault **remains slashable** for `DEALLOCATION_DELAY` blocks until the deallocation takes effect on the `AllocationManager`. This is standard EigenLayer behavior for any deallocation. +### `advanceToWithdrawals` + +```js +function advanceToWithdrawals() external +``` + +Transitions the vault from `ALLOCATIONS` to `WITHDRAWALS` **early**, after lock but before `unlockAt`. This is intended for use cases where an external agreement is violated (e.g., premiums not paid) and the vault should allow stakers to exit before the duration elapses. + +*Effects*: +* Sets `maturedAt` to current timestamp +* Transitions state to `WITHDRAWALS` +* Attempts to deallocate magnitude to 0 (best-effort) +* Attempts to deregister from operator set (best-effort) +* Emits `VaultAdvancedToWithdrawals(arbitrator, maturedAt)` (and `VaultMatured(maturedAt)`) + +*Requirements*: +* Caller MUST be `arbitrator` +* State MUST be `ALLOCATIONS` (i.e., vault must be locked) +* `block.timestamp` MUST be < `unlockAt` + ### Withdrawals After maturity, stakers can queue and complete withdrawals through the standard EigenLayer flow via `DelegationManager`. The `beforeRemoveShares` hook allows withdrawal queuing when state is `WITHDRAWALS`. @@ -374,6 +399,7 @@ Rewards follow the standard EigenLayer flow: |----------|---------| | `state()` | Current `VaultState` enum | | `vaultAdmin()` | Vault administrator address | +| `arbitrator()` | Vault arbitrator address | | `duration()` | Configured lock duration in seconds | | `lockedAt()` | Timestamp when vault was locked (0 if not locked) | | `unlockTimestamp()` | Timestamp when vault matures (0 if not locked) | @@ -412,9 +438,10 @@ Rewards follow the standard EigenLayer flow: | Event | Description | |-------|-------------| -| `VaultInitialized(vaultAdmin, underlyingToken, duration, maxPerDeposit, stakeCap, metadataURI)` | Vault initialized with configuration | +| `VaultInitialized(vaultAdmin, arbitrator, underlyingToken, duration, maxPerDeposit, stakeCap, metadataURI)` | Vault initialized with configuration | | `VaultLocked(lockedAt, unlockAt)` | Vault transitioned to `ALLOCATIONS` | | `VaultMatured(maturedAt)` | Vault transitioned to `WITHDRAWALS` | +| `VaultAdvancedToWithdrawals(arbitrator, maturedAt)` | Vault transitioned to `WITHDRAWALS` early by the arbitrator | | `MetadataURIUpdated(newMetadataURI)` | Metadata URI changed | | `MaxPerDepositUpdated(previousValue, newValue)` | Per-deposit cap changed | | `MaxTotalDepositsUpdated(previousValue, newValue)` | Total deposit cap changed | @@ -426,13 +453,17 @@ Rewards follow the standard EigenLayer flow: | Error | When Thrown | |-------|-------------| | `InvalidVaultAdmin` | Zero-address vault admin in config | +| `InvalidArbitrator` | Zero-address arbitrator in config | | `InvalidDuration` | Zero or excessive duration (> `MAX_DURATION`) in config | | `OnlyVaultAdmin` | Non-admin calls admin-only function | +| `OnlyArbitrator` | Non-arbitrator calls arbitrator-only function | | `VaultAlreadyLocked` | Attempting to lock an already locked vault | | `DepositsLocked` | Deposit attempted after vault is locked | | `WithdrawalsLockedDuringAllocations` | Withdrawal queuing attempted during `ALLOCATIONS` state | | `MustBeDelegatedToVaultOperator` | Staker not delegated to vault before deposit | | `DurationNotElapsed` | `markMatured()` called before `unlockAt` timestamp | +| `DurationAlreadyElapsed` | `advanceToWithdrawals()` called at/after `unlockAt` timestamp | +| `VaultNotLocked` | `advanceToWithdrawals()` called before the vault is locked | | `OperatorIntegrationInvalid` | Invalid operator integration config (zero AVS address) | | `UnderlyingTokenBlacklisted` | Deposit attempted with blacklisted token | | `PendingAllocation` | `lock()` attempted with pending allocation modification |