@@ -49,9 +49,9 @@ abstract contract VaultHub is PausableUntilWithRoles {
49
49
uint16 treasuryFeeBP;
50
50
/// @notice if true, vault is disconnected and fee is not accrued
51
51
bool isDisconnected;
52
- /// @notice timestamp when the vault can force withdraw in case it is unbalanced
52
+ /// @notice timestamp when the vault became unbalanced
53
53
/// @dev 0 if the vault is currently balanced
54
- uint40 forceWithdrawalUnlockTime ;
54
+ uint40 unbalancedSince ;
55
55
// ### we have 64 bits left in this slot
56
56
}
57
57
@@ -73,7 +73,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
73
73
uint256 internal constant CONNECT_DEPOSIT = 1 ether ;
74
74
75
75
/// @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 ;
77
77
78
78
/// @notice Lido stETH contract
79
79
IStETH public immutable STETH;
@@ -166,7 +166,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
166
166
uint16 (_reserveRatioThresholdBP),
167
167
uint16 (_treasuryFeeBP),
168
168
false , // isDisconnected
169
- 0 // forceWithdrawalUnlockTime
169
+ 0 // unbalancedSince
170
170
);
171
171
$.vaultIndex[_vault] = $.sockets.length ;
172
172
$.sockets.push (vr);
@@ -233,10 +233,11 @@ abstract contract VaultHub is PausableUntilWithRoles {
233
233
if (vaultSharesAfterMint > shareLimit) revert ShareLimitExceeded (_vault, shareLimit);
234
234
235
235
uint256 reserveRatioBP = socket.reserveRatioBP;
236
- uint256 maxMintableShares = _maxMintableShares (_vault, reserveRatioBP, shareLimit);
236
+ uint256 valuation = IStakingVault (_vault).valuation ();
237
+ uint256 maxMintableShares = _maxMintableShares (valuation, reserveRatioBP, shareLimit);
237
238
238
239
if (vaultSharesAfterMint > maxMintableShares) {
239
- revert InsufficientValuationToMint (_vault, IStakingVault (_vault). valuation () );
240
+ revert InsufficientValuationToMint (_vault, valuation);
240
241
}
241
242
242
243
socket.sharesMinted = uint96 (vaultSharesAfterMint);
@@ -272,7 +273,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
272
273
273
274
STETH.burnExternalShares (_amountOfShares);
274
275
275
- _updateUnbalancedState (_vault, socket);
276
+ _vaultAssessment (_vault, socket);
276
277
277
278
emit BurnedSharesOnVault (_vault, _amountOfShares);
278
279
}
@@ -293,7 +294,8 @@ abstract contract VaultHub is PausableUntilWithRoles {
293
294
294
295
VaultSocket storage socket = _connectedSocket (_vault);
295
296
296
- uint256 threshold = _maxMintableShares (_vault, socket.reserveRatioThresholdBP, socket.shareLimit);
297
+ uint256 valuation = IStakingVault (_vault).valuation ();
298
+ uint256 threshold = _maxMintableShares (valuation, socket.reserveRatioThresholdBP, socket.shareLimit);
297
299
uint256 sharesMinted = socket.sharesMinted;
298
300
if (sharesMinted <= threshold) {
299
301
// NOTE!: on connect vault is always balanced
@@ -316,13 +318,12 @@ abstract contract VaultHub is PausableUntilWithRoles {
316
318
// reserveRatio = BPS_BASE - maxMintableRatio
317
319
// X = (mintedStETH * BPS_BASE - vault.valuation() * maxMintableRatio) / reserveRatio
318
320
319
- uint256 amountToRebalance = (mintedStETH * TOTAL_BASIS_POINTS -
320
- IStakingVault (_vault).valuation () * maxMintableRatio) / reserveRatioBP;
321
+ uint256 amountToRebalance = (mintedStETH * TOTAL_BASIS_POINTS - valuation * maxMintableRatio) / reserveRatioBP;
321
322
322
323
// TODO: add some gas compensation here
323
324
IStakingVault (_vault).rebalance (amountToRebalance);
324
325
325
- // NB: check _updateUnbalancedState is calculated in rebalance() triggered from the `StakingVault`.
326
+ // NB: check _updateUnbalancedSince is calculated in rebalance() triggered from the `StakingVault`.
326
327
}
327
328
328
329
/// @notice rebalances the vault by writing off the amount of ether equal
@@ -341,8 +342,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
341
342
342
343
STETH.rebalanceExternalEtherToInternal {value: msg .value }();
343
344
344
- // Check if vault is still unbalanced after rebalance
345
- _updateUnbalancedState (msg .sender , socket);
345
+ _vaultAssessment (msg .sender , socket);
346
346
347
347
emit VaultRebalanced (msg .sender , sharesToBurn);
348
348
}
@@ -351,29 +351,31 @@ abstract contract VaultHub is PausableUntilWithRoles {
351
351
/// @param _vault vault address
352
352
/// @return bool whether the vault can force withdraw
353
353
function canForceValidatorWithdrawal (address _vault ) public view returns (bool ) {
354
- uint40 forceWithdrawalUnlockTime = _connectedSocket (_vault).forceWithdrawalUnlockTime ;
354
+ uint40 unbalancedSince = _connectedSocket (_vault).unbalancedSince ;
355
355
356
- if (forceWithdrawalUnlockTime == 0 ) return false ;
356
+ if (unbalancedSince == 0 ) return false ;
357
357
358
- return block .timestamp >= forceWithdrawalUnlockTime ;
358
+ return block .timestamp >= unbalancedSince + FORCE_WITHDRAWAL_TIMELOCK ;
359
359
}
360
360
361
361
/// @notice forces validator withdrawal from the beacon chain in case the vault is unbalanced
362
362
/// @param _vault vault address
363
363
/// @param _pubkeys pubkeys of the validators to withdraw
364
364
function forceValidatorWithdrawal (address _vault , bytes calldata _pubkeys ) external payable {
365
+ if (msg .value == 0 ) revert ZeroArgument ("msg.value " );
365
366
if (_vault == address (0 )) revert ZeroArgument ("_vault " );
366
367
if (_pubkeys.length == 0 ) revert ZeroArgument ("_pubkeys " );
367
368
368
369
VaultSocket storage socket = _connectedSocket (_vault);
369
370
370
- uint256 threshold = _maxMintableShares (_vault, socket.reserveRatioThresholdBP, socket.shareLimit);
371
+ uint256 valuation = IStakingVault (_vault).valuation ();
372
+ uint256 threshold = _maxMintableShares (valuation, socket.reserveRatioThresholdBP, socket.shareLimit);
371
373
if (socket.sharesMinted <= threshold) {
372
374
revert AlreadyBalanced (_vault, socket.sharesMinted, threshold);
373
375
}
374
376
375
377
if (! canForceValidatorWithdrawal (_vault)) {
376
- revert ForceWithdrawalTimelockActive (_vault, socket.forceWithdrawalUnlockTime );
378
+ revert ForceWithdrawalTimelockActive (_vault, socket.unbalancedSince + FORCE_WITHDRAWAL_TIMELOCK );
377
379
}
378
380
379
381
IStakingVault (_vault).forceValidatorWithdrawal {value: msg .value }(_pubkeys);
@@ -403,7 +405,12 @@ abstract contract VaultHub is PausableUntilWithRoles {
403
405
uint256 _preTotalShares ,
404
406
uint256 _preTotalPooledEther ,
405
407
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
+ ) {
407
414
/// HERE WILL BE ACCOUNTING DRAGON
408
415
409
416
// \||/
@@ -424,6 +431,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
424
431
425
432
treasuryFeeShares = new uint256 [](length);
426
433
lockedEther = new uint256 [](length);
434
+ thresholdEther = new uint256 [](length);
427
435
428
436
for (uint256 i = 0 ; i < length; ++ i) {
429
437
VaultSocket memory socket = $.sockets[i + 1 ];
@@ -444,6 +452,9 @@ abstract contract VaultHub is PausableUntilWithRoles {
444
452
(mintedStETH * TOTAL_BASIS_POINTS) / (TOTAL_BASIS_POINTS - socket.reserveRatioBP),
445
453
CONNECT_DEPOSIT
446
454
);
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);
447
458
}
448
459
}
449
460
}
@@ -472,7 +483,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
472
483
473
484
// TODO: optimize potential rewards calculation
474
485
uint256 potentialRewards = ((chargeableValue * (_postTotalPooledEther * _preTotalShares)) /
475
- (_postTotalSharesNoFees * _preTotalPooledEther) - chargeableValue);
486
+ (_postTotalSharesNoFees * _preTotalPooledEther) - chargeableValue);
476
487
uint256 treasuryFee = (potentialRewards * _socket.treasuryFeeBP) / TOTAL_BASIS_POINTS;
477
488
478
489
treasuryFeeShares = (treasuryFee * _preTotalShares) / _preTotalPooledEther;
@@ -482,6 +493,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
482
493
uint256 [] memory _valuations ,
483
494
int256 [] memory _inOutDeltas ,
484
495
uint256 [] memory _locked ,
496
+ uint256 [] memory _thresholds ,
485
497
uint256 [] memory _treasureFeeShares
486
498
) internal {
487
499
VaultHubStorage storage $ = _getVaultHubStorage ();
@@ -496,7 +508,8 @@ abstract contract VaultHub is PausableUntilWithRoles {
496
508
socket.sharesMinted += uint96 (treasuryFeeShares);
497
509
}
498
510
499
- _updateUnbalancedState (socket.vault, socket);
511
+ _epicrisis (_valuations[i], _thresholds[i], socket);
512
+
500
513
501
514
IStakingVault (socket.vault).report (_valuations[i], _inOutDeltas[i], _locked[i]);
502
515
}
@@ -517,21 +530,25 @@ abstract contract VaultHub is PausableUntilWithRoles {
517
530
}
518
531
}
519
532
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
+ }
524
540
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);
530
547
}
531
548
} 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) );
535
552
}
536
553
}
537
554
}
@@ -549,9 +566,8 @@ abstract contract VaultHub is PausableUntilWithRoles {
549
566
550
567
/// @dev returns total number of stETH shares that is possible to mint on the provided vault with provided reserveRatio
551
568
/// 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;
555
571
556
572
return Math256.min (STETH.getSharesByPooledEth (maxStETHMinted), _shareLimit);
557
573
}
0 commit comments