Разработка системы предупреждений о подозрительных апрувалах
Token approval — одна из наиболее опасных операций в Web3. Пользователь даёт смарт-контракту право тратить его токены без его участия в будущем. Approval scam — главный способ кражи активов через фишинг и вредоносные dApp. Revoke.cash зафиксировал более $2.8B потерь через approval-based атаки. Система предупреждений позволяет детектировать подозрительные approve запросы в реальном времени — до подтверждения транзакции.
Анатомия approval атаки
ERC-20 approve: token.approve(spender, amount) — разрешает spender переводить до amount токенов с адреса пользователя.
ERC-721/ERC-1155 setApprovalForAll: даёт право на все NFT из коллекции. Наиболее опасная форма — одна транзакция передаёт права на всю коллекцию.
Permit (EIP-2612): gasless подпись approval без on-chain транзакции. Пользователь подписывает сообщение — злоумышленник сам отправляет транзакцию permit(). Пользователь не видит approval в кошельке до момента кражи.
Архитектура системы предупреждений
Система работает на двух уровнях:
Pre-transaction screening: анализ pending транзакции до её подписания. Интегрируется в кошелёк или dApp через simulation API.
Post-approval monitoring: отслеживание уже выданных approvals, алерт при обнаружении аномального использования.
Анализ pending транзакций
Simulation через Tenderly или Alchemy
Перед подписанием транзакции симулируем её исполнение и анализируем state changes:
interface ApprovalAnalysis {
isSuspicious: boolean;
riskScore: number; // 0-100
riskFactors: string[];
simulatedStateChanges: StateChange[];
spenderInfo: SpenderInfo;
}
interface SpenderInfo {
address: string;
isVerified: boolean; // есть ли в whitelist известных dApp
isNewContract: boolean; // контракт < 30 дней
hasSourceCode: boolean; // верифицирован на Etherscan
blacklisted: boolean; // в списке известных scammers
}
async function analyzeApproval(
txParams: TransactionParams,
chainId: number
): Promise<ApprovalAnalysis> {
const riskFactors: string[] = [];
let riskScore = 0;
// 1. Симуляция транзакции
const simulation = await simulateTransaction(txParams, chainId);
// Извлекаем approve вызовы из simulation
const approvals = extractApprovals(simulation.stateChanges);
for (const approval of approvals) {
// 2. Проверка типа approval
if (approval.type === "setApprovalForAll") {
riskFactors.push("SET_APPROVAL_FOR_ALL — передаёт права на все NFT коллекции");
riskScore += 40;
}
if (approval.amount === MaxUint256) {
riskFactors.push("UNLIMITED_APPROVAL — безлимитный апрувал токена");
riskScore += 20;
}
// 3. Анализ spender контракта
const spenderInfo = await analyzeSpender(approval.spender, chainId);
if (spenderInfo.blacklisted) {
riskFactors.push("KNOWN_SCAMMER — адрес в чёрном списке");
riskScore += 60;
}
if (spenderInfo.isNewContract) {
riskFactors.push("NEW_CONTRACT — контракт задеплоен менее 30 дней назад");
riskScore += 25;
}
if (!spenderInfo.hasSourceCode) {
riskFactors.push("UNVERIFIED_CONTRACT — исходный код не верифицирован");
riskScore += 15;
}
if (!spenderInfo.isVerified && !spenderInfo.hasSourceCode) {
riskScore += 20; // дополнительный штраф за полную непрозрачность
}
}
return {
isSuspicious: riskScore >= 50,
riskScore: Math.min(100, riskScore),
riskFactors,
simulatedStateChanges: simulation.stateChanges,
spenderInfo: approvals[0]?.spender
? await analyzeSpender(approvals[0].spender, chainId)
: null
};
}
База данных известных spender адресов
// Whitelist известных dApp контрактов
const KNOWN_SAFE_SPENDERS: Record<number, Set<string>> = {
1: new Set([ // Ethereum mainnet
"0x000000000022d473030f116ddee9f6b43ac78ba3", // Permit2 (Uniswap)
"0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45", // Uniswap Universal Router
"0x7a250d5630b4cf539739df2c5dacb4c659f2488d", // Uniswap V2 Router
"0xe592427a0aece92de3edee1f18e0157c05861564", // Uniswap V3 Router
"0x1111111254eeb25477b68fb85ed929f73a960582", // 1inch V5
"0x00000000219ab540356cbb839cbe05303d7705fa", // ETH2 Deposit Contract
]),
// Arbitrum, Base, Polygon...
};
// Blacklist известных scam адресов
// Источники: Forta, Chainabuse, Revoke.cash, MobyMask
const KNOWN_SCAM_SPENDERS: Record<number, Set<string>> = {
1: new Set([
// Обновляется из Forta feeds и community reports
])
};
async function analyzeSpender(
spenderAddress: string,
chainId: number
): Promise<SpenderInfo> {
const normalizedAddress = spenderAddress.toLowerCase();
// Проверка whitelist
const isVerified = KNOWN_SAFE_SPENDERS[chainId]?.has(normalizedAddress) ?? false;
// Проверка blacklist
const blacklisted = KNOWN_SCAM_SPENDERS[chainId]?.has(normalizedAddress) ?? false;
// Проверка возраста контракта
const deployBlock = await getContractDeployBlock(spenderAddress, chainId);
const currentBlock = await getCurrentBlock(chainId);
const blockAge = currentBlock - deployBlock;
const isNewContract = blockAge < 200_000; // ~30 дней на Ethereum
// Проверка верифицированности на Etherscan
const hasSourceCode = await checkEtherscanVerification(spenderAddress, chainId);
return {
address: spenderAddress,
isVerified,
blacklisted,
isNewContract,
hasSourceCode
};
}
Мониторинг выданных approvals
Indexer одобренных апрувалов
interface ApprovalRecord {
owner: string;
spender: string;
tokenAddress: string;
tokenType: "ERC20" | "ERC721" | "ERC1155";
amount: bigint | "unlimited" | "all"; // для ERC-20 / setApprovalForAll
blockNumber: number;
txHash: string;
timestamp: number;
revoked: boolean;
}
// Слушаем Approval и ApprovalForAll события
async function indexApprovals(
provider: ethers.Provider,
userAddress: string
): Promise<ApprovalRecord[]> {
// ERC-20 Approval(owner, spender, value)
const erc20ApprovalFilter = {
topics: [
ethers.id("Approval(address,address,uint256)"),
ethers.zeroPadValue(userAddress, 32) // owner = userAddress
]
};
// ERC-721/1155 ApprovalForAll(owner, operator, approved)
const approvalForAllFilter = {
topics: [
ethers.id("ApprovalForAll(address,address,bool)"),
ethers.zeroPadValue(userAddress, 32)
]
};
const [erc20Logs, nftLogs] = await Promise.all([
provider.getLogs({ ...erc20ApprovalFilter, fromBlock: 0, toBlock: "latest" }),
provider.getLogs({ ...approvalForAllFilter, fromBlock: 0, toBlock: "latest" })
]);
return parseApprovalLogs([...erc20Logs, ...nftLogs]);
}
Алерт на использование approval
Когда выданный approval используется — это не всегда плохо (легитимный dApp). Алерт должен срабатывать на аномальное использование:
async function monitorApprovalUsage(
approval: ApprovalRecord,
provider: ethers.Provider
): Promise<void> {
const transferFilter = {
address: approval.tokenAddress,
topics: [
ethers.id("Transfer(address,address,uint256)"),
ethers.zeroPadValue(approval.owner, 32) // from = owner
]
};
// Подписываемся на Transfer события от owner через spender
provider.on(transferFilter, async (log) => {
const tx = await provider.getTransaction(log.transactionHash);
// Транзакцию отправил spender (не owner) — это использование approval
if (tx.from.toLowerCase() === approval.spender.toLowerCase()) {
const transferAmount = BigInt(log.data);
await sendAlert({
type: "APPROVAL_USED",
severity: "HIGH",
owner: approval.owner,
spender: approval.spender,
amount: transferAmount,
txHash: log.transactionHash,
message: `Ваш апрувал используется! Контракт ${approval.spender} ` +
`переводит ${formatAmount(transferAmount)} токенов с вашего адреса.`
});
}
});
}
Permit2 специфика
Uniswap Permit2 — новый стандарт, где пользователь делает один approve(permit2, unlimited) для контракта Permit2, а дальше все протоколы работают через него с signature-based разрешениями. Система должна понимать этот паттерн:
// Permit2 TransferFrom имеет другой ABI
const PERMIT2_TRANSFER_FROM_SELECTOR = "0x36c78516";
function isPermit2Transfer(calldata: string): boolean {
return calldata.startsWith(PERMIT2_TRANSFER_FROM_SELECTOR);
}
// Декодируем Permit2 транзакцию для понимания реального спендера
function decodePermit2Transfer(calldata: string): {
token: string;
from: string;
to: string;
amount: bigint;
} {
const iface = new ethers.Interface(PERMIT2_ABI);
const decoded = iface.decodeFunctionData("transferFrom", calldata);
return {
token: decoded.token,
from: decoded.from,
to: decoded.to,
amount: decoded.amount
};
}
Интеграция в кошелёк или dApp
Система предупреждений встраивается через wallet_watchAsset или как browser extension, перехватывающий eth_sendTransaction:
| Канал | Применение | Задержка |
|---|---|---|
| Wallet provider hook | Pre-sign screening в MetaMask Snaps | < 1 сек |
| Browser extension | Скрининг всех dApp транзакций | < 2 сек |
| dApp UI интеграция | SDK на стороне dApp | < 1 сек |
| Email/Telegram алерт | Post-approval monitoring | Real-time |
MetaMask Snaps — наиболее перспективный путь для интеграции transaction insights. Snap получает доступ к onTransaction хуку и может показать пользователю предупреждение до подписания.







