Розробка системи управління одобреннями токенів (revoke)
approve(spender, type(uint256).max) — рядок у транзакції, яку більшість користувачів підписують не глядячи, тому що без неї dApp не працює. Результат: сотні контрактів з необмеженим доступом до токенів гаманця. Коли один з них взламують, атакуючий дренує все — не тільки ту транзакцію, для якої було видано дозвіл. Revoke.cash та Etherscan Token Approvals вирішують цю проблему для кінцевих користувачів, але якщо вам потрібна кастомна система для конкретного протоколу, white-label продукту або корпоративного гаманця — це окрема розробка.
Технічні основи: як працюють approvals
ERC-20 allowance
Стандарт ERC-20 визначає allowance(owner, spender) — скільки токенів spender може витратити від імені owner. Встановлюється через approve(spender, amount). Значення type(uint256).max (2^256-1) означає «нескінченно» — більшість протоколів потребує саме це для зручності.
Проблема: allowance не має дати закінчення. Немає механізму автоматичного скасування. Якщо протокол скомпрометований рік після вашого approve — дозвіл все ще активний.
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) — підпис замість транзакції. Не створює постійного дозволу, працює один раз із конкретним дедлайном. Правильно спроектовані dApps використовують permit замість approve.
Але permit має нюанс: якщо DAI, USDC або інший токен підтримує permit — дозвіл через permit також можна переглянути через allowance(). Вони невідрізнювані від звичайних approves.
Читання даних про 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) });
// Отримайте всі события Approval, де owner = наша адреса
const approvalLogs = await client.getLogs({
event: ERC20_ABI[0], // Approval event
args: { owner: ownerAddress },
fromBlock: 0n,
toBlock: 'latest'
});
// Дедуплікація: лишіть тільки останній Approval для кожної пари token+spender
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: готові endpoints для token approvals (
alchemy_getTokenAllowances) - Incremental indexing: відслідковуємо останній проіндексований блок, при кожному оновленні отримуємо тільки нові события
Для production системи субграф The Graph — оптимальне рішення. Один запит повертає всі активні approvals із метаданими.
Operações de revoke
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]
});
}
Batch revoke через Multicall
Скасування 10 одобрень = 10 транзакцій, 10 підписів користувача. Неприйнятно. Але! ERC-20 approve не може бути викликаний від імені користувача без його підпису — немає способу multicall зробити batch approve/revoke за один клік без користувацького контракту або Permit2.
Permit2 batch revoke (якщо користувач використовує Permit2): Permit2 підтримує lockdown(TokenSpenderPair[] calldata approvals) — скасовує кілька Permit2 дозволів в одному викликі. Але не скасовує прямі ERC-20 approvals.
Практичне рішення: черга транзакцій revoke з автоматичною відправкою наступної після підтвердження попередньої. UI показує прогрес Скасовуємо 3 з 8.... Користувач підписує кожну, але не чекає вручну — наступний pop-up з'являється автоматично.
async function batchRevoke(approvals: Approval[]) {
for (const approval of approvals) {
await writeContractAsync({
address: approval.tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [approval.spenderAddress, 0n]
});
// Чекаємо підтвердження перед наступною
await waitForTransaction({ hash: txHash });
}
}
ERC-721 / ERC-1155 revoke
setApprovalForAll(operator, false) — скасування повного доступу до колекції. Більш критично, тому виділяємо червоним у UI. approve(operator, tokenId) з подальшим revoke менш критично — доступ до конкретного токена.
UI дизайн системи
Таблиця approvals
Основний компонент — таблиця з сортуванням та фільтруванням:
| Токен | Spender | Дозвіл | Ризик | Дія |
|---|---|---|---|---|
| USDC | Uniswap V3 | Необмежено | Середній | Скасувати |
| WETH | Old Protocol (deprecated) | Необмежено | Високий | Скасувати |
| DAI | Aave V3 | 1,000 DAI | Низький | Скасувати |
Risk scoring — важлива частина UX. Адреси spender ідентифікуються через:
- Etherscan Labels API
- DefiLlama базу протоколів
- Власний whitelist відомих протоколів
Верифікований протокол = середній ризик (одобрення існує, але протокол надійний). Невідомий контракт = високий ризик. Deprecated/мертвий контракт = критичний ризик.
Фільтри: за мережею, за типом (ERC-20 / NFT), за рівнем ризику, тільки необмежені одобрення.
Multi-chain
Користувачи мають одобрення на Ethereum, Base, Arbitrum, Polygon та інших мережах. Система повинна агрегувати дані зі всіх підтримуваних ланцюгів. Паралельні запити через Promise.all до RPC кожної мережі, об'єднуємо результати в єдиний список із піктограмою мережі.
Стек розробки
React + Next.js, wagmi 2.x + viem для on-chain операцій, TanStack Query для кешування та фонових оновлень, TanStack Table для таблиці approvals, The Graph для індексування подій (або Alchemy Transfers API). TypeScript.
Для multi-chain: конфіг wagmi з підтримуваними ланцюгами, окремий viem publicClient для кожної мережі.
Оцінки часових рамок
ERC-20 revoke система для однієї мережі з читанням через RPC Events, risk scoring за whitelist та batch revoke чергою — 2 дні. З підтримкою multi-chain (5+ мереж), ERC-721/ERC-1155 approvals, Graph субграфом та кастомним risk scoring — 3-5 днів.







