@@ -17,8 +17,9 @@ contract StvStETHPool is StvPool {
1717 event StethSharesMinted (address indexed account , uint256 stethShares );
1818 event StethSharesBurned (address indexed account , uint256 stethShares );
1919 event StethSharesRebalanced (address indexed account , uint256 stethShares , uint256 stvBurned );
20- event SocializedLoss (uint256 stv , uint256 assets );
20+ event SocializedLoss (uint256 stv , uint256 assets , uint256 maxLossSocializationBP );
2121 event VaultParametersUpdated (uint256 newReserveRatioBP , uint256 newForcedRebalanceThresholdBP );
22+ event MaxLossSocializationUpdated (uint256 newMaxLossSocializationBP );
2223
2324 error InsufficientMintingCapacity ();
2425 error InsufficientStethShares ();
@@ -31,6 +32,13 @@ contract StvStETHPool is StvPool {
3132 error VaultReportStale ();
3233 error UndercollateralizedAccount ();
3334 error CollateralizedAccount ();
35+ error ExcessiveLossSocialization ();
36+ error SameValue ();
37+ error InvalidValue ();
38+
39+ bytes32 public constant MINTING_FEATURE = keccak256 ("MINTING_FEATURE " );
40+ bytes32 public constant MINTING_PAUSE_ROLE = keccak256 ("MINTING_PAUSE_ROLE " );
41+ bytes32 public constant MINTING_RESUME_ROLE = keccak256 ("MINTING_RESUME_ROLE " );
3442
3543 bytes32 public constant LOSS_SOCIALIZER_ROLE = keccak256 ("LOSS_SOCIALIZER_ROLE " );
3644
@@ -47,6 +55,7 @@ contract StvStETHPool is StvPool {
4755 uint256 totalMintedStethShares;
4856 uint16 reserveRatioBP;
4957 uint16 forcedRebalanceThresholdBP;
58+ uint16 maxLossSocializationBP;
5059 }
5160
5261 // keccak256(abi.encode(uint256(keccak256("pool.storage.StvStETHPool")) - 1)) & ~bytes32(uint256(0xff))
@@ -67,10 +76,14 @@ contract StvStETHPool is StvPool {
6776 address _distributor ,
6877 bytes32 _poolType
6978 ) StvPool (_dashboard, _allowListEnabled, _withdrawalQueue, _distributor) {
79+ if (_reserveRatioGapBP >= TOTAL_BASIS_POINTS) revert InvalidValue ();
80+
7081 RESERVE_RATIO_GAP_BP = _reserveRatioGapBP;
82+ POOL_TYPE = _poolType;
7183 WSTETH = IWstETH (DASHBOARD.WSTETH ());
7284
73- POOL_TYPE = _poolType;
85+ // Pause features in implementation
86+ _pauseFeature (MINTING_FEATURE);
7487 }
7588
7689 function poolType () external view override returns (bytes32 ) {
@@ -301,7 +314,9 @@ contract StvStETHPool is StvPool {
301314 * on WSTETH contract during unwrapping. The dust from rounding accumulates on the WSTETH contract during unwrapping
302315 */
303316 function mintWsteth (uint256 _wsteth ) public {
317+ _checkFeatureNotPaused (MINTING_FEATURE);
304318 _checkRemainingMintingCapacityOf (msg .sender , _wsteth);
319+
305320 _increaseMintedStethShares (msg .sender , _wsteth);
306321 DASHBOARD.mintWstETH (msg .sender , _wsteth);
307322 }
@@ -311,7 +326,9 @@ contract StvStETHPool is StvPool {
311326 * @param _stethShares The amount of stETH shares to mint
312327 */
313328 function mintStethShares (uint256 _stethShares ) public {
329+ _checkFeatureNotPaused (MINTING_FEATURE);
314330 _checkRemainingMintingCapacityOf (msg .sender , _stethShares);
331+
315332 _increaseMintedStethShares (msg .sender , _stethShares);
316333 DASHBOARD.mintShares (msg .sender , _stethShares);
317334 }
@@ -453,7 +470,6 @@ contract StvStETHPool is StvPool {
453470 * @notice Sync reserve ratio and forced rebalance threshold from VaultHub
454471 * @dev Permissionless method to keep reserve ratio and forced rebalance threshold in sync with VaultHub
455472 * @dev Adds a gap defined by RESERVE_RATIO_GAP_BP to VaultHub's values
456- * @dev Reverts if the new reserve ratio or forced rebalance threshold is invalid (>= TOTAL_BASIS_POINTS)
457473 */
458474 function syncVaultParameters () public {
459475 IVaultHub.VaultConnection memory connection = DASHBOARD.vaultConnection ();
@@ -656,9 +672,14 @@ contract StvStETHPool is StvPool {
656672
657673 if (remainingStethShares > 0 ) DASHBOARD.rebalanceVaultWithShares (remainingStethShares);
658674
659- // TODO: Add sanity check for loss socialization
660675 if (stvToBurn > _maxStvToBurn) {
661- emit SocializedLoss (stvToBurn - _maxStvToBurn, ethToRebalance - _convertToAssets (_maxStvToBurn));
676+ _checkAllowedLossSocializationPortion (stvToBurn, _maxStvToBurn);
677+
678+ emit SocializedLoss (
679+ stvToBurn - _maxStvToBurn,
680+ ethToRebalance - _convertToAssets (_maxStvToBurn),
681+ _getStvStETHPoolStorage ().maxLossSocializationBP
682+ );
662683 stvToBurn = _maxStvToBurn;
663684 }
664685
@@ -681,10 +702,60 @@ contract StvStETHPool is StvPool {
681702 isBreached = _assets < assetsThreshold;
682703 }
683704
705+ function _checkAllowedLossSocializationPortion (uint256 stvRequired , uint256 stvAvailable ) internal view {
706+ // It's guaranteed that stvRequired > stvAvailable here
707+ uint256 portionToSocializeBP =
708+ Math.mulDiv (stvRequired - stvAvailable, TOTAL_BASIS_POINTS, stvRequired, Math.Rounding.Ceil);
709+
710+ if (portionToSocializeBP > _getStvStETHPoolStorage ().maxLossSocializationBP) {
711+ revert ExcessiveLossSocialization ();
712+ }
713+ }
714+
684715 function _checkFreshReport () internal view {
685716 if (! VAULT_HUB.isReportFresh (address (STAKING_VAULT))) revert VaultReportStale ();
686717 }
687718
719+ // =================================================================================
720+ // LOSS SOCIALIZATION LIMITER
721+ // =================================================================================
722+
723+ // During rebalancing, it's possible that the stv available for burning is not sufficient to cover the entire liability.
724+ // This may be due to a sharp drop in the stv price, which has resulted in an individual account or a request in Withdrawal Queue
725+ // no longer being collateralized (assets < liability).
726+ //
727+ // The limiter on loss socialization is introduced to prevent excessive losses from being socialized to all pool participants.
728+ // The limiter is defined as a maximum portion of the loss that can be socialized, expressed in basis points (BP).
729+ //
730+ // The default value is set to 0 BP, meaning that no loss socialization is allowed without explicit permission.
731+
732+ /**
733+ * @notice Maximum allowed loss socialization in basis points
734+ * @return maxSocializablePortionBP The maximum allowed portion of loss to be socialized in basis points
735+ * @dev Used to limit the portion of loss that can be socialized to all pool participants during rebalance
736+ */
737+ function maxLossSocializationBP () external view returns (uint256 maxSocializablePortionBP ) {
738+ maxSocializablePortionBP = uint256 (_getStvStETHPoolStorage ().maxLossSocializationBP);
739+ }
740+
741+ /**
742+ * @notice Set the maximum allowed loss socialization in basis points
743+ * @param _maxSocializablePortionBP The new maximum allowed loss socialization in basis points
744+ * @dev Sets the maximum portion of loss that can be socialized to all pool participants during rebalance
745+ * @dev Can only be called by accounts with the DEFAULT_ADMIN_ROLE
746+ */
747+ function setMaxLossSocializationBP (uint16 _maxSocializablePortionBP ) external {
748+ _checkRole (DEFAULT_ADMIN_ROLE, msg .sender );
749+
750+ if (_maxSocializablePortionBP > TOTAL_BASIS_POINTS) revert InvalidValue ();
751+
752+ StvStETHPoolStorage storage $ = _getStvStETHPoolStorage ();
753+ if (_maxSocializablePortionBP == $.maxLossSocializationBP) revert SameValue ();
754+ $.maxLossSocializationBP = _maxSocializablePortionBP;
755+
756+ emit MaxLossSocializationUpdated (_maxSocializablePortionBP);
757+ }
758+
688759 // =================================================================================
689760 // TRANSFER WITH LIABILITY
690761 // =================================================================================
@@ -727,4 +798,26 @@ contract StvStETHPool is StvPool {
727798
728799 if (balanceOf (_from) < stvToLock) revert InsufficientReservedBalance ();
729800 }
801+
802+ // =================================================================================
803+ // PAUSE / RESUME MINTING
804+ // =================================================================================
805+
806+ /**
807+ * @notice Pause (w)stETH minting
808+ * @dev Can only be called by accounts with the MINTING_PAUSE_ROLE
809+ */
810+ function pauseMinting () external {
811+ _checkRole (MINTING_PAUSE_ROLE, msg .sender );
812+ _pauseFeature (MINTING_FEATURE);
813+ }
814+
815+ /**
816+ * @notice Resume (w)stETH minting
817+ * @dev Can only be called by accounts with the MINTING_RESUME_ROLE
818+ */
819+ function resumeMinting () external {
820+ _checkRole (MINTING_RESUME_ROLE, msg .sender );
821+ _resumeFeature (MINTING_FEATURE);
822+ }
730823}
0 commit comments