Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

ADR-008: Dual-payment support (ETH + USDC)

  • Status: Accepted
  • Date: 2026-04-28
  • Deciders: Core team

Context

RegistrarController currently accepts only native ETH (msg.value) for registration and renewal. PriceOracle quotes prices in wei via a Chainlink ETH/USD feed.

Three problems with ETH-only payments on Base:

  1. Price volatility for users. A $3/month registration costs different amounts of ETH week to week. Users who think in dollars must check the ETH price before every transaction.
  2. Base is a USDC-native ecosystem. Circle's native USDC is widely held on Base. Many Base users hold USDC as their primary spending asset, not ETH.
  3. UX friction for non-crypto-native users. Target users (emerging markets, newcomers) are more familiar with stable dollar amounts than fluctuating ETH values.

Decision

Add USDC as a parallel payment option alongside ETH. ETH remains the default. Neither removes the other.

How it works

register() and renew() accept an optional paymentToken parameter:

  • address(0) → ETH path (current behavior, unchanged)
  • USDC_ADDRESS → USDC path (new)
function register(
    RegisterRequest calldata req,
    address paymentToken   // address(0) = ETH, USDC address = stablecoin
) external payable whenNotPaused {
    uint256 total = _totalPrice(req.name, req.tld, req.duration);
 
    if (paymentToken == address(0)) {
        // ETH path — existing behavior
        if (msg.value < total) revert InsufficientValue(total, msg.value);
        if (msg.value > total) _refundExcess(msg.sender, msg.value - total);
    } else if (paymentToken == USDC) {
        // USDC path — price in 6-decimal USD units
        uint256 usdcAmount = _toUsdc(total);
        IERC20(USDC).transferFrom(msg.sender, address(this), usdcAmount);
    } else {
        revert UnsupportedPaymentToken(paymentToken);
    }
    // ... rest of registration logic unchanged
}

Price conversion for USDC

PriceOracle already calculates basePriceUsd in 1e8 units. For USDC (6 decimals):

usdcAmount = basePriceUsd * duration / (YEAR * 1e2)

No Chainlink feed needed for USDC — it is already denominated in USD. This eliminates oracle risk for the USDC path entirely.

User flow (USDC)

  1. User approves USDC spend: USDC.approve(controller, amount)
  2. User calls register(req, USDC_ADDRESS) — no msg.value needed
  3. Controller calls transferFrom → pulls USDC from user
  4. Name minted, records set — identical to ETH path from here
Or with EIP-2612 Permit (single tx):
  1. User signs an off-chain permit message (no approve tx)
  2. Web app calls USDC.permit(...) then register(req, USDC_ADDRESS) in one batched call via multicall wrapper

Withdraw

Protocol owner can withdraw both ETH and USDC via separate withdraw() functions:

function withdrawEth(address to) external onlyOwner { ... }
function withdrawToken(address token, address to) external onlyOwner { ... }

Rationale

Why USDC specifically

  • Native Circle USDC on Base — no bridging, maximum liquidity
  • 1:1 USD peg removes all oracle risk on the payment path
  • Already the dominant stablecoin for on-chain commerce on Base
  • No additional price feed required — price is exact, not approximate

Why keep ETH

  • ETH holders should not be forced to acquire USDC to use the protocol
  • ETH path requires zero approvals — simpler UX for crypto-native users
  • Removing ETH would break any existing integrations and SDK usage

Why not support arbitrary ERC-20 tokens

  • Price oracle complexity explodes (need feed for every token)
  • Smart contract attack surface grows with each whitelisted token
  • USDC covers the stablecoin use case completely

Why not wrap ETH into USDC automatically on-chain

  • Requires on-chain DEX integration (Uniswap / Aerodrome)
  • Adds swap slippage, MEV risk, and smart contract dependency
  • Out of scope for v1 — can be added as a frontend convenience layer later

Consequences

Positive

  • Users can pay exact dollar amounts with zero ETH price exposure
  • Widens addressable market to USDC holders on Base
  • USDC path has no oracle dependency — simpler and safer
  • ETH and USDC revenue flows are separate and independently withdrawable

Negative

  • USDC path requires one extra approve transaction (or EIP-2612 permit complexity)
  • Contract size and logic surface increases slightly
  • RegistrarController must be redeployed — existing testnet deployments invalidated
  • SDK register() and renew() need a paymentToken option added

Mitigations

  • Default paymentToken to address(0) — all existing SDK and script callers are unaffected
  • Add permit2 support in a follow-up (Uniswap Permit2 on Base is already deployed)
  • Redeploy is already planned for mainnet — testnet redeploy has low cost

References