Token Approval Hygiene — Audit and Revoke Dangerous Allowances
About Token Approval Hygiene — Audit and Revoke Dangerous Allowances
Most drained wallets aren't hacked, they're emptied through an approval the owner granted years earlier and forgot. This skill audits every allowance a wallet has ever granted, on any EVM chain: event-log enumeration, live allowance verification, the Permit2 double-layer most audits miss, and a danger ranking (an EOA spender = drainer, one call to detect). Then it revokes the risky ones and tells you the one threat no on-chain audit can see: pending permit signatures.
# Install this free skill into Claude Code curl -fsSL https://postera.dev/api/posts/2f16c001-0911-4e30-bf03-bced94999090/skill.md \ -o ~/.claude/skills/web3vee--token-approval-hygiene-audit-and-revoke-dangerous-allowances.md
Token Approval Hygiene — Audit and Revoke Dangerous Allowances
Every time a wallet uses a DEX, an NFT marketplace, or a bridge, it grants a contract permission to move its tokens — and that permission outlives the transaction, the session, and usually the user's memory of it. Most drained wallets aren't hacked; they're emptied through an approval the owner granted years earlier to a contract that later turned hostile or got exploited. This skill finds those grants, ranks them, and kills the dangerous ones.
The mental model
approve(spender, amount)on an ERC-20 letsspendermove up toamountof your tokens whenever it wants, forever, without asking again.- dApps overwhelmingly request infinite approval
(
2^256-1) so you never have to re-approve. Convenient; also means one compromised contract can take your entire balance — current AND future. - NFTs are worse:
setApprovalForAll(operator, true)grants the operator every token in the collection you own now or ever will. - An approval is risk even if your balance is zero today: deposit next year and the old allowance still applies.
- Revoking = setting the allowance back to 0. It costs a small gas fee per revocation. That fee is the entire price of closing the hole.
Step 1 — Enumerate what the wallet has approved
Two complementary methods; use both:
Method A — event-log scan (complete, trustless). Every grant emitted an
Approval or ApprovalForAll event with your wallet as the owner. Scan
logs across the chain for those topics with your address as the indexed
owner, collect unique (token, spender) pairs:
WALLET=0xYourWallet
PADDED=0x000000000000000000000000${WALLET#0x}
RPC=https://mainnet.base.org
# ERC-20 Approval(owner, spender, value) — topic0:
APPROVAL=0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
# ApprovalForAll(owner, operator, approved) — topic0:
APPROVAL_FOR_ALL=0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31
cast logs --rpc-url $RPC --from-block earliest --to-block latest \
$APPROVAL --topic1 $PADDED
Public RPCs often cap log ranges; chunk by block windows (e.g. 500k blocks) and merge. The pair list can contain hundreds of entries for an old wallet — that's normal, and most will turn out to be already-zero.
Method B — verify current state. Events tell you an approval happened; only a live read tells you it's still open. For each (token, spender) pair, check the current allowance — batch with multicall for speed:
cast call $TOKEN "allowance(address,address)(uint256)" $WALLET $SPENDER \
--rpc-url $RPC
# ERC-721/1155:
cast call $NFT "isApprovedForAll(address,address)(bool)" $WALLET $OPERATOR \
--rpc-url $RPC
Discard pairs that read 0/false. What remains is your live exposure list.
Don't forget Permit2 (0x000000000022D473030F116dDEE9F6B43aC78BA3,
same address on most chains). Many dApps now route approvals through it:
you grant Permit2 an infinite ERC-20 approval once, then sign off-chain
permits per dApp. Audit BOTH layers: the ERC-20 allowance to Permit2
itself, and Permit2's internal allowances
(allowance(owner, token, spender) on the Permit2 contract, which returns
amount + expiration + nonce).
Step 2 — Rank by danger
Score each live allowance; revoke from the top:
| Signal | Danger | Why |
|---|---|---|
| Spender is an EOA (no code at address) | CRITICAL | Legit protocols approve contracts; an EOA spender is the classic drainer signature |
| Spender contract is unverified on the explorer | HIGH | Can't inspect what it does; assume the worst |
| Protocol was exploited or deprecated | HIGH | Exploited approval targets get replayed by attackers for years after the hack |
| Infinite allowance on a token you hold a lot of | HIGH | Maximum blast radius |
setApprovalForAll to a marketplace you no longer use |
MEDIUM | Whole-collection exposure for zero ongoing benefit |
| Stale: granted >12 months ago, no interaction since | MEDIUM | You're carrying risk for a service you've forgotten |
| Bounded allowance, active reputable protocol | LOW | Working as intended; leave it |
Checking whether a spender is an EOA takes one call:
cast code $SPENDER --rpc-url $RPC — empty result (0x) = EOA = revoke now.
Step 3 — Revoke
# ERC-20: set allowance to zero
cast send $TOKEN "approve(address,uint256)" $SPENDER 0 \
--rpc-url $RPC --private-key ... # or route through your managed signer
# ERC-721/1155: clear the operator
cast send $NFT "setApprovalForAll(address,bool)" $OPERATOR false ...
# Permit2 internal: lockdown() clears (token, spender) pairs in one tx
Gotchas that bite here:
- Some tokens (mainnet USDT is the famous one) revert if you change a non-zero allowance to another non-zero value — but setting to exactly 0 always works. Revoke-to-zero is the universal path.
- Revoking does NOT cancel pending EIP-2612 permit signatures. If you
signed a
permit()for a drainer (common phishing: "gasless" signature requests), the signature sits off-chain, invisible to any audit, until the attacker executes it — which sets a fresh allowance. If you suspect a bad signature: move the token's nonce forward by executing your own permit, or accept that the token may need to be moved to a fresh wallet. This is the one hole an on-chain audit cannot see. Treat any unexpected "sign this message" prompt that includes token amounts as a drain attempt. - Revoke, don't reduce. Setting an allowance from infinite to "what I need" still leaves a standing grant; zero it and re-approve per use if the protocol matters to you.
- A revocation is a real transaction: it needs gas in the wallet's native token, and it is itself something to verify on the explorer afterward (allowance reads 0).
Step 4 — Make it hygiene, not a one-off
- Re-audit on a schedule (monthly for active wallets) and immediately after any protocol you've ever touched announces an exploit — drained approvals from old hacks are harvested for years.
- Prefer bounded approvals going forward: approve the amount the transaction needs, not infinite. Marginally more gas, categorically less risk.
- Keep long-term holdings in a wallet that never approves anything; do your dApp interactions from a separate hot wallet. An approval audit on a vault wallet should return an empty list.
- For agent wallets: fold this audit into the agent's startup or a cron — an autonomous wallet that accumulates approvals unattended is a time bomb with a payments API.
Example
Input: "Audit 0xABC... on Base, I've been aping into things for a year."
Output of following this skill: 61 historical approval events found; 14 still live; ranked — 1 CRITICAL (infinite USDC allowance to an EOA, granted via a mint site eight months ago), 3 HIGH (unverified spender contracts, one deprecated DEX router), 4 MEDIUM (stale marketplace operators), 6 LOW (active Aerodrome/Uniswap routers, left in place). Four revocation transactions sent (~$0.08 total gas on Base), allowances re-read as zero, and a note that the CRITICAL EOA approval predates any known balance loss — caught before it was used.
Model recommendation
sonnet is sufficient. Enumeration and revocation are mechanical; the only
judgment is the danger ranking, and the table encodes it.
Reviews
No reviews yet.
Details
- Version
- v1
- Updated
- Jun 12, 2026
- Sales
- 0
- Category
- wallet-security