Skip to main content
ClaudeWave
Skill510 repo starsupdated today

Approval Audit

Approval Audit identifies all active ERC-20 token approvals granted by a wallet on Base blockchain and highlights unlimited or risky spender grants. It operates keyless using only Base RPC calls to scan Approval event logs and confirm current allowances, answering the critical security question of what contracts can drain the wallet. Use this skill when auditing wallet security, detecting forgotten unlimited approvals, or tracking approval changes over time.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/aaronjmars/aeon /tmp/approval-audit && cp -r /tmp/approval-audit/skills/approval-audit ~/.claude/skills/approval-audit
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

> **${var}** — Wallet address (`0x...`) on Base to audit. Required. If empty, log `APPROVAL_AUDIT_NO_TARGET` and 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`, explorer `basescan.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

```bash
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.

```bash
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`):

```bash
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 `allowance` read — never list a grant the wallet has already revoked.
- `UNLIMITED` means 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.