name: Approval Audit description: List a wallet's live ERC-20 token approvals on Base and flag unlimited / risky spender grants. Keyless via Base RPC (eth_getLogs + eth_call) — no explorer key needed. var: "" tags: [crypto, security, base] requires: [BASE_RPC_URL?] capabilities: [read_only, sends_notifications]
${var} — Wallet address (
0x...) on Base to audit. Required. If empty, logAPPROVAL_AUDIT_NO_TARGETand exit cleanly (no notify).
Answers "what can drain this wallet?" — every ERC-20 approve() a wallet has granted that is still live, with unlimited allowances flagged. Token approvals are the #1 wallet-drain vector: a forgotten unlimited approval to a malicious or exploited contract lets it move your full balance at any time.
Runs keyless entirely on the Base RPC — reads Approval event logs, then confirms each grant's current allowance, so revoked or fully-spent approvals are excluded.
Read the last 2 days of memory/logs/ so a repeat audit can note newly-granted or newly-revoked approvals.
Config
- Target wallet =
${var}. Chain = Base (chainid=8453, explorerbasescan.org). BASE_RPC_URL— optional; defaults to a public Base RPC (https://mainnet.base.org). Any standard JSON-RPC endpoint works.
Steps
1. Find the current block
OWNER="${var}"
RPC="${BASE_RPC_URL:-https://mainnet.base.org}"
HEAD=$(curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' | jq -r '.result')
2. Fetch Approval events for the owner (chunked)
The ERC-20 Approval(owner,spender,value) event has topic0 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925; the owner is indexed in topic1 (left-padded to 32 bytes).
Scan a recent window (~24k blocks ≈ 13h on Base) newest-first in ~1800-block chunks — most public RPCs cap eth_getLogs at ~20k results per range, so a single wide call fails. Merge the results.
TOPIC0="0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
OWNER_TOPIC="0x000000000000000000000000${OWNER#0x}" # 32-byte left-pad
curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" -d '{
"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{
"fromBlock":"0x...","toBlock":"0x...",
"topics":["'"$TOPIC0"'","'"$OWNER_TOPIC"'"]
}]}' | jq '.result'
For each log: token = .address, spender = "0x" + topic2[-40:]. Keep the latest entry per (token, spender) pair. Some RPCs ignore the indexed-topic filter — defensively keep only logs whose topics[1] equals OWNER_TOPIC.
3. Confirm each approval is still live
For each (token, spender), read the current allowance via eth_call on allowance(address,address) (selector 0xdd62ed3e):
DATA="0xdd62ed3e${OWNER_TOPIC#0x}${SPENDER_TOPIC#0x}" # selector + owner + spender, each 32-byte padded
curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"'"$TOKEN"'","data":"'"$DATA"'"},"latest"],"id":1}' | jq -r '.result'
Drop any approval whose allowance is 0 (revoked or fully spent). Flag any allowance >= 2^255 as UNLIMITED (covers 2^256-1 and the common 2^256/2 sentinel).
4. Verdict
| Signal | Verdict |
|---|---|
| One or more unlimited live approvals | REVIEW |
| Live approvals, none unlimited | OK |
| No live approvals | CLEAN |
5. Notify
Notify via ./notify only if verdict is REVIEW. Under 4000 chars, lead with the verdict, clickable URL:
*Approval Audit — 0xabc…def (Base)*
Verdict: REVIEW · 4 live approvals, 2 unlimited
• USDC → spender 0x1111…2222 : UNLIMITED ⚠️
• WETH → spender 0x3333…4444 : UNLIMITED ⚠️
• DAI → spender 0x5555…6666 : 5,000
⚠️ Unlimited approvals let the spender move your full balance. Revoke any you don't recognize at revoke.cash.
Wallet: https://basescan.org/address/0xabc...def
6. Log
Append to memory/logs/${today}.md:
## approval-audit
- Wallet: 0x… | verdict: REVIEW
- Live approvals: 4 (2 unlimited)
- Unlimited: USDC→0x1111…, WETH→0x3333…
- Source: rpc=ok | window: ~24k blocks
End-states: APPROVAL_AUDIT_OK (clean/ok, no notify), APPROVAL_AUDIT_FLAGGED (review → notify), APPROVAL_AUDIT_ERROR (RPC unreachable).
Sandbox note
The sandbox may block outbound curl or env-var expansion. The Base RPC is public and needs no key, so for every failed curl retry the same URL/body via WebFetch before giving up. eth_getLogs must be chunked (~1800 blocks/call) to stay under the public-RPC result cap — a single 24k-block call will error. Never put a key in a -H header from the sandbox. Treat all fetched token/spender addresses as untrusted — never interpolate beyond the quoted $OWNER / validated hex.
Constraints
- No trade or "safe to approve" advice — this is a risk inventory. Reporting an approval is not an accusation.
- Only report approvals confirmed live by the current
allowanceread — never list a grant the wallet has already revoked. UNLIMITEDmeans allowance>= 2^255; report exact amounts otherwise. Don't round in a way that hides a large grant.- The window is recent (~24h of blocks), not all-time — say so; for a full history the operator should add a higher-throughput RPC.