From 727db1e7b7a0de58d1ba47a5e3ba0f201e04d245 Mon Sep 17 00:00:00 2001 From: Jakub Kotula <520927+jkbktl@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:45:33 +0100 Subject: [PATCH 1/8] add withdraw --- .../hub/src/app/_components/emergency-bar.tsx | 24 +++ apps/hub/src/app/_components/hub-layout.tsx | 12 ++ .../vaults/modals/withdraw-vault-modal.tsx | 16 +- .../app/_components/vaults/table-columns.tsx | 71 ++++---- .../app/_components/vaults/vaults-table.tsx | 2 +- apps/hub/src/app/_constants/address.ts | 4 +- apps/hub/src/app/_hooks/useStakingVaults.ts | 29 ++- .../src/app/_hooks/useVaultEmergencyExit.ts | 165 ++++++++++++++++++ apps/hub/src/app/stake/page.tsx | 44 +++-- .../src/components/button-link/index.tsx | 22 ++- .../src/components/button/index.tsx | 11 +- packages/status-network/tailwind.config.ts | 4 + 12 files changed, 332 insertions(+), 72 deletions(-) create mode 100644 apps/hub/src/app/_components/emergency-bar.tsx create mode 100644 apps/hub/src/app/_hooks/useVaultEmergencyExit.ts diff --git a/apps/hub/src/app/_components/emergency-bar.tsx b/apps/hub/src/app/_components/emergency-bar.tsx new file mode 100644 index 000000000..3233a008e --- /dev/null +++ b/apps/hub/src/app/_components/emergency-bar.tsx @@ -0,0 +1,24 @@ +import { AlertIcon } from '@status-im/icons/20' +import { ButtonLink } from '@status-im/status-network/components' + +const EmergencyBar = () => { + return ( +
+
+

+ Contracts have been compromised. +

+ } + href="/stake" + > + Withdraw funds + +
+
+ ) +} + +export { EmergencyBar } diff --git a/apps/hub/src/app/_components/hub-layout.tsx b/apps/hub/src/app/_components/hub-layout.tsx index 0da89edee..04829fd79 100644 --- a/apps/hub/src/app/_components/hub-layout.tsx +++ b/apps/hub/src/app/_components/hub-layout.tsx @@ -2,7 +2,10 @@ import { useState } from 'react' import { Divider, Footer } from '@status-im/status-network/components' +import { useReadContract } from 'wagmi' +import { STAKING_MANAGER } from '../_constants/address' +import { EmergencyBar } from './emergency-bar' import { Sidebar } from './sidebar' import { TopBar } from './top-bar' @@ -12,6 +15,14 @@ interface HubLayoutProps { export function HubLayout({ children }: HubLayoutProps) { const [sidebarOpen, setSidebarOpen] = useState(false) + const { data: emergencyModeEnabled } = useReadContract({ + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'emergencyModeEnabled', + query: { + refetchInterval: 30000, + }, + }) return (
@@ -20,6 +31,7 @@ export function HubLayout({ children }: HubLayoutProps) { {/* Main Content Area */}
+ {Boolean(emergencyModeEnabled) && }
{/* Sidebar */} setSidebarOpen(false)} /> diff --git a/apps/hub/src/app/_components/vaults/modals/withdraw-vault-modal.tsx b/apps/hub/src/app/_components/vaults/modals/withdraw-vault-modal.tsx index 5258ffd4a..e281c29f4 100644 --- a/apps/hub/src/app/_components/vaults/modals/withdraw-vault-modal.tsx +++ b/apps/hub/src/app/_components/vaults/modals/withdraw-vault-modal.tsx @@ -7,7 +7,7 @@ import { InfoIcon } from '@status-im/icons/12' import { Button } from '@status-im/status-network/components' import { useAccount } from 'wagmi' -import { useVaultWithdraw } from '~hooks/useVaultWithdraw' +import { useVaultEmergencyExit } from '~hooks/useVaultEmergencyExit' import { BaseVaultModal } from './base-vault-modal' @@ -16,6 +16,7 @@ import type { Address } from 'viem' interface WithdrawVaultModalProps { onClose: () => void vaultAddress: Address + amountWei: bigint open?: boolean onOpenChange?: (open: boolean) => void children?: React.ReactNode @@ -25,21 +26,20 @@ interface WithdrawVaultModalProps { * Modal for emergency withdrawal from vault */ export function WithdrawVaultModal(props: WithdrawVaultModalProps) { - const { onClose, vaultAddress, open, onOpenChange, children } = props + const { onClose, vaultAddress, amountWei, open, onOpenChange, children } = + props const { address } = useAccount() - const { mutate: withdraw } = useVaultWithdraw() + const { mutate: emergencyExit } = useVaultEmergencyExit() const handleVaultWithdrawal = useCallback(() => { - const amountWei = 1000000000000000000n - if (!address) { console.error('No address found - wallet not connected') return } try { - withdraw({ + emergencyExit({ amountWei, vaultAddress, onSigned: () => { @@ -47,9 +47,9 @@ export function WithdrawVaultModal(props: WithdrawVaultModalProps) { }, }) } catch (error) { - console.error('Error calling withdraw:', error) + console.error('Error calling emergencyExit:', error) } - }, [address, onClose, vaultAddress, withdraw]) + }, [address, amountWei, onClose, vaultAddress, emergencyExit]) return ( { +const calculateTotalStaked = ( + vaults: StakingVault[], + emergencyMode: boolean +): bigint => { return vaults.reduce( - (acc, vault) => acc + (vault.data?.stakedBalance || 0n), + (acc, vault) => + acc + + (emergencyMode + ? vault.data?.depositedBalance || 0n + : vault.data?.stakedBalance || 0n), BigInt(0) ) } @@ -68,7 +75,10 @@ export const createVaultTableColumns = ({ isConnected, }: TableColumnsProps) => { // Calculate totals and current time once per column creation - const totalStaked = calculateTotalStaked(vaults) + const totalStaked = calculateTotalStaked( + vaults, + Boolean(emergencyModeEnabled) + ) const totalKarma = calculateTotalKarma(vaults) const currentTimestamp = getCurrentTimestamp() const columnHelper = createColumnHelper() @@ -109,12 +119,15 @@ export const createVaultTableColumns = ({ }, }), columnHelper.accessor('data.stakedBalance', { - header: 'Staked', + header: emergencyModeEnabled ? 'Vault balance' : 'Staked', cell: ({ row }) => { + const balance = emergencyModeEnabled + ? row.original.data?.depositedBalance + : row.original.data?.stakedBalance return (
- {formatSNT(row.original.data?.stakedBalance || 0n)} + {formatSNT(balance || 0n)} SNT
@@ -265,33 +278,31 @@ export const createVaultTableColumns = ({ return (
+ {Boolean(emergencyModeEnabled) && ( + + setOpenModalVaultId(open ? withdrawModalId : null) + } + onClose={() => setOpenModalVaultId(null)} + vaultAddress={row.original.address} + amountWei={row.original.data?.depositedBalance || 0n} + > + + + )} {isLocked ? (
- {!emergencyModeEnabled && ( - - setOpenModalVaultId(open ? withdrawModalId : null) - } - onClose={() => setOpenModalVaultId(null)} - vaultAddress={row.original.address} - > - - - )} diff --git a/apps/hub/src/app/_components/vaults/vaults-table.tsx b/apps/hub/src/app/_components/vaults/vaults-table.tsx index 43f9332d5..c93a2c88c 100644 --- a/apps/hub/src/app/_components/vaults/vaults-table.tsx +++ b/apps/hub/src/app/_components/vaults/vaults-table.tsx @@ -131,7 +131,7 @@ export function VaultsTable() { abi: STAKING_MANAGER.abi, functionName: 'emergencyModeEnabled', query: { - staleTime: 60_000, // Consider data fresh for 1 minute + refetchInterval: 30000, }, }) diff --git a/apps/hub/src/app/_constants/address.ts b/apps/hub/src/app/_constants/address.ts index 20a81cee3..8ceaea30c 100644 --- a/apps/hub/src/app/_constants/address.ts +++ b/apps/hub/src/app/_constants/address.ts @@ -8,12 +8,12 @@ import { import type { Abi, Address } from 'viem' export const STAKING_MANAGER = { - address: '0x5cDf1646E4c1D21eE94DED1DA8da3Ca450dc96D1' as Address, + address: '0x07301236DDAD37dCA93690e7a7049Bc13F55158E' as Address, abi: stakingManagerAbi as Abi, } as const export const VAULT_FACTORY = { - address: '0xddDcd43a0B0dA865decf3e4Ae71FbBE3e2DfFF14' as Address, + address: '0x489427Fad204FF494Cd8BE860D4af76b4Ce9F717' as Address, abi: vaultFactoryAbi as Abi, } as const diff --git a/apps/hub/src/app/_hooks/useStakingVaults.ts b/apps/hub/src/app/_hooks/useStakingVaults.ts index 09307efe1..8cc0a6237 100644 --- a/apps/hub/src/app/_hooks/useStakingVaults.ts +++ b/apps/hub/src/app/_hooks/useStakingVaults.ts @@ -4,7 +4,7 @@ import { useAccount, useChainId, useConfig } from 'wagmi' import { readContract, readContracts } from 'wagmi/actions' import { vaultAbi } from '~constants/contracts' -import { CACHE_CONFIG, STAKING_MANAGER } from '~constants/index' +import { CACHE_CONFIG, SNT_TOKEN, STAKING_MANAGER } from '~constants/index' // ============================================================================ // Types @@ -29,6 +29,8 @@ export interface StakingVaultData { lockUntil: bigint /** Total rewards accrued and available for claiming */ rewardsAccrued: bigint + /** Actual token balance in the vault (from balanceOf) - reliable in emergency mode */ + depositedBalance: bigint } /** @@ -112,32 +114,47 @@ async function fetchVaultData( functionName: 'lockUntil', args: [], }, + { + chainId, + address: SNT_TOKEN.address, + abi: SNT_TOKEN.abi, + functionName: 'balanceOf', + args: [vaultAddress], + }, ], }) - // Check if both contract calls succeeded - const [vaultResult, lockUntilResult] = results + // Check if all contract calls succeeded + const [vaultResult, lockUntilResult, depositedBalanceResult] = results if ( vaultResult.status !== 'success' || - lockUntilResult.status !== 'success' + lockUntilResult.status !== 'success' || + depositedBalanceResult.status !== 'success' ) { console.error( `Failed to fetch vault data for ${vaultAddress}:`, vaultResult.status !== 'success' ? vaultResult.error - : lockUntilResult.error + : lockUntilResult.status !== 'success' + ? lockUntilResult.error + : depositedBalanceResult.error ) return null } // Extract the actual data from successful results - const vaultData = vaultResult.result as Omit + const vaultData = vaultResult.result as Omit< + StakingVaultData, + 'lockUntil' | 'depositedBalance' + > const lockUntil = lockUntilResult.result as bigint + const depositedBalance = depositedBalanceResult.result as bigint return { ...vaultData, lockUntil, + depositedBalance, } } catch (error) { // Log error for debugging but don't throw - allows partial results diff --git a/apps/hub/src/app/_hooks/useVaultEmergencyExit.ts b/apps/hub/src/app/_hooks/useVaultEmergencyExit.ts new file mode 100644 index 000000000..071aacdba --- /dev/null +++ b/apps/hub/src/app/_hooks/useVaultEmergencyExit.ts @@ -0,0 +1,165 @@ +import { + useMutation, + type UseMutationResult, + useQueryClient, +} from '@tanstack/react-query' +import { type Address, formatUnits } from 'viem' +import { useAccount, useConfig, useWriteContract } from 'wagmi' +import { waitForTransactionReceipt } from 'wagmi/actions' + +import { vaultAbi } from '~constants/contracts' +import { SNT_TOKEN, testnet } from '~constants/index' +import { useVaultStateContext } from '~hooks/useVaultStateContext' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Parameters for emergency exit from a vault + */ +export interface EmergencyExitParams { + /** Amount being withdrawn (for display purposes only) */ + amountWei: bigint + /** Vault address */ + vaultAddress: Address + /** Optional callback called immediately after user signs transaction */ + onSigned?: () => void +} + +/** + * Return type for the useVaultEmergencyExit hook + */ +export type UseVaultEmergencyExitReturn = UseMutationResult< + void, + Error, + EmergencyExitParams, + unknown +> + +// ============================================================================ +// Constants +// ============================================================================ + +const MUTATION_KEY_PREFIX = 'vault-emergency-exit' as const +const CONFIRMATION_BLOCKS = 1 + +// ============================================================================ +// Mutation Hook +// ============================================================================ + +/** + * Mutation hook for emergency exit from a vault + * + * Performs emergency withdrawal from a vault contract when emergency mode is enabled. + * This withdraws ALL staked tokens from the vault to the connected wallet address. + * Manages the state machine transitions for the withdrawal process. + * + * **Process Flow:** + * 1. Validates wallet connection + * 2. Calls vault.emergencyExit(address) and waits for user to sign + * 3. After signing, sends START_WITHDRAW event → Goes directly to processing state + * 4. Calls onSigned callback (typically to close modal) + * 5. Waits for transaction confirmation + * 6. On success: Refetches data and resets state machine + * 7. On error: Sends REJECT event → Shows rejected state + * + * **Important:** emergencyExit withdraws ALL funds from the vault regardless of + * the amountWei parameter. The amount is only used for display purposes in the UI. + * + * @returns Mutation result with mutate function to trigger emergency withdrawal + * + * @throws {Error} When wallet is not connected + * @throws {Error} When transaction is reverted + * + * @example + * Basic usage with modal closing after sign + * ```tsx + * function EmergencyWithdrawModal({ vaultAddress, stakedAmount, onClose }: Props) { + * const { mutate: emergencyExit } = useVaultEmergencyExit() + * + * const handleEmergencyExit = () => { + * emergencyExit({ + * amountWei: stakedAmount, + * vaultAddress, + * onSigned: () => { + * // Close modal after user signs in wallet + * onClose() + * } + * }) + * } + * + * return + * } + * ``` + */ +export function useVaultEmergencyExit(): UseVaultEmergencyExitReturn { + const { address } = useAccount() + const { writeContractAsync } = useWriteContract() + const config = useConfig() + const queryClient = useQueryClient() + const { send: sendVaultEvent, reset: resetVault } = useVaultStateContext() + + return useMutation({ + mutationKey: [MUTATION_KEY_PREFIX, address], + mutationFn: async ({ + amountWei, + vaultAddress, + onSigned, + }: EmergencyExitParams): Promise => { + // Validate wallet connection + if (!address) { + throw new Error( + 'Wallet not connected. Please connect your wallet first.' + ) + } + + // Send START_WITHDRAW event first to transition state machine to processing + sendVaultEvent({ + type: 'START_WITHDRAW', + amount: formatUnits(amountWei, SNT_TOKEN.decimals), + }) + + // Close the modal immediately so the status dialog can show + onSigned?.() + + try { + // Execute emergency exit transaction + // emergencyExit withdraws ALL funds to the specified destination address + const hash = await writeContractAsync({ + chain: testnet, + account: address, + address: vaultAddress, + abi: vaultAbi, + functionName: 'emergencyExit', + args: [address], + }) + + // Wait for transaction confirmation + const { status } = await waitForTransactionReceipt(config, { + hash, + confirmations: CONFIRMATION_BLOCKS, + }) + + // Check if transaction was reverted + if (status === 'reverted') { + sendVaultEvent({ type: 'REJECT' }) + throw new Error('Transaction was reverted') + } + + // Transaction successful, invalidate cache to force fresh data from blockchain + await queryClient.invalidateQueries({ + queryKey: ['staking-vaults'], + }) + await queryClient.invalidateQueries({ + queryKey: ['multiplier-points-balance'], + }) + resetVault() + } catch (error) { + // Transaction failed or user rejected + sendVaultEvent({ type: 'REJECT' }) + throw error + } + }, + }) +} diff --git a/apps/hub/src/app/stake/page.tsx b/apps/hub/src/app/stake/page.tsx index f3efaf4f8..33f129843 100644 --- a/apps/hub/src/app/stake/page.tsx +++ b/apps/hub/src/app/stake/page.tsx @@ -74,6 +74,15 @@ export default function StakePage() { const weightedBoost = useWeightedBoost(vaults) const { data: exchangeRate } = useExchangeRate() + const { data: emergencyModeEnabled } = useReadContract({ + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'emergencyModeEnabled', + query: { + refetchInterval: 30000, + }, + }) + const form = useForm({ resolver: zodResolver(createStakeFormSchema()), mode: 'onChange', @@ -435,24 +444,27 @@ export default function StakePage() {
-
-
-

- Total staked + {!emergencyModeEnabled && ( +

+
+

+ Total staked +

+
+
+ + + {formatSNT(totalStaked ?? 0, { + includeSymbol: true, + })} + +
+

+ Next unlock in {STAKE_PAGE_CONSTANTS.NEXT_UNLOCK_DAYS}{' '} + days

-
- - - {formatSNT(totalStaked ?? 0, { - includeSymbol: true, - })} - -
-

- Next unlock in {STAKE_PAGE_CONSTANTS.NEXT_UNLOCK_DAYS} days -

-
+ )}
diff --git a/packages/status-network/src/components/button-link/index.tsx b/packages/status-network/src/components/button-link/index.tsx index 7fd09ee08..062765391 100644 --- a/packages/status-network/src/components/button-link/index.tsx +++ b/packages/status-network/src/components/button-link/index.tsx @@ -3,17 +3,17 @@ import { cva, cx } from 'cva' import { Link } from '../link' type Props = { - variant?: 'primary' | 'secondary' | 'white' | 'outline' + variant?: 'primary' | 'secondary' | 'white' | 'outline' | 'danger' | 'grey' backdropFilter?: boolean children?: React.ReactNode active?: boolean icon?: React.ReactNode iconBefore?: React.ReactNode - size?: '32' | '40' + size?: '24' | '32' | '40' } & React.ComponentProps const buttonStyles = cva({ - base: 'inline-flex w-fit cursor-pointer select-none items-center gap-1 border text-15 font-500 transition-all disabled:cursor-default disabled:opacity-[0.3]', + base: 'inline-flex w-fit cursor-pointer select-none items-center gap-1 border font-500 transition-all disabled:cursor-default disabled:opacity-[0.3]', variants: { variant: { primary: @@ -25,6 +25,9 @@ const buttonStyles = cva({ outline: [ 'border-neutral-30 text-neutral-100 hover:border-neutral-40 disabled:border-neutral-20', ], + danger: + 'border-[transparent] bg-danger-50 text-white-100 hover:bg-danger-60', + grey: 'bg-neutral-10 text-neutral-100 hover:bg-neutral-20 hover:text-neutral-100', }, withIcon: { true: '', @@ -35,8 +38,9 @@ const buttonStyles = cva({ false: '', }, size: { - '32': 'h-8 rounded-10 py-[5px]', - '40': 'h-10 rounded-12 py-[9px]', + '24': 'h-6 rounded-8 px-2 py-[3px] text-13', + '32': 'h-8 rounded-10 py-[5px] text-15', + '40': 'h-10 rounded-12 py-[9px] text-15', }, backdropFilter: { true: 'backdrop-blur-[20px]', @@ -50,10 +54,18 @@ const buttonStyles = cva({ compoundVariants: [ { size: '40', withIcon: false, className: 'px-4' }, { size: '32', withIcon: false, className: 'px-3' }, + { size: '24', withIcon: false, className: 'px-2' }, + { size: '24', withIcon: true, className: 'pl-2 pr-[6px]' }, { size: '40', withIcon: true, className: 'pl-4 pr-3' }, { size: '32', withIcon: true, className: 'pl-3 pr-2' }, { size: '40', withIconBefore: true, className: 'pl-3 pr-4' }, { size: '32', withIconBefore: true, className: 'pl-2 pr-3' }, + { size: '24', withIconBefore: true, className: 'pl-[6px] pr-2' }, + { + variant: 'grey', + active: true, + className: 'bg-neutral-50 text-white-100', + }, ], }) diff --git a/packages/status-network/src/components/button/index.tsx b/packages/status-network/src/components/button/index.tsx index 89bcd5857..a77aed3e2 100644 --- a/packages/status-network/src/components/button/index.tsx +++ b/packages/status-network/src/components/button/index.tsx @@ -13,7 +13,7 @@ type Props = { } & React.ComponentProps<'button'> const buttonStyles = cva({ - base: 'inline-flex w-fit cursor-pointer select-none items-center gap-1 whitespace-nowrap border text-15 font-500 transition-all disabled:cursor-default disabled:opacity-[0.3]', + base: 'inline-flex w-fit cursor-pointer select-none items-center gap-1 whitespace-nowrap border font-500 transition-all disabled:cursor-default disabled:opacity-[0.3]', variants: { variant: { primary: @@ -37,9 +37,9 @@ const buttonStyles = cva({ false: '', }, size: { - '32': 'h-8 rounded-10 py-[5px]', - '40': 'h-10 rounded-12 py-[9px]', - '24': 'h-6 rounded-8 px-2 py-[3px]', + '32': 'h-8 rounded-10 py-[5px] text-15', + '40': 'h-10 rounded-12 py-[9px] text-15', + '24': 'h-6 rounded-8 px-2 py-[3px] text-13', }, backdropFilter: { true: 'backdrop-blur-[20px]', @@ -53,10 +53,13 @@ const buttonStyles = cva({ compoundVariants: [ { size: '40', withIcon: false, className: 'px-4' }, { size: '32', withIcon: false, className: 'px-3' }, + { size: '24', withIcon: false, className: 'px-2' }, + { size: '24', withIcon: true, className: 'pl-2 pr-[6px]' }, { size: '40', withIcon: true, className: 'pl-4 pr-3' }, { size: '32', withIcon: true, className: 'pl-3 pr-2' }, { size: '40', withIconBefore: true, className: 'pl-3 pr-4' }, { size: '32', withIconBefore: true, className: 'pl-2 pr-3' }, + { size: '24', withIconBefore: true, className: 'pl-[6px] pr-2' }, { variant: 'grey', active: true, diff --git a/packages/status-network/tailwind.config.ts b/packages/status-network/tailwind.config.ts index 79c14ca7a..99bc5e17c 100644 --- a/packages/status-network/tailwind.config.ts +++ b/packages/status-network/tailwind.config.ts @@ -93,6 +93,10 @@ export default { sea: 'rgba(61, 150, 165, 1)', yellow: 'rgba(246, 176, 60, 1)', 'blue-50': 'rgba(42, 74, 245, 1)', + danger: { + 50: 'rgba(255, 125, 70, 1)', + 60: 'rgba(255, 125, 70, 0.6)', + }, neutral: { '2.5': 'rgba(250, 251, 252, 1)', '5': 'rgba(245, 246, 248, 1)', From deb9fb374bcee80c73c077d3ac6d18f60e07f75c Mon Sep 17 00:00:00 2001 From: Jakub <520927+jkbktl@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:50:21 +0100 Subject: [PATCH 2/8] Add changeset for patch updates --- .changeset/hungry-bananas-join.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/hungry-bananas-join.md diff --git a/.changeset/hungry-bananas-join.md b/.changeset/hungry-bananas-join.md new file mode 100644 index 000000000..c115cd0e4 --- /dev/null +++ b/.changeset/hungry-bananas-join.md @@ -0,0 +1,6 @@ +--- +"@status-im/status-network": patch +"hub": patch +--- + +withdraw From a145470cdb9b9c32856302ac718e6114fb1bd78f Mon Sep 17 00:00:00 2001 From: Jakub Kotula <520927+jkbktl@users.noreply.github.com> Date: Thu, 6 Nov 2025 23:23:54 +0100 Subject: [PATCH 3/8] address feedback --- .../app/_components/vaults/table-columns.tsx | 115 +++++++++--------- .../app/_components/vaults/vaults-table.tsx | 1 + apps/hub/src/app/stake/page.tsx | 17 ++- 3 files changed, 75 insertions(+), 58 deletions(-) diff --git a/apps/hub/src/app/_components/vaults/table-columns.tsx b/apps/hub/src/app/_components/vaults/table-columns.tsx index ec9170cbf..fa7af1619 100644 --- a/apps/hub/src/app/_components/vaults/table-columns.tsx +++ b/apps/hub/src/app/_components/vaults/table-columns.tsx @@ -278,7 +278,7 @@ export const createVaultTableColumns = ({ return (
- {Boolean(emergencyModeEnabled) && ( + {emergencyModeEnabled ? ( @@ -300,61 +300,66 @@ export const createVaultTableColumns = ({ Withdraw funds - )} - {isLocked ? ( -
- - setOpenModalVaultId(open ? lockModalId : null) - } - vaultAddress={row.original.address} - title="Extend lock time" - initialYears="2" - initialDays="732" - description="Extending lock time increasing Karma boost" - actions={[...EXTEND_LOCK_ACTIONS]} - onClose={() => setOpenModalVaultId(null)} - infoMessage={LOCK_INFO_MESSAGE} - > - - -
) : ( - - setOpenModalVaultId(open ? lockModalId : null) - } - vaultAddress={row.original.address} - title="Do you want to lock the vault?" - description="Lock this vault to receive more Karma" - actions={[...LOCK_VAULT_ACTIONS]} - onClose={() => setOpenModalVaultId(null)} - infoMessage={LOCK_INFO_MESSAGE} - onValidate={validateLockTime} - > - - + <> + {isLocked ? ( +
+ + setOpenModalVaultId(open ? lockModalId : null) + } + vaultAddress={row.original.address} + title="Extend lock time" + initialYears="2" + initialDays="732" + description="Extending lock time increasing Karma boost" + actions={[...EXTEND_LOCK_ACTIONS]} + onClose={() => setOpenModalVaultId(null)} + infoMessage={LOCK_INFO_MESSAGE} + > + + +
+ ) : ( + + setOpenModalVaultId(open ? lockModalId : null) + } + vaultAddress={row.original.address} + title="Do you want to lock the vault?" + description="Lock this vault to receive more Karma" + actions={[...LOCK_VAULT_ACTIONS]} + onClose={() => setOpenModalVaultId(null)} + infoMessage={LOCK_INFO_MESSAGE} + onValidate={validateLockTime} + > + + + )} + )}
) diff --git a/apps/hub/src/app/_components/vaults/vaults-table.tsx b/apps/hub/src/app/_components/vaults/vaults-table.tsx index c93a2c88c..65e48aecf 100644 --- a/apps/hub/src/app/_components/vaults/vaults-table.tsx +++ b/apps/hub/src/app/_components/vaults/vaults-table.tsx @@ -179,6 +179,7 @@ export function VaultsTable() { size="32" onClick={() => createVault()} className="w-full sm:w-auto" + disabled={Boolean(emergencyModeEnabled)} > Add vault diff --git a/apps/hub/src/app/stake/page.tsx b/apps/hub/src/app/stake/page.tsx index 33f129843..fdef84dcf 100644 --- a/apps/hub/src/app/stake/page.tsx +++ b/apps/hub/src/app/stake/page.tsx @@ -274,7 +274,10 @@ export default function StakePage() { ) : ( @@ -484,7 +492,10 @@ export default function StakePage() { {messageMultiplierPoints} diff --git a/apps/hub/src/app/_components/vaults/vaults-table.tsx b/apps/hub/src/app/_components/vaults/vaults-table.tsx index 06bb5a818..c6ffbd675 100644 --- a/apps/hub/src/app/_components/vaults/vaults-table.tsx +++ b/apps/hub/src/app/_components/vaults/vaults-table.tsx @@ -9,10 +9,10 @@ import { getCoreRowModel, useReactTable, } from '@tanstack/react-table' -import { useAccount, useChainId, useReadContract } from 'wagmi' +import { useAccount, useChainId } from 'wagmi' -import { STAKING_MANAGER } from '~constants/index' import { useCreateVault } from '~hooks/useCreateVault' +import { useEmergencyModeEnabled } from '~hooks/useEmergencyModeEnabled' import { type StakingVault, useStakingVaults } from '~hooks/useStakingVaults' import { createVaultTableColumns } from './table-columns' @@ -127,15 +127,7 @@ export function VaultsTable() { const { isConnected } = useAccount() const chainId = useChainId() const { mutate: createVault } = useCreateVault() - - const { data: emergencyModeEnabled } = useReadContract({ - address: STAKING_MANAGER.address, - abi: STAKING_MANAGER.abi, - functionName: 'emergencyModeEnabled', - query: { - refetchInterval: 30000, - }, - }) + const { data: emergencyModeEnabled } = useEmergencyModeEnabled() // Stable callback reference prevents column recreation on every render const handleSetOpenModalVaultId = useCallback( diff --git a/apps/hub/src/app/_constants/staking.ts b/apps/hub/src/app/_constants/staking.ts index 3d2368a8a..48965b9dc 100644 --- a/apps/hub/src/app/_constants/staking.ts +++ b/apps/hub/src/app/_constants/staking.ts @@ -46,6 +46,8 @@ export const CACHE_CONFIG = { MP_STALE_TIME: 30_000, /** Refetch interval for multiplier points (60 seconds) */ MP_REFETCH_INTERVAL: 60_000, + /** Refetch interval for emergency mode status (30 seconds) */ + EMERGENCY_MODE_REFETCH_INTERVAL: 30_000, } as const // ============================================================================ @@ -108,6 +110,16 @@ export const DEFAULT_LOCK_PERIOD = { INITIAL_DAYS: '90', } as const +/** + * Default lock period extension values for extending an existing lock + */ +export const EXTEND_LOCK_PERIOD = { + /** Extension period in years as a string (2 years) */ + INITIAL_YEARS: '2', + /** Extension period in days as a string (732 days = 2 years) */ + INITIAL_DAYS: '732', +} as const + // ============================================================================ // Validation Constants // ============================================================================ diff --git a/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts b/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts new file mode 100644 index 000000000..df1bd5e18 --- /dev/null +++ b/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts @@ -0,0 +1,15 @@ +import { useReadContract } from 'wagmi' + +import { STAKING_MANAGER } from '~constants/index' +import { CACHE_CONFIG } from '~constants/staking' + +export function useEmergencyModeEnabled() { + return useReadContract({ + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'emergencyModeEnabled', + query: { + refetchInterval: CACHE_CONFIG.EMERGENCY_MODE_REFETCH_INTERVAL, + }, + }) +} diff --git a/apps/hub/src/app/_hooks/useVaultEmergencyExit.ts b/apps/hub/src/app/_hooks/useVaultEmergencyExit.ts index 071aacdba..7088bdbbd 100644 --- a/apps/hub/src/app/_hooks/useVaultEmergencyExit.ts +++ b/apps/hub/src/app/_hooks/useVaultEmergencyExit.ts @@ -8,7 +8,7 @@ import { useAccount, useConfig, useWriteContract } from 'wagmi' import { waitForTransactionReceipt } from 'wagmi/actions' import { vaultAbi } from '~constants/contracts' -import { SNT_TOKEN, testnet } from '~constants/index' +import { SNT_TOKEN } from '~constants/index' import { useVaultStateContext } from '~hooks/useVaultStateContext' // ============================================================================ @@ -127,7 +127,6 @@ export function useVaultEmergencyExit(): UseVaultEmergencyExitReturn { // Execute emergency exit transaction // emergencyExit withdraws ALL funds to the specified destination address const hash = await writeContractAsync({ - chain: testnet, account: address, address: vaultAddress, abi: vaultAbi, diff --git a/apps/hub/src/app/stake/page.tsx b/apps/hub/src/app/stake/page.tsx index fdef84dcf..8c71f4d2a 100644 --- a/apps/hub/src/app/stake/page.tsx +++ b/apps/hub/src/app/stake/page.tsx @@ -37,6 +37,7 @@ import { import { useApproveToken } from '~hooks/useApproveToken' import { useCompoundMultiplierPoints } from '~hooks/useCompoundMultiplierPoints' import { useCreateVault } from '~hooks/useCreateVault' +import { useEmergencyModeEnabled } from '~hooks/useEmergencyModeEnabled' import { useExchangeRate } from '~hooks/useExchangeRate' import { useFaucetMutation, useFaucetQuery } from '~hooks/useFaucet' import { useMultiplierPointsBalance } from '~hooks/useMultiplierPoints' @@ -73,15 +74,7 @@ export default function StakePage() { const { data: vaults, refetch: refetchStakingVaults } = useStakingVaults() const weightedBoost = useWeightedBoost(vaults) const { data: exchangeRate } = useExchangeRate() - - const { data: emergencyModeEnabled } = useReadContract({ - address: STAKING_MANAGER.address, - abi: STAKING_MANAGER.abi, - functionName: 'emergencyModeEnabled', - query: { - refetchInterval: 30000, - }, - }) + const { data: emergencyModeEnabled } = useEmergencyModeEnabled() const form = useForm({ resolver: zodResolver(createStakeFormSchema()), From 00ff757f095bef9037c2170e86c220cdd46e9f81 Mon Sep 17 00:00:00 2001 From: Jakub Kotula <520927+jkbktl@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:16:47 +0100 Subject: [PATCH 5/8] change contracts address back to production --- apps/hub/src/app/_constants/address.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/hub/src/app/_constants/address.ts b/apps/hub/src/app/_constants/address.ts index 8ceaea30c..20a81cee3 100644 --- a/apps/hub/src/app/_constants/address.ts +++ b/apps/hub/src/app/_constants/address.ts @@ -8,12 +8,12 @@ import { import type { Abi, Address } from 'viem' export const STAKING_MANAGER = { - address: '0x07301236DDAD37dCA93690e7a7049Bc13F55158E' as Address, + address: '0x5cDf1646E4c1D21eE94DED1DA8da3Ca450dc96D1' as Address, abi: stakingManagerAbi as Abi, } as const export const VAULT_FACTORY = { - address: '0x489427Fad204FF494Cd8BE860D4af76b4Ce9F717' as Address, + address: '0xddDcd43a0B0dA865decf3e4Ae71FbBE3e2DfFF14' as Address, abi: vaultFactoryAbi as Abi, } as const From 025864717e2c0e2496100ba7e540100a95f7ce97 Mon Sep 17 00:00:00 2001 From: Jakub Kotula <520927+jkbktl@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:11:06 +0100 Subject: [PATCH 6/8] add use client to emergencyMode hook --- apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts b/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts index df1bd5e18..cc79bea06 100644 --- a/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts +++ b/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts @@ -1,3 +1,5 @@ +'use client' + import { useReadContract } from 'wagmi' import { STAKING_MANAGER } from '~constants/index' From 7f9109fd085c486fd57d7706b4e037e24e4655ae Mon Sep 17 00:00:00 2001 From: Jakub Kotula <520927+jkbktl@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:16:00 +0100 Subject: [PATCH 7/8] test fix build --- apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts b/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts index cc79bea06..a4f8805c2 100644 --- a/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts +++ b/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts @@ -12,6 +12,7 @@ export function useEmergencyModeEnabled() { functionName: 'emergencyModeEnabled', query: { refetchInterval: CACHE_CONFIG.EMERGENCY_MODE_REFETCH_INTERVAL, + enabled: typeof window !== 'undefined', }, }) } From 5a3c8aaa34da5030e46f3e8c71efaf6fbfc21597 Mon Sep 17 00:00:00 2001 From: Jakub Kotula <520927+jkbktl@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:02:07 +0100 Subject: [PATCH 8/8] test fix build --- apps/hub/src/app/_components/hub-layout.tsx | 13 +++++++++++-- apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts | 6 +----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/hub/src/app/_components/hub-layout.tsx b/apps/hub/src/app/_components/hub-layout.tsx index d5472698b..4fdd7d22f 100644 --- a/apps/hub/src/app/_components/hub-layout.tsx +++ b/apps/hub/src/app/_components/hub-layout.tsx @@ -2,8 +2,10 @@ import { useState } from 'react' import { Divider, Footer } from '@status-im/status-network/components' +import { useReadContract } from 'wagmi' -import { useEmergencyModeEnabled } from '~hooks/useEmergencyModeEnabled' +import { STAKING_MANAGER } from '~constants/index' +import { CACHE_CONFIG } from '~constants/staking' import { EmergencyBar } from './emergency-bar' import { Sidebar } from './sidebar' @@ -15,7 +17,14 @@ interface HubLayoutProps { export function HubLayout({ children }: HubLayoutProps) { const [sidebarOpen, setSidebarOpen] = useState(false) - const { data: emergencyModeEnabled } = useEmergencyModeEnabled() + const { data: emergencyModeEnabled } = useReadContract({ + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'emergencyModeEnabled', + query: { + refetchInterval: CACHE_CONFIG.EMERGENCY_MODE_REFETCH_INTERVAL, + }, + }) return (
diff --git a/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts b/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts index a4f8805c2..713233a20 100644 --- a/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts +++ b/apps/hub/src/app/_hooks/useEmergencyModeEnabled.ts @@ -1,9 +1,6 @@ -'use client' - import { useReadContract } from 'wagmi' -import { STAKING_MANAGER } from '~constants/index' -import { CACHE_CONFIG } from '~constants/staking' +import { CACHE_CONFIG, STAKING_MANAGER } from '~constants/index' export function useEmergencyModeEnabled() { return useReadContract({ @@ -12,7 +9,6 @@ export function useEmergencyModeEnabled() { functionName: 'emergencyModeEnabled', query: { refetchInterval: CACHE_CONFIG.EMERGENCY_MODE_REFETCH_INTERVAL, - enabled: typeof window !== 'undefined', }, }) }