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
45 changes: 45 additions & 0 deletions src/contracts/interfaces/IDurationVaultStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ interface IDurationVaultStrategyErrors {
error OperatorIntegrationInvalid();
/// @dev Thrown when attempting to deposit into a vault whose underlying token is blacklisted.
error UnderlyingTokenBlacklisted();

/// @dev Thrown when a deposit exceeds the configured `maxPerDeposit` limit.
error DepositExceedsMaxPerDeposit();

/// @dev Thrown when attempting to lock with an operator set that doesn't include this strategy.
error StrategyNotSupportedByOperatorSet();

/// @dev Thrown when attempting to allocate while a pending allocation modification already exists.
error PendingAllocation();
}

/// @title Types for IDurationVaultStrategy
Expand All @@ -58,6 +67,7 @@ interface IDurationVaultStrategyTypes {
/// @notice Configuration parameters for initializing a duration vault.
/// @param underlyingToken The ERC20 token that stakers deposit into this vault.
/// @param vaultAdmin The address authorized to manage vault configuration.
/// @param arbitrator The address authorized to call `advanceToWithdrawals` for early maturity.
/// @param duration The lock period in seconds after which the vault matures.
/// @param maxPerDeposit Maximum amount of underlying tokens accepted per deposit.
/// @param stakeCap Maximum total underlying tokens the vault will accept.
Expand Down Expand Up @@ -118,6 +128,20 @@ interface IDurationVaultStrategyEvents {
/// @notice Emitted when the vault metadata URI is updated.
/// @param newMetadataURI The new metadata URI.
event MetadataURIUpdated(string newMetadataURI);

/// @notice Emitted when deallocation from the operator set is attempted.
/// @param success Whether the deallocation call succeeded.
event DeallocateAttempted(bool success);

/// @notice Emitted when deregistration from the operator set is attempted.
/// @param success Whether the deregistration call succeeded.
event DeregisterAttempted(bool success);

/// @notice Emitted when `maxPerDeposit` value is updated from `previousValue` to `newValue`.
event MaxPerDepositUpdated(uint256 previousValue, uint256 newValue);

/// @notice Emitted when `maxTotalDeposits` value is updated from `previousValue` to `newValue`.
event MaxTotalDepositsUpdated(uint256 previousValue, uint256 newValue);
}

/// @title Interface for time-bound EigenLayer vault strategies.
Expand Down Expand Up @@ -166,6 +190,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
88 changes: 65 additions & 23 deletions src/contracts/strategies/DurationVaultStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,6 @@ import "../libraries/OperatorSetLib.sol";
contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
using OperatorSetLib for OperatorSet;

/// @dev Thrown when attempting to allocate while a pending allocation modification already exists.
error PendingAllocation();

/// @notice Emitted when `maxPerDeposit` value is updated from `previousValue` to `newValue`.
event MaxPerDepositUpdated(uint256 previousValue, uint256 newValue);

/// @notice Emitted when `maxTotalDeposits` value is updated from `previousValue` to `newValue`.
event MaxTotalDepositsUpdated(uint256 previousValue, uint256 newValue);

/// @notice Delegation manager reference used to register the vault as an operator.
IDelegationManager public immutable override delegationManager;

Expand Down Expand Up @@ -109,6 +100,17 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
function lock() external override onlyVaultAdmin {
require(depositsOpen(), VaultAlreadyLocked());

// Verify this strategy is supported by the operator set before allocating.
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(_operatorSet);
bool supported = false;
for (uint256 i = 0; i < strategies.length; ++i) {
if (strategies[i] == IStrategy(address(this))) {
supported = true;
break;
}
}
require(supported, StrategyNotSupportedByOperatorSet());

uint32 currentTimestamp = uint32(block.timestamp);
lockedAt = currentTimestamp;
unlockAt = currentTimestamp + duration;
Expand All @@ -123,7 +125,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 +134,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 +153,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 +164,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 @@ -236,7 +257,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {

// Enforce per-deposit cap using the minted shares as proxy for underlying.
uint256 amountUnderlying = sharesToUnderlyingView(shares);
require(amountUnderlying <= maxPerDeposit, MaxPerDepositExceedsMax());
require(amountUnderlying <= maxPerDeposit, DepositExceedsMaxPerDeposit());

// Enforce total cap using operatorShares (active, non-queued shares).
// At this point, operatorShares hasn't been updated yet, so we add the new shares.
Expand Down Expand Up @@ -283,7 +304,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 +367,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 +389,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 +407,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.deregisterFromOperatorSets.selector, params));
success; // suppress unused variable warning
return success;
}

/// @notice Best-effort cleanup after maturity, with retry tracking.
function _attemptOperatorCleanup() internal {
bool deallocateSuccess = _deallocateAll();
emit DeallocateAttempted(deallocateSuccess);

bool deregisterSuccess = _deregisterFromOperatorSet();
emit DeregisterAttempted(deregisterSuccess);
}
}
2 changes: 1 addition & 1 deletion src/test/integration/tests/DurationVaultIntegration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ contract Integration_DurationVault is IntegrationCheckUtils {
uint[] memory amounts = _singleAmountArray(60 ether);
ctx.asset.transfer(address(staker), amounts[0]);
_delegateToVault(staker, ctx.vault);
cheats.expectRevert(IStrategyErrors.MaxPerDepositExceedsMax.selector);
cheats.expectRevert(IDurationVaultStrategyErrors.DepositExceedsMaxPerDeposit.selector);
staker.depositIntoEigenlayer(strategies, amounts);

// Deposit within limits.
Expand Down
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;
}
}
Loading