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.