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
- Create a swap quote for an ERC-20 → token swap (e.g. cbBTC → USDC) using
account.quote_swap().
- Execute the returned quote via
swap_quote.execute() (equivalently, QuoteBasedSendSwapTransactionOptions with swap_quote set).
- Inspect
swap_quote.permit2_data (or the underlying getattr(swap_data, "permit2_data", None)) — it is {} or "0" rather than an object exposing .eip712.
- Observe the resulting transaction revert on-chain.
- 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.
- 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
Issue Summary
CDP SDK v1.48.1 (and v1.48.0) returns
permit2_dataas an empty dict ({}) or the string"0"instead of a properPermit2Datainstance with a valid.eip712field. 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
0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf) as source token; USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) as destinationSteps to Reproduce
account.quote_swap().swap_quote.execute()(equivalently,QuoteBasedSendSwapTransactionOptionswithswap_quoteset).swap_quote.permit2_data(or the underlyinggetattr(swap_data, "permit2_data", None)) — it is{}or"0"rather than an object exposing.eip712.account.swap()called directly without aswap_quote, i.e.InlineSendSwapTransactionOptionswithtaker=<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
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)⚠️ permit2_data present but invalid (no eip712), disabling permit2 signing")
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("
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)⚠️ permit2_data present but invalid (no eip712), disabling permit2 signing")
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("
swap_data.requires_signature = False
Result: Same as Case 1 — requires_signature disabled, transaction reverts on-chain.
Expected Behavior
quote_swap()returns apermit2_dataobject with a well-formed.eip712field suitable for signing.execute()call submits a valid transaction that completes successfully on-chain, and the account receives the quoted output token amount.Actual Behavior
permit2_datais malformed ({}or"0") for ERC-20-sourced swaps.quote_swap()+execute()) and, in effect, the all-in-oneaccount.swap()path, since it uses the same quote generation internally.account.swap()withoutswap_quote) avoidspermit2_dataentirely by usingtaker=<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
permit2_datainspection consistently returns{}or"0"instead of aPermit2Datainstance exposing.eip712.account.swap()(all-in-one) path also reverts, since it relies on the same quote generation as the two-step path.Possible Workarounds
Suggested Fix
quote_swap()/create_swap_quote()populatespermit2_datawith a validPermit2Datainstance (including a correctly structured.eip712payload) for all ERC-20 source tokens.approve()to the Permit2 contract) that bypasses per-swap Permit2 signing entirely, for cases where the token already has sufficient allowance.Issue Type