Suspicious Approval Warning System
Token approval—one of most dangerous Web3 operations. User gives smart contract right to spend tokens without participation. Approval scam—main asset theft method via phishing and malicious dApp. Revoke.cash recorded $2.8B+ losses via approval-based attacks. Warning system detects suspicious approve requests in real time—before transaction confirmation.
Anatomy of Approval Attack
ERC-20 approve: token.approve(spender, amount) allows spender to transfer up to amount tokens.
ERC-721/ERC-1155 setApprovalForAll: grants right to all NFTs in collection. Most dangerous—one tx grants rights to entire collection.
Permit (EIP-2612): gasless signature approval without on-chain tx. User signs message—attacker sends permit() tx. User doesn't see approval in wallet until theft.
System Architecture
Two-level system:
Pre-transaction screening: Analyze pending tx before signing. Integrates via simulation API.
Post-approval monitoring: Track approved tokens, alert on anomalous usage.
Pre-Transaction Analysis
Simulation via Tenderly or Alchemy
Before signing, simulate execution and analyze state changes:
interface ApprovalAnalysis {
isSuspicious: boolean;
riskScore: number; // 0-100
riskFactors: string[];
simulatedStateChanges: StateChange[];
spenderInfo: SpenderInfo;
}
async function analyzeApproval(txParams: TransactionParams): Promise<ApprovalAnalysis> {
const riskFactors = [];
let riskScore = 0;
// 1. Simulate transaction
const simulation = await simulateTransaction(txParams);
// Extract approvals from simulation
const approvals = extractApprovals(simulation.stateChanges);
for (const approval of approvals) {
// 2. Check approval type
if (approval.type === "setApprovalForAll") {
riskFactors.push("SET_APPROVAL_FOR_ALL—grants rights to all NFTs");
riskScore += 40;
}
if (approval.amount === MaxUint256) {
riskFactors.push("UNLIMITED_APPROVAL—unlimited token access");
riskScore += 20;
}
// 3. Analyze spender contract
const spenderInfo = await analyzeSpender(approval.spender);
if (spenderInfo.blacklisted) {
riskFactors.push("KNOWN_SCAMMER—address in blacklist");
riskScore += 60;
}
if (spenderInfo.isNewContract) {
riskFactors.push("NEW_CONTRACT—deployed < 30 days ago");
riskScore += 25;
}
if (!spenderInfo.hasSourceCode) {
riskFactors.push("UNVERIFIED_CONTRACT—source code not verified");
riskScore += 15;
}
}
return {
isSuspicious: riskScore >= 50,
riskScore: Math.min(100, riskScore),
riskFactors,
simulatedStateChanges: simulation.stateChanges,
spenderInfo: approvals[0]?.spender ? await analyzeSpender(approvals[0].spender) : null
};
}
Known Spender Database
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
]),
};
const KNOWN_SCAM_SPENDERS: Record<number, Set<string>> = {
1: new Set([
// Updated from Forta, Chainabuse, Revoke.cash, MobyMask
])
};
async function analyzeSpender(spenderAddress: string): Promise<SpenderInfo> {
const normalized = spenderAddress.toLowerCase();
const isVerified = KNOWN_SAFE_SPENDERS[chainId]?.has(normalized) ?? false;
const blacklisted = KNOWN_SCAM_SPENDERS[chainId]?.has(normalized) ?? false;
const deployBlock = await getContractDeployBlock(spenderAddress);
const blockAge = currentBlock - deployBlock;
const isNewContract = blockAge < 200_000; // ~30 days on Ethereum
const hasSourceCode = await checkEtherscanVerification(spenderAddress);
return { address: spenderAddress, isVerified, blacklisted, isNewContract, hasSourceCode };
}
Post-Approval Monitoring
Approval Indexer
Listen to Approval and ApprovalForAll events:
async function indexApprovals(provider: ethers.Provider, userAddress: string) {
const erc20ApprovalFilter = {
topics: [
ethers.id("Approval(address,address,uint256)"),
ethers.zeroPadValue(userAddress, 32) // owner = userAddress
]
};
const [erc20Logs, nftLogs] = await Promise.all([
provider.getLogs({ ...erc20ApprovalFilter, fromBlock: 0, toBlock: "latest" }),
provider.getLogs({ ...approvalForAllFilter, fromBlock: 0, toBlock: "latest" })
]);
return parseApprovalLogs([...erc20Logs, ...nftLogs]);
}
Alert on Approval Usage
When approval is used—not always bad (legitimate dApp). Alert on anomalous usage:
async function monitorApprovalUsage(approval: ApprovalRecord, provider: ethers.Provider) {
const transferFilter = {
address: approval.tokenAddress,
topics: [
ethers.id("Transfer(address,address,uint256)"),
ethers.zeroPadValue(approval.owner, 32) // from = owner
]
};
provider.on(transferFilter, async (log) => {
const tx = await provider.getTransaction(log.transactionHash);
// Tx sent by spender (not owner)—approval usage
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,
message: `Your approval is being used! Contract ${approval.spender} transfers ${transferAmount} tokens.`
});
}
});
}
Permit2 Specifics
Uniswap Permit2—new standard where user does one approve(permit2, unlimited) for Permit2 contract, then all protocols work via signature-based permissions. System must understand this pattern:
const PERMIT2_TRANSFER_FROM_SELECTOR = "0x36c78516";
function isPermit2Transfer(calldata: string): boolean {
return calldata.startsWith(PERMIT2_TRANSFER_FROM_SELECTOR);
}
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 decoded;
}
Integration into Wallet or dApp
Warning system integrates via MetaMask Snaps (pre-sign screening), browser extension (intercept dApp txs), or SDK in dApp.
| Channel | Use Case | Latency |
|---|---|---|
| Wallet provider hook | MetaMask Snaps pre-sign | < 1 sec |
| Browser extension | All dApp txs screening | < 2 sec |
| dApp UI SDK | Protocol-level integration | < 1 sec |
| Email/Telegram | Post-approval monitoring | Real-time |
MetaMask Snaps most promising path—Snap gets onTransaction hook, show warning to user before signing.







