Настройка приёма платежей в USDC
USDC — это ERC-20 токен с одним принципиальным отличием от большинства токенов: Centre (теперь Circle) может заморозить любой адрес и конфисковать средства через функцию blacklist. Это не гипотетическая возможность — её применяли при санкционных блокировках. Для бизнеса это означает KYC-совместимость, для разработчика — нужно понимать что принимаешь не просто «стейблкоин», а регулируемый инструмент с on-chain compliance.
Контракты USDC по сетям
Circle развернул нативный USDC (не bridged) на нескольких сетях — это важно, потому что нативный USDC напрямую minтится/burnится через Cross-Chain Transfer Protocol (CCTP), а bridged версии несут дополнительные риски bridge-контракта.
| Сеть | Адрес контракта | Тип |
|---|---|---|
| Ethereum | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
Native |
| Polygon | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 |
Native (новый) |
| Arbitrum One | 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 |
Native |
| Base | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
Native |
| Solana | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
Native |
Для большинства проектов Polygon, Arbitrum или Base — оптимальный выбор с точки зрения gas fees для пользователей.
Базовая схема приёма платежей
Принципиальное отличие от приёма ETH: USDC требует двухшагового процесса — сначала пользователь вызывает approve(), затем ваш контракт вызывает transferFrom(). Либо используется EIP-3009 (transferWithAuthorization) — подпись вместо approve-транзакции.
Вариант 1: Уникальный адрес на каждый платёж
Генерируете HD-кошелёк (BIP-32/44), для каждого платежа — новый адрес. Мониторите Transfer(from, to, value) событие ERC-20 на этих адресах. Просто, без смарт-контракта, но sweep-транзакции тратят gas (нужен ETH/MATIC на адресе для оплаты gas при переводе USDC).
from web3 import Web3
from eth_account import Account
import secrets
def generate_payment_address(order_id: str, master_key: bytes) -> dict:
# Детерминированная деривация от order_id
child_key = derive_child_key(master_key, order_id)
account = Account.from_key(child_key)
return {
"address": account.address,
"order_id": order_id,
"expires_at": int(time.time()) + 3600 # 1 час
}
Вариант 2: Единый контракт-шлюз
Пользователь делает approve(gateway_contract, amount), затем вызывает pay(order_id, amount). Контракт забирает USDC и эмитирует событие.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract USDCGateway is Ownable {
IERC20 public immutable usdc;
event PaymentReceived(
bytes32 indexed orderId,
address indexed payer,
uint256 amount
);
constructor(address _usdc) Ownable(msg.sender) {
usdc = IERC20(_usdc);
}
function pay(bytes32 orderId, uint256 amount) external {
require(amount > 0, "Zero amount");
usdc.transferFrom(msg.sender, address(this), amount);
emit PaymentReceived(orderId, msg.sender, amount);
}
function withdraw(address to, uint256 amount) external onlyOwner {
usdc.transfer(to, amount);
}
}
Вариант 3: EIP-3009 (gasless approve)
USDC поддерживает transferWithAuthorization — пользователь подписывает сообщение EIP-712 off-chain, ваш backend или контракт вызывает транзакцию от своего имени. Пользователь платит только один раз за газ (вместо approve + transfer).
import { signTypedData } from 'viem/accounts';
const authorization = await signTypedData({
domain: { name: 'USD Coin', version: '2', chainId: 137, verifyingContract: USDC_ADDRESS },
types: {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
]
},
primaryType: 'TransferWithAuthorization',
message: { from, to: GATEWAY, value: amount, validAfter: 0, validBefore: deadline, nonce: randomBytes32 }
});
Мониторинг и подтверждение
Мониторинг события Transfer на USDC-контракте с фильтром по адресу получателя:
usdc_contract = w3.eth.contract(address=USDC_ADDRESS, abi=ERC20_ABI)
# Polling подход
event_filter = usdc_contract.events.Transfer.create_filter(
fromBlock='latest',
argument_filters={'to': GATEWAY_ADDRESS}
)
def check_payments():
for event in event_filter.get_new_entries():
order_id = match_order(event['args']['from'], event['args']['value'])
if order_id:
confirmations = get_confirmations(event['blockNumber'])
if confirmations >= REQUIRED_CONFIRMATIONS:
mark_order_paid(order_id, event['transactionHash'])
Количество подтверждений: для Polygon — 128+ (финальность ~4 минуты с учётом checkpoint на Ethereum), для Arbitrum — 1 блок достаточно для большинства платежей, для Ethereum mainnet — 12-15 блоков.
Типичные проблемы
Сумма не совпадает. Пользователь отправил чуть меньше (ошибка округления на UI). Храните tolerance: abs(received - expected) < dust_threshold.
Replay атаки. Один Transfer может соответствовать нескольким ордерам по сумме. Привязывайте txHash к ордеру, не только сумму.
USDC blacklist. Если адрес пользователя в blacklist — transferFrom ревертнется. Нужна обработка ошибки с понятным сообщением.
Gas для sweep. При схеме с уникальными адресами нужен ETH/MATIC для оплаты sweep-транзакции. Держите резервный кошелёк для топ-апа газа.
Процесс внедрения
Выбор сети → деплой или настройка gateway → интеграция webhook-мониторинга → тестирование на testnet (USDC Faucet на Sepolia/Mumbai) → аудит логики подтверждений → production деплой.
Срок 2-3 дня включает: настройку мониторинга, деплой gateway-контракта (опционально), интеграцию с backend, тестирование end-to-end.







