Skip to content
Open
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
21 changes: 21 additions & 0 deletions src/contracts/interfaces/IDurationVaultStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,27 @@ interface IDurationVaultStrategy is
string calldata newMetadataURI
) external;

/// @notice Updates the delegation approver used for operator delegation approvals.
/// @param newDelegationApprover The new delegation approver (0x0 for open delegation).
/// @dev Only callable by the vault admin.
function updateDelegationApprover(
address newDelegationApprover
) external;

/// @notice Updates the operator metadata URI emitted by the DelegationManager.
/// @param newOperatorMetadataURI The new operator metadata URI.
/// @dev Only callable by the vault admin.
function updateOperatorMetadataURI(
string calldata newOperatorMetadataURI
) external;

/// @notice Sets the claimer for operator rewards accrued to the vault.
/// @param claimer The address authorized to claim rewards for the vault.
/// @dev Only callable by the vault admin.
function setRewardsClaimer(
address claimer
) external;

/// @notice Updates the TVL limits for max deposit per transaction and total stake cap.
/// @param newMaxPerDeposit New maximum deposit amount per transaction.
/// @param newStakeCap New maximum total deposits allowed.
Expand Down
63 changes: 50 additions & 13 deletions src/contracts/strategies/DurationVaultStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
/// @notice Marks the vault as matured once the configured duration elapses. Callable by anyone.
function markMatured() external override {
if (_state == VaultState.WITHDRAWALS) {
// already recorded; noop
_attemptOperatorCleanup();
return;
}
require(_state == VaultState.ALLOCATIONS, DurationNotElapsed());
Expand All @@ -132,15 +132,14 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
maturedAt = uint32(block.timestamp);
emit VaultMatured(maturedAt);

_deallocateAll();
_deregisterFromOperatorSet();
_attemptOperatorCleanup();
}

/// @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
_attemptOperatorCleanup();
return;
}
require(_state == VaultState.ALLOCATIONS, VaultNotLocked());
Expand All @@ -152,8 +151,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
emit VaultMatured(maturedAt);
emit VaultAdvancedToWithdrawals(msg.sender, maturedAt);

_deallocateAll();
_deregisterFromOperatorSet();
_attemptOperatorCleanup();
}

/// @notice Updates the metadata URI describing the vault.
Expand All @@ -164,6 +162,27 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
emit MetadataURIUpdated(newMetadataURI);
}

/// @notice Updates the delegation approver used for operator delegation approvals.
function updateDelegationApprover(
address newDelegationApprover
) external override onlyVaultAdmin {
delegationManager.modifyOperatorDetails(address(this), newDelegationApprover);
}

/// @notice Updates the operator metadata URI emitted by the DelegationManager.
function updateOperatorMetadataURI(
string calldata newOperatorMetadataURI
) external override onlyVaultAdmin {
delegationManager.updateOperatorMetadataURI(address(this), newOperatorMetadataURI);
}

/// @notice Sets the claimer for operator rewards accrued to the vault.
function setRewardsClaimer(
address claimer
) external override onlyVaultAdmin {
rewardsCoordinator.setClaimerFor(address(this), claimer);
}

/// @notice Updates the TVL limits for max deposit per transaction and total stake cap.
/// @dev Only callable by the vault admin while deposits are open (before lock).
function updateTVLLimits(
Expand Down Expand Up @@ -283,7 +302,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {

/// @inheritdoc IDurationVaultStrategy
function operatorSetRegistered() public view override returns (bool) {
return _state == VaultState.DEPOSITS || _state == VaultState.ALLOCATIONS;
return allocationManager.isMemberOfOperatorSet(address(this), _operatorSet);
}

/// @inheritdoc IDurationVaultStrategy
Expand Down Expand Up @@ -346,9 +365,18 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
allocationManager.modifyAllocations(address(this), params);
}

/// @notice Deallocates all magnitude from the configured operator set.
/// @notice Attempts to deallocate all magnitude from the configured operator set.
/// @dev Best-effort: failures are ignored to avoid bricking `markMatured()`.
function _deallocateAll() internal {
function _deallocateAll() internal returns (bool) {
IAllocationManager.Allocation memory alloc =
allocationManager.getAllocation(address(this), _operatorSet, IStrategy(address(this)));
if (alloc.currentMagnitude == 0 && alloc.pendingDiff == 0) {
return true;
}
// If an allocation modification is pending, wait until it clears.
if (alloc.effectBlock != 0) {
return false;
}
IAllocationManager.AllocateParams[] memory params = new IAllocationManager.AllocateParams[](1);
params[0].operatorSet = _operatorSet;
params[0].strategies = new IStrategy[](1);
Expand All @@ -359,12 +387,15 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
// 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
return success;
}

/// @notice Deregisters the vault from its configured operator set.
/// @notice Attempts to deregister the vault from its configured operator set.
/// @dev Best-effort: failures are ignored to avoid bricking `markMatured()`.
function _deregisterFromOperatorSet() internal {
function _deregisterFromOperatorSet() internal returns (bool) {
if (!allocationManager.isMemberOfOperatorSet(address(this), _operatorSet)) {
return true;
}
IAllocationManager.DeregisterParams memory params;
params.operator = address(this);
params.avs = _operatorSet.avs;
Expand All @@ -374,6 +405,12 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
// 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
return success;
}

/// @notice Best-effort cleanup after maturity, with retry tracking.
function _attemptOperatorCleanup() internal {
_deallocateAll();
_deregisterFromOperatorSet();
}
}
7 changes: 7 additions & 0 deletions src/test/mocks/AllocationManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ contract AllocationManagerMock is Test {
mapping(bytes32 operatorSetKey => mapping(address operator => bool)) internal _isOperatorSlashable;
mapping(address operator => mapping(bytes32 operatorSetKey => mapping(IStrategy strategy => IAllocationManagerTypes.Allocation)))
internal _allocations;
mapping(bytes32 operatorSetKey => mapping(address operator => bool)) internal _isMemberOfOperatorSet;

struct RegisterCall {
address operator;
Expand Down Expand Up @@ -101,6 +102,10 @@ contract AllocationManagerMock is Test {
return _isOperatorSet[operatorSet.key()];
}

function isMemberOfOperatorSet(address operator, OperatorSet memory operatorSet) external view returns (bool) {
return _isMemberOfOperatorSet[operatorSet.key()][operator];
}

function setMaxMagnitudes(address operator, IStrategy[] calldata strategies, uint64[] calldata maxMagnitudes) external {
for (uint i = 0; i < strategies.length; ++i) {
setMaxMagnitude(operator, strategies[i], maxMagnitudes[i]);
Expand Down Expand Up @@ -213,6 +218,7 @@ contract AllocationManagerMock is Test {
delete _lastRegisterCall.operatorSetIds;
for (uint i = 0; i < params.operatorSetIds.length; ++i) {
_lastRegisterCall.operatorSetIds.push(params.operatorSetIds[i]);
_isMemberOfOperatorSet[OperatorSet({avs: params.avs, id: params.operatorSetIds[i]}).key()][operator] = true;
}
_lastRegisterCall.data = params.data;
}
Expand Down Expand Up @@ -260,6 +266,7 @@ contract AllocationManagerMock is Test {
delete _lastDeregisterCall.operatorSetIds;
for (uint i = 0; i < params.operatorSetIds.length; ++i) {
_lastDeregisterCall.operatorSetIds.push(params.operatorSetIds[i]);
_isMemberOfOperatorSet[OperatorSet({avs: params.avs, id: params.operatorSetIds[i]}).key()][params.operator] = false;
}
}

Expand Down
33 changes: 33 additions & 0 deletions src/test/mocks/DelegationManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ contract DelegationManagerMock is Test {
RegisterAsOperatorCall internal _lastRegisterAsOperatorCall;
uint public registerAsOperatorCallCount;

struct ModifyOperatorDetailsCall {
address operator;
address newDelegationApprover;
}

struct UpdateOperatorMetadataURICall {
address operator;
string metadataURI;
}

ModifyOperatorDetailsCall internal _lastModifyOperatorDetailsCall;
UpdateOperatorMetadataURICall internal _lastUpdateOperatorMetadataURICall;
uint public modifyOperatorDetailsCallCount;
uint public updateOperatorMetadataURICallCount;

function getDelegatableShares(address staker) external view returns (IStrategy[] memory, uint[] memory) {}

function setMinWithdrawalDelayBlocks(uint32 newMinWithdrawalDelayBlocks) external {
Expand Down Expand Up @@ -96,10 +111,28 @@ contract DelegationManagerMock is Test {
});
}

function modifyOperatorDetails(address operator, address newDelegationApprover) external {
modifyOperatorDetailsCallCount++;
_lastModifyOperatorDetailsCall = ModifyOperatorDetailsCall({operator: operator, newDelegationApprover: newDelegationApprover});
}

function updateOperatorMetadataURI(address operator, string calldata metadataURI) external {
updateOperatorMetadataURICallCount++;
_lastUpdateOperatorMetadataURICall = UpdateOperatorMetadataURICall({operator: operator, metadataURI: metadataURI});
}

function lastRegisterAsOperatorCall() external view returns (RegisterAsOperatorCall memory) {
return _lastRegisterAsOperatorCall;
}

function lastModifyOperatorDetailsCall() external view returns (ModifyOperatorDetailsCall memory) {
return _lastModifyOperatorDetailsCall;
}

function lastUpdateOperatorMetadataURICall() external view returns (UpdateOperatorMetadataURICall memory) {
return _lastUpdateOperatorMetadataURICall;
}

function undelegate(address staker) external returns (bytes32[] memory withdrawalRoot) {
delegatedTo[staker] = address(0);
return withdrawalRoot;
Expand Down
16 changes: 16 additions & 0 deletions src/test/mocks/RewardsCoordinatorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@ contract RewardsCoordinatorMock is Test {
uint16 split;
}

struct SetClaimerForCall {
address earner;
address claimer;
}

SetOperatorAVSSplitCall internal _lastSetOperatorAVSSplitCall;
SetOperatorSetSplitCall internal _lastSetOperatorSetSplitCall;
SetClaimerForCall internal _lastSetClaimerForCall;
uint public setOperatorAVSSplitCallCount;
uint public setOperatorSetSplitCallCount;
uint public setClaimerForCallCount;

function setOperatorAVSSplit(address operator, address avs, uint16 split) external {
setOperatorAVSSplitCallCount++;
Expand All @@ -37,11 +44,20 @@ contract RewardsCoordinatorMock is Test {
_lastSetOperatorSetSplitCall = SetOperatorSetSplitCall({operator: operator, operatorSet: operatorSet, split: split});
}

function setClaimerFor(address earner, address claimer) external {
setClaimerForCallCount++;
_lastSetClaimerForCall = SetClaimerForCall({earner: earner, claimer: claimer});
}

function lastSetOperatorAVSSplitCall() external view returns (SetOperatorAVSSplitCall memory) {
return _lastSetOperatorAVSSplitCall;
}

function lastSetOperatorSetSplitCall() external view returns (SetOperatorSetSplitCall memory) {
return _lastSetOperatorSetSplitCall;
}

function lastSetClaimerForCall() external view returns (SetClaimerForCall memory) {
return _lastSetClaimerForCall;
}
}
50 changes: 49 additions & 1 deletion src/test/unit/DurationVaultStrategyUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,61 @@ contract DurationVaultStrategyUnitTests is StrategyBaseUnitTests {

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");
assertTrue(durationVault.operatorSetRegistered(), "operator set should remain registered on failure");

// 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");
}

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

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

durationVault.markMatured();
assertTrue(durationVault.operatorSetRegistered(), "operator set should remain registered after failure");

allocationManagerMock.setRevertModifyAllocations(false);
allocationManagerMock.setRevertDeregisterFromOperatorSets(false);

// markMatured is a permissionless retry path once in WITHDRAWALS.
durationVault.markMatured();

assertEq(allocationManagerMock.modifyAllocationsCallCount(), 2, "deallocation should be retried");
assertEq(allocationManagerMock.deregisterFromOperatorSetsCallCount(), 1, "deregistration should be retried");
assertFalse(durationVault.operatorSetRegistered(), "operator set should be deregistered after retry");
}

function testUpdateDelegationApprover() public {
address newApprover = address(0xDE1E6A7E);
durationVault.updateDelegationApprover(newApprover);

DelegationManagerMock.ModifyOperatorDetailsCall memory callDetails = delegationManagerMock.lastModifyOperatorDetailsCall();
assertEq(callDetails.operator, address(durationVault), "operator mismatch");
assertEq(callDetails.newDelegationApprover, newApprover, "delegation approver mismatch");
}

function testUpdateOperatorMetadataURI() public {
string memory newURI = "ipfs://updated-operator-metadata";
durationVault.updateOperatorMetadataURI(newURI);

DelegationManagerMock.UpdateOperatorMetadataURICall memory callDetails = delegationManagerMock.lastUpdateOperatorMetadataURICall();
assertEq(callDetails.operator, address(durationVault), "operator mismatch");
assertEq(callDetails.metadataURI, newURI, "metadata URI mismatch");
}

function testSetRewardsClaimer() public {
address claimer = address(0xC1A1A3);
durationVault.setRewardsClaimer(claimer);

RewardsCoordinatorMock.SetClaimerForCall memory callDetails = rewardsCoordinatorMock.lastSetClaimerForCall();
assertEq(callDetails.earner, address(durationVault), "earner mismatch");
assertEq(callDetails.claimer, claimer, "claimer mismatch");
}

function testAdvanceToWithdrawals_onlyArbitrator_and_onlyBeforeUnlock() public {
// Cannot advance before lock (even as arbitrator).
cheats.expectRevert(IDurationVaultStrategyErrors.VaultNotLocked.selector);
Expand Down