Summary
After a wallet-direct sequencer was fully slashed and ejected (testnet), the dashboard still showed the old position as TOTAL STAKED 200K / ACTIVE POSITIONS 1 / UNKNOWN (with withdrawal actions), and a fresh registration from the same wallet silently produced no transaction. Both are deterministic client-state bugs. The same affected code path is present in the separately built mainnet bundle (index-C3vGRYir.js), so this affects real funds display too.
Root cause 1 — ghost position: pending stakes can never be reconciled after a full slash
useAggregatedStakingData.ts removes a pending-erc20-direct-stakes entry only when its attester appears in the API response, and skips reconciliation entirely when the breakdown is empty:
if (!address || pendingStakes.length === 0 || apiErc20DirectStakeBreakdown.length === 0) return
A stake that is slashed/ejected before the user revisits leaves the API breakdown empty — so its localStorage entry survives (up to the 7-day TTL) and is merged into totals as an active UNKNOWN position. There is no reconciliation against terminal on-chain state anywhere.
Root cause 2 — blocked re-registration: flow state is keyed by wallet only
WalletDirectStakingFlow.tsx:
flowIdentifier = `wallet-direct-${address}` (L65) — no attester / attempt ID, and the transaction cart restores completed entries across reloads with no TTL.
- The previous registration's completed approval entry marks the new flow as approved (
isApprovalCompleted, auto-advance to step 3) while the real on-chain allowance is 0.
- Completion checks compare counts, not attesters:
allInQueue = depositTxs.length >= uploadedKeystores.length (L652) and completedDepositsCount >= uploadedKeystores.length (L146). The old completed deposit satisfies the new attempt, so no new transaction is ever created. (Per-row indicators do match by attester, so the UI contradicts itself.)
Observed (Sepolia, 2026-08-06 → 08-08)
Attester fully slashed (2 × 100k) and ejected; Rollup status NONE, GSE/effective balance 0, /api/staking/<wallet> all-zero — UI still showed the 200K position as active. Uploading a fresh keystore produced no transaction (wallet nonce and allowance unchanged). Clearing only transaction-cart, transaction-cart-current-executing, pending-erc20-direct-stakes and reloading immediately allowed a successful approve + deposit for the new attester.
Browser-agnostic (pure app logic; observed in Firefox + MetaMask).
Suggested fixes
- Reconcile pending entries against terminal on-chain evidence — note that
NONE + zero effective balance alone is not terminal, since a newly deposited attester has the same state while still in the entry queue; also check entry-queue membership or slash/ejection/exit evidence — and run reconciliation even when the API breakdown is empty.
- Key cart/flow entries by attester or attempt ID, and gate
allInQueue / completion by attester match, not count.
Happy to share the full timeline / tx hashes if useful.
Disclosure: I'm a hobbyist node operator, not a developer. The investigation and write-up were done with help from AI assistants (Codex / Claude); every code reference and on-chain fact above was verified against the deployed bundle and Sepolia receipts before filing.
Summary
After a wallet-direct sequencer was fully slashed and ejected (testnet), the dashboard still showed the old position as
TOTAL STAKED 200K / ACTIVE POSITIONS 1 / UNKNOWN(with withdrawal actions), and a fresh registration from the same wallet silently produced no transaction. Both are deterministic client-state bugs. The same affected code path is present in the separately built mainnet bundle (index-C3vGRYir.js), so this affects real funds display too.Root cause 1 — ghost position: pending stakes can never be reconciled after a full slash
useAggregatedStakingData.tsremoves apending-erc20-direct-stakesentry only when its attester appears in the API response, and skips reconciliation entirely when the breakdown is empty:A stake that is slashed/ejected before the user revisits leaves the API breakdown empty — so its localStorage entry survives (up to the 7-day TTL) and is merged into totals as an active
UNKNOWNposition. There is no reconciliation against terminal on-chain state anywhere.Root cause 2 — blocked re-registration: flow state is keyed by wallet only
WalletDirectStakingFlow.tsx:flowIdentifier =`wallet-direct-${address}`(L65) — no attester / attempt ID, and the transaction cart restores completed entries across reloads with no TTL.isApprovalCompleted, auto-advance to step 3) while the real on-chain allowance is 0.allInQueue = depositTxs.length >= uploadedKeystores.length(L652) andcompletedDepositsCount >= uploadedKeystores.length(L146). The old completed deposit satisfies the new attempt, so no new transaction is ever created. (Per-row indicators do match by attester, so the UI contradicts itself.)Observed (Sepolia, 2026-08-06 → 08-08)
Attester fully slashed (2 × 100k) and ejected; Rollup status
NONE, GSE/effective balance 0,/api/staking/<wallet>all-zero — UI still showed the 200K position as active. Uploading a fresh keystore produced no transaction (wallet nonce and allowance unchanged). Clearing onlytransaction-cart,transaction-cart-current-executing,pending-erc20-direct-stakesand reloading immediately allowed a successful approve + deposit for the new attester.Browser-agnostic (pure app logic; observed in Firefox + MetaMask).
Suggested fixes
NONE+ zero effective balance alone is not terminal, since a newly deposited attester has the same state while still in the entry queue; also check entry-queue membership or slash/ejection/exit evidence — and run reconciliation even when the API breakdown is empty.allInQueue/ completion by attester match, not count.Happy to share the full timeline / tx hashes if useful.
Disclosure: I'm a hobbyist node operator, not a developer. The investigation and write-up were done with help from AI assistants (Codex / Claude); every code reference and on-chain fact above was verified against the deployed bundle and Sepolia receipts before filing.