Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/contracts/interfaces/IDurationVaultStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,20 @@ interface IDurationVaultStrategy is
function lock() external;

/// @notice Marks the vault as matured once the configured duration has elapsed.
/// @dev Transitions state from ALLOCATIONS to WITHDRAWALS. Deallocates from the operator set
/// and deregisters the vault. After maturation, withdrawals are permitted while deposits
/// remain disabled. Callable by anyone once the duration has elapsed.
/// @dev Transitions state from ALLOCATIONS to WITHDRAWALS.
///
/// Best-effort operator cleanup:
/// - Attempts to deallocate from the configured operator set and deregister the vault as an operator.
/// - These external calls are intentionally best-effort so `markMatured()` cannot be bricked and user
/// withdrawals cannot be indefinitely locked.
///
/// Common reasons deallocation/deregistration can fail include:
/// - AllocationManager pausing allocations/deallocations or register/deregister operations.
/// - AVS-side registrar rejecting deregistration (e.g., operator removed/ejected from an operator set).
/// - AllocationManager state constraints (e.g., pending allocation modification preventing an update).
///
/// After maturation, withdrawals are permitted while deposits remain disabled. Callable by anyone once
/// the duration has elapsed.
function markMatured() external;

/// @notice Updates the vault metadata URI.
Expand Down
14 changes: 10 additions & 4 deletions src/contracts/strategies/DurationVaultStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
}

/// @notice Deallocates all magnitude from the configured operator set.
/// @dev Best-effort: failures are caught to avoid bricking `markMatured()`.
/// @dev Best-effort: failures are ignored to avoid bricking `markMatured()`.
function _deallocateAll() internal {
IAllocationManager.AllocateParams[] memory params = new IAllocationManager.AllocateParams[](1);
params[0].operatorSet = _operatorSet;
Expand All @@ -325,18 +325,24 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
params[0].newMagnitudes = new uint64[](1);
params[0].newMagnitudes[0] = 0;
// This call is best-effort: failures should not brick `markMatured()` and lock user funds.
try allocationManager.modifyAllocations(address(this), params) {} catch {}
// We use a low-level call instead of try/catch to avoid wallet gas-estimation pitfalls.
(bool success,) = address(allocationManager)
.call(abi.encodeWithSelector(IAllocationManagerActions.modifyAllocations.selector, address(this), params));
success; // suppress unused variable warning
}

/// @notice Deregisters the vault from its configured operator set.
/// @dev Best-effort: failures are caught to avoid bricking `markMatured()`.
/// @dev Best-effort: failures are ignored to avoid bricking `markMatured()`.
function _deregisterFromOperatorSet() internal {
IAllocationManager.DeregisterParams memory params;
params.operator = address(this);
params.avs = _operatorSet.avs;
params.operatorSetIds = new uint32[](1);
params.operatorSetIds[0] = _operatorSet.id;
// This call is best-effort: failures should not brick `markMatured()` and lock user funds.
try allocationManager.deregisterFromOperatorSets(params) {} catch {}
// We use a low-level call instead of try/catch to avoid wallet gas-estimation pitfalls.
(bool success,) = address(allocationManager)
.call(abi.encodeWithSelector(IAllocationManagerActions.deregisterFromOperatorSets.selector, params));
success; // suppress unused variable warning
}
}
13 changes: 13 additions & 0 deletions src/test/mocks/AllocationManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ contract AllocationManagerMock is Test {
uint public modifyAllocationsCallCount;
uint public deregisterFromOperatorSetsCallCount;

bool public revertModifyAllocations;
bool public revertDeregisterFromOperatorSets;

function setRevertModifyAllocations(bool shouldRevert) external {
revertModifyAllocations = shouldRevert;
}

function setRevertDeregisterFromOperatorSets(bool shouldRevert) external {
revertDeregisterFromOperatorSets = shouldRevert;
}

function getSlashCount(OperatorSet memory operatorSet) external view returns (uint) {
return _getSlashCount[operatorSet.key()];
}
Expand Down Expand Up @@ -211,6 +222,7 @@ contract AllocationManagerMock is Test {
}

function modifyAllocations(address operator, IAllocationManager.AllocateParams[] calldata params) external {
if (revertModifyAllocations) revert("AllocationManagerMock: modifyAllocations reverted");
modifyAllocationsCallCount++;
delete _lastAllocateCall;
_lastAllocateCall.operator = operator;
Expand Down Expand Up @@ -241,6 +253,7 @@ contract AllocationManagerMock is Test {
}

function deregisterFromOperatorSets(IAllocationManager.DeregisterParams calldata params) external {
if (revertDeregisterFromOperatorSets) revert("AllocationManagerMock: deregisterFromOperatorSets reverted");
deregisterFromOperatorSetsCallCount++;
_lastDeregisterCall.operator = params.operator;
_lastDeregisterCall.avs = params.avs;
Expand Down
19 changes: 19 additions & 0 deletions src/test/unit/DurationVaultStrategyUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,25 @@ contract DurationVaultStrategyUnitTests is StrategyBaseUnitTests {
assertFalse(durationVault.operatorSetRegistered(), "operator set should be deregistered");
}

function testMarkMaturedBestEffortWhenAllocationManagerReverts() public {
durationVault.lock();
cheats.warp(block.timestamp + defaultDuration + 1);

allocationManagerMock.setRevertModifyAllocations(true);
allocationManagerMock.setRevertDeregisterFromOperatorSets(true);

// Should not revert even if AllocationManager refuses deallocation/deregistration.
durationVault.markMatured();

assertTrue(durationVault.withdrawalsOpen(), "withdrawals must open after maturity");
assertFalse(durationVault.allocationsActive(), "allocations should be inactive after maturity");
assertFalse(durationVault.operatorSetRegistered(), "operator set should be unregistered after maturity");

// Since the mock reverts before incrementing, only the initial lock allocation is recorded.
assertEq(allocationManagerMock.modifyAllocationsCallCount(), 1, "unexpected modifyAllocations count");
assertEq(allocationManagerMock.deregisterFromOperatorSetsCallCount(), 0, "unexpected deregister count");
}

// ===================== VAULT STATE TESTS =====================

function testDepositsBlockedAfterLock() public {
Expand Down