# solidity_prescan — Solidity/EVM contract pre-scan

Deterministic, stdlib-only security pre-scan for `.sol` source — the
sibling of `clarity_prescan` (Stacks) for the EVM side. Function-body
aware: braces are balanced, comments and string contents are blanked
first, and every function is judged on its whole body — including
whether an access gate exists inside it or on it.

**Honest scope:** a pre-scan flags the *shapes* audit findings are
made of for human review. Clean output != a safe contract. It cannot
see cross-contract trust, token economics, proxy storage layout, or
compiler-version semantics.

## Usage

```
python3 solidity_prescan.py <file-or-dir> [--json]
python3 solidity_prescan.py --selftest      # 44 vectors
```

Exit 0 = no HIGH/MEDIUM findings, 1 = findings, 2 = usage. Python 3.8+,
no dependencies, fully offline.

## What it flags

| code | sev | signature |
|---|---|---|
| SOL-TRANSFER-NOAUTH | HIGH/MED | public/external fn moves funds that aren't the caller's (`.call{value:}`, `.send`, `.transfer`, `safeTransfer(From)`, `transferFrom` with a non-sender source) with no gate |
| SOL-MINT-NOAUTH | HIGH/MED | unguarded `_mint`/`mint` on an entry point |
| SOL-STATE-NOAUTH | MED/LOW | public/external state write with no gate |
| SOL-REENTRANCY | HIGH/MED | `.call`/`.call{value:}` (HIGH) or `.send`/`.transfer` (MED) before a state write; reentrancy-guard modifier silences |
| SOL-DELEGATECALL | HIGH/MED/LOW | target is caller arg (HIGH), re-pointable state var (MED), `implementation()` accessor (LOW) |
| SOL-SELFDESTRUCT | MED | `selfdestruct` present |
| SOL-TXORIGIN | HIGH/LOW | `tx.origin` in an auth-shaped statement |
| SOL-UNCHECKED-CALL | MED/LOW | discarded low-level call result; captured `(bool ok,)` never read |
| SOL-BLOCK-RAND | MED | block values feeding keccak/selection/% (TWAP-style `% 2**32` truncation excluded) |
| SOL-SPOT-PRICE | MED/LOW | `getReserves`/`slot0`/`balanceOf(this)`/`kLast` accessor reads on a price path (AMM producer verbs → LOW) |
| SOL-INIT-UNGUARDED | MED | public/external `initialize`/`init` without initializer modifier or gate |
| SOL-MAX-APPROVE | LOW | `approve`/`increaseAllowance` with a max-uint literal |
| SOL-BALANCE-EQ | MED | `address(this).balance` under `==`/`!=` |
| SOL-ECRECOVER | LOW | `ecrecover` — replay/malleability surface |
| SOL-SECRET | HIGH | secret-named declaration holding a long hex literal |

## Gate model

An entry point is **gated** when it carries an auth-named modifier
(`only*`, auth/admin/role/permit/allow/restrict/operator/minter/
pauser/initializer), an in-file modifier whose body requires or checks
the sender (fixpoint over modifier-chains — `onlySteward → onlyAdmin`),
a `require`/`if`/`assert` on `msg.sender`/`_msgSender()`, a
`_checkRole`/`_checkOwner`/`hasRole` call, signature-auth
(`ecrecover`/`checkSignatures` + `require`), or a `require`/`if` on a
local whose initializer reads caller-scoped storage
(`require(bal >= amt)` where `bal = balances[msg.sender]`).

Entry points that *reference* the caller without a provable gate flag
MEDIUM/LOW with "verify the check lives upstream" — deliberately, not
silently. `internal`/`private`/`view`/`pure` functions never take the
NOAUTH findings; constructors are not entry points; test/fixture paths
downgrade one tier.

## Validated

- 44 selftest vectors covering every code, gate shapes, downgrade
  paths, and FP guards
- 9 production contracts (OpenZeppelin ERC20/Ownable/ReentrancyGuard/
  Initializable/ERC20Pausable, Solmate ERC20, Safe.sol, WETH9,
  UniswapV2Pair): **zero HIGH**, one MEDIUM (UniV2's deliberately
  permissionless mint — correctly framed as "verify"), LOW verify-notes
  only
- Hostile fixture fires all 15 classes; hardened fixture is silent
  except the inherent caller-scoped deposit note

Sample report: `sample-report.md` (fixture under `sample-repo/`).

Built by ARION (autonomous agent; machine-produced, self-verified).
