# Clarity (Stacks) contract pre-scan (ARION service)

Deterministic scanner for the shapes audit findings are made of in
**Clarity smart contracts** — `.clar` source, `Clarinet.toml`, and
deployment plans. Eleven finding classes decided by form-level
properties — no chain, no node, no inference:

1. **`CLR-TRANSFER-NOAUTH` (HIGH/MEDIUM)** — a `define-public` moves
   funds that are not the caller's: the transfer/burn sender is the
   contract (`as-contract`, stored principal, literal, or param) rather
   than `tx-sender`/`contract-caller`, and no caller gate exists in the
   body (no check-shaped `asserts!`/`is-eq`/`map-get?` membership on the
   sender, and no call to a checker helper). HIGH when no caller
   identity appears at all; MEDIUM when the caller's identity is in
   play. `tx-sender` **inside** an `as-contract` block rebinds to the
   contract — the classic drain shape counts as contract funds.
2. **`CLR-MINT-NOAUTH` (HIGH)** — public `ft-mint?`/`nft-mint?` with no
   caller gate: unguarded supply.
3. **`CLR-AS-CONTRACT` (HIGH/MEDIUM)** — an `as-contract` block wrapping
   a value or mint op. HIGH when the function carries no caller gate
   *and* no caller identity; MEDIUM otherwise — `as-contract` drops the
   caller principal and acts with the contract's full authority, so
   every entry path warrants review.
4. **`CLR-STATE-NOAUTH` (MEDIUM/LOW)** — public `var-set`/`map-set`/
   `map-delete`/`map-insert` with no caller gate. MEDIUM when no caller
   identity appears anywhere; LOW on caller-referencing paths (likely
   caller-scoped — verify).
5. **`CLR-DYNAMIC-CALL` (MEDIUM/LOW)** — `contract-call?` whose target
   is dynamic: a caller-supplied `principal` arg with no trait bound
   (MEDIUM), an identifier that is neither a declared trait arg nor a
   pinned constant (MEDIUM), or a `define-data-var` principal that
   contract logic can re-point (LOW). Literal `.name`/`'name` targets,
   trait-bounded args (including `(optional <trait>)` and `match`-bound
   locals), and `define-constant` names stay silent — a trait bound
   pins the *interface*, never the identity.
6. **`CLR-BLOCK-RAND` (MEDIUM)** — miner-influenceable selection:
   `get-block-info?` on `vrf-seed`/`id-header-hash`/`time`/
   `header-hash`, or `mod`/`rem` arithmetic fed by `block-height`,
   `burn-block-height`, or `stacks-block-height`.
7. **`CLR-UNWRAP-PANIC` (MEDIUM, aggregated)** — `unwrap-panic` /
   `unwrap-err-panic` calls counted per file by inner fallible source
   (`map-get?`, `contract-call?`, `as-max-len?`, …). A forced abort
   bricks the call path — prefer `unwrap!`/`asserts!` with typed
   errors.
8. **`CLR-CALL-IGNORED` (LOW)** — `contract-call?` as a non-final child
   of a `begin` block: the response value is discarded, so a failed
   upstream call reads as success downstream. Calls in tail position
   (the returned value) stay silent.
9. **`CLR-ASSERT-TRUE` (LOW)** — `asserts!` on a tautology (`asserts!
   true`, `is-eq` of a term with itself): the guard cannot fail; the
   check it was meant to hold is missing.
10. **`CLR-DEPRECATED-HEIGHT` (LOW, aggregated)** — bare `block-height`
    reads, deprecated under Clarity 3 in favour of
    `stacks-block-height` (`burn-block-height` remains for burnchain
    comparisons).
11. **`CLR-SECRET-IN-FILE` (HIGH/MEDIUM)** — `mnemonic` / `private_key`
    / `seed_phrase` / `signing-key`-style literals in `Clarinet.toml`,
    `Mainnet.toml`, deploy plans, or `.clar` constants. HIGH in
    mainnet-named configs and code constants; MEDIUM elsewhere —
    devnet mnemonics are expected, verify it is the throwaway default.

## Caller-gate model

A public function is treated as **gated** when a `tx-sender` /
`contract-caller` token sits inside (or right after) a check-shaped
opener — `asserts!`, `is-eq`, `if`, `map-get?` membership,
`is-standard`, `unwrap!`, `try!` — or when it calls a **checker
helper**: a defined function that itself gates, resolves transitively
(three rounds covers `is-authorized → is-admin-calling` delegation),
including auth-named helpers (`auth|owner|admin|guard|only-|allow|
permit|role|access`) whose bodies contain assert/err shapes — signature
and passkey auth never reads `tx-sender`. Sender refs inside
`as-contract` spans name the contract, never the caller.

## Honest scope

This is a **deterministic pre-scan for human review**, not an audit and
not proof of safety. It cannot see post-conditions, trait
implementations, economic invariants, or cross-contract call graphs.
Permissionless-by-design functions (keepers, liquidation `seize`,
anyone-can-settle) will flag HIGH — that is the intended review order.
Clean output != a safe contract.

## Usage

```
python3 clarity_prescan.py <file-or-dir>          # markdown report
python3 clarity_prescan.py <file-or-dir> --json   # {"findings":[...]}
python3 clarity_prescan.py --selftest             # 43 vectors
```

Stdlib only. Exit 0 = no HIGH/MEDIUM findings, 1 = findings, 2 = usage.
Test/spec/fixture paths downgrade one severity tier.

`sample-report.md` is the verbatim output over `sample-repo/` —
a hostile fixture exercising every class and a clean fixture showing
the silent side.
