From 4591996e8b93f73d0f8de257a8055d1edc171000 Mon Sep 17 00:00:00 2001 From: y3v63n Date: Tue, 28 Jul 2026 12:52:21 +0200 Subject: [PATCH] Fix: show rewards as claimable on the V5 rollup The staking dashboard checked a rollup function (isRewardsClaimable) to decide whether to allow reward claims. The V5 rollup no longer has that function, so the check failed and the dashboard treated rewards as locked even though they could be claimed. Self-stake rewards showed "Locked on this rollup" and operators saw a "rewards are currently locked" banner. Now the dashboard only treats rewards as locked when a rollup explicitly reports them as locked. If the check is unavailable (as on V5), rewards are shown as claimable. Co-Authored-By: Claude Opus 4.8 --- .../ClaimSelfStakeRewardsModal.tsx | 2 +- .../RewardsManagement/CoinbaseAddressList.tsx | 11 ++++++----- .../src/hooks/rollup/useIsRewardsClaimable.ts | 10 +++++++++- .../useIsRewardsClaimableAcrossRollups.ts | 17 +++++++++++++---- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/staking-dashboard/src/components/ClaimSelfStakeRewardsModal/ClaimSelfStakeRewardsModal.tsx b/staking-dashboard/src/components/ClaimSelfStakeRewardsModal/ClaimSelfStakeRewardsModal.tsx index 2699a80e6..baeb1bd0a 100644 --- a/staking-dashboard/src/components/ClaimSelfStakeRewardsModal/ClaimSelfStakeRewardsModal.tsx +++ b/staking-dashboard/src/components/ClaimSelfStakeRewardsModal/ClaimSelfStakeRewardsModal.tsx @@ -233,7 +233,7 @@ export const ClaimSelfStakeRewardsModal = ({ rewards={row.rewards} decimals={decimals ?? 18} symbol={symbol ?? ""} - isClaimable={isClaimableForRollup(row.rollupAddress) === true} + isClaimable={isClaimableForRollup(row.rollupAddress)} isInBatch={isInBatch} onAddToBatch={() => handleAddToBatch(row.rollupAddress, row.rollupVersion, row.rewards)} onOpenCart={openCart} diff --git a/staking-dashboard/src/components/RewardsManagement/CoinbaseAddressList.tsx b/staking-dashboard/src/components/RewardsManagement/CoinbaseAddressList.tsx index 41fa4b76b..29cad679b 100644 --- a/staking-dashboard/src/components/RewardsManagement/CoinbaseAddressList.tsx +++ b/staking-dashboard/src/components/RewardsManagement/CoinbaseAddressList.tsx @@ -104,11 +104,12 @@ export const CoinbaseAddressList = ({ return (
{coinbaseBreakdown.map((item) => { - // Only enable the claim button when the rollup's claimability has been - // explicitly confirmed `true`. `undefined` (still loading, or the - // multicall reverted) is treated as not-claimable so the user doesn't - // sign a tx that's guaranteed to revert and waste gas. - const rowIsClaimable = isClaimableForRollup(item.rollupAddress) === true + // Lock a row only when its rollup explicitly reports rewards as not + // claimable (`isRewardsClaimable() === false`, the network-wide freeze on + // older rollups). A revert — e.g. the V5 rollup, which removed the + // function — or a still-loading read is treated as claimable, since + // claims on those rollups are live. See issue #111. + const rowIsClaimable = isClaimableForRollup(item.rollupAddress) const rowKey = `${item.address}-${item.rollupAddress}` const tx = buildClaimSequencerRewardsTx(item.address, item.rollupAddress) const isInBatch = checkTransactionInQueue(tx) diff --git a/staking-dashboard/src/hooks/rollup/useIsRewardsClaimable.ts b/staking-dashboard/src/hooks/rollup/useIsRewardsClaimable.ts index 5bb7f7113..a8cb56500 100644 --- a/staking-dashboard/src/hooks/rollup/useIsRewardsClaimable.ts +++ b/staking-dashboard/src/hooks/rollup/useIsRewardsClaimable.ts @@ -5,6 +5,14 @@ import { contracts } from "@/contracts" /** * Hook to check if rewards are claimable from a specific rollup contract. * + * `isRewardsClaimable()` was a network-wide reward lock on older rollups; a + * `false` return means the protocol has frozen claims. The V5 rollup removed + * the function, so the call reverts there — but claims on V5 are always live. + * We fail OPEN: `isRewardsClaimable` is `false` only when the rollup explicitly + * returns `false`; a revert (function absent, e.g. V5) or a still-loading read + * reads as `true`, so a removed view can't silently disable claims or surface a + * spurious "rewards locked" banner. See issue #111. + * * @param rollupAddress - Optional rollup contract to query. Defaults to the configured rollup. */ export function useIsRewardsClaimable(rollupAddress?: Address) { @@ -16,7 +24,7 @@ export function useIsRewardsClaimable(rollupAddress?: Address) { }) return { - isRewardsClaimable: query.data as boolean | undefined, + isRewardsClaimable: query.data !== false, isLoading: query.isLoading, error: query.error, refetch: query.refetch diff --git a/staking-dashboard/src/hooks/rollup/useIsRewardsClaimableAcrossRollups.ts b/staking-dashboard/src/hooks/rollup/useIsRewardsClaimableAcrossRollups.ts index 1d5f67eae..225827cab 100644 --- a/staking-dashboard/src/hooks/rollup/useIsRewardsClaimableAcrossRollups.ts +++ b/staking-dashboard/src/hooks/rollup/useIsRewardsClaimableAcrossRollups.ts @@ -5,8 +5,14 @@ import { contracts } from "@/contracts" /** * Multicalls `isRewardsClaimable()` across a list of rollup contracts. - * Returns a map keyed by lowercased rollup address; `undefined` means the - * value is still loading (or the call reverted). + * + * `isRewardsClaimable()` was a network-wide reward lock on older rollups; a + * `false` return means the protocol has frozen claims on that rollup. The V5 + * rollup removed the function entirely, so the call reverts there — but claims + * on V5 are always live. We therefore fail OPEN: a rollup is treated as locked + * only when it explicitly returns `false`. A revert (function absent, e.g. V5) + * or a still-loading read is treated as claimable, so a removed view can't + * silently disable claims. See issue #111. */ export function useIsRewardsClaimableAcrossRollups(rollupAddresses: Address[]) { const uniqueAddresses = useMemo(() => { @@ -50,8 +56,11 @@ export function useIsRewardsClaimableAcrossRollups(rollupAddresses: Address[]) { return map }, [data, uniqueAddresses]) - const isClaimable = (rollupAddress: Address): boolean | undefined => { - return claimableByRollup.get(rollupAddress.toLowerCase()) + // Fail open: locked only when the rollup explicitly returned `false`. A + // missing entry (revert on V5 where the function no longer exists, or the + // read hasn't resolved) is treated as claimable. + const isClaimable = (rollupAddress: Address): boolean => { + return claimableByRollup.get(rollupAddress.toLowerCase()) !== false } return {