Dev.to Security 🔐 Cybersecurity 👁 0 📖 3 min read

Auditing Permit2 Allowances with Python: A Practical Walkthrough

Wallet audits sound like an enterprise security product, but the core of one is a script you can run in a few minutes: enumerate what your address has permitted, through which contracts, and at what limits. This walkthro

Wallet audits sound like an enterprise security product, but the core of one is a script you can run in a few minutes: enumerate what your address has permitted, through which contracts, and at what limits. This walkthrough covers the Permit2 layer — the approval hub most DEX users have approved at least once — plus a delegation check for EIP-7702, the 2026 drain vector.

The conceptual background (why Permit2 centralizes risk and how 7702 delegation abuse works) is in two research posts: https://brainresearchhub.net/research/security/eip-7702-vs-permit2-comparison and https://brainresearchhub.net/research/security/revoke-approvals-guide

What we are checking

  1. Permit2 allowances. The Permit2 contract (0x000000000022D473030F116dDEE9F6B43aC78BA3) stores per-token, per-spender allowances for every user who approved it. The canonical way to enumerate them is to scan Approval events emitted by Permit2 where owner = your address, then re-read current amounts (events show the last write, not the current state).
  2. EIP-7702 delegation. An EOA under 7702 delegation has runtime code starting with the 0xef01 designator. eth_getCode returning anything for a "normal" wallet is a red flag unless you set the delegation yourself.

The script

Read-only. Works against any JSON-RPC endpoint you trust (your own node or a reputable provider).

"""
permit2_audit.py - read-only Permit2 allowance + EIP-7702 delegation audit.
Usage: python permit2_audit.py 0xYourAddress [--rpc https://your.node]
Deps: pip install requests
"""
import argparse, json
import requests

PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
APPROVAL_TOPIC = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

rpc_id = 0

def rpc(url, method, params):
    global rpc_id
    rpc_id += 1
    r = requests.post(url, json={"jsonrpc": "2.0", "id": rpc_id,
                                 "method": method, "params": params}, timeout=30)
    r.raise_for_status()
    out = r.json()
    if "error" in out:
        raise RuntimeError(out["error"])
    return out["result"]

def topic_addr(t):        # last 20 bytes of a 32-byte topic
    return "0x" + t[-40:]

def check_delegation(rpc_url, addr):
    code = rpc(rpc_url, "eth_getCode", [addr, "latest"])
    if code in ("0x", "", None):
        print("[OK]   no EIP-7702 delegation (EOA has no code)")
    else:
        prefix = code[:10]
        marker = "DELEGATED" if prefix.startswith("0xef01") else "HAS-CODE"
        print(f"[!!]   {marker}: code prefix {prefix}, size {len(code)//2 - 1} bytes")
        print("       -> inspect the delegation target NOW if you did not set this up")

def permit2_allowances(rpc_url, addr, from_block="0x0", max_logs=200):
    pad = "0" * 24
    params = {
        "fromBlock": from_block, "toBlock": "latest",
        "address": PERMIT2,
        "topics": [APPROVAL_TOPIC, None,
                   "0x" + pad + addr.lower().removeprefix("0x")],
    }
    logs = rpc(rpc_url, "eth_getLogs", [params])[-max_logs:]
    print(f"[i]    {len(logs)} Permit2 Approval events found")
    seen = {}
    for log in logs:
        token = log["address"]
        spender = topic_addr(log["topics"][2])
        seen[(token.lower(), spender.lower())] = log["blockNumber"]
    for (token, spender), block in sorted(seen.items(), key=lambda kv: -int(kv[1], 16)):
        print(f"       token {token} -> spender {spender} (last write at block {int(block, 16)})")
    print("[i]    re-read current amounts via eth_call to Permit2.allowance()")
    print("       near-unlimited + unused = revoke candidate")

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("address")
    ap.add_argument("--rpc", default="http://127.0.0.1:8545")
    ap.add_argument("--from-block", default="0x0")
    a = ap.parse_args()
    addr = a.address if a.address.startswith("0x") else "0x" + a.address
    check_delegation(a.rpc, addr)
    permit2_allowances(a.rpc, addr, a.from_block)

if __name__ == "__main__":
    main()

Reading the output

  • A delegation you did not set is the highest-severity finding. The sweeper patterns we profiled move funds on schedules; time matters more than tidy reporting.
  • Permit2 events with near-unlimited amounts to spenders you do not recognize are revocation candidates. Event logs only show the last write per (token, spender), so always re-read the current allowance before deciding.
  • Print your own address in the spender column: that is normal (internal permit flows).

The Polygon side of this matters for prediction-market users specifically — approvals you signed there are live there, regardless of what your mainnet wallet shows. General scam-pattern catalog for 2026: https://brainresearchhub.net/research/security/scam-catalog-2026

For neutral background: Ethereum's own intro at https://ethereum.org/en/what-is-ethereum/ and the Wikipedia overview https://en.wikipedia.org/wiki/Ethereum

Educational content only, not security advice. Read-only script, but verify everything against official documentation before acting.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.