Skip to content

[Bug]: CDP SDK v1.48.1 permit2_data Bug Causes On-Chain Swaps to Revert on Base #790

Description

@fdletter1980

Issue Summary

CDP SDK v1.48.1 (and v1.48.0) returns permit2_data as an empty dict ({}) or the string "0" instead of a proper Permit2Data instance with a valid .eip712 field. This causes every on-chain swap involving an ERC-20 source token (e.g. cbBTC) to revert on Base mainnet. Native ETH swaps are unaffected because they don't invoke Permit2 at all.

Environment

  • CDP SDK version: 1.48.1 (also reproduced on 1.48.0)
  • Language/runtime: Python 3.11.15
  • Network: Base mainnet (chainId 8453)
  • Account type: Regular EVM account (EOA), server-managed via CDP's TEE (not a smart account)
  • Affected tokens: cbBTC (0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf) as source token; USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) as destination

Steps to Reproduce

  1. Create a swap quote for an ERC-20 → token swap (e.g. cbBTC → USDC) using account.quote_swap().
  2. Execute the returned quote via swap_quote.execute() (equivalently, QuoteBasedSendSwapTransactionOptions with swap_quote set).
  3. Inspect swap_quote.permit2_data (or the underlying getattr(swap_data, "permit2_data", None)) — it is {} or "0" rather than an object exposing .eip712.
  4. Observe the resulting transaction revert on-chain.
  5. For contrast: repeat with an ETH → USDC swap using the quote-based path — same revert occurs, confirming this isn't token-specific to cbBTC alone.
  6. For further contrast: repeat with the inline path (account.swap() called directly without a swap_quote, i.e. InlineSendSwapTransactionOptions with taker=<own address>) — this succeeds for ETH→USDC (no Permit2 involved, since ETH is not an ERC-20) but cannot be used for cbBTC or any ERC-20 source token, which requires a Permit2-signed approval regardless of path.

Minimal repro

import asyncio
from cdp import CdpClient
from cdp.actions.evm.swap import AccountSwapOptions

CBBTC_BASE = "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"
USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"

async def main():
    async with CdpClient() as cdp:
        account = await cdp.evm.get_or_create_account(name="MyFundedAccount")

        # Step 1: create the quote
        swap_quote = await account.quote_swap(
            from_token=CBBTC_BASE,
            to_token=USDC_BASE,
            from_amount="100000",  # 0.001 cbBTC (8 decimals)
            network="base",
            slippage_bps=100,
        )

        # Inspect permit2_data before executing
        print("permit2_data:", getattr(swap_quote, "permit2_data", None))
        # Observed: {} or "0" — expected: Permit2Data(...) with a populated .eip712 field

        if not swap_quote.liquidity_available:
            print("Insufficient liquidity")
            return

        # Step 2: execute — reverts on-chain
        result = await swap_quote.execute()
        print(f"Transaction hash: {result.transaction_hash}")

asyncio.run(main())

Case 1: Empty Dict {}
permit2_data = {}
Characteristics:

Type: dict
Has no "eip712" key
hasattr(permit2_data, "eip712") → False
isinstance(permit2_data, dict) → True
permit2_data.get("eip712", None) → None
Code path (from signal_executor.py lines 435-453):

p2 = getattr(swap_data, "permit2_data", None)
if p2 is not None and not hasattr(p2, "eip712"):
if isinstance(p2, dict) and "eip712" in p2:
# Has eip712 nested key — would wrap it
...
else:
# No valid permit2 data — disable signing
print("⚠️ permit2_data present but invalid (no eip712), disabling permit2 signing")
swap_data.requires_signature = False
Result: requires_signature is set to False, but the transaction still reverts on-chain because the underlying permit2 structure is invalid.

Case 2: String "0"
permit2_data = "0"
Characteristics:

Type: str
hasattr(permit2_data, "eip712") → False (strings don't have attributes)
isinstance(permit2_data, dict) → False
Direct comparison: permit2_data == "0" → True
Code path:

p2 = getattr(swap_data, "permit2_data", None)
if p2 is not None and not hasattr(p2, "eip712"):
# p2 = "0" → hasattr("0", "eip712") is False → enters this branch
if isinstance(p2, dict) and "eip712" in p2:
# False — p2 is a string, not a dict
...
else:
# No valid permit2 data — disable signing
print("⚠️ permit2_data present but invalid (no eip712), disabling permit2 signing")
swap_data.requires_signature = False
Result: Same as Case 1 — requires_signature disabled, transaction reverts on-chain.

Expected Behavior

  • quote_swap() returns a permit2_data object with a well-formed .eip712 field suitable for signing.
  • The subsequent execute() call submits a valid transaction that completes successfully on-chain, and the account receives the quoted output token amount.

Actual Behavior

  • permit2_data is malformed ({} or "0") for ERC-20-sourced swaps.
  • The resulting transaction reverts immediately on submission; no output token is received and gas is consumed.
  • This affects both the quote-based path (quote_swap() + execute()) and, in effect, the all-in-one account.swap() path, since it uses the same quote generation internally.
  • The inline path (account.swap() without swap_quote) avoids permit2_data entirely by using taker=<own address> directly, but this only works for native ETH — it cannot satisfy the Permit2 approval flow required for ERC-20 source tokens like cbBTC.

Evidence

  • Reproduced on both SDK 1.48.0 and 1.48.1 — same failure mode, no change between versions.
  • permit2_data inspection consistently returns {} or "0" instead of a Permit2Data instance exposing .eip712.
  • Increasing the gas limit (tested up to 300,000) does not resolve the revert — confirms this is a calldata/signature validity issue, not a gas estimation issue.
  • The server-side account.swap() (all-in-one) path also reverts, since it relies on the same quote generation as the two-step path.
  • The inline path succeeds only for ETH→USDC; it fails for any ERC-20 source token requiring Permit2.

Possible Workarounds

  • Manual execution via app.coinbase.com (Trade → Swap) bypasses the SDK entirely and works — confirms the issue is isolated to the SDK's swap/quote generation, not the underlying CDP infrastructure or account permissions.
  • Inline swap path works around the bug only for native ETH; not viable for ERC-20 tokens.
  • No SDK-side workaround currently exists for ERC-20 → token swaps.

Suggested Fix

  • Ensure quote_swap() / create_swap_quote() populates permit2_data with a valid Permit2Data instance (including a correctly structured .eip712 payload) for all ERC-20 source tokens.
  • Alternatively, expose a supported path for submitting a pre-approved ERC-20 swap (via standing approve() to the Permit2 contract) that bypasses per-swap Permit2 signing entirely, for cases where the token already has sufficient allowance.

Issue Type

  • Bug report — regression introduced in SDK v1.48.0, persists in v1.48.1
  • Component: CDP EVM swap module → Permit2 data generation/handling
  • Severity: High — blocks all on-chain ERC-20 token swaps on Base mainnet via the SDK

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions