Skip to content

fix: resolve Safe wallet approval loop in staking flow#40

Open
BowTiedSwan wants to merge 4 commits intomainfrom
fix/improve-approval-polling-for-safe
Open

fix: resolve Safe wallet approval loop in staking flow#40
BowTiedSwan wants to merge 4 commits intomainfrom
fix/improve-approval-polling-for-safe

Conversation

@BowTiedSwan
Copy link
Copy Markdown
Contributor

@BowTiedSwan BowTiedSwan commented Dec 16, 2025

Problem

Users with Safe (multi-sig) wallets were stuck in an infinite approval loop when staking MOR tokens. After approving token spending, the UI continued to request another approval instead of allowing them to stake. Page reloads made the issue worse, requiring fresh approvals each time.

Root Cause

  1. Aggressive approval assumption: Code assumed approval was needed on mainnet whenever allowance data was undefined/loading
  2. Insufficient delay: 2-second delay before refetching allowance was too short for Safe multi-sig confirmations
  3. No retry logic: Single refetch attempt after approval often missed updated blockchain state
  4. Race condition on reload: Allowance queries loaded stale data before new approvals propagated

Solution

1. Removed Aggressive Assumptions

  • Changed checkAndUpdateApprovalNeeded() to maintain current state instead of assuming approval needed when data is loading
  • Prevents false positives that caused the approval loop

2. Implemented Allowance Polling with Safe Detection

  • Added pollAllowanceUntilUpdated() that automatically detects Safe wallets
  • Safe wallets: 5-second intervals × 15 attempts (75 seconds max)
  • Regular wallets: 2-second intervals × 10 attempts (20 seconds max)
  • Polls until allowance >= approved amount or timeout
  • Shows user-friendly toast notifications during polling

3. Added Loading State Guards

  • Prevent approval checks while data is loading
  • Added guards in effects and submit handlers
  • Shows "Loading..." state to users

4. Improved Button Text

  • Clear state progression: "Loading..." → "Approving..." → "Stake MOR"
  • Button disabled during loading to prevent premature clicks

Testing

  • ✅ No linting errors
  • ✅ TypeScript compilation passes
  • 🔍 Manual testing needed with Safe wallet to verify full flow

Files Changed

  • hooks/useStakingContractInteractions.ts - Core polling logic and approval state management
  • app/builders/[slug]/page.tsx - Loading guards and UI state improvements

Impact

Users can now successfully approve and stake with Safe wallets in a single approval. No more infinite loops or page reload issues.


Note

Fixes Safe wallet approval loop by deferring approval checks and polling allowance; adds wallet detection, WalletConnect cleanup/reconnect, and UI loading guards.

  • Staking flow (hooks/useStakingContractInteractions.ts, app/builders/[slug]/page.tsx)
    • Implement allowance polling with Safe wallet detection (pollAllowanceUntilUpdated) and track lastApprovedAmount to confirm approvals.
    • Remove aggressive "assume approval" logic; gate checks on loaded data to prevent false positives.
    • On submit, wait for data, force fresh approval check, and proceed to approve/stake accordingly.
    • Update UI state: disable staking and show "Loading.../Approving.../Staking..." based on isLoadingData and tx states.
  • Wallet/connectivity (components/web3-providers.tsx, context/auth-context.tsx, config/index.tsx, context/index.tsx)
    • Detect injected wallets (incl. Rabby); log conflicts; enable multiInjectedProviderDiscovery and featured wallets in Web3Modal.
    • Clean up expired/stale WalletConnect data; suppress noisy provider errors; show user-friendly toasts for proposal expiration.
    • Auto-reconnect WalletConnect sessions on app load and window focus; add connector event listeners; add safety timeout to avoid stuck loading.
  • Docs
    • Add docs/RABBY_WALLET_TROUBLESHOOTING.md; minor doc touch-ups.

Written by Cursor Bugbot for commit c991dfb. This will update automatically on new commits. Configure here.

…ully loaded before evaluating stake amounts. Enhance user experience by updating loading states and implementing polling for allowance verification after approval transactions.
@BowTiedSwan BowTiedSwan self-assigned this Dec 16, 2025
@vercel
Copy link
Copy Markdown

vercel bot commented Dec 16, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
dashboard-v2 Error Error Dec 16, 2025 7:56pm

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on January 18

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

// Keep current state and return false to avoid false positives
// This prevents the approval loop issue with Safe wallets
console.log("Data still loading - maintaining current approval state");
return needsApproval; // Return current state instead of assuming
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Stale closure returns outdated approval state value

The checkAndUpdateApprovalNeeded callback returns needsApproval when data is loading, but needsApproval is not included in the useCallback dependencies at line 705. This creates a stale closure where the returned value reflects the state when the callback was created, not the current state. Additionally, the comment on line 675 states "return false to avoid false positives" but the code returns needsApproval which could be true, contradicting the stated intent of preventing the approval loop issue.

Fix in Cursor Fix in Web

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on January 18

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.


Bug: Duplicate useEffects cause double calls to approval check

After adding !isLoadingData to the first useEffect's condition and isLoadingData to its dependencies, both useEffects at lines 568-574 and 576-583 now have identical conditions and dependencies. This causes checkAndUpdateApprovalNeeded to be invoked twice whenever stakeAmount, checkAndUpdateApprovalNeeded, or isLoadingData changes, resulting in redundant state updates and potential race conditions.

app/builders/[slug]/page.tsx#L567-L583

https://github.com/MorpheusAIs/dashboard-v2/blob/05d54cee14170fd976bcb5f39cffc1ea57c0eefc/app/builders/[slug]/page.tsx#L567-L583

Fix in Cursor Fix in Web


…on. Implement logging for detected wallets, improve configuration for injected wallets, and add troubleshooting documentation for common connection issues.
showEnhancedLoadingToast("Confirm approval in wallet...", "approval-tx");
}
if (isApproveTxSuccess) {
if (isApproveTxSuccess && lastApprovedAmount) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: BigInt zero is falsy causing stuck UI state

The condition isApproveTxSuccess && lastApprovedAmount uses lastApprovedAmount in a boolean context where BigInt 0n is falsy. If a user enters a sub-wei amount (e.g., less than 0.000000000000000001), parseFloat validation passes but parseEther rounds to 0n. When the approval transaction succeeds, the success handling block is skipped because 0n is falsy, so resetApproveContract() is never called and the "Confirm approval in wallet..." loading toast remains indefinitely, leaving the UI stuck in a pending state.

Fix in Cursor Fix in Web

// Improve wallet detection for injected wallets like Rabby
multiInjectedProviderDiscovery: true,
// Increase connection timeout for slower wallets
connectors: [],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Empty connectors array may break wallet connections

The comment states "Increase connection timeout for slower wallets" but the actual code sets connectors: [] (an empty array), which doesn't configure any timeout. Passing an empty connectors array to defaultWagmiConfig could override default wallet connectors and potentially break wallet connection functionality. The code doesn't match the stated intent in the comment, suggesting either incomplete implementation or accidentally committed code.

Fix in Cursor Fix in Web

…tic reconnection on app load, cleaning up expired proposals, and improving error handling for connection timeouts. Add user-friendly toast notifications for connection issues and streamline localStorage management for WalletConnect data.
changeUnsub?.();
disconnectUnsub?.();
errorUnsub?.();
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Connector event listeners never properly unsubscribed

The connector event subscription code assumes connector.emitter?.on?.(...) returns an unsubscribe function, storing the results in variables like connectUnsub. However, wagmi's emitter follows standard EventEmitter patterns where on() returns void or undefined, not an unsubscribe function. The cleanup calls connectUnsub?.() etc., which are no-ops since these values are undefined. Event listeners are never removed, causing memory leaks and potentially stale handlers continuing to execute after component re-renders. The correct cleanup pattern would use connector.emitter?.off?.(event, handler).

Fix in Cursor Fix in Web

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Duplicate useEffects trigger double approval checks

Two consecutive useEffect hooks at lines 568-574 and 576-583 now have identical dependency arrays [stakeAmount, checkAndUpdateApprovalNeeded, isLoadingData] and identical conditional logic. After this PR added the !isLoadingData check to the first effect, both effects perform the same operation. This causes checkAndUpdateApprovalNeeded() to be called twice whenever any dependency changes, leading to redundant API calls and potentially confusing race conditions.

app/builders/[slug]/page.tsx#L567-L583

https://github.com/MorpheusAIs/dashboard-v2/blob/c991dfbfc4290ea04aa06bbfd498f9a791760fe9/app/builders/[slug]/page.tsx#L567-L583

Fix in Cursor Fix in Web


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant