Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ These functions verify balances and authorize the caller to retrieve their accum

 

## :gear: _Feature activation_

[Feature activation](./specs/feature_activation.md)

## :page_facing_up: _Whitepaper_

[Whitepaper](https://ssv.network/wp-content/uploads/2025/01/SSV2.0-Based-Applications-Protocol-1.pdf)
Expand Down
100 changes: 100 additions & 0 deletions doc/feature_activation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Feature Flags in StrategyManager

## Purpose

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:

- Minimizes storage footprint and gas cost.
- Provides an upgrade‑friendly switchboard for safety (e.g., emergency pause).
- Centralizes feature management under DAO control.

## Technical Explanation

- **Storage**: A `uint32 disabledFeatures` field in `ProtocolStorageLib.Data`.
- **Bitmask Layout**:
- Bit 0 (LSB) → Slashing Disabled (`SLASHING_DISABLED = 1 << 0`)
- Bit 1 → Withdrawals Disabled (`WITHDRAWALS_DISABLED = 1 << 1`)
- Further bits reserved for future toggles.

- **Checks**: Two internal functions in `StrategyManager`:
```solidity
function _checkSlashingAllowed() internal view {
if (ProtocolStorageLib.load().disabledFeatures & SLASHING_DISABLED != 0)
revert SlashingDisabled();
}

function _checkWithdrawalsAllowed() internal view {
if (ProtocolStorageLib.load().disabledFeatures & WITHDRAWALS_DISABLED != 0)
revert WithdrawalsDisabled();
}
```
- Called at the entry points of:
- `slash(...)`
- `proposeWithdrawal(...)`
- `finalizeWithdrawal(...)`
- `proposeWithdrawalETH(...)`
- `finalizeWithdrawalETH(...)`

## Authorized Accounts

- Only the **DAO (owner)** can update the entire `disabledFeatures` bitmask via:
```solidity
function updateDisabledFeatures(uint32 disabledFeatures) external onlyOwner;
```
- No per-feature granularity: toggles are applied in bulk.

## Current Features That Can Be Enabled/Disabled

| Bit Position | Feature | Constant | Description |
|:------------:|:-------------------|:----------------------|:------------------------------------------|
| 0 | Slashing | `SLASHING_DISABLED` | Stops all calls to `slash(...)` |
| 1 | Withdrawals | `WITHDRAWALS_DISABLED`| Stops all withdrawal proposals and finalizations |

## Usage & Examples

1. **Disable slashing only**:
```js
// binary: 0b...01 → 1
SSVBasedApps.updateDisabledFeatures(1);
// `slash(...)` now reverts with `SlashingDisabled()`.
```
2. **Re-enable slashing, disable withdrawals**:
```js
// binary: 0b...10 → 2
SSVBasedApps.updateDisabledFeatures(2);
// `slash(...)` resumes; `proposeWithdrawal(...)` and `finalizeWithdrawal(...)` revert.
```
3. **Disable both**:
```js
SSVBasedApps.updateDisabledFeatures(3);
```
4. **Full example:**
```js
// bit-definitions
const SLASHING_DISABLED = 1 << 0; // 0b0001
const WITHDRAWALS_DISABLED = 1 << 1; // 0b0010

// Suppose you want to disable only withdrawals:
let flags = 0;
flags |= WITHDRAWALS_DISABLED; // flags === 0b0010

// Later you decide to also disable slashing:
flags |= SLASHING_DISABLED; // flags === 0b0011

// To re-enable withdrawals but keep slashing disabled:
flags &= ~WITHDRAWALS_DISABLED; // flags === 0b0001

// Finally, send the update on-chain:
await SSVBasedApps.updateDisabledFeatures(flags);
```

## Future Extensions

- Reserve bits 2–31 for other purposes:
- Feature disabling
- Emergency pause (global)


---
*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.*

3 changes: 2 additions & 1 deletion scripts/DeployProxy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ contract DeployProxy is Script {
obligationTimelockPeriod: 14 days,
obligationExpireTime: 3 days,
tokenUpdateTimelockPeriod: 14 days,
maxFeeIncrement: 500
maxFeeIncrement: 500,
disabledFeatures: 0
});

bytes memory initData = abi.encodeWithSelector(
Expand Down
11 changes: 11 additions & 0 deletions src/core/SSVBasedApps.sol
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ contract SSVBasedApps is
sp.obligationExpireTime = config.obligationExpireTime;
sp.tokenUpdateTimelockPeriod = config.tokenUpdateTimelockPeriod;
sp.maxShares = config.maxShares;
sp.disabledFeatures = config.disabledFeatures;

emit MaxFeeIncrementSet(sp.maxFeeIncrement);
}
Expand Down Expand Up @@ -342,6 +343,12 @@ contract SSVBasedApps is
_delegateTo(SSVCoreModules.SSV_PROTOCOL_MANAGER);
}

function updateDisabledFeatures(
uint32 disabledFeatures
) external onlyOwner {
_delegateTo(SSVCoreModules.SSV_PROTOCOL_MANAGER);
}

// *****************************
// ** Section: External Views **
// *****************************
Expand Down Expand Up @@ -554,6 +561,10 @@ contract SSVBasedApps is
return ProtocolStorageLib.load().obligationExpireTime;
}

function disabledFeatures() external view returns (uint32) {
return ProtocolStorageLib.load().disabledFeatures;
}

function tokenUpdateTimelockPeriod() external view returns (uint32) {
return ProtocolStorageLib.load().tokenUpdateTimelockPeriod;
}
Expand Down
1 change: 1 addition & 0 deletions src/core/interfaces/IProtocolManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface IProtocolManager {
event StrategyMaxSharesUpdated(uint256 maxShares);
event WithdrawalExpireTimeUpdated(uint32 withdrawalExpireTime);
event WithdrawalTimelockPeriodUpdated(uint32 withdrawalTimelockPeriod);
event DisabledFeaturesUpdated(uint32 disabledFeatures);

function updateFeeExpireTime(uint32 value) external;
function updateFeeTimelockPeriod(uint32 value) external;
Expand Down
2 changes: 2 additions & 0 deletions src/core/interfaces/IStrategyManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ interface IStrategyManager {
error ObligationAlreadySet();
error ObligationHasNotBeenCreated();
error RequestTimeExpired();
error SlashingDisabled();
error TimelockNotElapsed();
error TokenNotSupportedByBApp(address token);
error WithdrawTransferFailed();
error WithdrawalsDisabled();
}
5 changes: 5 additions & 0 deletions src/core/libraries/ProtocolStorageLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ library ProtocolStorageLib {
uint32 obligationExpireTime;
uint32 tokenUpdateTimelockPeriod;
uint32 maxFeeIncrement;
// each bit, starting from the LSB, represents a DISABLED feature
// bit 0 = slashingDisabled
// bit 1 = withdrawalsDisabled
// ...
uint32 disabledFeatures;
uint256 maxShares;
}

Expand Down
8 changes: 8 additions & 0 deletions src/core/modules/ProtocolManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { IProtocolManager } from "@ssv/src/core/interfaces/IProtocolManager.sol"
import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.sol";

contract ProtocolManager is IProtocolManager {
uint32 private constant SLASHING_DISABLED = 1 << 0;
uint32 private constant WITHDRAWALS_DISABLED = 1 << 1;

function updateFeeTimelockPeriod(uint32 feeTimelockPeriod) external {
ProtocolStorageLib.load().feeTimelockPeriod = feeTimelockPeriod;
emit FeeTimelockPeriodUpdated(feeTimelockPeriod);
Expand Down Expand Up @@ -61,4 +64,9 @@ contract ProtocolManager is IProtocolManager {
ProtocolStorageLib.load().maxFeeIncrement = maxFeeIncrement;
emit StrategyMaxFeeIncrementUpdated(maxFeeIncrement);
}

function updateDisabledFeatures(uint32 disabledFeatures) external {
ProtocolStorageLib.load().disabledFeatures = disabledFeatures;
emit DisabledFeaturesUpdated(disabledFeatures);
}
}
29 changes: 29 additions & 0 deletions src/core/modules/StrategyManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { IBasedApp } from "@ssv/src/middleware/interfaces/IBasedApp.sol";
contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
using SafeERC20 for IERC20;

uint32 private constant SLASHING_DISABLED = 1 << 0;
uint32 private constant WITHDRAWALS_DISABLED = 1 << 1;

/// @notice Checks if the caller is the strategy owner
/// @param strategyId The ID of the strategy
/// @param s The CoreStorageLib data
Expand Down Expand Up @@ -255,6 +258,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
address token,
uint256 amount
) external {
_checkWithdrawalsAllowed();

if (token == ETH_ADDRESS) revert InvalidToken();
_proposeWithdrawal(strategyId, token, amount);
}
Expand All @@ -266,6 +271,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
uint32 strategyId,
IERC20 token
) external nonReentrant {
_checkWithdrawalsAllowed();

uint256 amount = _finalizeWithdrawal(strategyId, address(token));

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

/// @notice Finalize the ETH withdrawal after the timelock period has passed.
/// @param strategyId The ID of the strategy.
function finalizeWithdrawalETH(uint32 strategyId) external nonReentrant {
_checkWithdrawalsAllowed();

uint256 amount = _finalizeWithdrawal(strategyId, ETH_ADDRESS);

payable(msg.sender).transfer(amount);
Expand Down Expand Up @@ -742,6 +752,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager {
uint32 percentage,
bytes calldata data
) external nonReentrant {
_checkSlashingAllowed();

ValidationLib.validatePercentageAndNonZero(percentage);

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

s.slashingFund[msg.sender][token] -= amount;
}

function _checkSlashingAllowed() internal view {
if (
ProtocolStorageLib.load().disabledFeatures & SLASHING_DISABLED != 0
) {
revert SlashingDisabled();
}
}

function _checkWithdrawalsAllowed() internal view {
if (
ProtocolStorageLib.load().disabledFeatures & WITHDRAWALS_DISABLED !=
0
) {
revert WithdrawalsDisabled();
}
}
}
11 changes: 5 additions & 6 deletions src/middleware/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# :construction_worker: :closed_lock_with_key: __Middleware Contracts__
# :construction_worker: :closed_lock_with_key: **Middleware Contracts**

## Modules & Examples for BApp Development

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

## :page_facing_up: To Have: Examples

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

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

* **Capped Strategy Example**: Implements a BApp that limits deposits to 100k SSV.

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

- **Whitelist Manager Example**: Uses whitelist-based permissions instead of owner-based control.
6 changes: 4 additions & 2 deletions test/SSVBasedApps.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ contract SSVBasedAppsTest is Setup, Ownable2StepUpgradeable {
obligationTimelockPeriod: 14 days,
obligationExpireTime: 3 days,
tokenUpdateTimelockPeriod: 14 days,
maxShares: 1e50
maxShares: 1e50,
disabledFeatures: 0
});
vm.expectRevert(
abi.encodeWithSelector(
Expand Down Expand Up @@ -194,7 +195,8 @@ contract SSVBasedAppsTest is Setup, Ownable2StepUpgradeable {
obligationTimelockPeriod: 14 days,
obligationExpireTime: 3 days,
tokenUpdateTimelockPeriod: 14 days,
maxFeeIncrement: 10_001
maxFeeIncrement: 10_001,
disabledFeatures: 0
});
vm.expectRevert(
abi.encodeWithSelector(
Expand Down
3 changes: 2 additions & 1 deletion test/helpers/Setup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ contract Setup is Test {
obligationTimelockPeriod: 14 days,
obligationExpireTime: 3 days,
tokenUpdateTimelockPeriod: 14 days,
maxShares: 1e50
maxShares: 1e50,
disabledFeatures: 0
});

bytes memory data = abi.encodeWithSelector(
Expand Down
Loading
Loading