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
14 changes: 7 additions & 7 deletions docs/FLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,15 +258,13 @@ emit ClusterDeposited(owner, operatorIds, msg.value, cluster);
- If cluster is active and has validators: cluster must not become liquidatable after withdrawal

> **Note — withdrawal allowed on liquidated clusters:** `withdraw` does not require the cluster to be active. A liquidated cluster may have received deposits (via `deposit`) in preparation for reactivation. If the owner decides not to reactivate, they can recover those funds via `withdraw`.
>
> **Note — operator removal and reactivation:** If one or more operators in a cluster's operator set have been removed (via `removeOperator`), the cluster can still be reactivated, but removed operators are silently skipped during `updateClusterOperatorsOnReactivation` (see `OperatorLib.sol:311`). The cluster will operate with reduced operator coverage (e.g., 3/4 instead of 4/4), which may compromise the cluster's fault tolerance. The reactivation fee calculation excludes removed operators' fees. No on-chain event signals which operators were skipped, but this is detectable off-chain by checking operator states before reactivation.


#### State Mutations
1. If cluster is active: update operator snapshots and settle cluster fees
2. `cluster.balance -= amount`
3. If cluster is active and has validators: liquidation check
4. Update stored cluster hash
5. Transfer `amount` ETH to caller
1. `cluster.balance -= amount`
2. If cluster is active and has validators: liquidation check
3. Update stored cluster hash
4. Transfer `amount` ETH to caller

#### Events
```solidity
Expand Down Expand Up @@ -334,6 +332,8 @@ Same flow as 1.9 but for SSV clusters. Uses `s.clusters` instead of `s.ethCluste


> **Note — Stale EB risk:** The solvency check uses the stored `clusterEB.vUnits` snapshot, which may be stale if the beacon-chain EB changed during liquidation. Ref: SPEC §2 "Stale EB Risk on Reactivation" for full analysis and mitigation options.
>
> **Note — operator removal and reactivation:** If one or more operators in a cluster's operator set have been removed (via `removeOperator`), the cluster can still be reactivated, but removed operators are silently skipped during `updateClusterOperatorsOnReactivation` (see `OperatorLib.sol:311`). The cluster will operate with reduced operator coverage (e.g., 3/4 instead of 4/4), which may compromise the cluster's fault tolerance. The reactivation fee calculation excludes removed operators' fees. No on-chain event signals which operators were skipped, but this is detectable off-chain by checking operator states before reactivation.

#### State Mutations
1. Update operator ETH snapshots
Expand Down
48 changes: 31 additions & 17 deletions ssv-review/planning/MAINNET-READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
| TEST-1 | Validator register/remove with non-zero operator fees | Unit Test Completeness | P0 | M |
| TEST-2 | EB-weighted operator earnings accumulation | Unit Test Completeness | P0 | M |
| TEST-3 | Balance delta assertions in liquidation paths | Unit Test Completeness | P0 | M |
| TEST-4 | ~~`updateClusterBalance` on liquidated clusters~~ | Unit Test Completeness | P0 | ✅ Closed (Addressed in PR #447) |
| TEST-4 | ~~`updateClusterBalance` on liquidated clusters~~ | Unit Test Completeness | P0 | ✅ Closed (PR #447 + enhanced with 3 edge cases) |
| TEST-5 | Oracle quorum edge cases | Unit Test Completeness | P0 | M |
| TEST-6 | EB decrease scenarios | Unit Test Completeness | P0 | M |
| TEST-7 | Reentrancy in staking functions | Unit Test Completeness | P0 | S |
Expand Down Expand Up @@ -1313,13 +1313,13 @@ A liquidation could emit the correct event but transfer the wrong amount (or not

---

### [TEST-4] `updateClusterBalance` on liquidated clusters
### [TEST-4] ~~`updateClusterBalance` on liquidated clusters~~
- **Type:** Unit Test Completeness
- **Priority:** P0
- **Status:** Open
- **Owner:** (unassigned)
- **Timeline:** (empty)
- **Github Link:** (empty)
- **Status:** ✅ **CLOSED**
- **Owner:** PR #447 + enhancements
- **Timeline:** Completed 2026-02-25
- **Github Link:** [test/unit/SSVClusters/updateClusterBalance.test.ts](../test/unit/SSVClusters/updateClusterBalance.test.ts) (lines 293-653), [test/integration/SSVNetwork/clusters.test.ts](../test/integration/SSVNetwork/clusters.test.ts) (lines 753-817)

**Requirement:**
Add tests for calling `updateClusterBalance` (EB oracle update) on an already-liquidated cluster.
Expand All @@ -1328,20 +1328,34 @@ Add tests for calling `updateClusterBalance` (EB oracle update) on an already-li
No test exists for this path. If the contract doesn't handle it, oracle updates on liquidated clusters could corrupt accounting or revert unexpectedly.

**Acceptance Criteria:**
- [ ] Test: Call `updateClusterBalance` with valid proof on a liquidated cluster → verify defined behavior (revert or update EB without settling fees)
- [ ] Test: EB update that makes a liquidated cluster even more insolvent → verify no state corruption
- [x] Test: Call `updateClusterBalance` with valid proof on a liquidated cluster → verify defined behavior (revert or update EB without settling fees)
- [x] Test: EB update that makes a liquidated cluster even more insolvent → verify no state corruption
- [x] **BONUS**: Multi-validator liquidated cluster EB update
- [x] **BONUS**: EB decrease on liquidated cluster (penalty scenario)
- [x] **BONUS**: Liquidated cluster with implicit EB → first EB update transitions to explicit tracking

**Agent Instructions:**
1. Read `test/unit/SSVClusters/updateClusterBalance.test.ts` for existing patterns.
2. Create a cluster, liquidate it, then call `updateClusterBalance` with a valid Merkle proof.
3. Verify behavior: does it revert? Does it update EB? Does it try to settle fees?
4. Read `contracts/modules/SSVClusters.sol` to trace the `updateClusterBalance` code path for liquidated clusters.
5. Add assertions based on actual contract behavior.
6. Run `npm run test:unit`.
**Implementation Summary:**
1. **Unit tests** ([updateClusterBalance.test.ts](../test/unit/SSVClusters/updateClusterBalance.test.ts)):
- Line 293-337: Basic liquidated cluster EB update — verifies EB snapshot updated, cluster stays inactive, no fee settlement
- Line 339-416: EB increase on insolvent liquidated cluster — verifies no operator/DAO vUnit corruption
- Line 463-527: **NEW** Multi-validator liquidated cluster EB update
- Line 529-602: **NEW** EB decrease on liquidated cluster (penalty scenario)
- Line 604-653: **NEW** Implicit→explicit EB transition on liquidated cluster

2. **Integration test** ([clusters.test.ts](../test/integration/SSVNetwork/clusters.test.ts)):
- Line 753-817: E2E flow with oracle quorum setup and multiple EB updates on liquidated cluster

3. **Additional improvements**:
- Fixed loose comparators in integration tests — now uses exact formula-based assertions per SSV standards
- Added block number tracking for precise fee calculations
- All tests passing with 100% exact `.to.equal()` assertions

#### Sub-items:
- [ ] Sub-task 1: `updateClusterBalance` on liquidated cluster — basic behavior
- [ ] Sub-task 2: EB increase on already-insolvent liquidated cluster
- [x] Sub-task 1: `updateClusterBalance` on liquidated cluster — basic behavior
- [x] Sub-task 2: EB increase on already-insolvent liquidated cluster
- [x] Sub-task 3: Multi-validator liquidated cluster EB update
- [x] Sub-task 4: EB decrease on liquidated cluster
- [x] Sub-task 5: Implicit→explicit EB transition

---

Expand Down
8 changes: 5 additions & 3 deletions test/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ export async function registerDefaultCluster(
): Promise<{
cluster: Cluster,
validatorKey: string,
operatorIds: number[]
operatorIds: number[],
receiptRegister: any
}> {
const validatorKey = makePublicKey(1);
const operatorIds = await registerOperators(network, operatorOwner, 4);
Expand All @@ -179,13 +180,14 @@ export async function registerDefaultCluster(
"0x" + (DEFAULT_ETH_REGISTER_VALUE + 10n ** 18n).toString(16),
]);

await network.connect(clusterOwner).registerValidator(
const tx = await network.connect(clusterOwner).registerValidator(
validatorKey,
operatorIds,
DEFAULT_SHARES,
EMPTY_CLUSTER,
{ value: DEFAULT_ETH_REGISTER_VALUE }
);
const receiptRegister = await tx.wait();

const cluster = await getCurrentClusterState(
connection,
Expand All @@ -195,7 +197,7 @@ export async function registerDefaultCluster(
);

return {
cluster, validatorKey, operatorIds
cluster, validatorKey, operatorIds, receiptRegister
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yurii-ssv do you think adding tx receipt here is useful? I only use it to get the block number in one test...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, why not. Can be very helpful when we will start addressing #435

}
}

Expand Down
Loading
Loading