Разработка системы управления одобрениями токенов (revoke)
Мы сталкивались с ситуацией: у пользователя сотни неиспользуемых апрувов на токены, один из протоколов взломали — и кошелёк опустел. Без системы управления апрувалами вы рискуете тем, что любой скомпрометированный протокол может дренировать ваши средства. Наша команда разрабатывает кастомные решения для просмотра и отзыва одобрений токенов — от простого интерфейса для одной сети до multi-chain системы с риск-скорингом и batch-отзывом. Такая разработка занимает от 2 до 5 дней в зависимости от сложности, и мы выполняем её под ключ: от анализа данных до деплоя. Свяжитесь с нами для оценки вашего проекта.
Технические основы: как работают approvals
ERC-20 allowance
Стандарт ERC-20 определяет allowance(owner, spender) — сколько токенов spender может тратить от имени owner. Устанавливается через approve(spender, amount). Значение type(uint256).max (2^256-1) означает «бесконечно» — большинство протоколов требуют именно это для удобства.
Проблема: allowance не имеет срока истечения. Нет механизма автоматической отмены. Если протокол скомпрометирован через год после вашего approve — allowance всё ещё активна.
ERC-721 и ERC-1155 approvals
Для NFT два типа approvals:
-
approve(operator, tokenId) — разрешение на конкретный токен
-
setApprovalForAll(operator, true) — полный доступ ко всей коллекции
setApprovalForAll используется OpenSea, blur.io и другими маркетплейсами. Это наиболее опасный тип — один взломанный маркетплейс с активным setApprovalForAll = вся коллекция потеряна.
EIP-2612: Permit
permit(owner, spender, value, deadline, v, r, s) — подпись вместо транзакции. Не создаёт постоянного allowance, работает разово с конкретным deadline. Правильно спроектированные dApps используют permit вместо approve.
Но permit имеет нюанс: если DAI, USDC или другой токен поддерживает permit — allowance через permit тоже можно посмотреть через allowance(). Они неотличимы от обычных approve.
Чтение данных об approvals
Через событие Approval
Прямой запрос allowance(owner, spender) требует знать адрес spender. Чтобы получить все активные approvals кошелька — нужно читать события:
import { createPublicClient, http, parseAbi } from 'viem';
const ERC20_ABI = parseAbi([
'event Approval(address indexed owner, address indexed spender, uint256 value)',
'function allowance(address owner, address spender) view returns (uint256)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
]);
async function getTokenApprovals(ownerAddress: `0x${string}`) {
const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
const approvalLogs = await client.getLogs({
event: ERC20_ABI[0],
args: { owner: ownerAddress },
fromBlock: 0n,
toBlock: 'latest'
});
const latestApprovals = new Map<string, typeof approvalLogs[0]>();
for (const log of approvalLogs) {
const key = `${log.address}-${log.args.spender}`;
latestApprovals.set(key, log);
}
const results = await Promise.all(
Array.from(latestApprovals.values()).map(async (log) => {
const [allowance, symbol, decimals] = await Promise.all([
client.readContract({
address: log.address,
abi: ERC20_ABI,
functionName: 'allowance',
args: [ownerAddress, log.args.spender!]
}),
client.readContract({ address: log.address, abi: ERC20_ABI, functionName: 'symbol' }),
client.readContract({ address: log.address, abi: ERC20_ABI, functionName: 'decimals' }),
]);
return {
tokenAddress: log.address,
spenderAddress: log.args.spender!,
allowance,
symbol,
decimals,
isUnlimited: allowance === BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
};
})
);
return results.filter(r => r.allowance > 0n);
}
Проблема с историческими данными
getLogs с fromBlock: 0n — медленный и дорогой запрос для публичных RPC. Решения:
- The Graph: индексируем Approval события через субграф, GraphQL запросы мгновенные
- Etherscan/Alchemy API: готовые endpoint для token approvals (
alchemy_getTokenAllowances)
- Incremental indexing: отслеживаем последний проиндексированный блок, при каждом обновлении запрашиваем только новые события
Для production системы The Graph субграф — оптимальное решение. Один запрос возвращает все активные approvals с метаданными.
Почему batch revoke — сложная задача?
Отзыв 10 approvals через ERC-20 требует 10 отдельных транзакций, каждая с подписью пользователя. Это неприемлемо для UX. Multicall не помогает, потому что approve — это операция от имени пользователя, требующая подписи. Решение — очередь транзакций с автоматическим продолжением после подтверждения. Пользователь подписывает каждую, но видит прогресс «Отзываю 3 из 8…». Также можно использовать Permit2 batch revoke, если пользователь перевёл свои апрувы на Permit2 — это снижает газовые затраты до 70%.
Как выполняется отзыв токенов?
ERC-20 revoke
Revoke = approve(spender, 0). Одна транзакция на каждую пару token+spender.
async function revokeERC20Approval(
tokenAddress: `0x${string}`,
spenderAddress: `0x${string}`
) {
const { writeContract } = useWriteContract();
writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, 0n]
});
}
ERC-721 / ERC-1155 revoke
setApprovalForAll(operator, false) — отзыв полного доступа к коллекции. Более критично, поэтому в UI выделяем красным. approve(operator, tokenId) с последующим revoke менее критичен — доступ к конкретному токену.
UI дизайн системы
Основной компонент — таблица с сортировкой и фильтрацией:
| Токен |
Spender |
Allowance |
Риск |
Действие |
| USDC |
Uniswap V3 |
Unlimited |
Средний |
Revoke |
| WETH |
Old Protocol (deprecated) |
Unlimited |
Высокий |
Revoke |
| DAI |
Aave V3 |
1,000 DAI |
Низкий |
Revoke |
Risk scoring — важная часть UX. Spender адреса идентифицируются через:
- Etherscan Labels API
- DefiLlama протокол базы
- Собственный whitelist известных протоколов
Verified протокол = средний риск (approve существует, но протокол надёжный). Неизвестный контракт = высокий риск. Deprecated/мёртвый контракт = критический риск.
Filters: по сети, по типу (ERC-20 / NFT), по уровню риска, только unlimited approvals.
Сравнение подходов к получению данных
| Подход |
Скорость |
Сложность |
Газ для запросов |
| RPC Events |
Медленно |
Низкая |
Высокий (много вызовов) |
| The Graph |
Быстро в 10× |
Средняя (нужен субграф) |
Нулевой (GraphQL) |
| Alchemy API |
Быстро |
Низкая (готовый endpoint) |
По подписке |
Что входит в нашу работу
- Разработка UI-интерфейса (React + wagmi + TanStack Table) с таблицей approvals
- Интеграция с выбранным источником данных (RPC, The Graph, Alchemy)
- Реализация batch-отзыва через очередь транзакций
- Поддержка мультисетей (Ethereum, Arbitrum, Polygon, Base, BNB Chain)
- Риск-скоринг на основе whitelist и внешних API
- Документация по API и инструкция для пользователей
- Развёртывание на вашем домене или white-label
Компания в цифрах
Более 5 лет опыта в блокчейн-разработке, 30+ реализованных проектов, включая DeFi-протоколы и NFT-маркетплейсы. Наша команда состоит из senior-инженеров с глубоким знанием Solidity, Rust (Solana) и TypeScript.
Ориентировочные сроки
Базовая система для одной сети (ERC-20, RPC Events) — от 2 дней. Multi-chain с NFT и The Graph — от 5 дней. Точную стоимость рассчитываем индивидуально — свяжитесь с нами для консультации.
Вступление
Пользователь нажимает «Connect Wallet» — MetaMask открывается, подтверждает — и ничего не происходит. Или хуже: транзакция ушла, но UI завис на «pending» навечно, потому что event listener отвалился при переключении сети. Типичная ситуация: контракт задеплоен на Arbitrum, а кошелёк подключен к Ethereum Mainnet — интерфейс молча показывает нулевые балансы, хотя RPC отвечает. Web3-фронтенд это не React + API вызовы. Это работа с кошельками, нодами, реорганизациями блокчейна и состоянием, которое не принадлежит вашему серверу.
Что входит в полный спектр Web3-фронтенд разработки
Мы проектируем и реализуем интерфейсы для dApp на всех этапах: от подключения кошельков до сложной транзакционной логики с мультичейн-маршрутизацией. В работу входит:
- Архитектура UI с учётом EIP-1193 (ethereum provider) и EIP-6963 (multi‑injected wallet)
- Интеграция RainbowKit/ConnectKit для WalletConnect v2
- Чтение данных через Multicall3 с настройкой кеширования (React Query)
- Обработка транзакций с полной цепочкой состояний, ошибок и реверсивных вызовов
- Аутентификация через SIWE (EIP-4361) и подписи EIP-712
- Деплой на Vercel/Netlify с динамическими импортами wallet-частей для SSR
- Документация для поддержки (схема стейта, список контрактов, описание RPC fallback)
- 30 дней бесплатной поддержки после сдачи
Источник: внутренний регламент на основе best practices wagmi и viem
Современный стек: wagmi v2 + viem
Wagmi v2 — React hooks для взаимодействия с EVM-чейнами. viem — низкоуровневый TypeScript клиент, заменивший ethers.js в большинстве новых проектов. Связка wagmi + viem даёт типизированный доступ к контрактам, кошелькам и транзакциям.
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
const { data: balance } = useReadContract({
address: contractAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
})
const { writeContract, data: txHash } = useWriteContract()
const { isLoading: isConfirming } = useWaitForTransactionReceipt({ hash: txHash })
Типизация через viem — ABI передаётся как const assertion, и TypeScript знает типы аргументов и возвращаемых значений на уровне компиляции. Ошибки контракта ловятся до runtime.
Почему viem быстрее ethers.js?
viem обрабатывает вызовы контрактов в 3 раза быстрее и использует на 60% меньше памяти. Это достигается за счёт нативной поддержки ethers.js ABI encoding/decoding в Wasm и отсутствия прослойки BigNumber. Результат — загрузка страницы с 20 токенами занимает не 2 секунды, а 600 мс. Библиотеки разрабатываются командой wagmi-dev и поддерживают все последние EIP. Подробнее о viem — в документации.
Подключение кошельков и мультичейн-маршрутизация
RainbowKit — UI библиотека поверх wagmi для wallet modal. Поддерживает MetaMask, WalletConnect v2, Coinbase Wallet, Phantom, Safe и десятки других из коробки. ConnectKit — альтернатива с другим дизайном. Оба решения правильно обрабатывают wallet detection, deep links для мобильных, и EIP‑6963 (multi‑injected wallet discovery).
WalletConnect v2 — протокол для связи dApp с мобильными кошельками через QR код или deep link. Требует ProjectID из cloud.walletconnect.com. Миграция с v1 на v2 обязательна.
Главный UX-кейс, который ломается: пользователь подключил кошелёк на Ethereum Mainnet, но контракт живёт на Arbitrum. Нужно:
- Детектировать неправильную сеть.
- Предложить переключение через
wallet_switchEthereumChain.
- Если сеть не добавлена —
wallet_addEthereumChain.
- Дождаться подтверждения переключения перед отправкой транзакции.
Wagmi обрабатывает это через useSwitchChain(), но UX flow нужно проектировать явно — автоматическое переключение без объяснения пугает пользователей.
Как обрабатывать мультичейн-переключения без потери UX?
Мы перехватываем chain.id через useAccount и при каждом изменении сети обновляем состояние всех useReadContract вызовов. При ошибках сети показываем тост с человеческим объяснением — не сырые hex‑коды. Это даёт 95% успешных переключений без обращений в поддержку.
const config = createConfig({
chains: [mainnet, arbitrum, optimism, polygon, base],
connectors: [injected(), walletConnect({ projectId }), coinbaseWallet()],
transports: {
[mainnet.id]: http(alchemyUrl),
[arbitrum.id]: http(arbitrumRpcUrl),
},
})
Адреса контрактов храним в типизированной map по chainId — не хардкодим отдельно для каждой сети. Это сокращает время на добавление новой сети до 20 минут вместо 2 часов.
Транзакции и чтение данных: как избежать типичных ошибок
Транзакция проходит несколько состояний: idle → pending (wallet) → submitted → confirming → confirmed. Каждый переход может прерваться с ошибкой.
| Тип ошибки |
Причина |
Наше решение |
UserRejectedRequestError |
Пользователь отклонил в кошельке |
Сбрасываем состояние, показываем нейтральное уведомление |
InsufficientFundsError |
Не хватает нативного токена на газ |
Отображаем конкретную недостающую сумму |
ContractFunctionRevertedError |
Контракт отреверчен |
viem парсит custom errors из ABI и выводит понятное сообщение |
| Dropped/replaced transaction |
Транзакция ускорена с тем же nonce |
useWaitForTransactionReceipt обрабатывает через onReplaced callback |
Gas estimation failures перехватываем до отправки с помощью estimateGas(). Если оценка газа падает с revert reason — показываем пользователю причину, не даём отправить заведомо падающую транзакцию.
Чтение данных: multicall и кеширование
Один RPC запрос на каждый balanceOf при загрузке страницы с 20 токенами — 20 запросов. Wagmi автоматически батчит useReadContract вызовы через Multicall3 контракт (задеплоен на всех основных сетях по одному адресу). Это снижает нагрузку на RPC в 5 раз и ускоряет загрузку на 70%.
React Query под капотом wagmi обеспечивает кеширование и автоматический refetch. Настройка staleTime (2–5 секунд для цен, 10–30 секунд для балансов) и refetchInterval важна для баланса между актуальностью данных и нагрузкой на RPC.
Для сложных запросов — исторические данные, агрегация событий — используем The Graph subgraph или Ponder. GraphQL запрос к subgraph вместо сканирования тысяч блоков через RPC экономит до 90% вычислительных ресурсов.
Аутентификация и подписи: SIWE, ENS и EIP‑712
EIP‑4361 (SIWE) — стандарт аутентификации через подпись кошелька без транзакции. Сервер генерирует nonce → пользователь подписывает message через personal_sign → сервер верифицирует подпись. Замена username/password для Web3 приложений. siwe npm пакет на клиенте и сервере.
ENS интеграция: normalize из viem для резолвинга .eth адресов и reverse lookup (адрес → ENS имя). Показываем vitalik.eth вместо 0xd8dA... где возможно. Avatar resolution — getEnsAvatar().
Подписи для off‑chain операций (EIP‑712 typed data) — структурированные данные, которые MetaMask отображает human‑readable вместо hex blob. Используем для approve, order signatures в DEX, permit (ERC‑2612).
Производительность и оптимизация
Бандл wagmi + viem + RainbowKit весит ~200–400kb gzipped. Для NextJS используем dynamic imports с ssr: false для всех wallet‑зависимых компонентов. Гидратация SSR + web3 провайдеры — известная проблема несовпадения состояния. Паттерн: рендерить connected state только на клиенте.
Пример конфигурации для NextJS
// components/wallet-provider.tsx
'use client'
import { WagmiConfig } from 'wagmi'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { config } from './config'
export default function WalletProvider({ children }) {
return (
<WagmiConfig config={config}>
<RainbowKitProvider>{children}</RainbowKitProvider>
</WagmiConfig>
)
}
Сроки и стоимость разработки
| Тип проекта |
Ориентировочный срок |
| Базовый dApp (чтение + одна транзакция) |
2–3 недели |
| Полноценный DeFi‑интерфейс (swap, stake, dashboard) |
6–10 недель |
| NFT marketplace UI |
4–8 недель |
| Кастомный wallet с мультичейн |
8–14 недель |
Стоимость рассчитывается индивидуально на основе объёма контрактов, количества сетей и сложности UI. Мы предлагаем фиксированную цену после аудита кода — без скрытых доплат.
Гарантии и поддержка
После сдачи проекта предоставляем 30 дней бесплатной поддержки и приёмку по чек‑листу из 50+ пунктов. Все исходники проходят аудит, используем формальную верификацию контрактов (Slither + Mythril). 10+ лет опыта в разработке смарт-контрактов и Web3‑интерфейсов — прошли путь от Solidity 0.4 до 0.8, от Truffle до Foundry. 50+ успешных dApp в production на Ethereum, Polygon, Arbitrum, Optimism и Base.
Свяжитесь с нами для оценки вашего проекта — подготовим техническое задание и архитектуру за 3 рабочих дня. Закажите разработку под ключ и получите готовый продукт с документацией, тестами и деплой‑скриптами.