> ## Documentation Index
> Fetch the complete documentation index at: https://docs.walletwall.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Scanner Evidence & Assurance Model

> How the Vault prototype's static-analysis evidence is produced, adjudicated, receipted and presented — and why a GitHub alert count is a presentation view, not a vulnerability count.

# Scanner Evidence & Assurance Model

<Warning>
  **EXPERIMENTAL · NOT AUDITED · NOT PRODUCTION · NO DEPLOYMENT.** Everything on this page describes assurance tooling for the [vNext kernel prototype](/vault/vnext-kernel-status) in the public [`Wallet-Wall/walletwall-vault`](https://github.com/Wallet-Wall/walletwall-vault) repository. Scanner evidence, triage and tests demonstrate repository-level properties of that prototype. They are not an external security audit, and they say nothing about a deployed WalletWall vault — there is none.
</Warning>

The vault prototype is analysed in CI by [Slither](https://github.com/crytic/slither). This page explains what happens to that output: which artifact is the evidence, which artifact is only a view of it, how a human adjudication is bound to the scanner run it adjudicated, and why a stable finding identity does not prove that the reasoning attached to it is still true.

## Two layers: evidence and presentation

One pinned Slither execution produces two kinds of output, and they serve different purposes.

```mermaid theme={null}
flowchart TD
    accTitle: Vault prototype scanner flow
    accDescr: A pinned Slither run writes raw JSON and raw SARIF containing every result. Both raw files are kept as workflow artifacts. The raw JSON feeds the triage completeness gate and the scanner receipt checks. A projection reduces the raw output to one result per distinct project-owned finding; only if projection integrity passes is that projection uploaded to GitHub Code Scanning. The triage gate runs separately, so an untriaged finding is uploaded and visible while CI fails.

    S["Pinned Slither run<br/>vNext kernel contracts"]
    RJ["Raw JSON<br/>every result"]
    RS["Raw SARIF<br/>every result"]
    ART["Workflow artifacts<br/>kept unmodified"]
    P["Projection<br/>drop dependency-only results<br/>collapse exact compilation-unit copies"]
    I{"Projection integrity<br/>holds?"}
    GH["GitHub Code Scanning<br/>one result per distinct<br/>project-owned finding"]
    NO["No upload<br/>fail closed"]
    T{"Every distinct finding<br/>adjudicated in triage?"}
    RC["Receipt checks<br/>SCANNER_EVIDENCE.json"]
    RED["CI fails<br/>finding stays visible"]

    S --> RJ
    S --> RS
    RJ --> ART
    RS --> ART
    RJ --> P
    RS --> P
    P --> I
    I -->|yes| GH
    I -->|no| NO
    RJ --> T
    T -->|yes| RC
    T -->|no| RED

    classDef input fill:#FAF8F3,stroke:#C9A47A,color:#1E1A14,stroke-width:1.5px;
    classDef process fill:#BF4E32,stroke:#8B3120,color:#FAF8F3,stroke-width:1.5px;
    classDef decision fill:#C9A47A,stroke:#8B6F47,color:#1E1A14,stroke-width:1.5px;
    classDef output fill:#E6DED2,stroke:#9A9186,color:#1E1A14,stroke-width:1.5px;
    classDef risk fill:#8B3120,stroke:#1E1A14,color:#FAF8F3,stroke-width:1.5px;
    classDef datastore fill:#1E1A14,stroke:#9A9186,color:#FAF8F3,stroke-width:1.5px;
    classDef external fill:#FAF8F3,stroke:#9A9186,color:#1E1A14,stroke-width:1.5px;
    classDef security fill:#1E1A14,stroke:#C9A47A,color:#FAF8F3,stroke-width:1.5px;

    class S,P process;
    class RJ,RS datastore;
    class ART output;
    class I,T decision;
    class GH external;
    class RC security;
    class NO,RED risk;
```

<Tabs>
  <Tab title="Raw evidence">
    **What it contains:** every result Slither reports, including results located entirely inside third-party dependencies and repeated copies of the same finding.

    **Where it goes:** the raw JSON feeds the triage completeness gate and the scanner receipt. Raw JSON and raw SARIF are both kept as unmodified workflow artifacts.

    **What it is authoritative for:** what the scanner observed. The evidence chain reads only the raw JSON; it has no SARIF input at all.
  </Tab>

  <Tab title="GitHub presentation">
    **What it contains:** one SARIF result per distinct project-owned finding.

    **Where it goes:** GitHub Code Scanning, where each result becomes an alert that a reviewer can read, discuss and track.

    **What it is authoritative for:** nothing. It is a presentation projection. No projection — correct, wrong or absent — can make the triage gate pass.
  </Tab>
</Tabs>

### Why a projection exists

Uploading the raw SARIF directly turned every raw result into its own GitHub alert. Two properties of the pinned toolchain made that view misleading:

* **Dependency-only results.** Findings located entirely inside OpenZeppelin library code are real scanner output, but they are not useful alerts about this repository.
* **Compilation-unit copies.** The prototype is compiled one entry point at a time, so a source file reached from several entry points is reported several times. GitHub does not collapse byte-identical results, so one finding appeared as several alerts.

The projection makes exactly two transformations and carries every other byte of the SARIF unchanged:

1. **Drop dependency-only results.** Ownership is decided from the raw JSON across *every* element of a finding, not only the primary location SARIF carries. A finding that involves project code and a dependency does not silently disappear: the upload fails and a human decides. The single narrow exception is compiler-version lints whose primary location is a dependency and whose only project elements are `pragma` directives; each such exclusion is listed by name, and the raw SARIF keeps it.
2. **Collapse exact copies.** Copies collapse only when every member is byte-identical in both the raw JSON and the SARIF. If two *distinct* project findings would collapse into one alert, the projection fails rather than keep one and hope.

Projection integrity — can the raw output be safely reduced to one result per distinct project-owned finding? — gates the upload. It fails closed on malformed input, unmapped or ambiguous results, and any silent collapse.

### New findings stay visible

Adjudication completeness is a **separate** gate, and it never suppresses presentation. A new, structurally valid finding that nobody has triaged yet is uploaded to GitHub *and* turns CI red. The finding is visible in Code Scanning while the build explains why it is failing.

```text theme={null}
scanner completeness  ≠  dashboard presentation
dashboard cleanliness ≠  scanner greenness
```

A short alert list does not mean Slither found little: the raw evidence is complete and preserved. A clean-looking alert list does not mean CI is green: an untriaged finding still fails the triage gate.

## Evidence lineage: subject, triage and container

Triage is a committed file, `slither-triage.json`, holding one human adjudication per distinct finding. A receipt, `SCANNER_EVIDENCE.json`, binds that adjudication to the scanner run it adjudicated. The repository distinguishes three commits:

```mermaid theme={null}
flowchart LR
    accTitle: Scanner evidence lineage
    accDescr: The source subject is the commit whose scanner inputs were analysed and yields the scanner observation. The triage subject is the commit whose triage file adjudicated those findings. The receipt names both subjects. Their currency is licensed only by byte-equal scanner input scope. The publication container, the commit that publishes the receipt, is established afterwards from git and is never named inside the receipt.

    SS["Source subject<br/>commit whose scanner inputs<br/>were analysed"]
    OBS["Scanner observation<br/>raw findings"]
    TS["Triage subject<br/>commit whose slither-triage.json<br/>adjudicated them"]
    ADJ["Adjudication<br/>one entry per distinct finding"]
    SCOPE["Scanner input scope<br/>byte-equal between subjects"]
    REC["Receipt<br/>SCANNER_EVIDENCE.json<br/>names both subjects"]
    PC["Publication container<br/>commit that publishes the receipt<br/>established afterwards from git"]

    SS --> OBS --> REC
    TS --> ADJ --> REC
    SCOPE -.->|licenses currency| REC
    REC --> PC

    classDef input fill:#FAF8F3,stroke:#C9A47A,color:#1E1A14,stroke-width:1.5px;
    classDef process fill:#BF4E32,stroke:#8B3120,color:#FAF8F3,stroke-width:1.5px;
    classDef decision fill:#C9A47A,stroke:#8B6F47,color:#1E1A14,stroke-width:1.5px;
    classDef output fill:#E6DED2,stroke:#9A9186,color:#1E1A14,stroke-width:1.5px;
    classDef datastore fill:#1E1A14,stroke:#9A9186,color:#FAF8F3,stroke-width:1.5px;
    classDef security fill:#1E1A14,stroke:#C9A47A,color:#FAF8F3,stroke-width:1.5px;

    class SS,TS input;
    class OBS,ADJ process;
    class SCOPE decision;
    class REC security;
    class PC output;
```

| Term                      | Meaning                                                                                                                                                                                                                                                                 |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Source subject**        | The commit whose scanner-relevant inputs were analysed.                                                                                                                                                                                                                 |
| **Triage subject**        | The commit whose `slither-triage.json` adjudicated those findings.                                                                                                                                                                                                      |
| **Publication container** | The commit that publishes the receipt. The receipt deliberately never names it — a file that embedded its own publishing commit would make its own bytes part of the identifier it is trying to state — so a separate check establishes it from git history afterwards. |

Carrying a scanner result from one commit to another is licensed **only** by byte equality of the scanner input scope (the contracts tree, the dependency import closure and the scanner configuration). Equal finding counts, equal raw hashes and commit ancestry are not enough: all three once held across a change that moved 21 of 33 adjudicated findings.

### A rationale edit is not evidence-neutral

The receipt generator requires the triage file on disk to be byte-identical to the triage file at the receipt's declared triage subject. That binding is what stops a receipt from claiming one adjudication while a different one is applied.

It has a consequence that is easy to miss: **changing only the prose of a rationale changes the triage bytes**, so the old receipt no longer describes the triage in the tree. The correction of two false-positive rationales in `v0.13.15` showed this directly:

* the scanner findings were unchanged, and the receipt's source subject did not move;
* both classifications stayed `FALSE_POSITIVE`;
* only the rationale text changed;
* so a successor triage subject and a republished receipt were still required.

<AccordionGroup>
  <Accordion title="The v0.13.15 commits, for reviewers">
    | Role                                                     | Commit                                                                                                        |
    | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
    | Source subject (unchanged)                               | [`6ecf0b58`](https://github.com/Wallet-Wall/walletwall-vault/commit/6ecf0b584f720fd256da1aa916884e3419f8603f) |
    | Successor triage subject — the two rationale corrections | [`a4e305ac`](https://github.com/Wallet-Wall/walletwall-vault/commit/a4e305ac1b5e66f57c2a2164d080184437bdfe68) |
    | Publication container — the republished receipt          | [`0f7080d7`](https://github.com/Wallet-Wall/walletwall-vault/commit/0f7080d76f3512850ea8f1fd72b847c1e5908bca) |
    | Merged to `main` as `v0.13.15`                           | [`6463caee`](https://github.com/Wallet-Wall/walletwall-vault/commit/6463caee352beda7a27009d29f1a48b42b063fd8) |

    Read the receipt itself: [`SCANNER_EVIDENCE.json` at `v0.13.15`](https://github.com/Wallet-Wall/walletwall-vault/blob/6463caee352beda7a27009d29f1a48b42b063fd8/prototype/vnext-kernel/SCANNER_EVIDENCE.json).
  </Accordion>
</AccordionGroup>

## Stable identity, stale rationale

<Info>
  **A stable finding identity proves that the scanner is still describing the same finding. It does not prove that the human rationale attached to that finding is still true after the surrounding code changes.**
</Info>

Findings are keyed by a **semantic identity** (`semanticId`): a hash over the detector, each element's file, element chain and signature, and the detector's message with line references removed. It contains no line numbers, so a finding that only moves keeps its identity and its adjudication. That is deliberate. In the same drift described above, an earlier location-based key re-keyed 21 of 33 adjudicated findings when code merely moved, and reported them as untriaged.

Stability of identity is the right property for *matching*. It is the wrong property for *trusting*. The triage validation gate proves that every triage key still matches a live finding; it never reads rationale prose. A rationale can therefore become false — or be false from the start — while its finding stays byte-stable and every gate stays green.

Two findings in the prototype showed both ways this happens.

<AccordionGroup>
  <Accordion title="A premise that was never true — zero-default movement in egress (GitHub alert #148)">
    **Finding:** `uninitialized-local` on the local `moved` in the kernel's `egress`.

    **Old premise:** `moved` "is assigned on every path" and is "never read before assignment".

    **Why it was false:** the ERC-20 branch assigns `moved` only when the vault holds a positive balance of the asset. When the balance is zero, nothing assigns it.

    **Correct rationale:** a Solidity numeric local holds zero until assigned — it is never indeterminate. A zero-balance ERC-20 egress intentionally attempts no transfer, and emitting `Egressed(asset, destination, 0)` reports the correct movement: none. The classification stays `FALSE_POSITIVE` on the corrected argument.

    **Pinned by execution (P-148):** a vault egressing an ERC-20 it holds none of succeeds, moves nothing, emits `Egressed(asset, destination, 0)` and changes no other state. The positive control shows the same revert-on-transfer token *with* a balance is reached and refused, so the zero-balance result is attributable to the premise and not to a broken fixture.
  </Accordion>

  <Accordion title="A premise invalidated by a later change — verifier admission in initialize (GitHub alert #349)">
    **Finding:** `reentrancy-events` on the factory's `deployVault`, which deploys a clone, calls its `initialize`, then emits `VaultDeployed`.

    **Old premise:** the clone's `initialize` "makes no external call". That was accurate when written.

    **What changed:** the verifier-provenance lane (SD-11) made `initialize` call the verifier authority the factory binds — one read-only `STATICCALL`. Slither's message names only the outer `initialize` call, so the finding never changed, and the entry was carried forward without its prose being re-read.

    **Correct rationale:** the static execution context propagates through every frame the authority opens, so a callback cannot `CREATE2`, `SSTORE` or `LOG` — it cannot deploy a second vault or emit a second event. A callback into the half-built clone's `initialize` is refused because the initialized flag is set before the call. The factory holds no mutable state. The classification stays `FALSE_POSITIVE` on the corrected argument.

    **Pinned by execution (P-349):** a hostile authority's reentrant `deployVault` halts with a state-change-during-static-call failure; no second vault or event appears and the factory is unchanged. Two controls make the result attributable: the identical callback from an ordinary `CALL` frame *does* deploy a vault, and a kernel mutant that makes the admission call with `CALL` instead of `STATICCALL` *does* reenter. The `STATICCALL` is the controlling mechanism — not the fixture and not the gas.
  </Accordion>
</AccordionGroup>

### Pinning a rationale's premises

The remedy is to turn the premises a rationale depends on into executable tests, so that a premise that stops holding turns a test red instead of leaving stale prose behind a green gate:

* **State the premise as behaviour.** Each premise test exercises the real kernel and factory, not a description of them.
* **Pair every attack with a positive control.** A refusal only counts when the same seam is shown to be reachable, so the test cannot pass because its setup is broken.
* **Tie the prose to the tests.** A separate check fails if a committed rationale drops its citation to the premise tests or reintroduces a withdrawn premise. It checks phrases and citations; it does not, and cannot, prove English prose true.

This is the same discipline as [Independent Authority](/architecture/independent-authority): proof credit belongs to the mechanism that independent evidence actually attributes the result to. A rationale is a claim about a mechanism, so it needs evidence that can contradict it.

## Accepted does not mean erased

The triage uses four classifications:

| Classification             | Meaning                                                                                                                             | GitHub presentation                  |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `ACCEPTED_DESIGN_TRADEOFF` | A real pattern the scanner correctly observes, kept deliberately with a documented reason.                                          | **Stays open and visible.**          |
| `FALSE_POSITIVE`           | The detector's concern does not apply, for a documented and — where it matters — executable reason.                                 | May be dismissed after adjudication. |
| `NON_SECURITY_STYLE`       | An observed pattern with no security consequence for the kernel — for example, a test adversary doing exactly what it exists to do. | May be dismissed after adjudication. |
| `OUT_OF_SCOPE_DEPENDENCY`  | A finding in supporting code outside the measured kernel, such as test-only fixtures that are never deployed as part of it.         | May be dismissed after adjudication. |

An accepted residual is **not** a false positive, and it is not hidden. It stays visible because:

* the scanner still observes the pattern, and the evidence should say so;
* the tradeoff is intentionally documented, and visibility keeps that documentation attached to the code it describes;
* a future code change can invalidate the reasoning that made the tradeoff acceptable — the same failure described in [stable identity, stale rationale](#stable-identity-stale-rationale);
* an open alert gives every reviewer, not only the original adjudicator, the chance to notice that drift.

GitHub's dismissal reasons are a coarser vocabulary than the triage classifications. The committed triage record, not the dismissal reason on an alert, is the adjudication.

## Status at the v0.13.15 checkpoint

<Note>
  **Measured at `walletwall-vault` `v0.13.15`** ([`6463caee`](https://github.com/Wallet-Wall/walletwall-vault/commit/6463caee352beda7a27009d29f1a48b42b063fd8), 19 September 2026). These figures are a dated snapshot of an operational view. They are not architectural constants, and any later code change can move them.
</Note>

| Measure                                                     | Value                                                                                                      |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| Raw Slither results (vNext kernel)                          | 295                                                                                                        |
| Project-owned raw rows                                      | 68                                                                                                         |
| Distinct project-owned findings                             | 42 (the other 26 rows are compilation-unit copies)                                                         |
| Dependency-only results                                     | 222                                                                                                        |
| Dependency-anchored compiler-version lints excluded by name | 5                                                                                                          |
| Results uploaded to GitHub for the vNext kernel             | 42                                                                                                         |
| Triage classifications                                      | 21 `ACCEPTED_DESIGN_TRADEOFF` · 11 `FALSE_POSITIVE` · 5 `NON_SECURITY_STYLE` · 5 `OUT_OF_SCOPE_DEPENDENCY` |
| Untriaged or stale triage entries                           | 0                                                                                                          |
| Open CodeQL alerts                                          | 0                                                                                                          |

At this checkpoint, GitHub Code Scanning exposes **26 intentionally retained observations**. Twenty-three are accepted residuals or design tradeoffs: the 21 vNext kernel findings classified `ACCEPTED_DESIGN_TRADEOFF`, and two calls-in-a-loop observations in the separate Slither scan of the repository's main `contracts/` tree (the current vault contracts, outside the vNext prototype). Three are separate source, style or conformance observations still under review, all in that same scan:

* **#110** — an interface/declaration conformance review;
* **#111** and **#115** — naming-style observations.

**The open-alert count is an operational view, not a vulnerability count.**

<AccordionGroup>
  <Accordion title="How the open-alert count moved from 309 to 26 — and what that was not">
    The drop in open alerts was **not** 283 vulnerabilities fixed. It was mostly scanner-presentation reconciliation. The raw evidence stayed complete throughout, and accepted residuals stayed visible.

    | Open alerts | What changed                                                                                                                                                                                                       | What it was not                                                |
    | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
    | **309**     | Before projection: 14 alerts from the main `contracts/` scan plus every one of the 295 raw vNext kernel results as its own alert.                                                                                  | —                                                              |
    | **56**      | The SARIF projection (`v0.13.14`) stopped uploading 222 dependency-only results, 5 dependency-anchored version lints and 26 duplicate copies. GitHub closed those 253 alerts because they were no longer reported. | Nothing was dismissed, and no scanner output was deleted.      |
    | **28**      | Adjudicated false-positive and test-fixture alerts were dismissed one by one, each against its adjudicated disposition.                                                                                            | Accepted residuals were not dismissed.                         |
    | **26**      | The two false-positive rationales described above were re-adjudicated on corrected premises (`v0.13.15`) and then dismissed.                                                                                       | Their classification did not change; only their reasoning did. |
  </Accordion>
</AccordionGroup>

## What this evidence does not establish

The scanner receipt states its own limits. In summary:

* **Detector coverage bounds the result.** Slither cannot report a defect its detector model cannot express.
* **No Solidity result from CodeQL.** GitHub CodeQL has no Solidity extractor, so CodeQL runs cover the prototype's TypeScript tooling only.
* **Identity is a judgement at the edges.** A finding reported under a different element chain presents as one finding removed and another added — the conservative direction, but a judgement.
* **A change the fingerprint cannot see is not proven harmless.** Surrounding-source changes force re-adjudication without asserting more than that.
* **Nothing outside the prototype's contracts and declared dependency closure.** Audit status, formal verification and verifier assurance are stated elsewhere and are not implied here.

## Related

<Columns cols={2}>
  <Card title="vNext kernel prototype status" icon="flask" href="/vault/vnext-kernel-status">
    What the prototype is, its boundary, and how verifier admission is checked.
  </Card>

  <Card title="Independent Authority" icon="scale-balanced" href="/architecture/independent-authority">
    The assurance pattern behind per-mechanism proof credit.
  </Card>

  <Card title="Vault Boundaries & Disclosures" icon="vault" href="/vault/boundaries">
    Custody, testnet and quantum-security boundaries for the Vault surfaces.
  </Card>

  <Card title="walletwall-vault on GitHub" icon="code-fork" href="https://github.com/Wallet-Wall/walletwall-vault">
    The public repository that owns the contracts, the triage and the receipts.
  </Card>
</Columns>
