# safe-hunt

Sweeps DeFi protocol Safe multisig wallets for governance misconfigurations and security weaknesses. Given a protocol name, Safe address, or "sweep all", fetches live config and tx history from the Safe Transaction Service API, scores each Safe against a finding pattern library, and produces an audit-ready ranked report. Use when auditing a protocol's admin controls, hunting for misconfigured multisigs, or generating governance risk leads across DeFi.

- **Kind:** skill
- **Source:** https://github.com/forefy/.context
- **Page:** https://forefy.com/skills/41b5bdb6-c8e9-439c-9511-7832e1001eb6
- **API (JSON + files):** https://forefy.com/api/skills/41b5bdb6-c8e9-439c-9511-7832e1001eb6

---

## SKILL.md

---
name: safe-hunt
description: Sweeps DeFi protocol Safe multisig wallets for governance misconfigurations and security weaknesses. Given a protocol name, Safe address, or "sweep all", fetches live config and tx history from the Safe Transaction Service API, scores each Safe against a finding pattern library, and produces an audit-ready ranked report. Use when auditing a protocol's admin controls, hunting for misconfigured multisigs, or generating governance risk leads across DeFi.
---

# Safe Governance Hunt Skill

## Identity

Protocol governance security analyst. Systematically assess Safe multisig configurations across DeFi protocols using live on-chain data via the Safe Transaction Service API. No auth required. Read-only - never send transactions, never modify state. Findings are ranked by severity and formatted as audit-ready report items. Ambiguous findings → document with evidence and flag for manual review. Never conclude "exploitable" without on-chain confirmation.

---

## Reference Files

Load on demand:

| File                                | Load When                                                                   |
| ----------------------------------- | --------------------------------------------------------------------------- |
| `references/safe-api.md`            | Making any Safe API call - endpoints, base URLs, response shapes            |
| `references/finding-patterns.md`    | Scoring any Safe - full criteria, severity thresholds, audit language       |
| `references/defillama-discovery.md` | Mode = `sweep` or `protocol` - how to extract Safe addresses from DeFiLlama |

---

## Modes

### Mode 1: Targeted

Input: one or more Safe addresses + optional network (default: ethereum).
→ Deep audit of each Safe: config + 50 most recent txs + full finding set.

### Mode 2: Protocol

Input: protocol name (e.g. "Aave", "Compound").
→ Look up protocol in DeFiLlama treasury adapters → extract Safe addresses → run targeted audit on each.

### Mode 3: Sweep

Input: "sweep all" or no address given.
→ Parse all ~292 DeFiLlama treasury adapters → extract all Safe addresses → run targeted audit on each → output ranked leaderboard by risk score.

---

## Engagement Protocol

**Step 0 - Determine mode:**

- Address(es) provided → Mode 1 (Targeted)
- Protocol name provided → Mode 2 (Protocol) - load `references/defillama-discovery.md`
- "sweep", "all protocols", or no input → Mode 3 (Sweep) - load `references/defillama-discovery.md`
- If unclear, ask: "Do you want to audit a specific Safe address, a named protocol, or sweep all DeFiLlama protocols?"

**Step 1 - Discover addresses** (skip for Mode 1):

- Use `scripts/sweep.py` for Mode 2/3 - it handles DeFiLlama parsing, Safe API calls, scoring, and report generation.
- Run: `python3 ~/.claude/skills/safe-hunt/scripts/sweep.py [--protocol <name>] [--address <0x>] [--network <net>] [--output report.md]`
- If script unavailable, do manual discovery per `references/defillama-discovery.md`.

**Step 2 - Fetch Safe config + tx history** (for manual/targeted flow):
Load `references/safe-api.md` for exact endpoints. Fetch:

1. `GET /api/v1/safes/{address}/` → config
2. `GET /api/v1/safes/{address}/multisig-transactions/?limit=50&ordering=-nonce` → recent txs

**Step 3 - Score findings:**
Load `references/finding-patterns.md`. Evaluate every pattern against the fetched data.

**Step 4 - Report:**
Output per finding-patterns.md report format. Always include: Safe address, network, protocol name, risk score, findings sorted by severity, evidence, audit language.

---

## Script Usage

```bash
# Audit one Safe
python3 ~/.claude/skills/safe-hunt/scripts/sweep.py --address 0xABC... --network ethereum

# Audit a protocol by name (DeFiLlama lookup)
python3 ~/.claude/skills/safe-hunt/scripts/sweep.py --protocol "Lido"

# Full DeFiLlama sweep (slow - ~292 protocols)
python3 ~/.claude/skills/safe-hunt/scripts/sweep.py --sweep --output sweep_report.md

# Sweep with network filter
python3 ~/.claude/skills/safe-hunt/scripts/sweep.py --sweep --network ethereum --top 20
```

---

## Report Format

```
# Safe Governance Hunt Report
Generated: <date> | Mode: <targeted|protocol|sweep> | Network: <network>

## 🔴 CRITICAL / HIGH RISK

### [Protocol Name] - 0xSafeAddress (ethereum)
Risk Score: 85/100
Owners: 3 | Threshold: 1/3 | Version: 1.2.0 | Guard: None | Modules: 1 unknown

**[HIGH] Single-signer threshold (1/3)**
Any single owner can execute transactions unilaterally without consensus.
Evidence: threshold=1, owners=3
Audit language: "The Safe multisig securing [protocol] admin functions is configured with a 1-of-3 threshold, effectively granting any single signer unilateral control over privileged operations including [X]. This constitutes a critical centralization risk."

**[HIGH] Unknown module enabled**
Module 0x1234... has unrestricted access to all Safe assets and is not in the known-safe module registry.
Evidence: module=0x1234..., not in whitelist
Audit language: "An unverified module (0x1234...) is enabled on the Safe, granting it the ability to execute arbitrary transactions. If compromised or malicious, this module bypasses the multisig threshold entirely."

---

## 🟠 MEDIUM RISK
...

## 🟡 LOW RISK / INFORMATIONAL
...

## ✅ CLEAN SAFES
The following Safes had no findings:
- [Protocol] 0xABC... (ethereum) - Score: 0
```

---

## Scope

- Only read public on-chain data - Safe API is fully public, no credentials required
- Do not send transactions, trigger signatures, or interact with Safe UI

## references

```

```

## references/defillama-discovery.md

# DeFiLlama Discovery Reference

## Overview

DeFiLlama maintains treasury adapters for ~292 protocols in the DefiLlama-Adapters GitHub repo.
Each adapter JS file contains the protocol's treasury/governance Safe addresses in `owners:` arrays.

---

## GitHub URL Pattern

```
https://raw.githubusercontent.com/DefiLlama/DefiLlama-Adapters/main/projects/treasury/{slug}.js
```

Where `{slug}` is the `treasury` field value from the DeFiLlama protocols API (without `.js` extension if it already has it).

### Get all protocol treasury slugs
```bash
curl -sL "https://api.llama.fi/protocols" | python3 -c "
import json, sys
data = json.load(sys.stdin)
slugs = [(p['name'], p['treasury']) for p in data if p.get('treasury')]
for name, slug in slugs:
    print(f'{name}\t{slug}')
"
```

---

## Address Extraction

Treasury adapter files follow several patterns. Extract ALL Ethereum addresses (`0x[0-9a-fA-F]{40}`) from the file, then validate each against the Safe API.

### Pattern 1: Named constant + owners array (most common)
```js
const LidoTreasury = "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c";
module.exports = treasuryExports({
  ethereum: {
    owners: [LidoTreasury],
  }
})
```

### Pattern 2: Direct address in owners array
```js
module.exports = treasuryExports({
  ethereum: {
    owners: ["0xb8e1f3b966af4Ca02F0A4c95F4e4C55Bd2E8e63"],
  }
})
```

### Pattern 3: Multiple networks
```js
module.exports = treasuryExports({
  ethereum: { owners: ["0xABC..."] },
  polygon:  { owners: ["0xDEF..."] },
  arbitrum: { owners: ["0x123..."] },
})
```

### Pattern 4: Mixed owners and tokens (ignore token addresses)
```js
// tokens[] → contract addresses, NOT Safes - but still try against Safe API
// owners[] → likely Safe addresses
// The Safe API returns 404 for non-Safes, so safe to try all
```

---

## Extraction Script (inline)

```python
import re
import requests

ETH_ADDR_RE = re.compile(r'0x[0-9a-fA-F]{40}')
ZERO_ADDR = "0x0000000000000000000000000000000000000000"

def extract_addresses_from_adapter(slug):
    url = f"https://raw.githubusercontent.com/DefiLlama/DefiLlama-Adapters/main/projects/treasury/{slug}"
    r = requests.get(url, timeout=10)
    if r.status_code != 200:
        return []
    addresses = list(set(ETH_ADDR_RE.findall(r.text)))
    # Filter out zero address and known non-Safe addresses (factory, etc.)
    return [a for a in addresses if a.lower() != ZERO_ADDR.lower()]

def get_all_treasury_slugs():
    r = requests.get("https://api.llama.fi/protocols", timeout=30)
    data = r.json()
    return [(p['name'], p['treasury']) for p in data if p.get('treasury')]
```

---

## Network Mapping

DeFiLlama uses these network keys in treasury adapters - map to Safe API network slugs:

| DeFiLlama key | Safe API slug |
|---------------|---------------|
| `ethereum` | `ethereum` |
| `polygon` | `polygon` |
| `arbitrum` | `arbitrum` |
| `optimism` | `optimism` |
| `base` | `base` |
| `bsc` / `binance` | `bnb` |
| `gnosis` / `xdai` | `gnosis` |
| `avalanche` | `avalanche` |

When an adapter lists addresses for multiple networks, check each address against the corresponding Safe API network.

---

## Protocol-Specific Lookup

For Mode 2 (single protocol), fuzzy-match the user's input against DeFiLlama protocol names:

```python
def find_protocol(name_query, protocols):
    name_lower = name_query.lower()
    # Exact match first
    exact = [p for p in protocols if p['name'].lower() == name_lower]
    if exact:
        return exact[0]
    # Partial match
    partial = [p for p in protocols if name_lower in p['name'].lower()]
    return partial[0] if len(partial) == 1 else partial  # return list if ambiguous
```

If ambiguous → ask user to clarify from the list.

---

## Expected Hit Rate

From testing:
- ~292 protocols have treasury adapters
- Each adapter typically contains 1–5 Safe addresses
- ~60–80% of `owners:` addresses are actual Safes (Safe API returns 200)
- ~20–40% are EOAs or token contracts (Safe API returns 404 - skip silently)
- Expect ~400–700 valid Safes from a full sweep

---

## Supplemental Discovery (when DeFiLlama misses a protocol)

If a protocol isn't in DeFiLlama:
1. **GitHub search**: `{protocol} multisig site:github.com` - look for deployed addresses in README or docs
2. **Etherscan labels**: `https://etherscan.io/accounts/label/safe` - labeled Safe accounts
3. **From known team member**: `GET /api/v1/owners/{known_team_address}/safes/`
4. **From known module**: `GET /api/v1/modules/{moduleAddress}/safes/` - if you know a module they use

## references/finding-patterns.md

# Finding Patterns - Safe Governance Hunt

## Scoring System

Each finding contributes to a `risk_score` (0–100+). Findings are bucketed by severity.

| Severity | Score Weight | Audit Category |
|----------|-------------|----------------|
| Critical | +35 | Immediate risk, likely exploitable path |
| High | +20–25 | Serious misconfiguration, material risk |
| Medium | +10–15 | Weakens security posture |
| Low | +5 | Centralization / best practice gap |
| Info | +0 | Noteworthy but not actionable alone |

Risk score thresholds:
- 0–15: ✅ Clean
- 16–35: 🟡 Low risk
- 36–55: 🟠 Medium risk
- 56–79: 🔴 High risk
- 80+: 🔴 Critical

---

## Finding Patterns

### CONFIG-01: Single-signer threshold
**Severity:** High (+25)
**Signal:** `threshold == 1`
**Condition:** Any single owner can execute without consensus
**Audit language:**
> "The Safe multisig is configured with a 1-of-N threshold, effectively granting any single owner unilateral control over privileged operations. This eliminates the core security guarantee of multisig governance."

---

### CONFIG-02: Low threshold ratio
**Severity:** Medium (+15)
**Signal:** `threshold / len(owners) < 0.4` AND `threshold > 1`
**Condition:** Too few signers relative to owner set
**Audit language:**
> "The threshold-to-owner ratio of {threshold}/{total} is below the recommended minimum of 40%. Compromising only {threshold} key(s) is sufficient to execute any transaction, disproportionate to the size of the signer set."

---

### CONFIG-03: No transaction guard
**Severity:** Medium (+10)
**Signal:** `guard == "0x0000000000000000000000000000000000000000"`
**Condition:** No policy enforcement layer on transactions
**Audit language:**
> "No transaction guard is configured on the Safe. Guards provide an additional enforcement layer that can validate or restrict transactions before execution. Without one, the Safe has no mechanism to enforce governance policies at the contract level."

---

### CONFIG-04: Unknown module enabled
**Severity:** High (+25)
**Signal:** `modules[]` contains address not in known-safe whitelist
**Condition:** Unaudited code with full Safe permissions
**Audit language:**
> "Module {address} is enabled on the Safe. Modules bypass the multisig threshold and can execute arbitrary transactions on behalf of the Safe. This module is not in the known-safe registry - if compromised or malicious, it represents a complete bypass of governance controls."

---

### CONFIG-05: Outdated Safe version
**Severity:** High (+20)
**Signal:** `version < "1.3.0"` (i.e. 1.2.0, 1.1.1, 1.1.0, 1.0.0)
**Condition:** Known vulnerabilities in old Safe versions
**Audit language:**
> "The Safe is running version {version}, which predates security improvements introduced in 1.3.0. Upgrading to a current version is recommended."

---

### CONFIG-06: Non-standard fallback handler
**Severity:** Medium (+10)
**Signal:** `fallbackHandler` not in known standard handlers list AND not zero address
**Condition:** Custom fallback handler may introduce unexpected behavior
**Audit language:**
> "A non-standard fallback handler ({address}) is configured. The fallback handler receives all calls to the Safe that don't match any function selector, including EIP-1271 signature validation. A custom handler could manipulate signature verification or introduce attack surface."

---

### HISTORY-01: Threshold decreased
**Severity:** High (+20)
**Signal:** Executed `changeThreshold` tx where new threshold < previous (infer from nonce ordering)
**Condition:** Governance was weakened
**Audit language:**
> "The threshold was decreased {N} time(s) in transaction history. Threshold reductions weaken the security posture of the multisig. Each decrease should be a documented, governance-approved action - unexplained threshold reductions are a red flag."

---

### HISTORY-02: Owner added
**Severity:** Medium (+10)
**Signal:** Executed `addOwner` or `addOwnerWithThreshold` txs
**Condition:** Signer set was expanded - verify authorization
**Audit language:**
> "{N} owner addition(s) were executed. Each owner addition expands the attack surface. Verify these were authorized governance decisions and that new owners' key management practices are adequate."

---

### HISTORY-03: Owner removed or swapped
**Severity:** Medium (+10)
**Signal:** Executed `removeOwner` or `swapOwner` txs
**Condition:** Signer changes may indicate compromise or internal conflict
**Audit language:**
> "{N} owner removal/swap transaction(s) in history. Verify these were authorized and that no owner was forcibly removed following a compromise event."

---

### HISTORY-04: Untrusted delegatecall
**Severity:** High (+25)
**Signal:** Executed tx with `operation == 1` and `to` not in trusted delegate list
**Condition:** Code execution in Safe's context from non-standard contract
**Trusted delegates:** MultiSend 1.3.0/1.4.0, MultiSendCallOnly 1.3.0/1.4.0
**Audit language:**
> "{N} delegatecall(s) to non-standard contract(s) ({targets}) found in history. Delegatecalls execute code in the Safe's storage context and can modify ownership, drain funds, or alter configuration. Any delegatecall target not in the trusted whitelist requires review."

---

### HISTORY-05: Gas token attack pattern
**Severity:** High (+20)
**Signal:** Executed tx with non-zero `gasToken` AND non-zero-address `refundReceiver`
**Condition:** Custom gas refund routing - potential gas token attack
**Audit language:**
> "Transaction(s) with custom gasToken ({token}) and refundReceiver ({receiver}) detected. This pattern can be used to drain Safe funds via gas refund manipulation. In legitimate usage, both fields should be the zero address."

---

### HISTORY-06: Single executor
**Severity:** Low (+5)
**Signal:** All (or >90%) recent executed txs submitted by same `executor` address, N >= 5
**Condition:** One person controls execution timing even if threshold > 1
**Audit language:**
> "All {N} recent executed transactions were submitted by a single executor ({address}). While the threshold requires multiple signatures, a single party controls execution timing and can delay or front-run transactions. Consider distributing executor responsibilities."

---

### HISTORY-07: Execution failures
**Severity:** Medium (+10)
**Signal:** Txs with `isExecuted == true` AND `isSuccessful == false`, count >= 2
**Condition:** Repeated failed executions may indicate attempted manipulation
**Audit language:**
> "{N} execution failure(s) in history. Failed executions still consume nonce and gas. Repeated failures may indicate misconfigured transactions, attempted exploits, or frontrunning."

---

### HISTORY-08: Pending high-risk tx
**Severity:** Critical (+35) if governance-changing, else High (+20)
**Signal:** Pending (unexecuted) tx with method in: `addOwnerWithThreshold`, `addOwner`, `removeOwner`, `swapOwner`, `changeThreshold`, `enableModule`, `changeMasterCopy`, `setGuard`
**Condition:** Active governance change awaiting execution - time-sensitive
**Audit language:**
> "A pending {method} transaction (nonce {nonce}) has {confirmations}/{required} confirmations and has not yet executed. This represents an active governance change that, if executed, would {impact}. Review urgently."

---

## Compound Findings

Some combinations escalate severity:

| Combo | Escalated Severity |
|-------|--------------------|
| threshold=1 + unknown module | Critical |
| no guard + threshold decreased + owner added | Critical |
| outdated version + unknown module | Critical |
| pending addOwner + threshold=1 | Critical |

---

## Not Findings (avoid false positives)

- `operation=1` to known MultiSend addresses → trusted, expected
- `changeThreshold` that *increased* the threshold → positive governance action
- `addOwner` with threshold increase → adding signer responsibly
- Standard fallback handler → expected, skip
- Empty `modules[]` → good, no finding
- `guard != 0x0` → good, has protection, note the address for review but not a finding

## references/safe-api.md

# Safe Transaction Service API Reference

## Base URLs (per network)

```
ethereum  → https://api.safe.global/tx-service/eth/api/v1
polygon   → https://api.safe.global/tx-service/pol/api/v1
arbitrum  → https://api.safe.global/tx-service/arb1/api/v1
optimism  → https://api.safe.global/tx-service/oeth/api/v1
base      → https://api.safe.global/tx-service/base/api/v1
sepolia   → https://api.safe.global/tx-service/sep/api/v1
gnosis    → https://api.safe.global/tx-service/gno/api/v1
avalanche → https://api.safe.global/tx-service/avax/api/v1
bnb       → https://api.safe.global/tx-service/bnb/api/v1
```

All endpoints are **publicly readable, no auth required** for GET requests.
Safe addresses must be EIP-55 checksummed - use `eth_utils.to_checksum_address()` or equivalent.

---

## Key Endpoints

### Get Safe Config
```
GET {base}/safes/{address}/
```
Returns: `address`, `nonce`, `threshold`, `owners[]`, `masterCopy`, `modules[]`, `fallbackHandler`, `guard`, `moduleGuard`, `version`

**Critical fields for analysis:**
- `threshold` - number of required confirmations
- `owners` - list of owner addresses
- `guard` - `0x000...000` means no guard set
- `modules` - list of enabled module addresses (empty = good)
- `version` - e.g. `"1.3.0"`, `"1.4.0"`, `"1.1.1"`
- `masterCopy` - implementation address (cross-ref with known versions)
- `fallbackHandler` - should be standard CompatibilityFallbackHandler

Returns 404 if address is not a registered Safe.

---

### Get Transaction History
```
GET {base}/safes/{address}/multisig-transactions/?limit=50&ordering=-nonce
GET {base}/safes/{address}/multisig-transactions/?executed=false&ordering=-nonce   (pending only)
GET {base}/safes/{address}/multisig-transactions/?executed=true&ordering=-nonce    (executed only)
```

Each transaction object:
```json
{
  "safeTxHash": "0x...",
  "to": "0x...",
  "value": "0",
  "data": "0x...",
  "operation": 0,           // 0=CALL, 1=DELEGATECALL
  "gasToken": "0x000...000",
  "safeTxGas": 0,
  "baseGas": 0,
  "gasPrice": "0",
  "refundReceiver": "0x000...000",
  "nonce": 42,
  "isExecuted": true,
  "isSuccessful": true,
  "executor": "0x...",
  "confirmationsRequired": 3,
  "confirmations": [...],
  "dataDecoded": {
    "method": "addOwnerWithThreshold",
    "parameters": [
      {"name": "owner", "type": "address", "value": "0x..."},
      {"name": "_threshold", "type": "uint256", "value": "2"}
    ]
  }
}
```

**Key fields for analysis:**
- `operation` - `1` = delegatecall (needs scrutiny)
- `gasToken` - non-zero = custom gas token (suspicious)
- `refundReceiver` - non-zero-address = custom refund target
- `dataDecoded.method` - decoded function name
- `executor` - who submitted the tx on-chain
- `isExecuted` / `isSuccessful` - execution state

---

### Get All Safes Owned By Address
```
GET {base}/owners/{address}/safes/
```
Returns: `{ "safes": ["0x...", "0x..."] }`

Use to: expand a known team member's address into all Safes they participate in.

---

### Get All Safes Using a Module
```
GET {base}/modules/{moduleAddress}/safes/
```
Returns: `{ "safes": ["0x...", "0x..."] }`

Use to: find all Safes using a specific (potentially risky) module address.

---

### Get Safe Creation Info
```
GET {base}/safes/{address}/creation/
```
Returns: `created`, `creator`, `transactionHash`, `factoryAddress`, `masterCopy`, `setupData`

---

## Known Contract Addresses

### Trusted MultiSend (safe to delegatecall)
```
0x40A2aCCbd92BCA938b02010E17A5b8929b49130D  MultiSend 1.3.0
0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761  MultiSend 1.4.0
0x998739BFdAAdde7C933B942a68053933098f9EDa  MultiSendCallOnly 1.3.0
0x9641d764fc13c8B624c04430C7356C1C7C8102e2  MultiSendCallOnly 1.4.0
```

### Standard Fallback Handlers
```
0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4  CompatibilityFallbackHandler 1.3.0
0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99  CompatibilityFallbackHandler 1.4.0
```

### Known Safe Versions by masterCopy
```
0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552  → 1.3.0
0x41675C099F32341bf84BFc5382aF534df5C7461a → 1.4.0
0x6851D6fDFAfD08c0295C392436245E5bc78B0185 → 1.2.0  (outdated)
0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F → 1.1.1  (outdated)
0xaE32496491b53841efb51829d6f886387708F99B → 1.1.0  (outdated)
```

---

## Rate Limiting

- No hard published limit, but be respectful: add 0.2–0.5s delay between requests in sweeps
- Use `asyncio` with semaphore (max 10 concurrent) for sweep mode
- The sweep.py script handles this automatically

---

## Error Handling

| Status | Meaning |
|--------|---------|
| 200 | OK |
| 404 | Address is not a registered Safe (skip silently) |
| 422 | Invalid address format (not checksummed) |
| 429 | Rate limited - back off 5s and retry |
| 5xx | API error - retry once, then skip |

## scripts

```

```

## scripts/sweep.py

```python
#!/usr/bin/env python3
"""
Safe Governance Hunt - sweep.py
Discovers and audits Safe multisig configurations across DeFi protocols.

Usage:
  python3 sweep.py --address 0xABC... [--network ethereum]
  python3 sweep.py --protocol "Lido"
  python3 sweep.py --sweep [--network ethereum] [--top 20] [--output report.md]
"""

import asyncio
import aiohttp
import argparse
import json
import re
import ssl
import sys
import certifi
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional


def checksum_address(addr: str) -> str:
    """Compute EIP-55 checksummed address."""
    try:
        from web3 import Web3
        return Web3.to_checksum_address(addr)
    except Exception:
        pass
    # Fallback: pycryptodome keccak256
    from Crypto.Hash import keccak as _keccak
    addr_clean = addr.lower().replace("0x", "")
    k = _keccak.new(digest_bits=256)
    k.update(addr_clean.encode())
    h = k.hexdigest()
    return "0x" + "".join(c.upper() if int(h[i], 16) >= 8 else c for i, c in enumerate(addr_clean))

# ──────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────

SAFE_API_BASES = {
    "ethereum":  "https://api.safe.global/tx-service/eth/api/v1",
    "polygon":   "https://api.safe.global/tx-service/pol/api/v1",
    "arbitrum":  "https://api.safe.global/tx-service/arb1/api/v1",
    "optimism":  "https://api.safe.global/tx-service/oeth/api/v1",
    "base":      "https://api.safe.global/tx-service/base/api/v1",
    "gnosis":    "https://api.safe.global/tx-service/gno/api/v1",
    "avalanche": "https://api.safe.global/tx-service/avax/api/v1",
    "bnb":       "https://api.safe.global/tx-service/bnb/api/v1",
    "sepolia":   "https://api.safe.global/tx-service/sep/api/v1",
}

DEFILLAMA_PROTOCOLS_URL = "https://api.llama.fi/protocols"
DEFILLAMA_ADAPTER_BASE = "https://raw.githubusercontent.com/DefiLlama/DefiLlama-Adapters/main/projects/treasury/"

TRUSTED_DELEGATES = {
    "0x40a2accbd92bca938b02010e17a5b8929b49130d",  # MultiSend 1.3.0
    "0xa238cbeb142c10ef7ad8442c6d1f9e89e07e7761",  # MultiSend 1.4.0
    "0x998739bfdaadde7c933b942a68053933098f9eda",  # MultiSendCallOnly 1.3.0
    "0x9641d764fc13c8b624c04430c7356c1c7c8102e2",  # MultiSendCallOnly 1.4.0
}

STANDARD_FALLBACK_HANDLERS = {
    "0xf48f2b2d2a534e402487b3ee7c18c33aec0fe5e4",  # CompatibilityFallbackHandler 1.3.0
    "0xfd0732dc9e303f09fcef3a7388ad10a83459ec99",  # CompatibilityFallbackHandler 1.4.0
}

ZERO_ADDR = "0x0000000000000000000000000000000000000000"
ETH_ADDR_RE = re.compile(r"0x[0-9a-fA-F]{40}")

GOVERNANCE_METHODS = {
    "addOwner", "addOwnerWithThreshold", "removeOwner", "swapOwner",
    "changeThreshold", "enableModule", "disableModule",
    "changeMasterCopy", "setGuard", "setFallbackHandler", "setup",
}

# ──────────────────────────────────────────────
# Data models
# ──────────────────────────────────────────────

@dataclass
class Finding:
    severity: str       # critical, high, medium, low, info
    code: str           # e.g. CONFIG-01
    title: str
    detail: str
    audit_language: str
    evidence: dict = field(default_factory=dict)
    score: int = 0

@dataclass
class SafeResult:
    address: str
    network: str
    protocol: str
    safe_info: Optional[dict] = None
    findings: list = field(default_factory=list)
    risk_score: int = 0
    error: Optional[str] = None

    @property
    def risk_label(self):
        s = self.risk_score
        if s >= 80: return "🔴 CRITICAL"
        if s >= 56: return "🔴 HIGH"
        if s >= 36: return "🟠 MEDIUM"
        if s >= 16: return "🟡 LOW"
        return "✅ CLEAN"

# ──────────────────────────────────────────────
# Safe API
# ──────────────────────────────────────────────

async def fetch_safe_info(session: aiohttp.ClientSession, address: str, network: str) -> Optional[dict]:
    base = SAFE_API_BASES.get(network)
    if not base:
        return None
    address = checksum_address(address)
    url = f"{base}/safes/{address}/"
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as r:
            if r.status == 404:
                return None
            if r.status == 429:
                await asyncio.sleep(5)
                async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as r2:
                    if r2.status != 200:
                        return None
                    return await r2.json()
            if r.status != 200:
                return None
            return await r.json()
    except Exception:
        return None


async def fetch_transactions(session: aiohttp.ClientSession, address: str, network: str, limit: int = 50) -> list:
    base = SAFE_API_BASES.get(network)
    if not base:
        return []
    address = checksum_address(address)
    url = f"{base}/safes/{address}/multisig-transactions/?limit={limit}&ordering=-nonce"
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=20)) as r:
            if r.status != 200:
                return []
            data = await r.json()
            return data.get("results", [])
    except Exception:
        return []

# ──────────────────────────────────────────────
# Analysis
# ──────────────────────────────────────────────

def analyze_safe(safe_info: dict, txs: list, address: str, network: str, protocol: str) -> SafeResult:
    result = SafeResult(address=address, network=network, protocol=protocol, safe_info=safe_info)

    threshold = safe_info.get("threshold", 0)
    owners = safe_info.get("owners", [])
    guard = (safe_info.get("guard") or ZERO_ADDR).lower()
    modules = safe_info.get("modules") or []
    version = safe_info.get("version") or "unknown"
    fallback = (safe_info.get("fallbackHandler") or ZERO_ADDR).lower()
    owner_count = len(owners)

    executed = [t for t in txs if t.get("isExecuted")]
    pending  = [t for t in txs if not t.get("isExecuted")]

    # ── CONFIG-01: Single-signer threshold ────────────────────────────────
    if threshold == 1:
        result.findings.append(Finding(
            severity="high", code="CONFIG-01",
            title=f"Single-signer threshold (1/{owner_count})",
            detail=f"Threshold is 1/{owner_count} - any single owner can execute transactions unilaterally.",
            audit_language=(
                f"The Safe multisig is configured with a 1-of-{owner_count} threshold, effectively granting "
                f"any single owner unilateral control over privileged operations. This eliminates the core "
                f"security guarantee of multisig governance."
            ),
            evidence={"threshold": threshold, "owners": owner_count},
            score=25,
        ))

    # ── CONFIG-02: Low threshold ratio ────────────────────────────────────
    elif owner_count > 0 and threshold > 1 and (threshold / owner_count) < 0.4:
        ratio = round(threshold / owner_count, 2)
        result.findings.append(Finding(
            severity="medium", code="CONFIG-02",
            title=f"Low threshold ratio ({threshold}/{owner_count} = {ratio})",
            detail=f"Only {threshold} of {owner_count} owners required - ratio {ratio} is below 0.40.",
            audit_language=(
                f"The threshold-to-owner ratio of {threshold}/{owner_count} ({ratio}) is below the recommended "
                f"minimum of 40%. Compromising only {threshold} key(s) is sufficient to execute any transaction."
            ),
            evidence={"threshold": threshold, "owners": owner_count, "ratio": ratio},
            score=15,
        ))

    # ── CONFIG-03: No guard ────────────────────────────────────────────────
    if guard == ZERO_ADDR:
        result.findings.append(Finding(
            severity="medium", code="CONFIG-03",
            title="No transaction guard configured",
            detail="guard == 0x0 - no policy enforcement layer on transaction execution.",
            audit_language=(
                "No transaction guard is configured on the Safe. Guards provide an additional enforcement "
                "layer that can validate or restrict transactions before execution. Without one, the Safe "
                "has no mechanism to enforce governance policies at the contract level."
            ),
            evidence={"guard": safe_info.get("guard")},
            score=10,
        ))

    # ── CONFIG-04: Unknown modules ─────────────────────────────────────────
    for module in modules:
        result.findings.append(Finding(
            severity="high", code="CONFIG-04",
            title=f"Unknown module enabled: {module[:10]}...",
            detail=f"Module {module} has unrestricted Safe access and is not in the known-safe whitelist.",
            audit_language=(
                f"Module {module} is enabled on the Safe. Modules bypass the multisig threshold and can "
                f"execute arbitrary transactions on behalf of the Safe. This module is not in the known-safe "
                f"registry - if compromised or malicious, it represents a complete bypass of governance controls."
            ),
            evidence={"module": module},
            score=25,
        ))

    # ── CONFIG-05: Outdated version ────────────────────────────────────────
    if version not in ("unknown",) and version < "1.3.0":
        result.findings.append(Finding(
            severity="high", code="CONFIG-05",
            title=f"Outdated Safe version ({version})",
            detail=f"Version {version} predates security improvements in Safe 1.3.0.",
            audit_language=(
                f"The Safe is running version {version}, which predates security improvements introduced "
                f"in 1.3.0. Upgrading to a current version is recommended."
            ),
            evidence={"version": version, "masterCopy": safe_info.get("masterCopy")},
            score=20,
        ))

    # ── CONFIG-06: Non-standard fallback handler ───────────────────────────
    if fallback not in STANDARD_FALLBACK_HANDLERS and fallback != ZERO_ADDR:
        result.findings.append(Finding(
            severity="medium", code="CONFIG-06",
            title=f"Non-standard fallback handler",
            detail=f"fallbackHandler {safe_info.get('fallbackHandler')} is not in the known-safe list.",
            audit_language=(
                f"A non-standard fallback handler ({safe_info.get('fallbackHandler')}) is configured. "
                f"The fallback handler receives EIP-1271 signature validation calls and all unmatched "
                f"function selectors. A custom handler could manipulate signature verification."
            ),
            evidence={"fallbackHandler": safe_info.get("fallbackHandler")},
            score=10,
        ))

    # ── HISTORY-01: Threshold decreased ───────────────────────────────────
    threshold_decreases = []
    for tx in executed:
        dd = tx.get("dataDecoded") or {}
        if dd.get("method") == "changeThreshold":
            params = dd.get("parameters") or []
            new_val = next((int(p["value"]) for p in params if p.get("name") == "_threshold"), None)
            if new_val is not None and new_val < threshold:
                threshold_decreases.append(tx)

    if threshold_decreases:
        result.findings.append(Finding(
            severity="high", code="HISTORY-01",
            title=f"Threshold decreased ({len(threshold_decreases)} time(s))",
            detail=f"changeThreshold executed with lower value - governance was weakened.",
            audit_language=(
                f"The threshold was decreased {len(threshold_decreases)} time(s) in transaction history. "
                f"Threshold reductions weaken the multisig's security posture. Unexplained threshold "
                f"reductions are a red flag for governance attacks."
            ),
            evidence={"count": len(threshold_decreases), "nonces": [t["nonce"] for t in threshold_decreases]},
            score=20,
        ))

    # ── HISTORY-02/03: Owner changes ──────────────────────────────────────
    owner_adds = [t for t in executed if (t.get("dataDecoded") or {}).get("method") in ("addOwner", "addOwnerWithThreshold")]
    owner_changes = [t for t in executed if (t.get("dataDecoded") or {}).get("method") in ("removeOwner", "swapOwner")]

    if owner_adds:
        result.findings.append(Finding(
            severity="medium", code="HISTORY-02",
            title=f"Owner(s) added in history ({len(owner_adds)} tx(s))",
            detail="Owner additions found - verify each was an authorized governance action.",
            audit_language=(
                f"{len(owner_adds)} owner addition(s) were executed. Each addition expands the attack surface. "
                f"Verify these were authorized governance decisions and that new owners' key management is adequate."
            ),
            evidence={"count": len(owner_adds), "nonces": [t["nonce"] for t in owner_adds[:5]]},
            score=10,
        ))

    if owner_changes:
        result.findings.append(Finding(
            severity="medium", code="HISTORY-03",
            title=f"Owner removal/swap in history ({len(owner_changes)} tx(s))",
            detail="Owner removal or swap detected - may indicate compromise or forced key rotation.",
            audit_language=(
                f"{len(owner_changes)} owner removal/swap transaction(s) found in history. "
                f"Verify these were authorized and that no owner was forcibly removed following a compromise."
            ),
            evidence={"count": len(owner_changes), "nonces": [t["nonce"] for t in owner_changes[:5]]},
            score=10,
        ))

    # ── HISTORY-04: Untrusted delegatecall ────────────────────────────────
    untrusted_delegatecalls = [
        t for t in executed
        if t.get("operation") == 1 and t.get("to", "").lower() not in TRUSTED_DELEGATES
    ]
    if untrusted_delegatecalls:
        targets = list({t["to"] for t in untrusted_delegatecalls})[:3]
        result.findings.append(Finding(
            severity="high", code="HISTORY-04",
            title=f"Delegatecall to non-standard contract ({len(untrusted_delegatecalls)} tx(s))",
            detail=f"operation=1 to non-whitelisted contracts: {', '.join(targets)}",
            audit_language=(
                f"{len(untrusted_delegatecalls)} delegatecall(s) to non-standard contract(s) "
                f"({', '.join(targets)}) found in history. Delegatecalls execute code in the Safe's "
                f"storage context and can modify ownership or drain funds. Any target not in the trusted "
                f"whitelist requires review."
            ),
            evidence={"count": len(untrusted_delegatecalls), "targets": targets},
            score=25,
        ))

    # ── HISTORY-05: Gas token attack pattern ──────────────────────────────
    gas_attacks = [
        t for t in executed
        if (t.get("gasToken") or ZERO_ADDR) != ZERO_ADDR
        and (t.get("refundReceiver") or ZERO_ADDR).lower() != ZERO_ADDR
    ]
    if gas_attacks:
        result.findings.append(Finding(
            severity="high", code="HISTORY-05",
            title=f"Gas token attack pattern ({len(gas_attacks)} tx(s))",
            detail="Transactions with custom gasToken + custom refundReceiver - potential gas drainage.",
            audit_language=(
                f"{len(gas_attacks)} transaction(s) with a custom gasToken and refundReceiver found. "
                f"This pattern can drain Safe funds via gas refund manipulation. In legitimate usage, "
                f"both fields should be the zero address."
            ),
            evidence={"count": len(gas_attacks), "nonces": [t["nonce"] for t in gas_attacks[:5]]},
            score=20,
        ))

    # ── HISTORY-06: Single executor ───────────────────────────────────────
    executed_with_executor = [t for t in executed if t.get("executor")]
    if len(executed_with_executor) >= 5:
        executors = [t["executor"] for t in executed_with_executor]
        unique = set(executors)
        dominant = max(unique, key=executors.count)
        dominant_count = executors.count(dominant)
        if dominant_count / len(executors) > 0.9:
            result.findings.append(Finding(
                severity="low", code="HISTORY-06",
                title="Single executor pattern",
                detail=f"{dominant_count}/{len(executors)} txs executed by same address ({dominant[:10]}...).",
                audit_language=(
                    f"All {dominant_count} recent executed transactions were submitted by a single executor "
                    f"({dominant}). While threshold requires multiple signatures, one party controls execution "
                    f"timing and can delay or front-run transactions."
                ),
                evidence={"executor": dominant, "count": dominant_count, "total": len(executors)},
                score=5,
            ))

    # ── HISTORY-07: Execution failures ────────────────────────────────────
    failures = [t for t in txs if t.get("isExecuted") and t.get("isSuccessful") is False]
    if len(failures) >= 2:
        result.findings.append(Finding(
            severity="medium", code="HISTORY-07",
            title=f"Execution failures in history ({len(failures)})",
            detail="Repeated failed executions may indicate misconfiguration or attempted exploits.",
            audit_language=(
                f"{len(failures)} execution failure(s) found in history. Failed executions still consume "
                f"nonce and gas. Repeated failures may indicate misconfigured transactions or attempted manipulation."
            ),
            evidence={"count": len(failures), "nonces": [t["nonce"] for t in failures[:5]]},
            score=10,
        ))

    # ── HISTORY-08: Pending governance tx ─────────────────────────────────
    pending_governance = [
        t for t in pending
        if (t.get("dataDecoded") or {}).get("method") in GOVERNANCE_METHODS
    ]
    for tx in pending_governance:
        method = tx["dataDecoded"]["method"]
        confs = len(tx.get("confirmations") or [])
        required = tx.get("confirmationsRequired", threshold)
        is_critical = method in ("addOwnerWithThreshold", "addOwner", "removeOwner",
                                  "swapOwner", "changeThreshold", "enableModule", "changeMasterCopy")
        result.findings.append(Finding(
            severity="critical" if is_critical else "high",
            code="HISTORY-08",
            title=f"Pending governance tx: {method} (nonce {tx['nonce']})",
            detail=f"{confs}/{required} confirmations - not yet executed.",
            audit_language=(
                f"A pending {method} transaction (nonce {tx['nonce']}) has {confs}/{required} confirmations "
                f"and has not yet executed. This represents an active governance change. Review urgently."
            ),
            evidence={"method": method, "nonce": tx["nonce"], "confirmations": confs, "required": required},
            score=35 if is_critical else 20,
        ))

    # Compound escalation: threshold=1 + unknown module → bump score
    has_threshold_1 = any(f.code == "CONFIG-01" for f in result.findings)
    has_unknown_module = any(f.code == "CONFIG-04" for f in result.findings)
    if has_threshold_1 and has_unknown_module:
        result.risk_score += 10  # escalation bonus

    result.risk_score = min(sum(f.score for f in result.findings) + result.risk_score, 100)
    return result

# ──────────────────────────────────────────────
# Discovery
# ──────────────────────────────────────────────

async def get_treasury_protocols(session: aiohttp.ClientSession) -> list[tuple[str, str]]:
    """Returns list of (protocol_name, treasury_slug)."""
    async with session.get(DEFILLAMA_PROTOCOLS_URL, timeout=aiohttp.ClientTimeout(total=120)) as r:
        data = await r.json(content_type=None)
    return [(p["name"], p["treasury"]) for p in data if p.get("treasury")]


async def extract_addresses_from_adapter(session: aiohttp.ClientSession, slug: str) -> list[str]:
    """Fetches DeFiLlama treasury adapter JS and extracts all Ethereum addresses."""
    url = f"{DEFILLAMA_ADAPTER_BASE}{slug}"
    if not slug.endswith(".js"):
        url += ".js" if "." not in slug else ""
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r:
            if r.status != 200:
                return []
            text = await r.text()
    except Exception:
        return []

    addresses = list(set(ETH_ADDR_RE.findall(text)))
    checksummed = []
    for a in addresses:
        if a.lower() != ZERO_ADDR and len(a) == 42:
            try:
                checksummed.append(checksum_address(a))
            except Exception:
                pass
    return list(set(checksummed))


async def audit_address(session: aiohttp.ClientSession, address: str, network: str, protocol: str, sem: asyncio.Semaphore) -> SafeResult:
    async with sem:
        safe_info = await fetch_safe_info(session, address, network)
        if safe_info is None:
            return SafeResult(address=address, network=network, protocol=protocol, error="Not a Safe or unreachable")
        txs = await fetch_transactions(session, address, network, limit=50)
        await asyncio.sleep(0.2)  # polite delay
    return analyze_safe(safe_info, txs, address, network, protocol)

# ──────────────────────────────────────────────
# Report
# ──────────────────────────────────────────────

def render_report(results: list[SafeResult], mode: str, network: str) -> str:
    from datetime import timezone
    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
    lines = [
        f"# Safe Governance Hunt Report",
        f"Generated: {now} | Mode: {mode} | Network: {network}",
        f"Audited: {len(results)} Safe(s) | Findings: {sum(len(r.findings) for r in results if not r.error)}",
        "",
    ]

    valid = [r for r in results if not r.error and r.safe_info]
    errored = [r for r in results if r.error]

    buckets = {
        "🔴 CRITICAL / HIGH RISK (score ≥ 56)": [r for r in valid if r.risk_score >= 56],
        "🟠 MEDIUM RISK (score 36–55)": [r for r in valid if 36 <= r.risk_score < 56],
        "🟡 LOW RISK (score 16–35)": [r for r in valid if 16 <= r.risk_score < 36],
        "✅ CLEAN (score < 16)": [r for r in valid if r.risk_score < 16],
    }

    for bucket_label, bucket_results in buckets.items():
        if not bucket_results:
            continue
        lines.append(f"## {bucket_label}")
        lines.append("")
        for r in sorted(bucket_results, key=lambda x: x.risk_score, reverse=True):
            info = r.safe_info or {}
            owners = info.get("owners", [])
            modules = info.get("modules") or []
            lines += [
                f"### [{r.protocol}] `{r.address}` ({r.network})",
                f"Risk Score: **{r.risk_score}/100** {r.risk_label}",
                f"Owners: {len(owners)} | Threshold: {info.get('threshold', '?')}/{len(owners)} | "
                f"Version: {info.get('version', '?')} | Guard: {'Set' if (info.get('guard') or ZERO_ADDR) != ZERO_ADDR else 'None'} | "
                f"Modules: {len(modules)} {'(unknown)' if modules else ''}",
                "",
            ]
            severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
            for finding in sorted(r.findings, key=lambda f: severity_order.get(f.severity, 5)):
                emoji = {"critical": "🚨", "high": "🔴", "medium": "🟠", "low": "🟡", "info": "ℹ️"}.get(finding.severity, "•")
                lines += [
                    f"**{emoji} [{finding.severity.upper()}] {finding.code}: {finding.title}**",
                    f"_{finding.detail}_",
                    f"> {finding.audit_language}",
                    f"Evidence: `{json.dumps(finding.evidence)}`",
                    "",
                ]
            lines.append("---")
            lines.append("")

    if errored:
        lines += ["## ⚠️ Skipped (not a Safe or unreachable)", ""]
        for r in errored:
            lines.append(f"- `{r.address}` ({r.network}, {r.protocol}): {r.error}")
        lines.append("")

    return "\n".join(lines)

# ──────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────

async def main():
    parser = argparse.ArgumentParser(description="Safe Governance Hunt")
    parser.add_argument("--address", help="Single Safe address to audit")
    parser.add_argument("--protocol", help="Protocol name to look up via DeFiLlama")
    parser.add_argument("--sweep", action="store_true", help="Sweep all DeFiLlama treasury protocols")
    parser.add_argument("--network", default="ethereum", help="Network (default: ethereum)")
    parser.add_argument("--top", type=int, help="Show only top N results by risk score")
    parser.add_argument("--output", help="Write report to file (default: stdout)")
    parser.add_argument("--min-score", type=int, default=0, help="Only show results with risk_score >= N")
    args = parser.parse_args()

    sem = asyncio.Semaphore(8)
    results: list[SafeResult] = []

    connector = aiohttp.TCPConnector(limit=20, ssl=ssl.create_default_context(cafile=certifi.where()))
    async with aiohttp.ClientSession(connector=connector) as session:

        # ── Mode 1: Single address ─────────────────────────────────────────
        if args.address:
            print(f"[*] Auditing {args.address} on {args.network}...", file=sys.stderr)
            r = await audit_address(session, args.address, args.network, "manual", sem)
            results.append(r)
            mode = "targeted"

        # ── Mode 2: Protocol lookup ────────────────────────────────────────
        elif args.protocol:
            print(f"[*] Looking up '{args.protocol}' in DeFiLlama...", file=sys.stderr)
            protocols = await get_treasury_protocols(session)
            query = args.protocol.lower()
            matches = [(name, slug) for name, slug in protocols if query in name.lower()]

            if not matches:
                print(f"[!] No DeFiLlama treasury found for '{args.protocol}'", file=sys.stderr)
                sys.exit(1)
            if len(matches) > 1:
                print(f"[?] Multiple matches - using first: {matches[0][0]}", file=sys.stderr)

            name, slug = matches[0]
            print(f"[*] Found: {name} (slug: {slug})", file=sys.stderr)
            addresses = await extract_addresses_from_adapter(session, slug)
            print(f"[*] Extracted {len(addresses)} address(es) - checking Safe API...", file=sys.stderr)

            tasks = [audit_address(session, addr, args.network, name, sem) for addr in addresses]
            results = await asyncio.gather(*tasks)
            mode = "protocol"

        # ── Mode 3: Sweep ──────────────────────────────────────────────────
        elif args.sweep:
            print("[*] Fetching DeFiLlama protocol list...", file=sys.stderr)
            protocols = await get_treasury_protocols(session)
            print(f"[*] Found {len(protocols)} protocols with treasury adapters", file=sys.stderr)

            all_targets: list[tuple[str, str]] = []  # (address, protocol_name)
            for i, (name, slug) in enumerate(protocols):
                addrs = await extract_addresses_from_adapter(session, slug)
                for addr in addrs:
                    all_targets.append((addr, name))
                if i % 20 == 0:
                    print(f"[*] Parsed {i+1}/{len(protocols)} adapters, {len(all_targets)} addresses so far...", file=sys.stderr)
                await asyncio.sleep(0.05)

            print(f"[*] Auditing {len(all_targets)} addresses across {len(protocols)} protocols...", file=sys.stderr)
            tasks = [audit_address(session, addr, args.network, proto, sem) for addr, proto in all_targets]
            completed = 0
            total = len(tasks)
            results_list = []
            for coro in asyncio.as_completed(tasks):
                r = await coro
                results_list.append(r)
                completed += 1
                if completed % 50 == 0 or completed == total:
                    found = sum(1 for x in results_list if not x.error and x.risk_score >= (args.min_score or 0))
                    print(f"[*] Progress: {completed}/{total} | findings ≥{args.min_score or 0}: {found}", file=sys.stderr)
            results = results_list
            mode = "sweep"

        else:
            parser.print_help()
            sys.exit(0)

    # Filter and sort
    valid_results = [r for r in results if not r.error]
    valid_results.sort(key=lambda r: r.risk_score, reverse=True)

    if args.min_score:
        valid_results = [r for r in valid_results if r.risk_score >= args.min_score]
    if args.top:
        valid_results = valid_results[:args.top]

    errored = [r for r in results if r.error]
    final_results = valid_results + errored

    report = render_report(final_results, mode, args.network)

    if args.output:
        with open(args.output, "w") as f:
            f.write(report)
        print(f"[+] Report written to {args.output}", file=sys.stderr)
    else:
        print(report)


if __name__ == "__main__":
    asyncio.run(main())
```

