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 {