From 40b188b1574158a18a0e06bfd2e774a71518bc9d Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:30:55 -0600 Subject: [PATCH 1/6] fix(rfox): disable un-staking on arbitrum ahead of the migration The cooldown is fixed per request at un-stake time, so anyone un-staking now is held to the full 28 days even once it is set to zero on October 1 - leaving them worse off than someone who waits. Disables the action with a tooltip explaining why and what to do instead. Gated on the cooldown read from the contract rather than the date, so it lifts itself as soon as the cooldown is zeroed and needs no follow up deploy. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TW35mc6vnZHXgTStTksuFc --- src/assets/translations/en/main.json | 1 + src/pages/Fox/components/RFOXSection.tsx | 55 ++++++++++++++++++++---- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 688581d890d..6375889fc2d 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -3040,6 +3040,7 @@ "foxBurnAmount": "FOX Burn Amount (This Epoch)", "pendingDistribution": "Pending Distribution", "selectRuneAddress": "Select a RUNE address", + "unstakeDisabledMigrationTooltip": "Un-staking is temporarily disabled ahead of the rFOX migration to Ethereum. Your cooldown is fixed when you un-stake, so un-staking now would lock your FOX for the full %{cooldownPeriod}. The cooldown is removed on October 1 \u2014 un-stake then to claim straight away.", "lpSunsetWarningTitle": "Program Ended", "lpSunsetWarningDescription": "The Arbitrum WETH/FOX Rewards program has been sunset. Please un-stake and claim your balance at your earliest convenience." }, diff --git a/src/pages/Fox/components/RFOXSection.tsx b/src/pages/Fox/components/RFOXSection.tsx index 7ce4ed358d3..35fe0cbaf2c 100644 --- a/src/pages/Fox/components/RFOXSection.tsx +++ b/src/pages/Fox/components/RFOXSection.tsx @@ -15,6 +15,7 @@ import { Stack, Tag, Text as CText, + Tooltip, usePrevious, } from '@chakra-ui/react' import { @@ -45,6 +46,7 @@ import { Stats } from '@/pages/RFOX/components/Overview/Stats' import { StakeModal } from '@/pages/RFOX/components/StakeModal' import { UnstakeModal } from '@/pages/RFOX/components/UnstakeModal' import { selectStakingBalance } from '@/pages/RFOX/helpers' +import { useCooldownPeriodQuery } from '@/pages/RFOX/hooks/useCooldownPeriodQuery' import { useCurrentApyQuery } from '@/pages/RFOX/hooks/useCurrentApyQuery' import { useCurrentEpochMetadataQuery } from '@/pages/RFOX/hooks/useCurrentEpochMetadataQuery' import { useCurrentEpochRewardsQuery } from '@/pages/RFOX/hooks/useCurrentEpochRewardsQuery' @@ -63,6 +65,8 @@ import { } from '@/state/slices/selectors' import { useAppDispatch, useAppSelector } from '@/state/store' +const tooltipWrapperSx = { '& > span': { display: 'block', width: '100%' } } + const tbArrowUp = const tbArrowDown = @@ -332,6 +336,26 @@ export const RFOXSection = () => { setIsClaimModalOpen(false) }, []) + const cooldownPeriodQuery = useCooldownPeriodQuery(stakingAssetId) + + // The cooldown is fixed per request when un-staking, so anyone un-staking before it is zeroed on + // the migration date is held to the full period regardless. Lifts itself once the cooldown is + // zeroed on chain, so this needs no follow up deploy on the day. + const isUnstakeDisabled = useMemo( + () => + stakingAssetId === foxOnArbitrumOneAssetId && + Boolean(cooldownPeriodQuery.data?.cooldownPeriodSeconds), + [cooldownPeriodQuery.data?.cooldownPeriodSeconds, stakingAssetId], + ) + + const unstakeDisabledTooltip = useMemo( + () => + translate('RFOX.unstakeDisabledMigrationTooltip', { + cooldownPeriod: cooldownPeriodQuery.data?.cooldownPeriod, + }), + [cooldownPeriodQuery.data?.cooldownPeriod, translate], + ) + const actionsButtons = useMemo(() => { return ( @@ -346,15 +370,26 @@ export const RFOXSection = () => { {translate('defi.stake')} )} - + {/* Tooltip wraps its child in a span to catch hover on a disabled button, so the flex + sizing lives on the wrapper to keep this button growing with its siblings */} + + + + + )} - {/* Tooltip wraps its child in a span to catch hover on a disabled button, so the flex - sizing lives on the wrapper to keep this button growing with its siblings */} { chainId: arbitrum.id, query: { // Ops set this on chain, so it cannot be treated as immutable - notably it is zeroed at the - // rFOX migration, which the UI keys un-staking off + // rFOX migration, which the UI keys un-staking off. staleTime: 60 * 1000, // 1 minute in milliseconds + // refetchOnMount and refetchOnWindowFocus are both disabled app wide, so without its own + // triggers a stale value would persist until a full page reload. + refetchOnMount: true, + // Only poll while a cooldown is actually set. The transition worth catching is it being + // zeroed at the migration, so polling stops for good once that is read back rather than + // running forever for an event that has already happened. + refetchInterval: query => (query.state.data === 0n ? false : 60 * 1000), select: data => { const cooldownPeriod = formatSecondsToDuration(Number(data)) return { From 6468da89022eba4421bb004ceb19470bdd2ca426 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:57:25 -0600 Subject: [PATCH 5/6] fix(rfox): refetch the cooldown on window focus too refetchOnMount only fires when a component mounts an observer, which returning to a background tab does not do, and the poll is paused while the tab is backgrounded. Refetching on focus catches a cooldown changed while the user was away without waiting for the interval to resume. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TW35mc6vnZHXgTStTksuFc --- src/pages/RFOX/hooks/useCooldownPeriodQuery.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/RFOX/hooks/useCooldownPeriodQuery.ts b/src/pages/RFOX/hooks/useCooldownPeriodQuery.ts index 3f987821d55..235dfd58e53 100644 --- a/src/pages/RFOX/hooks/useCooldownPeriodQuery.ts +++ b/src/pages/RFOX/hooks/useCooldownPeriodQuery.ts @@ -20,6 +20,9 @@ export const useCooldownPeriodQuery = (stakingAssetId: AssetId) => { // refetchOnMount and refetchOnWindowFocus are both disabled app wide, so without its own // triggers a stale value would persist until a full page reload. refetchOnMount: true, + // Independent of the above - the poll is paused while the tab is in the background, so this + // catches a change made while the user was away without waiting for the next tick + refetchOnWindowFocus: true, // Only poll while a cooldown is actually set. The transition worth catching is it being // zeroed at the migration, so polling stops for good once that is read back rather than // running forever for an event that has already happened. From b72f23a872a3f374c0a0f21257cab157c887ec17 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:58:16 -0600 Subject: [PATCH 6/6] fix(rfox): drop the cooldown poll in favour of mount and focus The cooldown changes once, at the migration, so an interval would poll for a month to catch a single change that mounting the page or refocusing the tab already picks up. Keeps the two event driven triggers and removes the timer. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TW35mc6vnZHXgTStTksuFc --- src/pages/RFOX/hooks/useCooldownPeriodQuery.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/pages/RFOX/hooks/useCooldownPeriodQuery.ts b/src/pages/RFOX/hooks/useCooldownPeriodQuery.ts index 235dfd58e53..c359616c941 100644 --- a/src/pages/RFOX/hooks/useCooldownPeriodQuery.ts +++ b/src/pages/RFOX/hooks/useCooldownPeriodQuery.ts @@ -14,19 +14,9 @@ export const useCooldownPeriodQuery = (stakingAssetId: AssetId) => { functionName: 'cooldownPeriod', chainId: arbitrum.id, query: { - // Ops set this on chain, so it cannot be treated as immutable - notably it is zeroed at the - // rFOX migration, which the UI keys un-staking off. - staleTime: 60 * 1000, // 1 minute in milliseconds - // refetchOnMount and refetchOnWindowFocus are both disabled app wide, so without its own - // triggers a stale value would persist until a full page reload. + staleTime: 60 * 1000, refetchOnMount: true, - // Independent of the above - the poll is paused while the tab is in the background, so this - // catches a change made while the user was away without waiting for the next tick refetchOnWindowFocus: true, - // Only poll while a cooldown is actually set. The transition worth catching is it being - // zeroed at the migration, so polling stops for good once that is read back rather than - // running forever for an event that has already happened. - refetchInterval: query => (query.state.data === 0n ? false : 60 * 1000), select: data => { const cooldownPeriod = formatSecondsToDuration(Number(data)) return {