@@ -49,9 +49,9 @@ abstract contract VaultHub is PausableUntilWithRoles {
4949 uint16 treasuryFeeBP;
5050 /// @notice if true, vault is disconnected and fee is not accrued
5151 bool isDisconnected;
52- /// @notice timestamp when the vault can force withdraw in case it is unbalanced
52+ /// @notice timestamp when the vault became unbalanced
5353 /// @dev 0 if the vault is currently balanced
54- uint40 forceWithdrawalUnlockTime ;
54+ uint40 unbalancedSince ;
5555 // ### we have 64 bits left in this slot
5656 }
5757
@@ -73,7 +73,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
7373 uint256 internal constant CONNECT_DEPOSIT = 1 ether ;
7474
7575 /// @notice Time-lock for force validator withdrawal
76- uint256 public constant FORCE_WITHDRAWAL_TIMELOCK = 3 days ;
76+ uint40 public constant FORCE_WITHDRAWAL_TIMELOCK = 3 days ;
7777
7878 /// @notice Lido stETH contract
7979 IStETH public immutable STETH;
@@ -166,7 +166,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
166166 uint16 (_reserveRatioThresholdBP),
167167 uint16 (_treasuryFeeBP),
168168 false , // isDisconnected
169- 0 // forceWithdrawalUnlockTime
169+ 0 // unbalancedSince
170170 );
171171 $.vaultIndex[_vault] = $.sockets.length ;
172172 $.sockets.push (vr);
@@ -233,10 +233,11 @@ abstract contract VaultHub is PausableUntilWithRoles {
233233 if (vaultSharesAfterMint > shareLimit) revert ShareLimitExceeded (_vault, shareLimit);
234234
235235 uint256 reserveRatioBP = socket.reserveRatioBP;
236- uint256 maxMintableShares = _maxMintableShares (_vault, reserveRatioBP, shareLimit);
236+ uint256 valuation = IStakingVault (_vault).valuation ();
237+ uint256 maxMintableShares = _maxMintableShares (valuation, reserveRatioBP, shareLimit);
237238
238239 if (vaultSharesAfterMint > maxMintableShares) {
239- revert InsufficientValuationToMint (_vault, IStakingVault (_vault). valuation () );
240+ revert InsufficientValuationToMint (_vault, valuation);
240241 }
241242
242243 socket.sharesMinted = uint96 (vaultSharesAfterMint);
@@ -272,7 +273,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
272273
273274 STETH.burnExternalShares (_amountOfShares);
274275
275- _updateUnbalancedState (_vault, socket);
276+ _vaultAssessment (_vault, socket);
276277
277278 emit BurnedSharesOnVault (_vault, _amountOfShares);
278279 }
@@ -293,7 +294,8 @@ abstract contract VaultHub is PausableUntilWithRoles {
293294
294295 VaultSocket storage socket = _connectedSocket (_vault);
295296
296- uint256 threshold = _maxMintableShares (_vault, socket.reserveRatioThresholdBP, socket.shareLimit);
297+ uint256 valuation = IStakingVault (_vault).valuation ();
298+ uint256 threshold = _maxMintableShares (valuation, socket.reserveRatioThresholdBP, socket.shareLimit);
297299 uint256 sharesMinted = socket.sharesMinted;
298300 if (sharesMinted <= threshold) {
299301 // NOTE!: on connect vault is always balanced
@@ -316,13 +318,12 @@ abstract contract VaultHub is PausableUntilWithRoles {
316318 // reserveRatio = BPS_BASE - maxMintableRatio
317319 // X = (mintedStETH * BPS_BASE - vault.valuation() * maxMintableRatio) / reserveRatio
318320
319- uint256 amountToRebalance = (mintedStETH * TOTAL_BASIS_POINTS -
320- IStakingVault (_vault).valuation () * maxMintableRatio) / reserveRatioBP;
321+ uint256 amountToRebalance = (mintedStETH * TOTAL_BASIS_POINTS - valuation * maxMintableRatio) / reserveRatioBP;
321322
322323 // TODO: add some gas compensation here
323324 IStakingVault (_vault).rebalance (amountToRebalance);
324325
325- // NB: check _updateUnbalancedState is calculated in rebalance() triggered from the `StakingVault`.
326+ // NB: check _updateUnbalancedSince is calculated in rebalance() triggered from the `StakingVault`.
326327 }
327328
328329 /// @notice rebalances the vault by writing off the amount of ether equal
@@ -341,8 +342,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
341342
342343 STETH.rebalanceExternalEtherToInternal {value: msg .value }();
343344
344- // Check if vault is still unbalanced after rebalance
345- _updateUnbalancedState (msg .sender , socket);
345+ _vaultAssessment (msg .sender , socket);
346346
347347 emit VaultRebalanced (msg .sender , sharesToBurn);
348348 }
@@ -351,29 +351,31 @@ abstract contract VaultHub is PausableUntilWithRoles {
351351 /// @param _vault vault address
352352 /// @return bool whether the vault can force withdraw
353353 function canForceValidatorWithdrawal (address _vault ) public view returns (bool ) {
354- uint40 forceWithdrawalUnlockTime = _connectedSocket (_vault).forceWithdrawalUnlockTime ;
354+ uint40 unbalancedSince = _connectedSocket (_vault).unbalancedSince ;
355355
356- if (forceWithdrawalUnlockTime == 0 ) return false ;
356+ if (unbalancedSince == 0 ) return false ;
357357
358- return block .timestamp >= forceWithdrawalUnlockTime ;
358+ return block .timestamp >= unbalancedSince + FORCE_WITHDRAWAL_TIMELOCK ;
359359 }
360360
361361 /// @notice forces validator withdrawal from the beacon chain in case the vault is unbalanced
362362 /// @param _vault vault address
363363 /// @param _pubkeys pubkeys of the validators to withdraw
364364 function forceValidatorWithdrawal (address _vault , bytes calldata _pubkeys ) external payable {
365+ if (msg .value == 0 ) revert ZeroArgument ("msg.value " );
365366 if (_vault == address (0 )) revert ZeroArgument ("_vault " );
366367 if (_pubkeys.length == 0 ) revert ZeroArgument ("_pubkeys " );
367368
368369 VaultSocket storage socket = _connectedSocket (_vault);
369370
370- uint256 threshold = _maxMintableShares (_vault, socket.reserveRatioThresholdBP, socket.shareLimit);
371+ uint256 valuation = IStakingVault (_vault).valuation ();
372+ uint256 threshold = _maxMintableShares (valuation, socket.reserveRatioThresholdBP, socket.shareLimit);
371373 if (socket.sharesMinted <= threshold) {
372374 revert AlreadyBalanced (_vault, socket.sharesMinted, threshold);
373375 }
374376
375377 if (! canForceValidatorWithdrawal (_vault)) {
376- revert ForceWithdrawalTimelockActive (_vault, socket.forceWithdrawalUnlockTime );
378+ revert ForceWithdrawalTimelockActive (_vault, socket.unbalancedSince + FORCE_WITHDRAWAL_TIMELOCK );
377379 }
378380
379381 IStakingVault (_vault).forceValidatorWithdrawal {value: msg .value }(_pubkeys);
@@ -403,7 +405,12 @@ abstract contract VaultHub is PausableUntilWithRoles {
403405 uint256 _preTotalShares ,
404406 uint256 _preTotalPooledEther ,
405407 uint256 _sharesToMintAsFees
406- ) internal view returns (uint256 [] memory lockedEther , uint256 [] memory treasuryFeeShares , uint256 totalTreasuryFeeShares ) {
408+ ) internal view returns (
409+ uint256 [] memory lockedEther ,
410+ uint256 [] memory thresholdEther ,
411+ uint256 [] memory treasuryFeeShares ,
412+ uint256 totalTreasuryFeeShares
413+ ) {
407414 /// HERE WILL BE ACCOUNTING DRAGON
408415
409416 // \||/
@@ -424,6 +431,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
424431
425432 treasuryFeeShares = new uint256 [](length);
426433 lockedEther = new uint256 [](length);
434+ thresholdEther = new uint256 [](length);
427435
428436 for (uint256 i = 0 ; i < length; ++ i) {
429437 VaultSocket memory socket = $.sockets[i + 1 ];
@@ -444,6 +452,9 @@ abstract contract VaultHub is PausableUntilWithRoles {
444452 (mintedStETH * TOTAL_BASIS_POINTS) / (TOTAL_BASIS_POINTS - socket.reserveRatioBP),
445453 CONNECT_DEPOSIT
446454 );
455+
456+ // Minimum amount of ether that should be in the vault to avoid unbalanced state
457+ thresholdEther[i] = (mintedStETH * TOTAL_BASIS_POINTS) / (TOTAL_BASIS_POINTS - socket.reserveRatioThresholdBP);
447458 }
448459 }
449460 }
@@ -472,7 +483,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
472483
473484 // TODO: optimize potential rewards calculation
474485 uint256 potentialRewards = ((chargeableValue * (_postTotalPooledEther * _preTotalShares)) /
475- (_postTotalSharesNoFees * _preTotalPooledEther) - chargeableValue);
486+ (_postTotalSharesNoFees * _preTotalPooledEther) - chargeableValue);
476487 uint256 treasuryFee = (potentialRewards * _socket.treasuryFeeBP) / TOTAL_BASIS_POINTS;
477488
478489 treasuryFeeShares = (treasuryFee * _preTotalShares) / _preTotalPooledEther;
@@ -482,6 +493,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
482493 uint256 [] memory _valuations ,
483494 int256 [] memory _inOutDeltas ,
484495 uint256 [] memory _locked ,
496+ uint256 [] memory _thresholds ,
485497 uint256 [] memory _treasureFeeShares
486498 ) internal {
487499 VaultHubStorage storage $ = _getVaultHubStorage ();
@@ -496,7 +508,8 @@ abstract contract VaultHub is PausableUntilWithRoles {
496508 socket.sharesMinted += uint96 (treasuryFeeShares);
497509 }
498510
499- _updateUnbalancedState (socket.vault, socket);
511+ _epicrisis (_valuations[i], _thresholds[i], socket);
512+
500513
501514 IStakingVault (socket.vault).report (_valuations[i], _inOutDeltas[i], _locked[i]);
502515 }
@@ -517,21 +530,25 @@ abstract contract VaultHub is PausableUntilWithRoles {
517530 }
518531 }
519532
520- function _updateUnbalancedState (address _vault , VaultSocket storage _socket ) internal {
521- uint256 threshold = _maxMintableShares (_vault, _socket.reserveRatioThresholdBP, _socket.shareLimit);
522- bool isUnbalanced = _socket.sharesMinted > threshold;
523- uint40 currentUnlockTime = _socket.forceWithdrawalUnlockTime;
533+ /// @notice Evaluates if vault's valuation meets minimum threshold and marks it as unbalanced if below threshold
534+ function _vaultAssessment (address _vault , VaultSocket storage _socket ) internal {
535+ uint256 valuation = IStakingVault (_vault).valuation ();
536+ uint256 threshold = (_socket.sharesMinted * TOTAL_BASIS_POINTS) / (TOTAL_BASIS_POINTS - _socket.reserveRatioThresholdBP);
537+
538+ _epicrisis (valuation, threshold, _socket);
539+ }
524540
525- if (isUnbalanced) {
526- if (currentUnlockTime == 0 ) {
527- uint40 newUnlockTime = uint40 (block .timestamp + FORCE_WITHDRAWAL_TIMELOCK);
528- _socket.forceWithdrawalUnlockTime = newUnlockTime;
529- emit VaultBecameUnbalanced (_vault, newUnlockTime);
541+ /// @notice Updates vault's unbalanced state based on if valuation is above/below threshold
542+ function _epicrisis (uint256 _valuation , uint256 _threshold , VaultSocket storage _socket ) internal {
543+ if (_valuation < _threshold) {
544+ if (_socket.unbalancedSince == 0 ) {
545+ _socket.unbalancedSince = uint40 (block .timestamp );
546+ emit VaultBecameUnbalanced (address (_socket.vault), _socket.unbalancedSince + FORCE_WITHDRAWAL_TIMELOCK);
530547 }
531548 } else {
532- if (currentUnlockTime != 0 ) {
533- _socket.forceWithdrawalUnlockTime = 0 ;
534- emit VaultBecameBalanced (_vault );
549+ if (_socket.unbalancedSince != 0 ) {
550+ _socket.unbalancedSince = 0 ;
551+ emit VaultBecameBalanced (address (_socket.vault) );
535552 }
536553 }
537554 }
@@ -549,9 +566,8 @@ abstract contract VaultHub is PausableUntilWithRoles {
549566
550567 /// @dev returns total number of stETH shares that is possible to mint on the provided vault with provided reserveRatio
551568 /// it does not count shares that is already minted, but does count shareLimit on the vault
552- function _maxMintableShares (address _vault , uint256 _reserveRatio , uint256 _shareLimit ) internal view returns (uint256 ) {
553- uint256 maxStETHMinted = (IStakingVault (_vault).valuation () * (TOTAL_BASIS_POINTS - _reserveRatio)) /
554- TOTAL_BASIS_POINTS;
569+ function _maxMintableShares (uint256 _valuation , uint256 _reserveRatio , uint256 _shareLimit ) internal view returns (uint256 ) {
570+ uint256 maxStETHMinted = (_valuation * (TOTAL_BASIS_POINTS - _reserveRatio)) / TOTAL_BASIS_POINTS;
555571
556572 return Math256.min (STETH.getSharesByPooledEth (maxStETHMinted), _shareLimit);
557573 }
0 commit comments