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 {