Skip to content

Commit cbef692

Browse files
authored
Feature deactivation on demand (#42)
* feat: feature deactivation on demand
1 parent 2659fdf commit cbef692

File tree

14 files changed

+462
-10
lines changed

14 files changed

+462
-10
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ These functions verify balances and authorize the caller to retrieve their accum
122122

123123
 
124124

125+
## :gear: _Feature activation_
126+
127+
[Feature activation](./specs/feature_activation.md)
128+
125129
## :page_facing_up: _Whitepaper_
126130

127131
[Whitepaper](https://ssv.network/wp-content/uploads/2025/01/SSV2.0-Based-Applications-Protocol-1.pdf)

doc/feature_activation.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Feature Flags in StrategyManager
2+
3+
## Purpose
4+
5+
Introduce a compact, on‑chain mechanism to enable or disable selected features of the `StrategyManager` contract without a full redeploy. By packing multiple boolean toggles into a single `uint32`, the design:
6+
7+
- Minimizes storage footprint and gas cost.
8+
- Provides an upgrade‑friendly switchboard for safety (e.g., emergency pause).
9+
- Centralizes feature management under DAO control.
10+
11+
## Technical Explanation
12+
13+
- **Storage**: A `uint32 disabledFeatures` field in `ProtocolStorageLib.Data`.
14+
- **Bitmask Layout**:
15+
- Bit 0 (LSB) → Slashing Disabled (`SLASHING_DISABLED = 1 << 0`)
16+
- Bit 1 → Withdrawals Disabled (`WITHDRAWALS_DISABLED = 1 << 1`)
17+
- Further bits reserved for future toggles.
18+
19+
- **Checks**: Two internal functions in `StrategyManager`:
20+
```solidity
21+
function _checkSlashingAllowed() internal view {
22+
if (ProtocolStorageLib.load().disabledFeatures & SLASHING_DISABLED != 0)
23+
revert SlashingDisabled();
24+
}
25+
26+
function _checkWithdrawalsAllowed() internal view {
27+
if (ProtocolStorageLib.load().disabledFeatures & WITHDRAWALS_DISABLED != 0)
28+
revert WithdrawalsDisabled();
29+
}
30+
```
31+
- Called at the entry points of:
32+
- `slash(...)`
33+
- `proposeWithdrawal(...)`
34+
- `finalizeWithdrawal(...)`
35+
- `proposeWithdrawalETH(...)`
36+
- `finalizeWithdrawalETH(...)`
37+
38+
## Authorized Accounts
39+
40+
- Only the **DAO (owner)** can update the entire `disabledFeatures` bitmask via:
41+
```solidity
42+
function updateDisabledFeatures(uint32 disabledFeatures) external onlyOwner;
43+
```
44+
- No per-feature granularity: toggles are applied in bulk.
45+
46+
## Current Features That Can Be Enabled/Disabled
47+
48+
| Bit Position | Feature | Constant | Description |
49+
|:------------:|:-------------------|:----------------------|:------------------------------------------|
50+
| 0 | Slashing | `SLASHING_DISABLED` | Stops all calls to `slash(...)` |
51+
| 1 | Withdrawals | `WITHDRAWALS_DISABLED`| Stops all withdrawal proposals and finalizations |
52+
53+
## Usage & Examples
54+
55+
1. **Disable slashing only**:
56+
```js
57+
// binary: 0b...01 → 1
58+
SSVBasedApps.updateDisabledFeatures(1);
59+
// `slash(...)` now reverts with `SlashingDisabled()`.
60+
```
61+
2. **Re-enable slashing, disable withdrawals**:
62+
```js
63+
// binary: 0b...10 → 2
64+
SSVBasedApps.updateDisabledFeatures(2);
65+
// `slash(...)` resumes; `proposeWithdrawal(...)` and `finalizeWithdrawal(...)` revert.
66+
```
67+
3. **Disable both**:
68+
```js
69+
SSVBasedApps.updateDisabledFeatures(3);
70+
```
71+
4. **Full example:**
72+
```js
73+
// bit-definitions
74+
const SLASHING_DISABLED = 1 << 0; // 0b0001
75+
const WITHDRAWALS_DISABLED = 1 << 1; // 0b0010
76+
77+
// Suppose you want to disable only withdrawals:
78+
let flags = 0;
79+
flags |= WITHDRAWALS_DISABLED; // flags === 0b0010
80+
81+
// Later you decide to also disable slashing:
82+
flags |= SLASHING_DISABLED; // flags === 0b0011
83+
84+
// To re-enable withdrawals but keep slashing disabled:
85+
flags &= ~WITHDRAWALS_DISABLED; // flags === 0b0001
86+
87+
// Finally, send the update on-chain:
88+
await SSVBasedApps.updateDisabledFeatures(flags);
89+
```
90+
91+
## Future Extensions
92+
93+
- Reserve bits 231 for other purposes:
94+
- Feature disabling
95+
- Emergency pause (global)
96+
97+
98+
---
99+
*This document outlines the bitmask‑driven feature gating mechanism for `StrategyManager`. It ensures rapid reaction to on‑chain emergencies and fine‑grained control over critical operations.*
100+

scripts/DeployProxy.s.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ contract DeployProxy is Script {
3333
obligationTimelockPeriod: 14 days,
3434
obligationExpireTime: 3 days,
3535
tokenUpdateTimelockPeriod: 14 days,
36-
maxFeeIncrement: 500
36+
maxFeeIncrement: 500,
37+
disabledFeatures: 0
3738
});
3839

3940
bytes memory initData = abi.encodeWithSelector(

src/core/SSVBasedApps.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ contract SSVBasedApps is
116116
sp.obligationExpireTime = config.obligationExpireTime;
117117
sp.tokenUpdateTimelockPeriod = config.tokenUpdateTimelockPeriod;
118118
sp.maxShares = config.maxShares;
119+
sp.disabledFeatures = config.disabledFeatures;
119120

120121
emit MaxFeeIncrementSet(sp.maxFeeIncrement);
121122
}
@@ -342,6 +343,12 @@ contract SSVBasedApps is
342343
_delegateTo(SSVCoreModules.SSV_PROTOCOL_MANAGER);
343344
}
344345

346+
function updateDisabledFeatures(
347+
uint32 disabledFeatures
348+
) external onlyOwner {
349+
_delegateTo(SSVCoreModules.SSV_PROTOCOL_MANAGER);
350+
}
351+
345352
// *****************************
346353
// ** Section: External Views **
347354
// *****************************
@@ -554,6 +561,10 @@ contract SSVBasedApps is
554561
return ProtocolStorageLib.load().obligationExpireTime;
555562
}
556563

564+
function disabledFeatures() external view returns (uint32) {
565+
return ProtocolStorageLib.load().disabledFeatures;
566+
}
567+
557568
function tokenUpdateTimelockPeriod() external view returns (uint32) {
558569
return ProtocolStorageLib.load().tokenUpdateTimelockPeriod;
559570
}

src/core/interfaces/IProtocolManager.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface IProtocolManager {
1111
event StrategyMaxSharesUpdated(uint256 maxShares);
1212
event WithdrawalExpireTimeUpdated(uint32 withdrawalExpireTime);
1313
event WithdrawalTimelockPeriodUpdated(uint32 withdrawalTimelockPeriod);
14+
event DisabledFeaturesUpdated(uint32 disabledFeatures);
1415

1516
function updateFeeExpireTime(uint32 value) external;
1617
function updateFeeTimelockPeriod(uint32 value) external;

src/core/interfaces/IStrategyManager.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ interface IStrategyManager {
188188
error ObligationAlreadySet();
189189
error ObligationHasNotBeenCreated();
190190
error RequestTimeExpired();
191+
error SlashingDisabled();
191192
error TimelockNotElapsed();
192193
error TokenNotSupportedByBApp(address token);
193194
error WithdrawTransferFailed();
195+
error WithdrawalsDisabled();
194196
}

src/core/libraries/ProtocolStorageLib.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ library ProtocolStorageLib {
1313
uint32 obligationExpireTime;
1414
uint32 tokenUpdateTimelockPeriod;
1515
uint32 maxFeeIncrement;
16+
// each bit, starting from the LSB, represents a DISABLED feature
17+
// bit 0 = slashingDisabled
18+
// bit 1 = withdrawalsDisabled
19+
// ...
20+
uint32 disabledFeatures;
1621
uint256 maxShares;
1722
}
1823

src/core/modules/ProtocolManager.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { IProtocolManager } from "@ssv/src/core/interfaces/IProtocolManager.sol"
55
import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.sol";
66

77
contract ProtocolManager is IProtocolManager {
8+
uint32 private constant SLASHING_DISABLED = 1 << 0;
9+
uint32 private constant WITHDRAWALS_DISABLED = 1 << 1;
10+
811
function updateFeeTimelockPeriod(uint32 feeTimelockPeriod) external {
912
ProtocolStorageLib.load().feeTimelockPeriod = feeTimelockPeriod;
1013
emit FeeTimelockPeriodUpdated(feeTimelockPeriod);
@@ -61,4 +64,9 @@ contract ProtocolManager is IProtocolManager {
6164
ProtocolStorageLib.load().maxFeeIncrement = maxFeeIncrement;
6265
emit StrategyMaxFeeIncrementUpdated(maxFeeIncrement);
6366
}
67+
68+
function updateDisabledFeatures(uint32 disabledFeatures) external {
69+
ProtocolStorageLib.load().disabledFeatures = disabledFeatures;
70+
emit DisabledFeaturesUpdated(disabledFeatures);
71+
}
6472
}

src/core/modules/StrategyManager.sol

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { IBasedApp } from "@ssv/src/middleware/interfaces/IBasedApp.sol";
1818
contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
1919
using SafeERC20 for IERC20;
2020

21+
uint32 private constant SLASHING_DISABLED = 1 << 0;
22+
uint32 private constant WITHDRAWALS_DISABLED = 1 << 1;
23+
2124
/// @notice Checks if the caller is the strategy owner
2225
/// @param strategyId The ID of the strategy
2326
/// @param s The CoreStorageLib data
@@ -255,6 +258,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
255258
address token,
256259
uint256 amount
257260
) external {
261+
_checkWithdrawalsAllowed();
262+
258263
if (token == ETH_ADDRESS) revert InvalidToken();
259264
_proposeWithdrawal(strategyId, token, amount);
260265
}
@@ -266,6 +271,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
266271
uint32 strategyId,
267272
IERC20 token
268273
) external nonReentrant {
274+
_checkWithdrawalsAllowed();
275+
269276
uint256 amount = _finalizeWithdrawal(strategyId, address(token));
270277

271278
token.safeTransfer(msg.sender, amount);
@@ -283,12 +290,15 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
283290
/// @param strategyId The ID of the strategy.
284291
/// @param amount The amount of ETH to withdraw.
285292
function proposeWithdrawalETH(uint32 strategyId, uint256 amount) external {
293+
_checkWithdrawalsAllowed();
286294
_proposeWithdrawal(strategyId, ETH_ADDRESS, amount);
287295
}
288296

289297
/// @notice Finalize the ETH withdrawal after the timelock period has passed.
290298
/// @param strategyId The ID of the strategy.
291299
function finalizeWithdrawalETH(uint32 strategyId) external nonReentrant {
300+
_checkWithdrawalsAllowed();
301+
292302
uint256 amount = _finalizeWithdrawal(strategyId, ETH_ADDRESS);
293303

294304
payable(msg.sender).transfer(amount);
@@ -742,6 +752,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
742752
uint32 percentage,
743753
bytes calldata data
744754
) external nonReentrant {
755+
_checkSlashingAllowed();
756+
745757
ValidationLib.validatePercentageAndNonZero(percentage);
746758

747759
CoreStorageLib.Data storage s = CoreStorageLib.load();
@@ -888,4 +900,21 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
888900

889901
s.slashingFund[msg.sender][token] -= amount;
890902
}
903+
904+
function _checkSlashingAllowed() internal view {
905+
if (
906+
ProtocolStorageLib.load().disabledFeatures & SLASHING_DISABLED != 0
907+
) {
908+
revert SlashingDisabled();
909+
}
910+
}
911+
912+
function _checkWithdrawalsAllowed() internal view {
913+
if (
914+
ProtocolStorageLib.load().disabledFeatures & WITHDRAWALS_DISABLED !=
915+
0
916+
) {
917+
revert WithdrawalsDisabled();
918+
}
919+
}
891920
}

src/middleware/README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# :construction_worker: :closed_lock_with_key: __Middleware Contracts__
1+
# :construction_worker: :closed_lock_with_key: **Middleware Contracts**
22

33
## Modules & Examples for BApp Development
44

@@ -78,11 +78,10 @@ Ideal for BApps that want to avoid excessive reward costs.
7878

7979
## :page_facing_up: To Have: Examples
8080

81-
* **BLS Strategy Example**: Demonstrates how to implement and use the BLS verification module securely.
81+
- **BLS Strategy Example**: Demonstrates how to implement and use the BLS verification module securely.
8282

83-
* **ECDSA Strategy Example**: Showcases a lighter alternative using ECDSA verification.
83+
- **ECDSA Strategy Example**: Showcases a lighter alternative using ECDSA verification.
8484

85-
* **Capped Strategy Example**: Implements a BApp that limits deposits to 100k SSV.
86-
87-
* **Whitelist Manager Example**: Uses whitelist-based permissions instead of owner-based control.
85+
- **Capped Strategy Example**: Implements a BApp that limits deposits to 100k SSV.
8886

87+
- **Whitelist Manager Example**: Uses whitelist-based permissions instead of owner-based control.

0 commit comments

Comments
 (0)