Skip to content

Commit b5f4711

Browse files
committed
refactor: security review
1 parent 44ac0d6 commit b5f4711

File tree

9 files changed

+153
-84
lines changed

9 files changed

+153
-84
lines changed

src/contracts/interfaces/IDurationVaultStrategy.sol

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ interface IDurationVaultStrategy is IStrategy {
2929
OperatorSet operatorSet;
3030
bytes operatorSetRegistrationData;
3131
address delegationApprover;
32-
uint32 operatorAllocationDelay;
3332
string operatorMetadataURI;
3433
}
3534

@@ -107,7 +106,4 @@ interface IDurationVaultStrategy is IStrategy {
107106
function operatorSetRegistered() external view returns (bool);
108107
function allocationsActive() external view returns (bool);
109108
function operatorSetInfo() external view returns (address avs, uint32 operatorSetId);
110-
111-
/// @notice Underlying amount currently queued for withdrawal but not yet completed.
112-
function queuedUnderlying() external view returns (uint256);
113109
}

src/contracts/strategies/DurationVaultStrategy.sol

Lines changed: 35 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ import "../libraries/OperatorSetLib.sol";
1414
contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
1515
using OperatorSetLib for OperatorSet;
1616

17-
/// @dev Thrown when `_queuedUnderlying` exceeds the vault's underlying balance.
18-
/// This indicates inconsistent queued-withdrawal accounting and is treated as an invariant violation.
19-
error QueuedUnderlyingExceedsBalance();
17+
/// @dev Thrown when attempting to allocate while a pending allocation modification already exists.
18+
error PendingAllocation();
2019

2120
/// @notice Emitted when `maxPerDeposit` value is updated from `previousValue` to `newValue`.
2221
event MaxPerDepositUpdated(uint256 previousValue, uint256 newValue);
@@ -141,16 +140,14 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
141140
}
142141

143142
/// @inheritdoc StrategyBase
143+
/// @dev State and TVL limit checks are performed in `beforeAddShares` which is called
144+
/// after deposit by the StrategyManager. This hook only validates the token type.
144145
function _beforeDeposit(
145146
IERC20 token,
146-
uint256 amount
147+
uint256 /*amount*/
147148
) internal virtual override {
148149
require(_state == VaultState.DEPOSITS, DepositsLocked());
149-
require(amount <= maxPerDeposit, MaxPerDepositExceedsMax());
150-
uint256 balance = _tokenBalance();
151-
require(balance >= _queuedUnderlying, QueuedUnderlyingExceedsBalance());
152-
require(balance - _queuedUnderlying <= maxTotalDeposits, BalanceExceedsMaxTotalDeposits());
153-
super._beforeDeposit(token, amount);
150+
super._beforeDeposit(token, 0);
154151
}
155152

156153
/// @inheritdoc IDurationVaultStrategy
@@ -196,24 +193,33 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
196193
require(_state == VaultState.DEPOSITS, DepositsLocked());
197194
require(delegationManager.delegatedTo(staker) == address(this), MustBeDelegatedToVaultOperator());
198195

199-
// Enforce per-deposit and total caps using the minted shares as proxy for underlying.
196+
// Enforce per-deposit cap using the minted shares as proxy for underlying.
200197
uint256 amountUnderlying = sharesToUnderlyingView(shares);
201198
require(amountUnderlying <= maxPerDeposit, MaxPerDepositExceedsMax());
202199

203-
uint256 balance = _tokenBalance();
204-
require(balance >= _queuedUnderlying, QueuedUnderlyingExceedsBalance());
205-
require(balance - _queuedUnderlying <= maxTotalDeposits, BalanceExceedsMaxTotalDeposits());
200+
// Enforce total cap using operatorShares (active, non-queued shares).
201+
// At this point, operatorShares hasn't been updated yet, so we add the new shares.
202+
uint256 currentOperatorShares = _getOperatorShares();
203+
uint256 postDepositUnderlying = sharesToUnderlyingView(currentOperatorShares + shares);
204+
require(postDepositUnderlying <= maxTotalDeposits, BalanceExceedsMaxTotalDeposits());
205+
}
206+
207+
/// @notice Returns the current operator shares for this vault/strategy.
208+
function _getOperatorShares() internal view returns (uint256) {
209+
IStrategy[] memory strategies = new IStrategy[](1);
210+
strategies[0] = IStrategy(address(this));
211+
uint256[] memory sharesArray = delegationManager.getOperatorShares(address(this), strategies);
212+
return sharesArray[0];
206213
}
207214

208215
/// @inheritdoc IStrategy
209216
function beforeRemoveShares(
210217
address,
211-
uint256 shares
212-
) external override(IStrategy, StrategyBase) onlyStrategyManager {
218+
uint256
219+
) external view override(IStrategy, StrategyBase) onlyStrategyManager {
220+
// Queuing withdrawals is blocked during ALLOCATIONS. Withdrawals queued during
221+
// DEPOSITS can complete during ALLOCATIONS since they were queued before lock.
213222
require(_state != VaultState.ALLOCATIONS, WithdrawalsLockedDuringAllocations());
214-
// Track queued underlying when withdrawals are queued (only possible during DEPOSITS).
215-
uint256 amountUnderlying = sharesToUnderlyingView(shares);
216-
_queuedUnderlying += amountUnderlying;
217223
}
218224

219225
/// @notice Sets the maximum deposits (in underlyingToken) that this strategy will hold and accept per deposit.
@@ -238,11 +244,6 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
238244
return (_operatorSet.avs, _operatorSet.id);
239245
}
240246

241-
/// @inheritdoc IDurationVaultStrategy
242-
function queuedUnderlying() external view override returns (uint256) {
243-
return _queuedUnderlying;
244-
}
245-
246247
function operatorSetRegistered() public view override returns (bool) {
247248
return _state == VaultState.DEPOSITS || _state == VaultState.ALLOCATIONS;
248249
}
@@ -252,51 +253,28 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
252253
}
253254

254255
function _beforeWithdrawal(
255-
address recipient,
256+
address, /*recipient*/
256257
IERC20 token,
257-
uint256 amountShares
258-
) internal virtual override {
259-
// Withdrawals can be completed during ALLOCATIONS only if they were queued before lock.
260-
// Queuing is blocked by `beforeRemoveShares` once ALLOCATIONS starts, so any withdrawal
261-
// reaching this point in ALLOCATIONS must have been queued earlier. Redistribution
262-
// during slashing is also allowed while locked.
263-
super._beforeWithdrawal(recipient, token, amountShares);
264-
265-
// Mirror `StrategyBase.withdraw`'s totalShares guard to avoid overflow in
266-
// `sharesToUnderlyingView(amountShares)` when `amountShares` is extremely large.
267-
require(amountShares <= totalShares, WithdrawalAmountExceedsTotalDeposits());
268-
uint256 amountUnderlying = sharesToUnderlyingView(amountShares);
269-
// Adjust queued-underlying accounting when withdrawals complete as tokens.
270-
if (_state == VaultState.ALLOCATIONS) {
271-
// During ALLOCATIONS, only withdrawals that were queued pre-lock should be allowed.
272-
// Slashing redistribution is also allowed and should not consume queuedUnderlying.
273-
if (recipient != allocationManager.getRedistributionRecipient(_operatorSet)) {
274-
require(_queuedUnderlying >= amountUnderlying, WithdrawalsLocked());
275-
_queuedUnderlying -= amountUnderlying;
276-
}
277-
} else {
278-
if (_queuedUnderlying >= amountUnderlying) {
279-
_queuedUnderlying -= amountUnderlying;
280-
} else {
281-
_queuedUnderlying = 0;
282-
}
283-
}
258+
uint256 /*amountShares*/
259+
) internal view virtual override {
260+
// Withdrawals are only blocked at the queue stage (beforeRemoveShares).
261+
// Once queued, withdrawals can complete in any state. The DelegationManager
262+
// validates that only properly-queued withdrawals reach this point.
263+
require(token == underlyingToken, OnlyUnderlyingToken());
284264
}
285265

286266
function _configureOperatorIntegration(
287267
VaultConfig memory config
288268
) internal {
289-
require(config.operatorSet.avs != address(0) && config.operatorSet.id != 0, OperatorIntegrationInvalid());
269+
require(config.operatorSet.avs != address(0), OperatorIntegrationInvalid());
290270
_operatorSet = config.operatorSet;
291271

292272
// Set allocation delay strictly greater than withdrawal delay to protect pre-lock queued withdrawals.
293273
uint32 minWithdrawal = delegationManager.minWithdrawalDelayBlocks();
294-
allocationDelayBlocks = minWithdrawal + 1;
274+
uint32 allocationDelay = minWithdrawal + 1;
295275

296276
// apply allocation delay at registration
297-
delegationManager.registerAsOperator(
298-
config.delegationApprover, allocationDelayBlocks, config.operatorMetadataURI
299-
);
277+
delegationManager.registerAsOperator(config.delegationApprover, allocationDelay, config.operatorMetadataURI);
300278

301279
IAllocationManager.RegisterParams memory params;
302280
params.avs = config.operatorSet.avs;
@@ -311,7 +289,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBase {
311289
// Pending modifications would cause ModificationAlreadyPending() in AllocationManager.modifyAllocations.
312290
IAllocationManager.Allocation memory alloc =
313291
allocationManager.getAllocation(address(this), _operatorSet, IStrategy(address(this)));
314-
require(alloc.effectBlock == 0, "PendingAllocation");
292+
require(alloc.effectBlock == 0, PendingAllocation());
315293

316294
IAllocationManager.AllocateParams[] memory params = new IAllocationManager.AllocateParams[](1);
317295
params[0].operatorSet = _operatorSet;

src/contracts/strategies/DurationVaultStrategyStorage.sol

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,16 @@ abstract contract DurationVaultStrategyStorage is IDurationVaultStrategy {
3838
/// @notice Stored operator set metadata for integration with the allocation manager.
3939
OperatorSet internal _operatorSet;
4040

41-
/// @notice Amount of underlying tokens corresponding to shares that have been queued for withdrawal but not completed yet.
42-
uint256 internal _queuedUnderlying;
43-
4441
/// @notice The maximum deposit (in underlyingToken) that this strategy will accept per deposit.
4542
uint256 public maxPerDeposit;
4643

4744
/// @notice The maximum deposits (in underlyingToken) that this strategy will hold.
4845
uint256 public maxTotalDeposits;
4946

50-
/// @notice Allocation delay (in blocks) applied when the vault locks.
51-
uint32 internal allocationDelayBlocks;
52-
5347
/// @dev This empty reserved space is put in place to allow future versions to add new
5448
/// variables without shifting down storage in the inheritance chain.
55-
uint256[39] private __gap;
49+
/// Storage slots used: vaultAdmin (1) + duration/lockedAt/unlockAt/maturedAt/_state (packed, 1) +
50+
/// metadataURI (1) + _operatorSet (2) + maxPerDeposit (1) + maxTotalDeposits (1) = 8.
51+
/// Gap: 50 - 8 = 42.
52+
uint256[42] private __gap;
5653
}

src/contracts/strategies/StrategyFactoryStorage.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,7 @@ abstract contract StrategyFactoryStorage is IStrategyFactory {
3131
/// @dev This empty reserved space is put in place to allow future versions to add new
3232
/// variables without shifting down storage in the inheritance chain.
3333
/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
34+
/// Storage slots used: strategyBeacon (1) + deployedStrategies (1) + isBlacklisted (1) +
35+
/// durationVaultBeacon (1) + durationVaultsByToken (1) = 5 slots. Gap: 51 - 5 = 46.
3436
uint256[46] private __gap;
3537
}

src/test/integration/tests/DurationVaultIntegration.t.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,6 @@ contract Integration_DurationVault is IntegrationCheckUtils {
403403
config.operatorSet = opSet;
404404
config.operatorSetRegistrationData = "";
405405
config.delegationApprover = address(0);
406-
config.operatorAllocationDelay = 0;
407406
config.operatorMetadataURI = "ipfs://duration-vault/operator";
408407

409408
IDurationVaultStrategy vault = IDurationVaultStrategy(address(strategyFactory.deployDurationVaultStrategy(config)));

src/test/mocks/DelegationManagerMock.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ contract DelegationManagerMock is Test {
105105
return withdrawalRoot;
106106
}
107107

108+
function getOperatorShares(address operator, IStrategy[] memory strategies) external view returns (uint[] memory) {
109+
uint[] memory shares = new uint[](strategies.length);
110+
for (uint i = 0; i < strategies.length; i++) {
111+
shares[i] = operatorShares[operator][strategies[i]];
112+
}
113+
return shares;
114+
}
115+
108116
function getOperatorsShares(address[] memory operators, IStrategy[] memory strategies) external view returns (uint[][] memory) {
109117
uint[][] memory operatorSharesArray = new uint[][](operators.length);
110118
for (uint i = 0; i < operators.length; i++) {

0 commit comments

Comments
 (0)