Skip to content

[01/03] DeepSec approvals: permit cache and token identity#7837

Open
fairlighteth wants to merge 5 commits into
developfrom
deepsec/approval-permit-01-cache-storage
Open

[01/03] DeepSec approvals: permit cache and token identity#7837
fairlighteth wants to merge 5 commits into
developfrom
deepsec/approval-permit-01-cache-storage

Conversation

@fairlighteth

@fairlighteth fairlighteth commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What changed

  • Key in-flight permit generation by normalized token, chain, owner, spender, effective amount, nonce, permit type, effective token name, and version.
  • Key permit-support detection by normalized token, chain, spender, effective amount, and minimum gas threshold.
  • Evict transient permit-detection errors instead of caching them for the page session.
  • Persist spender-aware permit capability metadata under permittableTokens:v4, while the regular token selector derives its Gas-free badge only from the default vault relayer.
  • Classify object-shaped native currencies by native-token address instead of display metadata.

Why

  • Permit calldata and permit-support estimation are sensitive to the full signing context. Reusing a request across different inputs can return calldata prepared for another nonce, domain, amount, owner, or spender.
  • Custom hook spenders must not change the default-spender Gas-free status shown by the regular token selector.
  • Native-token approval behavior must depend on token identity, not a mutable symbol or name.

DeepSec findings addressed

  • Source: DeepSec vulnerability scan report dated 2026-05-05. This waterfall supersedes the closed aggregate PR fix: align approval hooks and permit spender checks #7620.
  • MEDIUM - Concurrent permit generation can reuse a permit for the wrong spender / In-flight permit cache omits spender from the cache key: concurrent signing requests are now separated by every input that affects the permit payload, including spender, owner, amount, nonce, type, effective name, and version.
  • BUG - Permit-info cache permanently stores transient errors and ignores estimation parameters: temporary RPC or estimation failures no longer poison permit detection for the page session, and cached support results are separated by spender, effective amount, and minimum gas threshold.
  • Review follow-ups: custom-spender capability results no longer overwrite the default Gas-free badge, and object-shaped ERC-20 data cannot be treated as native solely because its display metadata resembles the native asset.

Review guide

QA Testing

Preview URL QA (not yet completed):

  • Test on the swap-dev branch preview.
  • Use a test wallet on Ethereum mainnet with a small balance of a permit-compatible token such as USDC or DAI. Use a wallet/token that has not already approved CoW Swap, or whose allowance is lower than the amount entered.
  • Use a small amount. Stop at the wallet prompt and reject it; do not sign, approve, or submit a trade.

1. Fresh-profile permit flow (Chromium and Firefox)

  1. Use a new browser profile, or clear site data for the preview domain before opening it.
  2. Connect the test wallet, select the permit-compatible token as the sell token, and enter an amount covered by the wallet balance.
  3. Confirm the token selector shows the Gas-free badge.
  4. Continue until the wallet opens the permit signature request.
  5. Check that the token, amount, and spender (the contract allowed to move the token) match the intended CoW Swap trade.
  6. Reject the request.

Expected result: the wallet requests a permit signature rather than an on-chain approval transaction. After rejection, the app remains usable and no transaction or order is submitted.

2. Warm-storage repeat (Chromium and Firefox)

  1. Reload the same preview without clearing site data or changing wallets.
  2. Repeat the same token selection and amount.
  3. Reject the wallet request again.

Expected result: the Gas-free badge and permit-signature path remain unchanged after reload; the app does not unexpectedly switch to an on-chain approval.

3. Custom-spender isolation

  1. Open Hooks from the trade menu and select the permit-token hook.
  2. Enter the same token and a different valid test spender. The burn address 0x000000000000000000000000000000000000dEaD can be used because the request will not be signed.
  3. Wait for the compatibility check, but do not add the hook or sign anything.
  4. Return to the regular Swap screen and select the same token.

Expected result: checking the custom spender does not remove, add, or change the regular token selector's Gas-free badge. A subsequent regular permit request must still show CoW Swap's spender, never the test spender.

4. Non-permit approval fallback

  1. Select WETH on Ethereum, or another token without a Gas-free badge for which the test wallet has a balance.
  2. Enter a small amount and continue to the approval step.
  3. Reject the wallet request.

Expected result: the token has no Gas-free badge and the wallet requests an on-chain approval transaction, not a permit signature. Rejection returns to a usable trade screen.

Record the browser, wallet, chain, token, expected result, actual result, and pass/fail status for each scenario. Attach a screenshot or short recording of the badge and wallet request where policy allows.

Developer verification on e0d08f908:

  • Frontend permit hooks: 2 suites, 8 tests passed.
  • Permit-utils cache and permit-generation coverage: 2 suites, 12 tests passed with Nx cache disabled.
  • cowswap-frontend typecheck and direct permit-utils TypeScript validation passed.
  • File-level ESLint passed for all amended files.
  • DeepSec PR mode completed against [01/03] DeepSec approvals: permit cache and token identity #7837's exact changed-file set: exit code 0, 8 analyses, 0 findings, and no comment artifact (20260710114946-058a9a29995b8463).
  • CI passes Test, Lint, Typecheck, Agent Harness, Setup, i18n extraction, Socket Security, CLA, and both Cloudflare previews.
  • Cypress has the same four missing-#do-trade-button timeouts as the exact develop base run at a2e8a457d; this does not appear introduced by this PR.

Reviewer note:

  • Concurrent cache-key isolation is covered by focused unit tests and DeepSec; there is no practical preview-only flow that deterministically changes nonce or permit domain while another wallet signature request is in flight.

Preview URLs

Surface URL
swap-dev - branch preview URL https://swap-dev-git-deepsec-approval-permit-01-cach-6e69b9-cowswap-dev.vercel.app
explorer-dev - branch preview URL https://explorer-dev-git-deepsec-approval-permit-01-390b1d-cowswap-dev.vercel.app
cowfi - branch preview URL https://cowfi-git-deepsec-approval-permit-01-cache-storage-cowswap.vercel.app
storybook - branch preview URL https://storybook-git-deepsec-approval-permit-01-cac-7af736-cowswap-dev.vercel.app
widget-configurator - branch preview URL https://widget-configurator-git-deepsec-approval-per-e19ae0-cowswap-dev.vercel.app
sdk-tools - branch preview URL https://sdk-tools-git-deepsec-approval-permit-01-cac-d7c263-cowswap-dev.vercel.app

Summary by CodeRabbit

  • Improvements

    • Permit compatibility and permit information are now correctly tracked per token and spender.
    • Custom spender checks use the appropriate permit data without reusing unrelated cached results.
    • Failed permit checks can be retried instead of remaining cached.
    • Permit request caching now responds correctly to changes in spender, amount, gas limit, and permit details.
    • Explicit zero permit amounts are preserved.
    • Native token detection is more accurate across supported token formats.
  • Bug Fixes

    • Improved detection of wallet rejection errors during permit requests.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Permit handling is now scoped by token and spender across frontend state, hooks, and utility caches. Permit generation preserves explicit zero values and handles rejection errors more precisely. Native-token detection uses address equality, and a localization source reference was updated.

Changes

Frontend permit scoping

Layer / File(s) Summary
Spender-scoped permit state
apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts, apps/cowswap-frontend/src/modules/permit/types.ts
Permit entries use composite token-spender keys, storage version v4, and a required spender field.
Spender-aware permit hooks
apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts, apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts
Permit lookup, caching, and compatibility mapping distinguish default and custom spenders.
Frontend permit behavior tests
apps/cowswap-frontend/src/modules/permit/hooks/*test.ts
Tests cover spender-specific lookup, precedence, compatibility mapping, and invalid spenders.

Permit utility caching

Layer / File(s) Summary
Permit generation behavior
libs/permit-utils/src/index.ts, libs/permit-utils/src/lib/generatePermitHook.ts, libs/permit-utils/src/lib/generatePermitHook.test.ts
Rejection detection and structured request keys are updated, and explicit zero permit amounts are preserved.
Token permit request cache
libs/permit-utils/src/lib/getTokenPermitInfo.ts, libs/permit-utils/src/lib/getTokenPermitInfo.test.ts
Request keys include spender and gas parameters, while failed results are removed from the cache.

Native-token detection

Layer / File(s) Summary
Address-based native detection
libs/common-utils/src/getIsNativeToken.ts, libs/common-utils/src/getIsNativeToken.test.ts
Native-token matching uses address equality and includes coverage for canonical, route, and ERC20-like tokens.

Localization reference

Layer / File(s) Summary
Receipt modal translation reference
apps/cowswap-frontend/src/locales/en-US.po
The “Total fee” source reference points to the receipt modal.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant usePermitInfo
  participant permittableTokensAtom
  participant getTokenPermitInfo
  usePermitInfo->>permittableTokensAtom: read token-spender cache
  usePermitInfo->>getTokenPermitInfo: request permit info
  getTokenPermitInfo-->>usePermitInfo: return permit result
  usePermitInfo->>permittableTokensAtom: cache result with spender
Loading

Suggested reviewers: alfetopito, elena-zh, limitofzero

Poem

I hop through keys of token and spender,
Keeping each permit neat and tender.
Zero stays zero, caches know their place,
Native coins match by address space.
The receipt’s words now point just right—
A bunny-approved refactor tonight! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 The title is concise and accurately summarizes the main permit-cache and token-identity changes.
Description check ✅ Passed The description is detailed and covers what changed, why, testing, and background, though it does not mirror the template headings exactly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch deepsec/approval-permit-01-cache-storage

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.

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cowfi Ready Ready Preview Jul 10, 2026 3:00pm
explorer-dev Ready Ready Preview Jul 10, 2026 3:00pm
storybook Ready Ready Preview Jul 10, 2026 3:00pm
swap-dev Ready Ready Preview Jul 10, 2026 3:00pm
widget-configurator Ready Ready Preview Jul 10, 2026 3:00pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
cosmos Ignored Ignored Jul 10, 2026 3:00pm
sdk-tools Ignored Ignored Preview Jul 10, 2026 3:00pm

Request Review

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 9, 2026

Copy link
Copy Markdown

Deploying explorer-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3307c25
Status: ✅  Deploy successful!
Preview URL: https://28c979d7.explorer-dev-dxz.pages.dev
Branch Preview URL: https://deepsec-approval-permit-01-c.explorer-dev-dxz.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 9, 2026

Copy link
Copy Markdown

Deploying swap-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3307c25
Status: ✅  Deploy successful!
Preview URL: https://3ef6be7b.swap-dev-5u6.pages.dev
Branch Preview URL: https://deepsec-approval-permit-01-c.swap-dev-5u6.pages.dev

View logs

@fairlighteth fairlighteth left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AI Review (Codex GPT-5, worked 9m): follow-up addressed; no new findings

Rechecked

  • generatePermitHook.getCacheKey now isolates requests by token, chain, owner, spender, amount, nonce, permit type, effective token name, and version.
  • usePermitInfo and usePermitCompatibleTokens keep custom-spender results separate from the default-relayer Gas-free badge.
  • The four focused suites pass: 20 tests covering cache isolation, transient-error eviction, spender-specific capability state, and default-spender presentation.

Result: Fixed. The original wrong-spender cache collision and custom-spender badge contamination paths no longer apply, and I found no new regression in the amended diff.

Review scope and related context
  • Current head: e0d08f908dc38c987ae518b146ca8e247a0f42cc.
  • Harness used: DeepSec PR mode plus code inspection and targeted unit tests.
  • DeepSec status: fresh output inspected for the current 13-file PR scope; 8 analyses, exit code 0, no findings, and no comment artifact. Run: 20260710114946-058a9a29995b8463.
  • DeepSec helper: run-deepsec-pr-mode.sh --pr 7837 --github-repo cowprotocol/cowswap; scope resolved through gh pr diff 7837 --name-only.
  • CI: Test, Lint, Typecheck, and Agent Harness pass. Cypress has the same four #do-trade-button timeouts as the exact base run.
  • Existing comments checked: no review threads or human findings; only deployment and skipped-draft bot comments.
  • Confidence limit: real-wallet browser QA has not been performed. The PR body accurately leaves the fresh-profile, warm-storage, and custom-spender checklist open.
🤖 Verification notes for AI agents
The two prior findings were rechecked against current head e0d08f908:

1. Permit-generation requests with different spender, nonce, permit type,
   effective token name, or version now have distinct in-flight cache keys.
2. Custom-spender permit capability entries cannot override or create the
   regular token selector's default-relayer Gas-free state.

Focused regression tests and DeepSec PR mode pass. Do not reopen these findings
without a new concrete failure mode.

Generated using the pr-review and security-review skills from the CoW Protocol skills repo.

@fairlighteth

Copy link
Copy Markdown
Contributor Author
🧪 Scoped browser QA passed: fresh/warm permit paths and on-chain approval fallback

Outcome

  • Checked scenarios:
    • ✅ Fresh USDC flow opened a real MetaMask eth_signTypedData_v4 permit request for the staging vault relayer.
    • ✅ Warm, spender-scoped permittableTokens:v4 state opened a second typed-data permit request correctly.
    • ✅ WETH fallback opened an on-chain approval through eth_sendTransaction.
    • ✅ Rejecting every wallet prompt returned control safely; zero order POSTs or transaction broadcasts were observed.
  • Primary evidence: sanitized runtime assertions record the wallet methods, cache scenario, prompt rejection, and zero order submissions.
  • Not checked / follow-up:
    • ⚠️ Confirming a permit, broadcasting an approval, or posting an order
    • ⚠️ Custom-spender Hook Store and Safe bundle flows
    • ⚠️ Firefox, Safari, and mobile layouts

Run details

  • Source: PR head e0d08f9 via the public preview
  • Environment: Linux; Chromium 150.0.7871.100
  • Wallet: real MetaMask 13.35.1; wallet-connected UI and real signature/approval prompts exercised through rejection
  • Test data: deterministic 100 USDC, 5 WETH, and zero allowances through the app's multicall fallback; the warm USDC v4 entry was seeded. Quotes, the preview application, and MetaMask prompts were real.
  • AI assistance: Codex orchestrated the Playwright/MetaMask run, inspected the artifacts, and drafted this note from observed results.

Artifacts

How to retest

  1. Open the preview on Ethereum and connect a QA wallet holding USDC and WETH with zero existing allowance.
  2. Enter a USDC → WETH trade and select Approve and Swap.
    • Expected: MetaMask opens a typed-data spending-cap signature request.
  3. Reject the request and retry with warm permit state.
    • Expected: the trade action opens another typed-data permit request without switching to an on-chain approval.
  4. Enter a WETH → USDC trade requiring approval.
    • Expected: MetaMask opens an on-chain spending-cap transaction.
  5. Reject the transaction.
    • Expected: the app reports that the approval was rejected and does not submit an order.

Generated using the pr-qa skill from the CoW Protocol skills repo.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
libs/permit-utils/src/lib/generatePermitHook.ts (1)

21-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer typeof guard over as number assertion.

After 'code' in error, error.code is still unknown. Using typeof error.code === 'number' would narrow the type without an assertion, making the code safer and the as cast unnecessary.

♻️ Proposed refactor
 function hasUserRejectionCode(error: unknown): boolean {
   return (
     typeof error === 'object' &&
     error !== null &&
     'code' in error &&
-    USER_REJECTION_CODES.includes(error.code as number)
+    typeof error.code === 'number' &&
+    USER_REJECTION_CODES.includes(error.code)
   )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/permit-utils/src/lib/generatePermitHook.ts` around lines 21 - 28, In
hasUserRejectionCode, replace the error.code as number assertion with a typeof
error.code === 'number' guard before calling USER_REJECTION_CODES.includes,
preserving the existing object and property checks while relying on type
narrowing.
libs/permit-utils/src/lib/generatePermitHook.test.ts (1)

36-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for rejection and non-rejection error handling.

The new isUserRejectionError logic and the .catch handler in generatePermitHook (re-throwing rejections, swallowing other errors) are not covered by tests. Consider adding cases for: (1) user rejection errors propagate, (2) non-rejection errors resolve to undefined, (3) cache entry is evicted after either error type.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/permit-utils/src/lib/generatePermitHook.test.ts` around lines 36 - 128,
Add tests covering the error handling in generatePermitHook: mock the permit
calldata builder to reject with a user-rejection error and assert the rejection
propagates, then reject with a non-rejection error and assert the result is
undefined. For both cases, verify the in-flight cache entry is evicted by
invoking generatePermitHook again and asserting the builder is called again; use
the existing request-cache test setup and identify the relevant
isUserRejectionError and generatePermitHook catch behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@libs/permit-utils/src/lib/generatePermitHook.test.ts`:
- Around line 36-128: Add tests covering the error handling in
generatePermitHook: mock the permit calldata builder to reject with a
user-rejection error and assert the rejection propagates, then reject with a
non-rejection error and assert the result is undefined. For both cases, verify
the in-flight cache entry is evicted by invoking generatePermitHook again and
asserting the builder is called again; use the existing request-cache test setup
and identify the relevant isUserRejectionError and generatePermitHook catch
behavior.

In `@libs/permit-utils/src/lib/generatePermitHook.ts`:
- Around line 21-28: In hasUserRejectionCode, replace the error.code as number
assertion with a typeof error.code === 'number' guard before calling
USER_REJECTION_CODES.includes, preserving the existing object and property
checks while relying on type narrowing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bb5421a2-d334-4448-b8d3-37b7218dd1df

📥 Commits

Reviewing files that changed from the base of the PR and between 8ab088d and 3307c25.

📒 Files selected for processing (14)
  • apps/cowswap-frontend/src/locales/en-US.po
  • apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.test.ts
  • apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts
  • apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.test.ts
  • apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts
  • apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts
  • apps/cowswap-frontend/src/modules/permit/types.ts
  • libs/common-utils/src/getIsNativeToken.test.ts
  • libs/common-utils/src/getIsNativeToken.ts
  • libs/permit-utils/src/index.ts
  • libs/permit-utils/src/lib/generatePermitHook.test.ts
  • libs/permit-utils/src/lib/generatePermitHook.ts
  • libs/permit-utils/src/lib/getTokenPermitInfo.test.ts
  • libs/permit-utils/src/lib/getTokenPermitInfo.ts
💤 Files with no reviewable changes (1)
  • apps/cowswap-frontend/src/locales/en-US.po

@elena-zh elena-zh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Works as described

@fairlighteth

Copy link
Copy Markdown
Contributor Author

Cross-PR coordination heads-up: #7697 adds ON_BEFORE_APPROVAL gating around permit signing, while this PR changes permit request/support cache identity that the stacked approval work relies on. The direct implementation overlap is in #7838.

The review on #7697 also found that the approval hook can run before the persistent cached-permit lookup, so cached reuse may still prompt the integrator. That issue is outside this PR's direct diff, but the cache contract here is foundational to fixing it consistently across the stack.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants