Byaigo

The Hidden Risk in Your Wallet

Every time you swap tokens on Uniswap or deposit into a DeFi protocol, you grant ERC-20 token allowances — permissions that let smart contracts spend your tokens. Most users accumulate dozens of these approvals over time, many with unlimited amounts, creating a silent attack surface. A compromised contract with an active allowance can drain your wallet without needing your private key.

In this article, well build an automated allowance scanner with Python and Web3.py that audits every approval across your address, identifies risky ones, and generates revocation transactions.

What Were Building

Our tool will:

  • Query all ERC-20 Approval events for a given address using Etherscans API
  • Analyze each allowance: spender, token, amount, and remaining balance
  • Flag unlimited approvals (type(uint256).max) and approvals to unverified contracts
  • Generate a summary report with severity ratings

Prerequisites

pip install web3 requests eth-account

Enter fullscreen mode Exit fullscreen mode

Step 1: Fetching Approval Events

We query the Etherscan API for Approval events across all ERC-20 tokens:

import requests
from web3 import Web3

ETHERSCAN_API_KEY = "your_key_here"
ADDRESS = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb5"

def fetch_approval_events(address, api_key, start_block=0):
    url = "https://api.etherscan.io/api"
    params = {
        "module": "account",
        "action": "tokentx",  # Well use logs for more precision
        "address": address,
        "apikey": api_key,
        "sort": "desc"
    }
    # In practice, use /api?module=logs&action=getLogs
    # with topic0 = Approval(address,address,uint256)
    pass

Enter fullscreen mode Exit fullscreen mode

Step 2: Parsing Allowance Data

For each Approval event, we decode the spender, amount, and token contract. The real power is classifying them:

def classify_allowance(token_address, spender, amount, w3):
    MAX_UINT256 = 2**256 - 1
    risk_level = "low"
    reasons = []

    if amount == MAX_UINT256:
        risk_level = "high"
        reasons.append("Unlimited approval")

    # Check if spender is a verified contract
    spender_code = w3.eth.get_code(Web3.to_checksum_address(spender))
    if spender_code == b"":
        risk_level = "critical"
        reasons.append("Spender is an EOA, not a contract")

    token_symbol = get_token_symbol(token_address, w3)
    readable_amount = w3.from_wei(amount, "ether") if amount != MAX_UINT256 else ""

    return {
        "token": token_symbol,
        "token_address": token_address,
        "spender": spender,
        "amount": str(readable_amount),
        "risk": risk_level,
        "reasons": reasons
    }

Enter fullscreen mode Exit fullscreen mode

Step 3: Building the Revoke Function

Once we identify risky allowances, we can generate a revocation transaction:

def build_revoke_tx(token_address, spender, wallet_address, w3):
    erc20_abi = [
        {"constant": False, "inputs": [
            {"name": "_spender", "type": "address"},
            {"name": "_value", "type": "uint256"}
        ], "name": "approve", "outputs": [{"name": "", "type": "bool"}],
        "type": "function"}
    ]
    contract = w3.eth.contract(
        address=Web3.to_checksum_address(token_address),
        abi=erc20_abi
    )
    tx = contract.functions.approve(
        Web3.to_checksum_address(spender), 0
    ).build_transaction({
        "from": Web3.to_checksum_address(wallet_address),
        "nonce": w3.eth.get_transaction_count(wallet_address),
        "gas": 50000,
        "gasPrice": w3.eth.gas_price
    })
    return tx

Enter fullscreen mode Exit fullscreen mode

The Full Scanner in Action

Heres the complete script that ties it together. It scans all approvals, produces a ranked risk report, and saves the results:

def scan_allowances(address, etherscan_key, w3):
    approvals = fetch_approval_logs(address, etherscan_key)
    report = []

    for approval in approvals:
        entry = classify_allowance(
            approval["token"],
            approval["spender"],
            approval["value"],
            w3
        )
        report.append(entry)

    # Sort by risk: critical > high > medium > low
    risk_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
    report.sort(key=lambda x: risk_order.get(x["risk"], 99))

    critical_count = sum(1 for r in report if r["risk"] == "critical")
    high_count = sum(1 for r in report if r["risk"] == "high")

    print(f"Found {len(report)} approvals")
    print(f"  🔴 Critical: {critical_count}")
    print(f"  🟠 High: {high_count}")

    for entry in report:
        flag = "🔴" if entry["risk"] == "critical" else "🟠" if entry["risk"] == "high" else "🟡" if entry["risk"] == "medium" else "🟢"
        reasons = ", ".join(entry["reasons"]) if entry["reasons"] else "No issues"
        print(f"{flag} {entry["token"]} → Spender {entry["spender"][:10]}... [{entry["risk"]}] {reasons}")

    return report

Enter fullscreen mode Exit fullscreen mode

Why This Matters

The average DeFi user has 15–30 active token approvals, many unlimited. High-profile exploits like the Multichain hack and various phishing campaigns exploit exactly this vector. A tool like this gives you visibility and control before an incident happens — not after.

Going Further

  • Add multi-chain support: Ethereum, BSC, Polygon, Arbitrum, and Optimism
  • Integrate real-time monitoring: Alert on new approvals via WebSocket subscriptions
  • Build a web dashboard: Streamlit or Flask frontend with one-click revoke
  • Add EIP-2612 permit detection: Gasless approvals that expire automatically
  • Aggregate known spender labels: Match spenders against known protocol databases (Uniswap, Aave, etc.)

The full source is available on github.com/Byaigo — clone it, run the scanner against your own addresses, and audit what youve approved over the years. You might be surprised.

If you found this useful, consider supporting:

  • ETH: 0x18da907cb9d981bc798acb87ac27b03a2dc3cbb7

Stay safe out there. 🛡️