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
- 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).
-
EIP-7702 delegation. An EOA under 7702 delegation has runtime code starting with the 0xef01 designator.
eth_getCodereturning 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.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.