Skip to content

Stale localStorage state shows a fully-slashed stake as active and blocks re-registration from the same wallet #113

Description

@suna4

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions