Repository navigation
fix(cluster): exclude removed operators from fee sums [SSV-WEB-001] - #1881
Conversation
A removed operator's fee is 0 on-chain, but the API keeps its last fee and sumOperatorsFee added it to the burn rate, so runway was too short and funding/reactivation amounts too high. Drop the ignore-removed flag: every fee sum (ETH and SSV) now treats a removed operator as 0. Per-row display of the last fee is unchanged.
… [SSV-WEB-001] - select-operators: the reshare flow preselects every current cluster operator, removed ones included; route the Operators Yearly Fee total through sumOperatorsFee so a removed operator adds 0 on both the ETH and SSV paths
|
Reviewed against the product decision on SSV-WEB-001. The change does what we decided: the flag is gone, both the ETH and SSV paths return 0 for a removed operator, and every total goes through one helper. I also grepped the PR head for remaining Main issue: CI does not run the testsThe test step in It does not need to block this PR, but it needs its own ticket and an owner, together with the two setup problems it depends on:
Minor 1: a missing fee now counts as 0 without a warning
Minor 2: rows and totals can disagreeIn the fee comparison and the reshare operator list, a removed operator's row still shows its last fee, while the total leaves it out. That is within the decision ("a label outside the sum"), but a user who adds up the rows gets a different number. On SSV-EXPL-002 we decided the explorer shows "—" for a removed operator's fee. A follow-up should do the same in the webapp, ideally with a "Removed" tag on the row. Neither minor point blocks the merge. |
A missing fee counts as 0, which understates burn rate and lengthens runway. Warn once per operator and field so the case shows up.
|
Thanks, Massimo. All three points are handled:
|
stefan-ssv-labs
left a comment
There was a problem hiding this comment.
LGTM at 95440c0. Reviewed against SSV-WEB-001 and the agreed product decision. Removing the flag and treating removed operators as zero in the shared ETH and SSV fee helpers is the right fix. The runway, funding, reactivation, fee-comparison and reshare totals use the corrected helpers; I found no missed fee total or blocking regression.
Verified the premise in the contract and Subsquid code: removal zeroes both on-chain fees, while the indexed record retains the last fees. Independently ran all 131 tests across 11 files, type-check and lint. All passed; lint reports one existing warning outside this diff. I reviewed the recorded Hoodi smoke results but did not independently repeat the full browser/wallet flow.
SSV-CI-001 remains a pre-existing, non-blocking follow-up: CI does not run the tests, and the ticket still needs an owner. The stale per-row fee display is explicitly accepted for this PR and tracked separately in SSV-WEB-006. Neither item blocks this approval.
sebastian-ssvlabs
left a comment
There was a problem hiding this comment.
Reviewed against the on-chain behaviour: removeOperator resets both fee and ethFee to zero, and burn rate is a plain sum of current operator fees, so counting a removed operator as 0 in every total matches the contract. All sums now go through sumOperatorsFee; remaining raw fee reads are per-row display only. Reshare keeps Next disabled while a removed operator is selected. No blocking findings.
Why
When a cluster contains a removed operator, the API still returns that operator's last fee, and the webapp added it to the cluster's burn rate. On-chain, a removed operator's fee is 0. As a result the webapp showed a shorter runway than the real one (fixture: 75 days instead of 100), asked for funding and reactivation amounts that were too high, and disagreed with the explorer about the same cluster.
The ignore-removed flag fixed this only on the ETH path, only where a caller opted in (the SSV→ETH switch wizard), and never on the SSV path. Product decision (SSV-WEB-001, 2026-10-29): remove the flag. A removed operator now counts as 0 in every fee sum, on both the ETH and SSV paths. A removed operator's last fee may still appear as a per-row display label, but never in a total.
What changed
src/lib/utils/operator.tsgetOperatorEthFeeand the newgetOperatorSsvFeereturn0nfor a removed operator (is_deleted). A missing fee on an operator that isn't removed counts as 0, and logs aconsole.warnonce per operator and field. A 0 here understates the burn rate, so the warning makes the case visible.sumOperatorsFee(operators, by)no longer takes a third parameter.src/hooks/use-compute-funding-cost.ts: theignoreRemovedOperatorsoption, its default and its queryKey entry are removed. The per-operatoris_deletedstays in the queryKey.src/components/wizard/switch-wizard-step-two.tsx: the fourignoreRemovedOperators: truearguments are removed.src/hooks/use-migration-calculation-data.ts:getOperatorEthFee(operator). No visible change.src/components/wizard/operator-fee-comparison.tsx: both yearly totals (SSV and ETH) now go throughsumOperatorsFee. The per-row display is unchanged.src/app/routes/create-cluster/select-operators/index.tsx: the "Operators Yearly Fee" total goes throughsumOperatorsFee. The reshare flow preselects every current cluster operator, including removed ones.src/lib/utils/__tests__/operator-fee.test.ts(new): 12 tests. They cover helper-level and runway-level checks for both ETH and SSV, and the missing-fee warning: it warns once, never for a removed operator, and never for a valid fee.src/hooks/cluster/use-cluster-runway.tsneeded no change: fixing the helper corrects it.Visible effect: for a cluster that contains a removed operator, runway goes up and funding and reactivation amounts go down. Clusters without one show the same numbers as before.
Gates (run on 95440c0)
pnpm run test:run:Test Files 11 passed (11)/Tests 131 passed (131)pnpm type-check: exit 0pnpm lint:✖ 1 problem (0 errors, 1 warning). The warning already exists onstage, in a file this PR doesn't touch (migration-effective-balance-form.tsx:61,react-hooks/exhaustive-deps).git diff --checkis clean.npx commitlint --from f9d7e8e3 --to HEAD).Tests need
VITE_SSV_NETWORKSin the environment.src/config/networks.tsthrows at import time without it.Repro
On
stage@ f9d7e8e, with a fixture of 4 operators where operator 4 is removed:Vitest on
stage:Tests 4 failed | 1 passed (5). On this branch, all 12 tests in the file pass.Mutation checks
getOperatorSsvFee(operator.ts:177) →getOperatorSsvFee returns 0n for a removed operator,sumOperatorsFee ssv excludes removed operatorsand the SSV runway test fail.getOperatorEthFee(operator.ts:174) →getOperatorEthFee returns 0n for a removed operator,sumOperatorsFee eth excludes removed operators,sumOperatorsFee defaults to ethand the ETH runway test fail.|| "0"guard →sumOperatorsFee treats empty or missing fee values as 0nfails. On the head that guard is replaced by thereadFeefallback (operator.ts:155-157); M3 was not re-run against it.BigInt(operator.fee)→ the SSV tests fail.Not verified
is_deletedon each operator inGET clusters/{hash}?operatorDetails=true, and returns the stale last fee for removed operators. Confirmed against the live Hoodi API (operator 182:is_deleted: true,fee 382640000000,eth_fee 1770000000). The getter contract returns a fee of 0 for the removed operators checked.select-operators/index.tsx:112) has no test of its own. It is a component, and its logic is the tested helper. It was checked in the running app (S8 below).Known, not fixed here
.husky/commit-msgis committed without the executable bit, so git skips it and commitlint never runs at commit time. Tracked in SSV-CI-001.VITE_SSV_NETWORKSfor tests (no setupFiles, no.env.test), and the CI test step is commented out (build_deploy.yml:89-90). Tracked in SSV-CI-001.CLAUDE.mdsays tests run in Node, butvitest.config.tsuses jsdom.stagesometimes shows the right total there by accident. This behaviour already exists onstage.Smoke
Run in the app on Hoodi. Fixed = this branch @ 2c29679 (95440c0 only adds the warning) under
pnpm dev. Baseline =stage@ f9d7e8e underpnpm dev, plus app.ssv.network. Production already includes the reshare total change but not the runway change, sostageis the baseline for S8. The wallet was a read-only stand-in for each cluster owner's address: no keys, and nothing signed or submitted.pnpm vitest run src/lib/utils/__tests__/operator-fee.test.ts(withVITE_SSV_NETWORKS) →Tests 12 passed (12)0x5146…(removed operator 182): 351 → 394 days. The other rows, including a liquidated cluster, are unchanged.0x5146…: 351 → 394 days.0xd4c7…(removed operators 182 and 184)0x03ee…: 53 → 82 days.0x5146…: totals 8.4999 SSV / 0.0185 ETH → 7.4999 SSV / 0.0139 ETH. That equals the three non-removed rows (3 × 2.5 SSV, 3 × 0.0046 ETH).0x5146…0x090c…(SSV) and0x02e5…(ETH)0xd4c7…: 0.0185 ETH onstage→ 0.0093 ETH. On SSV cluster0x5146…both builds show 7.4999 SSV, because the removed operator arrived as a placeholder with a fee of 0 (see "Known, not fixed here").0x3b1a…, 1 year: 0.3671 → 0.306 ETH.0x7c5b…, 1 year: 0.0288 → 0.0242 ETH.