Investigate an unverified smart contract end-to-end — given an address (or block-explorer URL) and a network, resolve the RPC, detect EIP-1967 proxies, disassemble with Heimdall, extract every function selector, enrich them via local artifacts / cast / 4byte.directory, and emit one structured markdown report. Use this skill whenever a user mentions an unverified or unknown contract; pastes an etherscan/basescan/arbiscan/etc. URL with no source; asks "what does this contract do" or "what funct...
215 stars
0 votes
0 copies
0 views
Added May 26, 2026
blockchaingobashnodegitapi
Works with
api
Security analysis
D42/100
criticalPipes output to a shell interpreter
mediumUses curl or wget to download content
criticalDownloads and executes remote scripts — classic supply chain attack
Installs into .claude/skills of the current project.
Are you the author of Analyze Unverified Contract?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/lifinance-analyze-unverified-contract)
---
name: analyze-unverified-contract
description: Investigate an unverified smart contract end-to-end — given an address (or block-explorer URL) and a network, resolve the RPC, detect EIP-1967 proxies, disassemble with Heimdall, extract every function selector, enrich them via local artifacts / cast / 4byte.directory, and emit one structured markdown report. Use this skill whenever a user mentions an unverified or unknown contract; pastes an etherscan/basescan/arbiscan/etc. URL with no source; asks "what does this contract do" or "what functions does this expose" for an address with no published source; asks to decompile, disassemble, or "look inside" bytecode; wants to identify a contract's interfaces (ERC-20 / 721 / 1155 / 4337 / 1271 / UUPS / Safe) from bytecode alone; needs to enumerate selectors for an opaque contract; or is chained from /analyze-tx because the target turned out to be unverified. Even partial phrasings like "decompile this proxy" or "I have a contract address but no ABI" should trigger.
usage: /analyze-unverified-contract <address> <network> | /analyze-unverified-contract <block_explorer_contract_url>
---
# Analyze Unverified Contract
**Goal**: turn an unknown address into a self-contained markdown report a reviewer can read in one sitting. Single deliverable: `report-unverified-<network>-<short_addr>.md`.
## Conventions
Defined once; used throughout.
- **`ADDRESS`** — 0x-prefixed 40-hex EVM address (the user input).
- **`NETWORK`** — short network name as keyed in [`config/networks.json`](../../config/networks.json) (e.g. `base`, `arbitrum`, `mainnet`).
- **`TARGET`** — the address Heimdall actually analyzes. Equals `ADDRESS` for direct contracts; equals the resolved implementation for EIP-1967 proxies. Step 3 sets it.
- **`<short_addr>`** — first 8 hex chars of `ADDRESS`, lowercase. Used in every output filename so multiple analyses in the same checkout don't collide.
- **Output naming**: `<purpose>-<network>-<short_addr>.<ext>` (e.g. `opcodes-base-c7828327.txt`, `report-unverified-base-c7828327.md`).
## Bundled helpers
This skill ships four exported bash functions in [`script/playgroundHelpers.sh`](../../script/playgroundHelpers.sh). Source `script/helperFunctions.sh` (provides `isValidEvmAddress`, `getRpcUrlFromNetworksJson`, `error`, `warning`) and then `script/playgroundHelpers.sh`. Then call:
| Function | Replaces | Output |
|---|---|---|
| `resolveContractTarget ADDRESS RPC_URL` | Step 3 — manual `cast storage` / `cast code` / `cast call facets()` reads | `target=… proxy=… kind=direct\|eip1967\|eip1967-beacon\|eip1167\|diamond` (3 lines). For `kind=diamond`, `target` is `<facet1>,<facet2>,…` (comma-separated). |
| `extractSelectorsFromOpcodes OPCODES_FILE` | Step 5 — manual `grep \| awk \| sort \| grep -v` pipeline | one lowercase selector per line |
| `decodeSelectors4byte` (and `decodeSelector4byte`) | Step 6.4 — manual API calls | merged selectors file with signatures |
| `generateUnverifiedContractReportSkeleton ADDRESS NETWORK` | Step 7 — typing out the 7-section template | markdown skeleton to stdout |
Prefer the helpers. They're consistent with the rest of `script/playgroundHelpers.sh`, handle edge cases (empty slots, mixed-case hex, duplicate selectors, rate limits), and short-circuit invalid input early.
## Workflow
### 1. Parse and validate input
- **Two args** `ADDRESS NETWORK` → use directly.
- **Single arg = URL** → extract `ADDRESS` from the path (segment after `/address/` or `/contract/`) and derive `NETWORK` from the host. Look up the host in `config/networks.json` (each network entry's `explorerUrl` field) rather than hardcoding a list — the repo supports 60+ networks. For the common cases: `etherscan.io → mainnet`, `basescan.org → base`, `arbiscan.io → arbitrum`, `polygonscan.com → polygon`, `optimistic.etherscan.io → optimism`. If the host doesn't resolve, ask the user.
- **Validate**: `isValidEvmAddress "$ADDRESS"`. Reject invalid input rather than guessing.
- **Set `$SHORT` once** (the first 8 hex chars of `ADDRESS` in lowercase — used by every output filename in steps 4–7): `SHORT="$(printf '%s' "${ADDRESS#0x}" | tr '[:upper:]' '[:lower:]' | cut -c1-8)"`.
### 2. Resolve RPC
- **Preferred**: `getRpcUrlFromNetworksJson "$NETWORK"` (reads `config/networks.json`).
- **Alternative**: `getRPCUrl "$NETWORK"` (reads `ETH_NODE_URI_<NETWORK>` from env).
- **Fallback**: if neither resolves, ask the user. Do not pick a random public RPC — it leaks the investigation to a third party and may rate-limit during Heimdall's bytecode fetch.
### 3. Detect proxies and set TARGET
```bash
eval "$(resolveContractTarget "$ADDRESS" "$RPC_URL")"
# Now $target, $proxy, $kind are set.
```
If `$kind` is `direct`, set `TARGET=$ADDRESS`. If `$kind` is `eip1967`, `eip1967-beacon`, or `eip1167`, set `TARGET=$target` (the resolved implementation). If `$kind` is `diamond`, **iterate** — split `$target` on commas and run Steps 4–7 once per facet, writing artifacts as `opcodes-$NETWORK-$SHORT-<facet_short>.txt` etc., then aggregate the per-facet selector tables into a single §4 in the report. Record `$proxy` (the diamond / proxy address) and `$target` (impl or facet list) in §2.
Proxies hold dispatch bytecode only — running Heimdall on the proxy itself yields an empty selector list. The helper covers EIP-1967 standard, EIP-1967 beacon, EIP-1167 (Clones), and EIP-2535 (Diamond); for the less-common patterns (EIP-1822 UUPS pre-1967, GnosisSafe) you'll see `kind=direct` and zero selectors in Step 5 — that's your signal to investigate manually (slot 0, custom proxy slots, etc.).
### 4. Disassemble / decompile with Heimdall
Docs: <https://github.com/Jon-Becker/heimdall-rs/wiki/modules>. Install: `command -v heimdall || curl -L https://get.heimdall.rs | bash`. Don't proceed without it.
- **Disassemble** (bytecode → opcodes): `heimdall disassemble "$TARGET" -r "$RPC_URL" -o "opcodes-$NETWORK-$SHORT.txt"`. Optional flag `-d` / `--decimal-counter` — show the program counter in decimal instead of hex. (Different meaning from `-d` in `decompile`.)
- **Decompile** (bytecode → pseudo-Solidity): `heimdall decompile "$TARGET" -r "$RPC_URL" -o "decompiled-$SHORT/" -d`. Here `-d` / `--default` = non-interactive. Add `--include-sol` / `--include-yul` for richer output; `--skip-resolving` to skip Heimdall's own selector resolution (we do it ourselves in Step 6, more thoroughly).
- **Output shape**: Heimdall may create a directory containing `disassembled.asm`. Treat that file as the opcodes input for Step 5.
- **Optional extras** (only if you want richer §6/§7 in the report): `heimdall dump` (storage slots), `heimdall cfg` (control-flow graph), `heimdall inspect <TX_HASH>` (a specific tx through this contract). Link the artifact paths from §3 of the report; don't paste their full output.
### 5. Extract selectors
```bash
extractSelectorsFromOpcodes "opcodes-$NETWORK-$SHORT.txt" > opcodes-selectors.md
```
The default output filename matters: Step 6.4's `decodeSelectors4byte` reads `opcodes-selectors.md` when called with no args.
**Caveat — false positives.** `PUSH4` matches every 4-byte constant in bytecode, not just this contract's selectors. Common false positives: interface IDs passed to `supportsInterface(bytes4)` (e.g. `0x80ac58cd` = ERC-721 — the contract is *asking about* the interface, not exposing it); selectors of external contracts the code calls (e.g. `token.transfer` from a router); 4-byte literals from `keccak256(...)`. Vet resolved signatures in §5 of the report — selectors that appear in the dispatch jump table (paired with `EQ` + `JUMPI` near the entrypoint) are real entrypoints; selectors that only appear once, embedded mid-function, are likely false positives. For diamonds, use the `facets()` return as the authoritative selector → facet mapping and treat the opcode-extracted set as a secondary signal only.
### 6. Resolve selector signatures
**Order matters** — local resolution is free; the API is rate-limited. Run in order, marking each selector as resolved as soon as one source matches.
1. **Known table** (instant, in-memory): `0x01ffc9a7` supportsInterface(bytes4); `0x1626ba7e` isValidSignature(bytes32,bytes); `0x150b7a02` onERC721Received(...); `0xf23a6e61` onERC1155Received(...); `0xbc197c81` onERC1155BatchReceived(...); `0x52d1902d` proxiableUUID(); `0x4f1ef286` upgradeToAndCall(address,bytes); `0xb61d27f6` execute(address,uint256,bytes); `0x3f707e6b` execute((address,uint256,bytes)[]). Add ERC20/721 standards (balanceOf, transfer, approve, ownerOf) if relevant.
2. **Local artifacts** (instant): `out/*/<ContractName>.json` → `.methodIdentifiers`. `jq -r '.methodIdentifiers | to_entries[] | "\(.value)\t\(.key)"' out/*.sol/*.json 2>/dev/null | grep -i "<selector-without-0x>"`.
3. **cast** (fast, no rate limit): `cast 4byte "<selector>"` for each still-unresolved selector.
4. **4byte.directory** (rate-limited; covers everything else): `decodeSelectors4byte` with no args reads `opcodes-selectors.md` and updates it. Set `FOURBYTE_DELAY=0.5` if you hit 429s.
Merge results into `opcodes-selectors.md` as a table **Selector | Signature** (use `—` for selectors that remain unresolved after all four steps).
**Short-circuit for very large contracts** (>500 selectors — diamonds, NFT marketplaces): if steps 1+2+3 already cover ≥80% of selectors, skip step 4 and note it in the report. The marginal value of resolving the long tail via 4byte is low and the runtime cost is high.
### 7. Write the report
```bash
generateUnverifiedContractReportSkeleton "$ADDRESS" "$NETWORK" > "report-unverified-$NETWORK-$SHORT.md"
```
Then fill in the body of each section. The skeleton already has the seven sections (Metadata, Input & validation, Artifacts, Selectors, Interface hints, Structure summary, Optional extras) — see the function definition in `script/playgroundHelpers.sh` for the canonical layout.
**Rule for the whole report: each fact lives in exactly one section.** Selectors go in §4; interface mapping in §5 references §4 by selector group, not by re-listing; artifact paths in §3, never repeated. A reference like "see §4" is fine and preferred over copying.
**Chat summary**: in the conversation, give a one-paragraph summary and the report path. Don't paste the full report back — the user can open the file.
## Failure modes
| What fails | What to do |
|---|---|
| `getRpcUrlFromNetworksJson` returns empty | Try `getRPCUrl` (env-based). If still empty, ask the user. Don't pick a public RPC unilaterally — see Step 2. |
| `cast storage` returns empty / `0x0` | EIP-1967 slot was never written → not a proxy (or uses a non-standard slot). `resolveContractTarget` handles this gracefully and returns `kind=direct`. |
| Heimdall decompile errors or times out | Disassemble-only is sufficient for selector extraction (Step 5). Skip decompile and note in §6 that decompile was not available. |
| 4byte returns 429 / network error | `decodeSelector4byte` prints `(no match)` and the loop continues — no abort. Re-run later with `FOURBYTE_DELAY=1.0` to fill in. |
| Opcodes file is huge (>50k lines) | Don't read it into the chat. §3 of the report keeps the path only. If you need a sample, `head -200`. |
| `extractSelectorsFromOpcodes` returns 0 selectors | Almost certainly an unresolved proxy. Re-run Step 3 against a different RPC and verify the implementation slot returned a non-zero address. |
| Token budget pressure on a very large contract | Skip step 6.4 (4byte for unknown selectors) and the optional extras in step 4. The core deliverable (selectors + interface hints + summary) survives without them. |