Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions pkg/evm/chains.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,34 @@ func KnownChainIDs() []uint64 {
}
}

// usdcDomainName returns the EIP-712 domain name of the USDC contract
// deployed on chainID. The mainnet FiatToken deployments use "USD Coin",
// but Circle's Base Sepolia testnet deployment uses "USDC" — signing with
// the wrong name produces a different domain separator, so the contract
// (and any x402 facilitator) rejects the signature. Values verified
// against each contract's on-chain name() and DOMAIN_SEPARATOR(); they
// also match the per-network EIP-712 metadata in x402's asset registry
// (go/mechanisms/evm/constants.go).
//
// Unknown chains fall back to "USD Coin", preserving the previous
// behavior for custom deployments passed via token override.
func usdcDomainName(chainID uint64) string {
if chainID == ChainBaseSepolia {
return "USDC"
}
return "USD Coin"
}

// USDCDomain returns the EIP-712 Domain for USDC on chainID, ready to
// pass to EIP3009Digest. USDC's EIP-712 domain uses name="USD Coin"
// and version="2" on every chain (verified against the deployed FiatToken
// contracts).
// pass to EIP3009Digest. version="2" on every supported chain; the
// domain name is per-chain (see usdcDomainName).
func USDCDomain(chainID uint64) (Domain, error) {
addr, err := USDCAddress(chainID)
if err != nil {
return Domain{}, err
}
return Domain{
Name: "USD Coin",
Name: usdcDomainName(chainID),
Version: "2",
ChainID: chainID,
VerifyingContract: addr,
Expand Down
36 changes: 33 additions & 3 deletions pkg/evm/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,19 @@ func TestTransferWithAuthorizationTypeHash(t *testing.T) {
}

func TestUSDCDomainKnownChains(t *testing.T) {
for _, chainID := range []uint64{ChainEthereumMainnet, ChainBaseMainnet, ChainBaseSepolia} {
wantName := map[uint64]string{
ChainEthereumMainnet: "USD Coin",
ChainBaseMainnet: "USD Coin",
ChainPolygonMainnet: "USD Coin",
ChainBaseSepolia: "USDC", // Circle's testnet deployment differs
}
for chainID, name := range wantName {
d, err := USDCDomain(chainID)
if err != nil {
t.Fatalf("USDCDomain(%d): %v", chainID, err)
}
if d.Name != "USD Coin" || d.Version != "2" || d.ChainID != chainID {
t.Errorf("domain for chain %d wrong: %+v", chainID, d)
if d.Name != name || d.Version != "2" || d.ChainID != chainID {
t.Errorf("domain for chain %d wrong: %+v (want name %q)", chainID, d, name)
}
// Separator must be 32 bytes.
sep := d.Separator()
Expand All @@ -131,6 +137,30 @@ func TestUSDCDomainKnownChains(t *testing.T) {
}
}

// TestUSDCDomainSeparatorsMatchOnChain pins Domain.Separator() for every
// supported chain to the value the deployed USDC contract returns from
// DOMAIN_SEPARATOR() (fetched via eth_call, 2026-07-29). If any of these
// fail, signatures produced for that chain are unverifiable on-chain —
// this is the regression test for the Base Sepolia "USD Coin"/"USDC"
// domain-name mismatch.
func TestUSDCDomainSeparatorsMatchOnChain(t *testing.T) {
onChain := map[uint64]string{
ChainEthereumMainnet: "06c37168a7db5138defc7866392bb87a741f9b3d104deb5094588ce041cae335",
ChainBaseMainnet: "02fa7265e7c5d81118673727957699e4d68f74cd74b7db77da710fe8a2c7834f",
ChainPolygonMainnet: "caa2ce1a5703ccbe253a34eb3166df60a705c561b44b192061e28f2a985be2ca",
ChainBaseSepolia: "71f17a3b2ff373b803d70a5a07c046c1a2bc8e89c09ef722fcb047abe94c9818",
}
for chainID, want := range onChain {
d, err := USDCDomain(chainID)
if err != nil {
t.Fatalf("USDCDomain(%d): %v", chainID, err)
}
if got := hex.EncodeToString(d.Separator()); got != want {
t.Errorf("chain %d separator %s, on-chain DOMAIN_SEPARATOR() is %s", chainID, got, want)
}
}
}

func TestUSDCDomainRejectsUnknownChain(t *testing.T) {
_, err := USDCDomain(999999)
if err == nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/evm/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (m *EVMMethod) Satisfy(_ context.Context, c payment.Contract) (payment.Rece
Nonce: nonce,
}
domain := Domain{
Name: "USD Coin",
Name: usdcDomainName(m.chainID),
Version: "2",
ChainID: m.chainID,
VerifyingContract: m.token,
Expand Down Expand Up @@ -225,7 +225,7 @@ func (m *EVMMethod) Verify(_ context.Context, c payment.Contract, r payment.Rece
Nonce: nonce,
}
domain := Domain{
Name: "USD Coin",
Name: usdcDomainName(p.ChainID),
Version: "2",
ChainID: p.ChainID,
VerifyingContract: p.Token,
Expand Down