[Bug]: CDP SDK v1.48.1 permit2_data Bug Causes On-Chain Swaps to Revert on Base
- Dominant language
- Python
- Stars
- 200
- Forks
- 191
- Avg merge
- 6h 16m
- Merged PRs (30d)
- 17
Description
## 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=`) — 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
```python
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=` 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
Contributor guide
Assessment
This issue has not been assessed yet.