Skip to content

feat: CoW Swap cash out after permit claim (#386)#457

Open
energypantry wants to merge 11 commits intoubiquity:developmentfrom
energypantry:feat/386-cow-swap-cash-out
Open

feat: CoW Swap cash out after permit claim (#386)#457
energypantry wants to merge 11 commits intoubiquity:developmentfrom
energypantry:feat/386-cow-swap-cash-out

Conversation

@energypantry
Copy link

Fixes #386

What

  • Integrate real CoW Protocol quote + order submission (EIP-712 signing)
  • After successful permit claim (single / sequential / batch), optionally submit CoW Swap orders to cash out claimed rewards into the user's preferred reward token
  • Best-effort approve flow for CoW vault relayer (only when needed)

Notes

  • Claim flow remains the source of truth; swap submission is best-effort and won't block a successful claim if CoW quote/order fails
  • Quote logic is gated to the currently connected chain to avoid cross-network quoting

Testing

  • bun run lint
  • bun run typecheck
  • bun run build

@energypantry
Copy link
Author

PR marked ready for review.

Recent updates to better match the original Issue #386 spec:

  • Enforced UUSD-only settlements for both quote display and order submission.
    • CoW order receiver is set to the permit beneficiary.
    • Default aggregated display now shows token units (e.g. 100.00 UUSD) instead of $100.
      Would appreciate a review, and please call out any spec changes since this issue is fairly old.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a65d26961

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +241 to +246
const { orderId } = await postCowSwapOrder({
tokenIn,
tokenOut,
amountIn,
owner: address,
receiver,

Choose a reason for hiding this comment

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

P2 Badge Skip swaps when beneficiary differs from signer

Here owner is always the connected address, but earlier you set receiver from permit.beneficiary. If a permit’s beneficiary isn’t the connected wallet, the claimed tokens are sent to the beneficiary while the order is signed and funded by address, so the order will be unfillable (and the allowance check above also uses address). This means users claiming on behalf of someone else will see “submitted” swaps that can never settle. Consider skipping CoW submissions unless permit.beneficiary matches the connected wallet (or use the beneficiary as signer, which isn’t possible here).

Useful? React with 👍 / 👎.

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

Replaced single aggregated claimable total with per-token totals (claimableTotalsByToken) and claimablePermitCount; updated dashboard display, resume-on-network-switch logic, and claim flows to use preferredRewardTokenAddress. Added getTokenBySymbol. Restricted CoW Swap quoting to UUSD on the connected chain. Introduced CoW Swap utilities (vault relayer lookup, partner-fee handling, quoting, EIP‑712 signing, postCowSwapOrder) and expanded use-permit-claiming with preferredRewardTokenAddress, swap submission flow (maybeSubmitCowSwap), allowance handling, batch/simulation helpers, swap status tracking, and claim-record retry logic.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarizes the main feature: CoW Swap cash out integration after permit claim.
Description check ✅ Passed Description is relevant, detailing CoW Protocol integration, swap submission flow, and testing commands related to the changeset.
Linked Issues check ✅ Passed PR satisfies all core Issue #386 objectives: UUSD-only settlements enforced, CoW quote/order submission integrated, preferred token configuration, and per-token aggregation.
Out of Scope Changes check ✅ Passed All changes align with Issue #386 scope: CoW Swap integration, per-token reward aggregation, quote filtering, and UI display updates for preferred tokens.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
src/utils/cowswap-utils.ts (2)

120-148: Significant duplication between postCowSwapOrder and getCowSwapQuote.

Lines 128-148 replicate the appData building and quote-fetching logic from lines 76-96. If slippage, appCode, or partner-fee logic changes, both must be updated in lockstep.

Consider extracting a shared helper (e.g. buildQuoteRequest) that both functions call.


159-165: Double as unknown cast on the quote message is fragile.

quoteResponse.quote as unknown as Record<string, unknown> bypasses all type safety. If the CoW SDK changes the quote shape, this will silently produce an invalid EIP-712 signature.

Consider typing the message more precisely or at least adding a runtime guard for required fields (sellToken, buyToken, sellAmount, buyAmount, validTo, etc.) before signing.

src/hooks/use-permit-claiming.ts (2)

226-233: MAX_UINT256 unlimited approval — intentional?

Granting unlimited allowance to the CoW vault relayer is common in DeFi but means the relayer can spend all of the user's tokens of that type indefinitely. Consider approving only the needed amountIn to limit exposure.

Proposed change
-            const MAX_UINT256 = (1n << 256n) - 1n;
             const approveTx = await walletClient.writeContract({
               address: tokenIn,
               abi: erc20Abi,
               functionName: "approve",
-              args: [spender, MAX_UINT256],
+              args: [spender, amountIn],
               account: address,
               chain,
             });

191-202: Grouping key is normalized but status key (line 207) is not.

Line 199 builds the group key with .toLowerCase() on all addresses, but line 207 builds the status key without normalization. This inconsistency makes correlating group → status unreliable during debugging.

Proposed fix
-        const key = `${chainId}:${tokenIn}->${tokenOut}:${receiver}`;
+        const key = `${chainId}:${tokenIn.toLowerCase()}->${tokenOut.toLowerCase()}:${receiver.toLowerCase()}`;
src/components/dashboard-page.tsx (2)

532-550: Swap status messages persist indefinitely — no way to dismiss.

Once a swap completes or errors, the status banners stay on screen forever. Consider adding a dismiss mechanism (like TxBanner has onDismiss) or auto-clearing after a timeout.


142-194: Redundant BigInt() wrapper on permit.amount (line 173).

permit.amount is already bigint (used with > comparison at line 110). The BigInt() call is a no-op here but adds noise and a try/catch for an impossible error.

@energypantry
Copy link
Author

@coderabbitai I added JSDoc for the CoW swap helpers and permit hooks to address the docstring coverage gate from the previous review. Could you please re-review when you have a moment?

Tested locally:

  • bun run lint
    • bun run typecheck
    • bun run build

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

@energypantry: Thanks for adding the JSDoc documentation to address the coverage gate. I'll review the changes now, including the newly added documentation for the CoW swap helpers and permit hooks.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
src/utils/cowswap-utils.ts (2)

46-48: asSupportedChainId performs no validation.

The cast blindly trusts upstream callers. If an unsupported chain slips through, OrderBookApi silently targets the wrong endpoint. A runtime guard here is cheap insurance.

Proposed fix
 function asSupportedChainId(chainId: number): SupportedChainId {
+  const supported: number[] = Object.values(SupportedChainId).filter((v) => typeof v === "number");
+  if (!supported.includes(chainId)) {
+    throw new Error(`Unsupported CoW Protocol chainId: ${chainId}`);
+  }
   return chainId as SupportedChainId;
 }

87-107: Duplicate appData construction and quote fetching between getCowSwapQuote and postCowSwapOrder.

Both functions build identical appDataInfo and call orderBookApi.getQuote with the same shape. Extract a shared helper to reduce duplication and keep the two paths in sync.

Also applies to: 139-159

src/hooks/use-permit-claiming.ts (4)

244-261: MAX_UINT256 approval is a broad security posture.

Infinite approval to the CoW vault relayer means a compromise of the relayer contract drains all user tokens. Consider approving only amountIn for a tighter security profile, or at least document the trade-off.


203-283: maybeSubmitCowSwap swallows errors silently for individual groups.

If the first group succeeds but the second fails (e.g., user rejects the second signing prompt), the user gets a generic "Swap failed" status string but no differentiation between user-rejection and infra errors. Consider checking isUserRejectedRequest here too and using a distinct status like "rejected".


674-682: swapSubmissionStatus is exposed but never cleared.

Once statuses accumulate, they persist across claims. Consider resetting swapSubmissionStatus at the start of each claim flow (or providing a reset function) so stale entries don't confuse the UI.


446-522: Function name handleClaimSequential is misleading—it executes permits concurrently via Promise.allSettled, not sequentially.

Either rename to reflect concurrent execution (e.g., handleClaimIndividual) if concurrent behavior is intentional, or implement true sequential processing with a serial loop if nonce conflicts or rate limiting requires sequential execution.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/utils/cowswap-utils.ts (3)

70-75: assertQuoteShape only checks 5 of the ~12 EIP-712 Order fields.

Fields like receiver, feeAmount, kind, partiallyFillable, sellTokenBalance, buyTokenBalance are all required in the Order struct for a valid EIP-712 signature. If the quote response ever omits one of these, you'll get an opaque signing or submission error rather than a clear assertion failure.

Consider validating the full set of Order-required fields, or better yet, explicitly picking the Order fields from the quote (which also addresses the concern below about extra fields leaking into the signature).


103-108: partnerFeeBps condition in buildCowAppDataInfo relies on truthiness — will silently skip fee if bps is 0.

Currently safe because getPartnerFeeBps returns either COWSWAP_PARTNER_FEE_BPS (10) or undefined. But if someone later passes 0, line 115's partnerFeeBps ? ... would treat it as "no fee". Use an explicit !== undefined check for robustness.

Proposed fix
- ...(partnerFeeBps
+ ...(partnerFeeBps !== undefined

216-224: sendOrder spreads quoteResponse.quote which may include fields the API doesn't expect.

Same root cause as the signing concern — spreading the raw quote may send extraneous fields (signingScheme, from, etc.) to the order submission endpoint. Prefer constructing the order payload explicitly from picked Order fields plus the additional metadata (appData, appDataHash, from, quoteId, signature, signingScheme).

@energypantry
Copy link
Author

@coderabbitai review

Pushed a fix to approve the total required amount once per token before submitting multi-group orders so allowances are not overwritten across groups.

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

@energypantry: Thanks for the update. I'll review the changes, including the fix to approve the total required amount once per token to prevent allowance overwrites across multi-group orders.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@energypantry
Copy link
Author

@coderabbitai review

Pushed a follow-up to build the EIP-712 message strictly from types.Order fields (no extra keys leaked into the signature) and validate all required Order fields before signing. Also adjusted partnerFeeBps handling to only include partnerFee when the value is defined.

Thanks!

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

@energypantry: Thanks for the update. I'll review the changes, including the EIP-712 message construction fix and the improved partnerFeeBps handling.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/hooks/use-permit-claiming.ts (1)

413-414: maybeSubmitCowSwap is fire-and-forget but has no top-level catch.

The void call here (and Lines 569, 668) relies on maybeSubmitCowSwap never throwing. As noted above, the unprotected approve call breaks that assumption. Once the approve is wrapped in try/catch, consider also adding a top-level .catch on these call sites as a safety net.

- void maybeSubmitCowSwap([permit]);
+ void maybeSubmitCowSwap([permit]).catch((err) => console.warn("CoW swap submission failed", err));

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

@energypantry
Copy link
Author

@coderabbitai review

Pushed fixes:

  • wrap allowance readContract in try/catch to avoid UI stuck in submitting
  • avoid overwriting existing per-group error messages with generic approval missing
  • ensure OrderSigningUtils.getDomain uses validated SupportedChainId
  • typo fix: simulatePermitTransferFrom

Commit: 2a88e98

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

@energypantry: Thanks for the update. I'll review the changes, including the error handling improvements for allowance checks, the per-group error message handling, the OrderSigningUtils domain validation, and the typo fix.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/utils/cowswap-utils.ts (1)

224-232: The as unknown as SendOrderParams double-cast disables all type safety on the request body.

This is pragmatic but fragile — any field name drift between the SDK's SendOrderParams type and the EIP-712 Order struct will silently produce invalid requests. Consider adding a brief inline comment explaining why the cast is necessary (e.g., SDK type mismatch) so future maintainers don't remove it or miss regressions.

@energypantry
Copy link
Author

@coderabbitai review

Addressed the latest sendOrder request by providing appData (JSON string) + appDataHash (keccak256), and added a runtime guard to ensure the hash matches the signed message.appData.

Commit: 70dcff9

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

@energypantry: Thanks for the update. I'll review the changes, including the addition of appData and appDataHash to the sendOrder request, and the runtime guard to validate that the hash matches the signed message.appData.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@energypantry
Copy link
Author

@coderabbitai review

Addressed the remaining actionable item about an unhandled throw from getCowSwapVaultRelayerAddress leaving groups stuck at "submitting" by guarding it with try/catch and transitioning all groups to an error state on unsupported chains.

Commit: ea4af46

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

@energypantry: Thanks for the update. I'll review the changes, including the error handling for getCowSwapVaultRelayerAddress to prevent groups from getting stuck at "submitting" on unsupported chains.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@energypantry
Copy link
Author

CodeRabbit approved and PR is mergeable (CLEAN).

Latest commit: ea4af46.

Could a maintainer please merge when you have a moment so the bounty can move to payout? Thanks.

@gentlementlegen @0x4007

@energypantry
Copy link
Author

Quick ping (2026-02-08): this PR is still CodeRabbit-approved and mergeable (CLEAN). If you have a moment to merge, it would help move the bounty toward payout. Happy to adjust anything if needed. Thanks!

@gentlementlegen
Copy link
Member

Any QA to support this?

@energypantry
Copy link
Author

@gentlementlegen QA notes from my side:

On the PR head branch, I ran:

  • bun run lint
    • bun run typecheck
    • bun run build
      I also tried bun run deno:check, but this machine doesn’t have the deno binary installed (deno: command not found), so I couldn’t execute that step locally.

@energypantry
Copy link
Author

Quick merge follow-up: this PR is approved and shows mergeState=CLEAN.

If nothing else is needed, could a maintainer merge it when convenient to unblock the bounty payout?

Thanks!

@energypantry
Copy link
Author

Quick follow-up: PR is still showing as MERGEABLE + APPROVED on my side.

If there's anything else needed (extra QA, docs, or tweaks) to get this merged, tell me and I'll turn it around immediately.

@energypantry
Copy link
Author

Thanks for the reviews and QA on this.

For payout, please use my EVM address (USDT on Arbitrum preferred):
0x608aFcd86a82b62b062c7Cfa4d95Ef96CC74A7fE

If you need an off-chain rail instead, PayPal also works: steven8nee@gmail.com.

Once merged, could you confirm the expected payout timeline / next step on your side?

@energypantry
Copy link
Author

Friendly ping on this one when you have a moment.

PR is ready to merge (approved, checks green, mergeable_state=clean). If you'd like any tweaks (rebase/split/extra tests), I'm happy to do them quickly.

Thanks!

@energypantry
Copy link
Author

Quick follow-up on this one.

I have already pushed the latest requested updates and the PR is still clean/approved on my side. If there is any remaining blocker before merge, I can address it immediately.

If everything looks good, could we proceed with merge and payout when convenient?

@gentlementlegen
Copy link
Member

@energypantry as long as there is not real E2E testing with proven results, I will not accept the merge.

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.

Cow Swap Cash Out

2 participants