DeFi Safety Basics: Approval Management That Actually Works
Why start a DeFi safety basics series now
I’m kicking off a beginner-friendly series focused on the unglamorous habits that keep funds safe. Approval hygiene is the first pillar because it’s the easiest way to lose tokens without ever signing a swap. I’m doing this from a practice wallet capped under $1k, so everything here is sized for small, realistic balances. This post covers what an approval really does, how allowances accumulate, and a lightweight routine for revoking the ones you don’t need.
How ERC-20 approvals really behave on-chain
An approval writes an allowance mapping in the token contract: allowance[owner][spender] = amount. DEXs and routers typically request 2**256-1 to avoid repeated prompts. That single transaction lets the spender move tokens at any time until the allowance is changed. Approvals are token-specific and chain-specific—revoking USDC on mainnet does nothing for the same token on Arbitrum. Routers that cascade into other contracts (e.g., Uniswap > Permit2 > specific pool) create multiple active spenders, so checking only the last UI prompt misses earlier allowances.
Quick script to surface and revoke risky approvals
I keep a small Web3.py snippet to scan active allowances and revoke the outsized ones. It pulls spender addresses from past Approval events, filters to non-zero balances, and sends a zero-allowance transaction for anything above a threshold tuned for a sub-$1k wallet.
from web3 import Web3
from eth_account import Account
w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/<PROJECT_ID>"))
owner = Web3.to_checksum_address("0xYourWallet")
private_key = "0x..." # hardware wallet preferred
ERC20_ABI = [
{"anonymous": False, "inputs": [
{"indexed": True, "name": "owner", "type": "address"},
{"indexed": True, "name": "spender", "type": "address"},
{"indexed": False, "name": "value", "type": "uint256"}],
"name": "Approval", "type": "event"},
{"inputs": [
{"name": "owner", "type": "address"},
{"name": "spender", "type": "address"}],
"name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [
{"name": "spender", "type": "address"},
{"name": "value", "type": "uint256"}],
"name": "approve", "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"}
]
token_address = Web3.to_checksum_address("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") # USDC
contract = w3.eth.contract(address=token_address, abi=ERC20_ABI)
# Pull unique spenders from recent Approval logs
logs = contract.events.Approval.get_logs(from_block=w3.eth.block_number - 500_000, to_block="latest", argument_filters={"owner": owner})
spenders = {log["args"]["spender"] for log in logs}
# Identify risky allowances
risky = []
for spender in spenders:
current = contract.functions.allowance(owner, spender).call()
if current > 0 and current > 500 * 10**6: # >$500 USDC (cap for a sub-$1k wallet)
risky.append((spender, current))
if not risky:
print("No high-risk approvals found.")
else:
acct = Account.from_key(private_key)
for spender, _ in risky:
txn = contract.functions.approve(spender, 0).build_transaction({
"from": owner,
"nonce": w3.eth.get_transaction_count(owner),
"gas": 80_000,
"maxFeePerGas": w3.to_wei("35", "gwei"),
"maxPriorityFeePerGas": w3.to_wei("1", "gwei"),
"chainId": 1,
})
signed = acct.sign_transaction(txn)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
print(f"Revocation sent for {spender}: {tx_hash.hex()}")
I run it weekly on mainnet and Arbitrum. The gas ceiling keeps revokes under control; USDC’s approve cost averaged 31–35k gas in recent runs. If you use a hardware wallet, swap the local private key for an external signer and bump gas caps during congestion.
Risk analysis: where approvals still bite
- Router upgrades: A protocol can redeploy a router and leave the old address live; attackers target the legacy contract once it holds approvals.
- Airdrop scams: Fake tokens can still be approved; some swap UIs auto-add them. Always verify the token address before approving.
- Permit2 interactions: Signatures via Permit2 look safer but still authorize spends; treat them like standard approvals and revoke when done.
- NFT marketplaces: Allowances for ERC-721/1155 listings persist; a drain contract only needs one approved operator.
Practical takeaways for the new series
- Keep a recurring calendar reminder to revoke approvals on every chain you use.
- Cap approvals to realistic trade sizes when the UI allows; unlimited is rarely necessary.
- If your working wallet is <$1k, keep per-spender allowances in the low hundreds so a single compromise can’t drain the whole balance.
- Prefer routers with clear spender addresses and published upgrade paths.
- Save explorer links for your most-used tokens so you can revoke even if a dApp UI disappears.
This is the first entry in the DeFi Safety Basics series. I checked the last five posts to confirm we hadn’t promised a different beginner track, so next up is spotting reentrancy risk patterns before signing anything.