sc-audit

SkillSecurity

Lets your agent run a deep security audit of a smart contract repo or live on-chain contract and report exploitable bugs.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the sc-audit skill

About this capability

Deep security audit of a smart-contract repo OR a live on-chain contract by address - detect Solidity, model the protocol invariants and trust boundaries first, run Slither (best-effort) plus a bounded agentic pass that hunts for a path breaking each invariant, triage, adversarially verify, prove wi

What this skill tells your AI

The instructions your AI receives, as published by aeonfun/aeon in skills/sc-audit/SKILL.md and read by ahel’s review.

${var} - Target selector. Four forms:

  • `` (empty) -> auto-select the day's fresh feed target, audit only if it contains Solidity, else exit clean. See §S1 for the selection order (an optional sc-targets.json ledger, else the github-trending feed).
  • owner/repo -> audit that GitHub repo (e.g. Uniswap/v4-core, aave/aave-v3-origin).
  • <chain>:0x<address> -> on-chain mode: audit a live deployed contract by address. It fetches the verified source from the block explorer (Etherscan V2) or Sourcify, materializes it as a Foundry project, and runs the same pipeline - plus on-chain context (proxy/implementation, owner/admin, funds at risk). chain in eth/base/arbitrum/optimism/polygon/bsc/... (bare 0x<address> defaults to eth). Examples: base:0x4200000000000000000000000000000000000006, eth:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2. On-chain findings are operator-gated - never auto-filed/auto-emailed (see §S7). See §S1/§S2.
  • fixture:<name> -> local regression mode: audit the bundled fixture at skills/sc-audit/fixtures/<name>/ (e.g. fixture:vault). No fork, no dedup, no disclosure - a self-contained way to exercise the whole pipeline including the fuzz arm, with no external repo. See §S1/§S2.

Today is ${today}. Read memory/MEMORY.md and the last 30 days of memory/logs/ before starting.

Why this skill exists

Smart-contract bugs are logic and economics, not syntax. Slither matches known static patterns; it is weak on access control, protocol invariants, oracle/price manipulation, rounding/precision, upgradeability storage collisions, and cross-contract reentrancy - the classes that actually drain funds, and where exploitation on-chain is immediate and irreversible. That whole class is what an agentic reviewer catches by reading the source and reasoning about who can call what and which invariant breaks.

This skill is the contract arm split out of vuln-scanner. vuln-scanner detects Solidity and hands the repo here rather than running Slither inline; this skill owns the deep audit and then routes findings through vuln-scanner's shared disclosure machinery. It does not duplicate the disclosure/PVR/email logic - see §S7.

It audits two kinds of target: a GitHub repo (source in a repo you fork), and a live contract deployed on-chain by address (<chain>:0x<addr>) - the latter fetches the verified source from the block explorer/Sourcify and adds on-chain context (proxy/implementation, owner, funds at risk). A live deployed contract is where a bug is already exploitable with real money at stake, so on-chain findings are treated as the highest-stakes disclosure and are operator-gated - staged for a human, never auto-filed (§S7).

The agentic source pass is the reliable core. Slither is best-effort: a headless run has slither allow-listed but not solc / forge / solc-select, so contracts that need a compiler to build may not compile in-run. The source pass needs no compiler, so a clean audit never depends on Slither succeeding.

S1. Detect Solidity and select the target

REPO="${var}"                    # owner/repo | empty (auto) | fixture:<name> | <chain>:0x<addr> | 0x<addr>
if   [ "${REPO#fixture:}" != "$REPO" ]; then MODE=fixture; FIXTURE="${REPO#fixture:}"
elif [[ "$REPO" =~ ^([a-zA-Z0-9-]+:)?0x[0-9a-fA-F]{40}$ ]]; then MODE=onchain   # deployed-contract selector
else MODE=repo; fi                                                              # owner/repo, or empty -> auto-select
  • MODE=fixture -> audit the bundled fixture skills/sc-audit/fixtures/$FIXTURE/ (see §S2). Skip the dedup ledger and skip disclosure entirely (§S7/§S8) - fixtures are deliberately-vulnerable regression targets meant to be re-run on demand, never disclosed. If the fixture dir is missing, log no-fixture: $FIXTURE and exit clean.

  • MODE=repo, $REPO set -> that is the target. Confirm it holds Solidity before forking: gh api /search/code?q=repo:$REPO+extension:sol --jq '.total_count' (or just proceed and detect after clone in S2). If the repo has no *.sol, log no-solidity: $REPO and exit clean - this is the wrong skill for it.

  • MODE=repo, $REPO empty -> auto-select a repo target, best-first:

    1. Optional ledger first. If an sc-source-style ledger exists at memory/sc-targets.json (schema {updated, repos:[{repo, stars, tier, desc, first_seen}]}, best-first, already Solidity-scoped and deduped), read it and take repos[0].repo. If that repo was scanned within 30 days per memory/vuln-scanned.json, or turns out to hold no real Solidity after clone (S2), walk down repos[]. This ledger is optional; the skill stands alone without it.
    2. Trending feed fallback. If no ledger is present or it is exhausted, read the github-trending feed at output/.chains/github-trending.md (most recent by ISO header date) and walk its repos for one that contains Solidity.
    3. If none contain Solidity, log no-solidity-target and exit clean.

    On-chain candidates never enter auto-mode - the empty-$REPO path is repo-only. Audit a live contract by passing an explicit <chain>:0x<addr> selector.

  • MODE=onchain -> resolve the chain to an Etherscan V2 chain id and normalize the address, then fetch verified source in §S2:

    ADDR="${REPO##*:}"                                    # after last ':' (or the whole string if none)
    CHAIN="${REPO%:*}"; [ "$CHAIN" = "$REPO" ] && CHAIN="eth"   # before ':' or default eth
    CHAIN=$(printf '%s' "$CHAIN" | tr 'A-Z' 'a-z')
    ADDR=$(printf '%s' "$ADDR" | tr 'A-Z' 'a-z')          # lowercase; explorer/Sourcify are checksum-insensitive
    case "$CHAIN" in
      eth|ethereum|mainnet) CID=1 ;;      base)          CID=8453 ;;
      arbitrum|arb)         CID=42161 ;;  optimism|op)   CID=10 ;;
      polygon|matic)        CID=137 ;;    bsc|bnb)       CID=56 ;;
      avalanche|avax)       CID=43114 ;;  gnosis|xdai)   CID=100 ;;
      scroll)               CID=534352 ;; linea)         CID=59144 ;;
      zksync)               CID=324 ;;    blast)         CID=81457 ;;
      sepolia)              CID=11155111 ;; base-sepolia) CID=84532 ;;
      *) echo "unknown-chain: $CHAIN (add its Etherscan V2 chainid to S1 to support it)"; exit 0 ;;
    esac
    echo "onchain target: chain=$CHAIN cid=$CID addr=$ADDR"
    

    For a Blockscout-only chain not on Etherscan V2, add its BLOCKSCOUT host in §S2b and the Etherscan calls no-op cleanly (verified source comes from the Sourcify/Blockscout fallback).

    On-chain findings are operator-gated - a live contract holding funds is the highest-stakes disclosure, so this mode NEVER auto-files a PVR, auto-sends an email, or opens any public channel; it stages an operator-gated draft and notifies (see §S7).

Dedup (mandatory in MODE=repo and MODE=onchain, same ledger as vuln-scanner). Before auditing, skip a target already covered in the last 30 days: read memory/vuln-scanned.json and skip any row inside the window - keyed on $REPO for a repo, on onchain:$CHAIN:$ADDR for an address. For a repo, also check gh api /repos/$REPO/security-advisories - a repo with a published/credited advisory for the same finding class is already handled; skip and log. This is the identical dedup contract described in vuln-scanner §A1 / §A6. MODE=fixture bypasses dedup (re-runnable).

S2. Get the code (fork a repo, copy a fixture, or fetch on-chain source)

Capture $WORKDIR first so every write lands in the real repo, not the throwaway target. Every mode works inside gitignored .scan/, so the build artifacts (out/, cache/, crytic-export/, fuzz corpus/) never touch the tracked tree.

WORKDIR="$(git rev-parse --show-toplevel)"   # aeon repo root - memory/ and state live here
mkdir -p "$WORKDIR/.scan"
if [ "$MODE" = fixture ]; then
  # Local regression: copy the bundled fixture into gitignored .scan/ and audit the COPY
  # (never the tracked fixture) so forge's out/cache stay out of the working tree. No fork.
  SRC="$WORKDIR/skills/sc-audit/fixtures/$FIXTURE"
  [ -d "$SRC" ] || { echo "no-fixture: $FIXTURE"; exit 0; }
  rm -rf "$WORKDIR/.scan/$FIXTURE"; cp -r "$SRC" "$WORKDIR/.scan/$FIXTURE"
  # If the sandbox refuses `cp`, replicate the fixture files with the Read/Write tools instead
  # and verify byte-identical with `diff -r "$SRC" "$WORKDIR/.scan/$FIXTURE"`.
  cd "$WORKDIR/.scan/$FIXTURE"
elif [ "$MODE" = onchain ]; then
  # Live contract by address: fetch the VERIFIED source from the explorer/Sourcify and
  # materialize it as a Foundry project under .scan/. See §S2b for the fetch + materialize +
  # on-chain-context steps; it lands you in the project dir. If no verified source exists,
  # §S2b exits clean (bytecode-only audit is out of scope).
  PROJ="$WORKDIR/.scan/onchain-$CHAIN-$ADDR"
  echo "onchain project dir: $PROJ  (materialize per §S2b, then cd there)"
  # >>> run §S2b here <<<  - after it, you are in "$PROJ" with src/ + foundry.toml written.
else
  cd "$WORKDIR/.scan"
  gh repo fork "$REPO" --clone --default-branch-only -- --depth 50 --quiet
  cd "$(basename "$REPO")"                     # now in <workdir>/.scan/<repo>
fi
# --- Scratch dir for this run's intermediate files (scan JSON, sources.txt, fuzz harness).
# Prefer /tmp; fall back to a gitignored dir beside the clone under .scan/ when the skill
# sandbox blocks /tmp. RE-RUN these three lines at the top of every later Bash block that
# touches scratch (S4, S6.5) - claude -p spawns a FRESH shell per Bash call, so $SCRATCH does
# NOT persist (cwd does, shell vars don't). `$(cd .. && pwd)` is .scan/ (you're in .scan/<target>).
SCRATCH=/tmp/sc-audit
mkdir -p "$SCRATCH" 2>/dev/null && [ -w "$SCRATCH" ] || SCRATCH="$(cd .. && pwd)/_sc-audit"
mkdir -p "$SCRATCH"; echo "scratch: $SCRATCH"
# Confirm Solidity actually present (auto-select already filtered, but a direct $REPO may not have):
if ! ls **/*.sol >/dev/null 2>&1 && [ -z "$(find . -name '*.sol' -not -path '*/node_modules/*' 2>/dev/null | head -1)" ]; then
  echo "no-solidity after clone: $REPO"        # log a clean no-op row in S8 and exit
fi

S2b. On-chain source fetch, materialize, and context (MODE=onchain only)

Run this only when MODE=onchain. It fetches the contract's verified source, writes it as a Foundry project under $PROJ, and records on-chain context that drives severity. If no verified source exists, log it and exit clean - a bytecode-only audit is out of scope (decompilation is unreliable and would produce unfalsifiable findings; the honest output is "source not verified, cannot audit").

1. Fetch the verified source. Prefer Etherscan V2 (one key, ~60 chains, best coverage); fall back to Sourcify (keyless), then to Blockscout's keyless REST for Blockscout-explorer chains that are on neither. The Etherscan key goes through ./secretcurl as a {ETHERSCAN_API_KEY} placeholder (it ends _KEY, so it substitutes) - never put the raw key on the command line. Presence-check the key with ${VAR:+x}, not a bare $VAR (a bare secret expansion is blocked by the Bash layer):

SCRATCH=/tmp/sc-audit; mkdir -p "$SCRATCH" 2>/dev/null && [ -w "$SCRATCH" ] || SCRATCH="$WORKDIR/.scan/_sc-audit"; mkdir -p "$SCRATCH"
# Blockscout base URL for chains NOT on Etherscan V2 (keyless REST). One line per chain.
case "$CHAIN" in
  *)              BLOCKSCOUT="" ;;
esac
VERIFIED=no
# (a) Etherscan V2 getsourcecode - only if a key is configured
if [ -n "${ETHERSCAN_API_KEY:+x}" ]; then
  ./secretcurl -s -w 'http=%{http_code}\n' -o "$SCRATCH/etherscan.json" \
    "https://api.etherscan.io/v2/api?chainid=$CID&module=contract&action=getsourcecode&address=$ADDR&apikey={ETHERSCAN_API_KEY}"
  # verified iff result[0].ABI is real source (NOT the literal "Contract source code not verified")
  if python3 - "$SCRATCH/etherscan.json" <<'PY'
import json,sys
try: r=json.load(open(sys.argv[1]))["result"][0]
except Exception: sys.exit(1)
sys.exit(0 if r.get("ABI","").strip() and r["ABI"]!="Contract source code not verified" and r.get("SourceCode","").strip() else 1)
PY
  then VERIFIED=etherscan; fi
fi
# (b) Sourcify keyless fallback (no key, or Etherscan had no verified source).
# Use API **v2**. The old v1 route (/server/files/any/$CID/$ADDR) is deprecated; do not fall
# back to it. v2 returns ONE object (not a file list) whose `sources` maps path -> {content},
# so it needs its own parse branch below.
if [ "$VERIFIED" = no ]; then
  curl -s -o "$SCRATCH/sourcify.json" \
    "https://sourcify.dev/server/v2/contract/$CID/$ADDR?fields=sources,compilation,proxyResolution,deployment" \
    2>/dev/null || true
  grep -q '"sources"' "$SCRATCH/sourcify.json" 2>/dev/null && VERIFIED=sourcify || true
fi
# (c) Blockscout v2 fallback - Blockscout-explorer chains that are on NEITHER Etherscan V2 nor
# Sourcify. Keyless GET /api/v2/smart-contracts/<addr> returns ONE object: source_code + file_path
# + additional_sources[{file_path,source_code}] + proxy_type + implementations
# + compiler_version/evm_version. Parsed by its own branch in step 2.
if [ "$VERIFIED" = no ] && [ -n "$BLOCKSCOUT" ]; then
  curl -s -o "$SCRATCH/blockscout.json" "$BLOCKSCOUT/api/v2/smart-contracts/$ADDR" 2>/dev/null || true
  if python3 - "$SCRATCH/blockscout.json" <<'PY'
import json,sys
try: d=json.load(open(sys.argv[1]))
except Exception: sys.exit(1)
sys.exit(0 if d.get("is_verified") and (d.get("source_code") or "").strip() else 1)
PY
  then VERIFIED=blockscout; fi
fi
echo "verified-source: $VERIFIED"
if [ "$VERIFIED" = no ]; then
  echo "onchain: source NOT verified for $CHAIN:$ADDR - cannot audit (bytecode-only out of scope)."
  # Write a clean dedup row (channel: skipped) + coverage note in S8, notify nothing, exit.
fi

2. Materialize the Foundry project at $PROJ (src/ + a synthesized foundry.toml). Etherscan's SourceCode field has three shapes - a plain flattened string, a single-brace { "File.sol": {"content": ...} } map, or a double-brace {{ ...standard-json... }} object - so parse with Python, not jq. This snippet handles all three plus the Sourcify file list, and pins solc/evm_version from the compiler metadata:

python3 - "$SCRATCH" "$PROJ" "$VERIFIED" <<'PY'
import json, os, re, sys
scratch, proj, src_from = sys.argv[1], sys.argv[2], sys.argv[3]
os.makedirs(os.path.join(proj, "src"), exist_ok=True)
def write(relpath, content):
    p = os.path.join(proj, "src", relpath.lstrip("/"))
    os.makedirs(os.path.dirname(p) or os.path.join(proj, "src"), exist_ok=True)
    open(p, "w").write(content)
solc = evm = name = ""; proxy = "0"; impl = ""
if src_from == "etherscan":
    r = json.load(open(os.path.join(scratch, "etherscan.json")))["result"][0]
    name = r.get("ContractName") or "Contract"; proxy = str(r.get("Proxy", "0")); impl = r.get("Implementation", "")
    m = re.search(r"v?(\d+\.\d+\.\d+)", r.get("CompilerVersion", "")); solc = m.group(1) if m else ""
    ev = (r.get("EVMVersion") or "").strip().lower(); evm = "" if ev in ("", "default") else ev
    s = (r.get("SourceCode") or "").strip()
    if s.startswith("{{") and s.endswith("}}"):
        for path, v in json.loads(s[1:-1]).get("sources", {}).items(): write(path, v.get("content", ""))
    elif s.startswith("{"):
        obj = json.loads(s)
        srcs = obj.get("sources", obj)
        for path, v in srcs.items():
            if isinstance(v, dict) and "content" in v: write(path, v["content"])
    else:
        write(f"{name}.sol", s)
elif src_from == "sourcify":  # sourcify API v2 - ONE object: sources{path:{content}} + compilation + proxyResolution
    d = json.load(open(os.path.join(scratch, "sourcify.json")))
    for path, v in (d.get("sources") or {}).items():
        write(path, v.get("content", "") if isinstance(v, dict) else v)
    comp = d.get("compilation") or {}
    name = comp.get("name") or "Contract"
    m = re.search(r"(\d+\.\d+\.\d+)", comp.get("compilerVersion", "")); solc = m.group(1) if m else solc
    ev = ((comp.get("compilerSettings") or {}).get("evmVersion") or "").strip().lower()
    evm = "" if ev in ("", "default") else ev
    pres = d.get("proxyResolution") or {}
    if pres.get("isProxy"):
        proxy = "1"
        impls = pres.get("implementations") or []
        # v2 implementation entries are dicts ({"address": ...}) or bare strings
        if impls: impl = impls[0].get("address", "") if isinstance(impls[0], dict) else impls[0]
else:  # blockscout v2 - /api/v2/smart-contracts: source_code + file_path + additional_sources[]
    d = json.load(open(os.path.join(scratch, "blockscout.json")))
    if d.get("file_path"): write(d["file_path"], d.get("source_code") or "")
    else: write(f'{d.get("name") or "Contract"}.sol', d.get("source_code") or "")
    for a in (d.get("additional_sources") or []):
        if a.get("file_path"): write(a["file_path"], a.get("source_code") or "")
    name = d.get("name") or "Contract"
    m = re.search(r"(\d+\.\d+\.\d+)", d.get("compiler_version", "")); solc = m.group(1) if m else solc
    ev = (d.get("evm_version") or "").strip().lower(); evm = "" if ev in ("", "default") else ev
    pt = d.get("proxy_type")
    if pt and str(pt).lower() not in ("none", "unverified"):
        proxy = "1"
        impls = d.get("implementations") or []
        if impls: impl = impls[0].get("address", "") if isinstance(impls[0], dict) else impls[0]
ft = ['[profile.default]', 'src = "src"', 'out = "out"', 'libs = ["lib"]']
if solc: ft.append(f'solc = "{solc}"')
if evm and evm != "default": ft.append(f'evm_version = "{evm}"')
open(os.path.join(proj, "foundry.toml"), "w").write("\n".join(ft) + "\n")
json.dump({"name": name, "solc": solc, "evm": evm, "proxy": proxy, "implementation": impl},
          open(os.path.join(scratch, "materialized.json"), "w"))
print(f"materialized: name={name} solc={solc} evm={evm} proxy={proxy} impl={impl}")
PY
cd "$PROJ"

3. If it's a proxy, also materialize the implementation - the proxy shell holds almost no logic; the bug is in the implementation it delegatecalls. When materialized.json has proxy=1 and a non-empty implementation, re-run steps 1-2 for that implementation address (fetch impl on the same $CID, write it under src/impl/) so S4/S5 audit the real logic. Note the proxy->impl relationship prominently in the report. If the explorer didn't flag a proxy, still probe the EIP-1967 implementation slot before concluding it's non-upgradeable (step 4).

4. Record on-chain context (drives severity - a live contract holding funds turns a "possible" into "exploitable now"). Best-effort via the Etherscan V2 proxy/account modules (same key), Blockscout's keyless Etherscan-compatible /api for Blockscout chains, or a public RPC; each is optional, never fatal. Write $SCRATCH/onchain-context.json:

if [ -n "$BLOCKSCOUT" ]; then
  # Blockscout chains: Etherscan V2 does not cover $CID, so read the native balance from
  # Blockscout's keyless Etherscan-compatible /api. Its result is a wei string in the SAME
  # {"status","message","result"} shape as Etherscan V2's balance response, so it parses
  # identically into native_balance_wei. Proxy/impl already came from the materialize step.
  # An optional BLOCKSCOUT_API_KEY is passed via secretcurl's {BLOCKSCOUT_API_KEY} placeholder -
  # the legacy /api honors ?apikey= on key-enforcing instances; instances that don't enforce
  # keyed tiers ignore it. Keyless otherwise. The v2 source endpoint above ignores the key.
  if [ -n "${BLOCKSCOUT_API_KEY:+x}" ]; then
    ./secretcurl -s -o "$SCRATCH/balance.json" \
      "$BLOCKSCOUT/api?module=account&action=balance&address=$ADDR&tag=latest&apikey={BLOCKSCOUT_API_KEY}"
  else
    curl -s -o "$SCRATCH/balance.json" \
      "$BLOCKSCOUT/api?module=account&action=balance&address=$ADDR&tag=latest" 2>/dev/null || true
  fi
elif [ -n "${ETHERSCAN_API_KEY:+x}" ]; then
  # native balance at the address = funds directly at risk (wei)
  ./secretcurl -s -o "$SCRATCH/balance.json" \
    "https://api.etherscan.io/v2/api?chainid=$CID&module=account&action=balance&address=$ADDR&tag=latest&apikey={ETHERSCAN_API_KEY}"
  # EIP-1967 implementation slot (proxy detection independent of the explorer flag)
  ./secretcurl -s -o "$SCRATCH/eip1967.json" \
    "https://api.etherscan.io/v2/api?chainid=$CID&module=proxy&action=eth_getStorageAt&address=$ADDR&position=0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc&tag=latest&apikey={ETHERSCAN_API_KEY}"
fi

Capture into $SCRATCH/onchain-context.json: {chain, cid, address, name, solc, evm_version, is_proxy, implementation, native_balance_wei, admin_or_owner (best-effort), notes}. Also note the ERC20/TVL exposure qualitatively if it's obvious from the contract type (a router/vault/bridge holding user funds is high-exposure). This context is non-sensitive (it does not aid exploitation) and rides into the S9 report and the staged disclosure so the operator sees the blast radius. From here, S3-S6.5 run unchanged against $PROJ - the materialized source is just another Foundry project in .scan/.

S3. Stage the toolchain and harden the build

slither, solc, solc-select, forge, crytic-compile, echidna, medusa are the contract toolchain. Every step self-guards with command -v, so any tool that is not present is skipped, never fatal - the source pass (S5) is the reliable core and needs none of them. Staging splits two ways:

  • In-run (pip-installable): slither-analyzer, solc-select (downloads solc), and crytic-compile install via the allow-listed python3 -m pip. This covers Slither on hardhat (via npx) and bare-solc layouts.
  • Workflow-staged (binaries): forge, echidna, medusa ship as tarballs/binaries whose installers need shell tooling that may not be allow-listed in-run, so they are only available when the instance provides a pre-claude -p staging step that installs them and appends their dir to $GITHUB_PATH. When no such step ran or a tool failed to stage: a foundry repo may not build (Slither degrades to compile-fail, source pass S5 still runs), and the fuzz arm (S6.5) is skipped cleanly.

Harden first - you are about to compile and (in S6.5) fuzz UNTRUSTED contract code. Compiling and running a foundry test executes the target repo's build and test code in this runner. Disable foundry FFI so a malicious foundry.toml/test can't shell out to the host, and only ever work inside the throwaway .scan/<repo> clone.

export FOUNDRY_FFI=false            # block forge tests from shelling out to the host
export PATH="/tmp/bin:$HOME/.local/bin:/usr/local/bin:$HOME/.foundry/bin:$PATH"
# In-run pip tools (crytic-compile drives the build for slither/echidna/medusa):
command -v slither      >/dev/null 2>&1 || python3 -m pip install --quiet --disable-pip-version-check slither-analyzer 2>/dev/null || true
command -v solc-select  >/dev/null 2>&1 || python3 -m pip install --quiet --disable-pip-version-check solc-select    2>/dev/null || true
command -v crytic-compile >/dev/null 2>&1 || python3 -m pip install --quiet --disable-pip-version-check crytic-compile 2>/dev/null || true
# forge/echidna/medusa: use them only if a workflow step already staged them (command -v guards below).

Pick the contract's solc version when a compile fails on version mismatch: read the pragma solidity line and solc-select install <ver> && solc-select use <ver> (solc-select downloads the binary via Python - this works in-run). crytic-compile auto-detects hardhat (npx hardhat), foundry (forge, if staged), and bare-solc layouts.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
750
Forks
264
Last commit
Sep 2026

ahel review

  • K1binfo
    installs-packages

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
sc-audit
Source
github.com/aeonfun/aeon