From 7990356940a03c748cf1b3a4329cda780bada512 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sun, 1 Mar 2026 23:01:32 +0400 Subject: [PATCH 01/13] bump int --- foundry.lock | 2 +- lib/rain.interpreter.interface | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry.lock b/foundry.lock index 88f55bf78..d388f8460 100644 --- a/foundry.lock +++ b/foundry.lock @@ -6,7 +6,7 @@ "rev": "f9a46744a66daf4004a3edc1d4387ae14a9dc8e4" }, "lib/rain.interpreter.interface": { - "rev": "639b80f929d8a8213fe75e35b446708ba49a8e6a" + "rev": "f83d8e0e2ea529acb9b76f622ff73464c65a3ba8" }, "lib/rain.lib.memkv": { "rev": "83e607990be8b3e06549338043c6b18f430f6bd2" diff --git a/lib/rain.interpreter.interface b/lib/rain.interpreter.interface index 639b80f92..f83d8e0e2 160000 --- a/lib/rain.interpreter.interface +++ b/lib/rain.interpreter.interface @@ -1 +1 @@ -Subproject commit 639b80f929d8a8213fe75e35b446708ba49a8e6a +Subproject commit f83d8e0e2ea529acb9b76f622ff73464c65a3ba8 From db29b03418af15fdde165f30f6d18eb0c06bab0c Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Tue, 3 Mar 2026 19:40:04 +0400 Subject: [PATCH 02/13] Fix external audit findings and triage internal audit External audit fixes (all MEDIUM): - EXT-M01: Bounds check on second-byte read in literal dispatch - EXT-M02: Cursor bounds check after parseInterstitial in pragma - EXT-M03: Dispatch length overflow check in subParseLiteral - EXT-M04: LHS item count overflow check in parse Additional fixes: - EXT-L01: Explicit UppercaseHexPrefix error for 0X literals - EXT-I02: Documented unused ParseState param in boundHex - EXT-I03: Fixed misleading comment about non-ASCII in skipMask - R02-RUST-01: Genesis block underflow guard in replay_transaction - P0-1: Added Bash tool to audit-pass2 skill - P0-2: Added full command syntax to CLAUDE.md build pipeline - P0-3: Referenced foundry.toml as source of truth in CLAUDE.md - P0-4: Added file paths to TESTING.md base contracts - P0-5: Extracted shared GENERAL_RULES.md for audit skills - P0-6: Referenced known-false-positives.md in audit rules - P0-7: Added DISPaiRegistry to architecture docs Removed audit finding labels from test NatSpec comments. Regenerated pointers and deploy constants. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 17 +- TESTING.md | 10 +- audit/2026-03-01-01/pass0/process.md | 63 ++ .../pass1/BaseRainterpreterExtern.md | 249 ++++++++ audit/2026-03-01-01/pass1/ErrAll.md | 220 +++++++ .../2026-03-01-01/pass1/LibDeployRegistry.md | 191 ++++++ audit/2026-03-01-01/pass1/LibEval.md | 177 ++++++ audit/2026-03-01-01/pass1/LibExtern.md | 156 +++++ .../2026-03-01-01/pass1/LibIntegrityCheck.md | 172 +++++ .../pass1/LibInterpreterState.md | 260 ++++++++ audit/2026-03-01-01/pass1/LibOpBitwise.md | 277 ++++++++ audit/2026-03-01-01/pass1/LibOpCoreOps.md | 311 +++++++++ audit/2026-03-01-01/pass1/LibOpERC20.md | 229 +++++++ audit/2026-03-01-01/pass1/LibOpERC721EVM.md | 262 ++++++++ audit/2026-03-01-01/pass1/LibOpLogic.md | 66 ++ audit/2026-03-01-01/pass1/LibOpMath.md | 267 ++++++++ audit/2026-03-01-01/pass1/LibOpStore.md | 77 +++ audit/2026-03-01-01/pass1/LibParse.md | 121 ++++ audit/2026-03-01-01/pass1/LibParseLiteral.md | 281 ++++++++ audit/2026-03-01-01/pass1/LibParseState.md | 144 +++++ .../2026-03-01-01/pass1/LibParseUtilities.md | 116 ++++ audit/2026-03-01-01/pass1/Rainterpreter.md | 139 ++++ .../pass1/RainterpreterExpressionDeployer.md | 139 ++++ .../pass1/RainterpreterParser.md | 115 ++++ .../pass1/RainterpreterReferenceExtern.md | 223 +++++++ .../2026-03-01-01/pass1/RainterpreterStore.md | 170 +++++ audit/2026-03-01-01/pass1/RustCrates.md | 155 +++++ audit/2026-03-01-01/pass2/CoreConcrete.md | 22 + audit/2026-03-01-01/pass2/ErrAll.md | 13 + .../pass2/ExternAbstractDeploy.md | 30 + audit/2026-03-01-01/pass2/LibEvalIntegrity.md | 18 + audit/2026-03-01-01/pass2/LibOpAll.md | 43 ++ audit/2026-03-01-01/pass2/LibParse.md | 30 + .../2026-03-01-01/pass2/LibParseUtilities.md | 34 + audit/2026-03-01-01/pass2/RustCrates.md | 49 ++ audit/2026-03-01-01/pass3/CoreConcrete.md | 165 +++++ audit/2026-03-01-01/pass3/ErrAll.md | 169 +++++ audit/2026-03-01-01/pass3/ExternAbstract.md | 226 +++++++ audit/2026-03-01-01/pass3/LibEvalIntegrity.md | 136 ++++ audit/2026-03-01-01/pass3/LibOpAll.md | 601 ++++++++++++++++++ audit/2026-03-01-01/pass3/LibParse.md | 429 +++++++++++++ audit/2026-03-01-01/pass3/RustCrates.md | 258 ++++++++ audit/2026-03-01-01/pass4/CoreConcrete.md | 395 ++++++++++++ audit/2026-03-01-01/pass4/ErrRust.md | 144 +++++ audit/2026-03-01-01/pass4/ExternAbstract.md | 259 ++++++++ audit/2026-03-01-01/pass4/LibEvalParse.md | 263 ++++++++ audit/2026-03-01-01/pass4/LibOpAll.md | 297 +++++++++ .../2026-03-01-01/pass4/LibParseUtilities.md | 254 ++++++++ audit/2026-03-01-01/triage.md | 123 ++++ crates/eval/src/error.rs | 2 + crates/eval/src/fork.rs | 9 +- src/concrete/Rainterpreter.sol | 4 + .../extern/RainterpreterReferenceExtern.sol | 7 + src/error/ErrParse.sol | 16 + src/error/ErrSubParse.sol | 6 + ...interpreterExpressionDeployer.pointers.sol | 2 +- .../RainterpreterParser.pointers.sol | 6 +- .../RainterpreterReferenceExtern.pointers.sol | 8 +- src/lib/deploy/LibInterpreterDeploy.sol | 12 +- .../op/LibExternOpContextRainlen.sol | 9 + src/lib/integrity/LibIntegrityCheck.sol | 6 +- src/lib/op/LibAllStandardOps.sol | 2 +- src/lib/op/erc20/LibOpERC20Allowance.sol | 2 +- src/lib/parse/LibParse.sol | 15 +- src/lib/parse/LibParsePragma.sol | 7 + src/lib/parse/LibParseState.sol | 10 + src/lib/parse/LibSubParse.sol | 10 +- src/lib/parse/literal/LibParseLiteral.sol | 38 +- src/lib/parse/literal/LibParseLiteralHex.sol | 4 +- .../literal/LibParseLiteralSubParseable.sol | 9 +- ...RainterpreterParser.parsePragmaEmpty.t.sol | 2 +- .../RainterpreterReferenceExtern.repeat.t.sol | 13 +- ...enceExtern.subParserIndexOutOfBounds.t.sol | 2 +- .../RainterpreterStore.getUninitialized.t.sol | 2 +- .../RainterpreterStore.overwriteKey.t.sol | 2 +- .../RainterpreterStore.setEmpty.t.sol | 2 +- .../RainterpreterStore.setEvent.t.sol | 2 +- test/src/lib/op/math/LibOpSub.t.sol | 13 +- test/src/lib/parse/LibParse.lhsOverflow.t.sol | 77 +++ .../lib/parse/LibParsePragma.keyword.t.sol | 38 ++ ...ParseState.endSourceTotalOpsOverflow.t.sol | 52 ++ .../LibSubParse.constantAccumulation.t.sol | 5 +- .../parse/LibSubParse.subParseLiteral.t.sol | 23 + .../literal/LibParseLiteral.dispatch.t.sol | 38 +- 84 files changed, 8950 insertions(+), 70 deletions(-) create mode 100644 audit/2026-03-01-01/pass0/process.md create mode 100644 audit/2026-03-01-01/pass1/BaseRainterpreterExtern.md create mode 100644 audit/2026-03-01-01/pass1/ErrAll.md create mode 100644 audit/2026-03-01-01/pass1/LibDeployRegistry.md create mode 100644 audit/2026-03-01-01/pass1/LibEval.md create mode 100644 audit/2026-03-01-01/pass1/LibExtern.md create mode 100644 audit/2026-03-01-01/pass1/LibIntegrityCheck.md create mode 100644 audit/2026-03-01-01/pass1/LibInterpreterState.md create mode 100644 audit/2026-03-01-01/pass1/LibOpBitwise.md create mode 100644 audit/2026-03-01-01/pass1/LibOpCoreOps.md create mode 100644 audit/2026-03-01-01/pass1/LibOpERC20.md create mode 100644 audit/2026-03-01-01/pass1/LibOpERC721EVM.md create mode 100644 audit/2026-03-01-01/pass1/LibOpLogic.md create mode 100644 audit/2026-03-01-01/pass1/LibOpMath.md create mode 100644 audit/2026-03-01-01/pass1/LibOpStore.md create mode 100644 audit/2026-03-01-01/pass1/LibParse.md create mode 100644 audit/2026-03-01-01/pass1/LibParseLiteral.md create mode 100644 audit/2026-03-01-01/pass1/LibParseState.md create mode 100644 audit/2026-03-01-01/pass1/LibParseUtilities.md create mode 100644 audit/2026-03-01-01/pass1/Rainterpreter.md create mode 100644 audit/2026-03-01-01/pass1/RainterpreterExpressionDeployer.md create mode 100644 audit/2026-03-01-01/pass1/RainterpreterParser.md create mode 100644 audit/2026-03-01-01/pass1/RainterpreterReferenceExtern.md create mode 100644 audit/2026-03-01-01/pass1/RainterpreterStore.md create mode 100644 audit/2026-03-01-01/pass1/RustCrates.md create mode 100644 audit/2026-03-01-01/pass2/CoreConcrete.md create mode 100644 audit/2026-03-01-01/pass2/ErrAll.md create mode 100644 audit/2026-03-01-01/pass2/ExternAbstractDeploy.md create mode 100644 audit/2026-03-01-01/pass2/LibEvalIntegrity.md create mode 100644 audit/2026-03-01-01/pass2/LibOpAll.md create mode 100644 audit/2026-03-01-01/pass2/LibParse.md create mode 100644 audit/2026-03-01-01/pass2/LibParseUtilities.md create mode 100644 audit/2026-03-01-01/pass2/RustCrates.md create mode 100644 audit/2026-03-01-01/pass3/CoreConcrete.md create mode 100644 audit/2026-03-01-01/pass3/ErrAll.md create mode 100644 audit/2026-03-01-01/pass3/ExternAbstract.md create mode 100644 audit/2026-03-01-01/pass3/LibEvalIntegrity.md create mode 100644 audit/2026-03-01-01/pass3/LibOpAll.md create mode 100644 audit/2026-03-01-01/pass3/LibParse.md create mode 100644 audit/2026-03-01-01/pass3/RustCrates.md create mode 100644 audit/2026-03-01-01/pass4/CoreConcrete.md create mode 100644 audit/2026-03-01-01/pass4/ErrRust.md create mode 100644 audit/2026-03-01-01/pass4/ExternAbstract.md create mode 100644 audit/2026-03-01-01/pass4/LibEvalParse.md create mode 100644 audit/2026-03-01-01/pass4/LibOpAll.md create mode 100644 audit/2026-03-01-01/pass4/LibParseUtilities.md create mode 100644 audit/2026-03-01-01/triage.md create mode 100644 test/src/lib/parse/LibParse.lhsOverflow.t.sol create mode 100644 test/src/lib/parse/LibParseState.endSourceTotalOpsOverflow.t.sol diff --git a/CLAUDE.md b/CLAUDE.md index 92639637d..82ad26062 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,16 +44,16 @@ nix develop -c cargo doc # Rust docs 1. `BuildAuthoringMeta.sol` exports raw ABI-encoded authoring meta to `meta/` 2. `i9r-prelude` runs `rain meta build` to CBOR-encode and deflate the meta -3. `BuildPointers.sol` deploys contracts in local EVM, extracts function pointer tables, and writes `src/generated/*.pointers.sol` -4. `forge build` compiles everything using the generated pointers +3. `nix develop -c forge script --silent ./script/BuildPointers.sol` deploys contracts in local EVM, extracts function pointer tables, and writes `src/generated/*.pointers.sol` +4. `nix develop -c forge build` compiles everything using the generated pointers The `src/generated/` directory contains build-time generated constants (bytecode hashes, function pointer tables, parse meta). These are regenerated by `BuildPointers.sol`. -After any source change affecting bytecode: run `i9r-prelude` → `BuildPointers.sol` → `forge fmt`, then run `LibInterpreterDeployTest` to get new deploy addresses/codehashes. Update `LibInterpreterDeploy.sol` and repeat until stable (constants cascade through the deploy chain). +After any source change affecting bytecode: run `nix develop -c i9r-prelude` → `nix develop -c forge script --silent ./script/BuildPointers.sol` → `nix develop -c forge fmt`, then run `LibInterpreterDeployTest` to get new deploy addresses/codehashes. Update `LibInterpreterDeploy.sol` and repeat until stable (constants cascade through the deploy chain). ## Architecture -### Four Core Components +### Core Components 1. **RainterpreterParser** (`src/concrete/RainterpreterParser.sol`) — Converts Rainlang text to bytecode. Uses bloom filter + fingerprint table for word lookup. @@ -63,7 +63,9 @@ After any source change affecting bytecode: run `i9r-prelude` → `BuildPointers 4. **RainterpreterExpressionDeployer** (`src/concrete/RainterpreterExpressionDeployer.sol`) — Coordinates parse → integrity check → serialize. Enforces bytecode hash checks for the other three components. Implements `IParserV2`. -All four are deployed to deterministic addresses via Zoltu deployer. Addresses and code hashes are in `src/lib/deploy/LibInterpreterDeploy.sol`. +5. **RainterpreterDISPaiRegistry** (`src/concrete/RainterpreterDISPaiRegistry.sol`) — On-chain registry of DISPair (deployer, interpreter, store, parser) tuples. Embeds the addresses of the other four components. + +All five are deployed to deterministic addresses via Zoltu deployer. Addresses and code hashes are in `src/lib/deploy/LibInterpreterDeploy.sol`. Deploy constants cascade: parser → expression deployer → DISPaiRegistry. Interpreter changes also cascade to DISPaiRegistry. ### Opcode System @@ -97,6 +99,7 @@ External contracts can extend the interpreter with additional opcodes. `src/conc - **Solidity version**: exactly `0.8.25`, EVM target `cancun` - **Optimizer**: enabled, 1000 runs - **Fuzz runs**: 2048 +- Source of truth for these settings is `foundry.toml` - **License**: `LicenseRef-DCL-1.0` with copyright `Rain Open Source Software Ltd` - Custom bytecode serialization is used instead of ABI encoding for gas efficiency - Function pointer dispatch (no switch/if chains) for opcode routing @@ -115,7 +118,9 @@ Testing patterns and conventions are in `TESTING.md`. Read that file before writ Jidoka is a priority: process correctness (correct future) over ad hoc progress (present state). Quality at the source enables throughput; skipping quality steps creates rework and slows overall flow. Process introspection takes precedence over following the process. -Each fix is a complete cycle: understand → fix → build → test → verify. Do not move to the next item with incomplete work. The "test" step means both: write tests for any new code paths introduced by the fix, then run the full test suite to confirm nothing is broken. New code must meet the same audit requirements defined in the `/audit` skill — a fix that introduces untested error paths, missing NatSpec, or other audit findings is not complete. +Each fix is a complete cycle: understand → write test → run test (confirm fail) → write fix → run test (confirm pass) → run full suite → verify. Do not move to the next item with incomplete work. New code must meet the same audit requirements defined in the `/audit` skill — a fix that introduces untested error paths, missing NatSpec, or other audit findings is not complete. + +When fixing bugs, follow TDD: write a test that reproduces the bug, run it to confirm it fails, then write the fix and run the test again to confirm it passes. Do not write the fix before running the test and confirming it reproduces the bug. When the user says "jidoka," they are signaling a process defect. The response is: 1. Identify the process defect. diff --git a/TESTING.md b/TESTING.md index 44e97db9c..b3bac63bf 100644 --- a/TESTING.md +++ b/TESTING.md @@ -4,11 +4,11 @@ Test base contracts in `test/abstract/`: -- **`RainterpreterExpressionDeployerDeploymentTest`** — Full stack deployment. Exposes `I_PARSER`, `I_INTERPRETER`, `I_STORE`, `I_DEPLOYER`. -- **`OpTest`** — Opcode tests. Provides `opReferenceCheck()`, `checkHappy()`, `checkUnhappy()`. -- **`ParseTest`** — Parser tests. Provides `parseExternal()`. -- **`OperandTest`** — Operand handler tests. Provides `checkOperandParse()`. -- **`ParseLiteralTest`** — Literal parsing tests. Provides `checkLiteralBounds()`. +- **`RainterpreterExpressionDeployerDeploymentTest`** (`test/abstract/RainterpreterExpressionDeployerDeploymentTest.sol`) — Full stack deployment. Exposes `I_PARSER`, `I_INTERPRETER`, `I_STORE`, `I_DEPLOYER`. +- **`OpTest`** (`test/abstract/OpTest.sol`) — Opcode tests. Provides `opReferenceCheck()`, `checkHappy()`, `checkUnhappy()`. +- **`ParseTest`** (`test/abstract/ParseTest.sol`) — Parser tests. Provides `parseExternal()`. +- **`OperandTest`** (`test/abstract/OperandTest.sol`) — Operand handler tests. Provides `checkOperandParse()`. +- **`ParseLiteralTest`** (`test/abstract/ParseLiteralTest.sol`) — Literal parsing tests. Provides `checkLiteralBounds()`. ## Fuzz Testing diff --git a/audit/2026-03-01-01/pass0/process.md b/audit/2026-03-01-01/pass0/process.md new file mode 100644 index 000000000..cdf829a80 --- /dev/null +++ b/audit/2026-03-01-01/pass0/process.md @@ -0,0 +1,63 @@ +# Pass 0: Process Review + +**Audit:** 2026-03-01-01 +**Date:** 2026-03-01 + +## Documents Reviewed + +1. `CLAUDE.md` (132 lines) — Main process document +2. `TESTING.md` (46 lines) — Test conventions +3. `audit/known-false-positives.md` (31 lines) — Known false positives registry +4. Audit skill files: `audit/SKILL.md`, `audit-pass0/SKILL.md` through `audit-pass4/SKILL.md`, `audit-triage/SKILL.md` +5. Auto-memory: `MEMORY.md` + +## Evidence of Thorough Reading + +### CLAUDE.md +- Sections: Build Environment (lines 7-53), Architecture (lines 54-93), Solidity Conventions (lines 95-104), Test Conventions (lines 106-112), Process/Jidoka (lines 114-127), Audit Review (lines 129-132) +- Terms defined: Nix flakes, `i9r-prelude`, `BuildPointers.sol`, `BuildAuthoringMeta.sol`, four core components, opcode system, extern system, Rust crates, deployment +- Referenced external docs: `TESTING.md`, `/audit` skill + +### TESTING.md +- Sections: Base Contracts (lines 3-11), Fuzz Testing (lines 13-17), Library Internals (lines 19-21), Revert Paths (lines 23-25), Bytecode Construction (lines 27-29), Bytecode Inspection (lines 31-33), Opcode Testing (lines 35-37), Boundary Tests (lines 39-41), One Test at a Time (lines 43-46) + +### Audit skill files +- Master SKILL.md (122 lines): General rules, proposed fixes, severity levels, pass definitions (0-4), triage +- Per-pass SKILL.md files: Each duplicates the general rules section and adds pass-specific instructions + +### known-false-positives.md +- Entries: LibOpGet read-only key persistence, ERC20 float opcodes `decimals()` optional + +## Findings + +### P0-1: (LOW) Proposed fix procedure in audit SKILL.md has no Bash tool for pass2 + +`audit-pass2/SKILL.md` line 5 lists `allowed-tools: Read, Grep, Glob, Task, Write` but does not include `Bash`. Agents running pass 2 that need to verify test compilation or run a specific test to confirm coverage cannot do so. Other passes that may not need Bash (pass 3) also lack it, but pass 2 is the most impacted because test coverage verification benefits from compilation checks. Compare with pass 1 and pass 4 which include Bash. + +### P0-2: (LOW) CLAUDE.md Build Pipeline step 3 names `BuildPointers.sol` but the actual command is `forge script ./script/BuildPointers.sol` + +CLAUDE.md line 47 says "BuildPointers.sol deploys contracts in local EVM..." but the actual invocation requires `nix develop -c forge script --silent ./script/BuildPointers.sol`. A future session reconstructing from a compressed summary might attempt `nix develop -c BuildPointers.sol` as a direct command. The MEMORY.md entry under "Pointer Regeneration" includes the correct full command, but CLAUDE.md itself does not provide the command syntax despite providing exact syntax for other commands. + +### P0-3: (LOW) CLAUDE.md "Fuzz runs: 2048" is stated but the foundry.toml location is not referenced + +CLAUDE.md line 99 states "Fuzz runs: 2048" as a convention but does not point to `foundry.toml` as the source of truth. If a future session needs to verify or change this value, it has no guidance on where the configuration lives. Other conventions (Solidity version, optimizer settings) are similarly stated without pointing to their source of truth in `foundry.toml`. + +### P0-4: (LOW) TESTING.md references test base contracts without file paths + +TESTING.md lines 5-11 list test base contracts (`RainterpreterExpressionDeployerDeploymentTest`, `OpTest`, `ParseTest`, `OperandTest`, `ParseLiteralTest`) with the directory `test/abstract/` but without full file paths. If files are renamed or reorganized, this list becomes stale. A future session would need to glob to find the actual files. + +### P0-5: (LOW) Audit skill files duplicate general rules across 7 files + +The general rules section (agent IDs, evidence requirements, finding format, severity definitions) is duplicated verbatim in `audit/SKILL.md` and each of the 6 per-pass/triage SKILL.md files. If any rule is updated in one file but not the others, the documents become inconsistent. The per-pass files should reference the master document rather than duplicating. + +### P0-6: (LOW) `known-false-positives.md` is not referenced from CLAUDE.md or audit skill files + +`audit/known-false-positives.md` exists but is not referenced from CLAUDE.md's audit section or from any audit skill SKILL.md file. An agent running a security audit has no instruction to consult this file, so the same false positives may be re-flagged in every audit cycle. The triage process cross-references prior triage.md files but not this document. + +### P0-7: (LOW) CLAUDE.md does not document the RainterpreterDISPaiRegistry component + +CLAUDE.md lines 57-66 describe "Four Core Components" but `src/concrete/RainterpreterDISPaiRegistry.sol` exists as a fifth concrete contract. The architecture section mentions only four components and the expression deployer as implementing `IParserV2`. The DISPaiRegistry is missing from the architecture description. The deploy constants cascade (parser -> expression deployer -> DISPaiRegistry) documented in MEMORY.md is not present in CLAUDE.md. + +### P0-8: (LOW) Audit pass numbering inconsistency: "Proposed Fixes" section mentions writing fixes during passes but pass0 has no `.fixes` instruction + +The master audit SKILL.md says "Each LOW+ finding must include a proposed fix written to `.fixes/`" and "Fix files are written alongside findings during each pass." However, pass 0 reviews process documents where fixes are textual edits to .md files — the `.fixes/` convention is designed for code changes. This ambiguity means pass 0 findings either need fix files (unusual for process docs) or the rule should explicitly exempt pass 0. diff --git a/audit/2026-03-01-01/pass1/BaseRainterpreterExtern.md b/audit/2026-03-01-01/pass1/BaseRainterpreterExtern.md new file mode 100644 index 000000000..1523c05d2 --- /dev/null +++ b/audit/2026-03-01-01/pass1/BaseRainterpreterExtern.md @@ -0,0 +1,249 @@ +# Pass 1 (Security) -- BaseRainterpreterExtern.sol & BaseRainterpreterSubParser.sol + +**Auditor**: A01 / A02 +**Date**: 2026-03-01 +**Files**: +- `src/abstract/BaseRainterpreterExtern.sol` (131 lines) +- `src/abstract/BaseRainterpreterSubParser.sol` (220 lines) + +## Evidence of Thorough Reading + +### BaseRainterpreterExtern.sol + +**Contract**: `BaseRainterpreterExtern` (line 29), abstract, inherits `IInterpreterExternV4`, `IIntegrityToolingV1`, `IOpcodeToolingV1`, `ERC165` + +#### Imports + +| Import | Source | Line | +|---|---|---| +| `ERC165` | `openzeppelin-contracts/contracts/utils/introspection/ERC165.sol` | 5 | +| `OperandV2` | `rain.interpreter.interface/interface/IInterpreterV4.sol` | 7 | +| `IInterpreterExternV4`, `ExternDispatchV2`, `StackItem` | `rain.interpreter.interface/interface/IInterpreterExternV4.sol` | 8-12 | +| `IIntegrityToolingV1` | `rain.sol.codegen/interface/IIntegrityToolingV1.sol` | 13 | +| `IOpcodeToolingV1` | `rain.sol.codegen/interface/IOpcodeToolingV1.sol` | 14 | +| `ExternOpcodeOutOfRange`, `ExternPointersMismatch`, `ExternOpcodePointersEmpty` | `../error/ErrExtern.sol` | 15 | + +#### File-level Constants + +| Constant | Type | Value | Line | +|---|---|---|---| +| `OPCODE_FUNCTION_POINTERS` | `bytes` | `hex""` | 20 | +| `INTEGRITY_FUNCTION_POINTERS` | `bytes` | `hex""` | 24 | + +#### Functions + +| Function | Signature | Visibility | Modifiers | Line | +|---|---|---|---|---| +| `constructor` | `()` | -- | -- | 34 | +| `extern` | `(ExternDispatchV2, StackItem[] memory) -> (StackItem[] memory)` | external view virtual override | -- | 46 | +| `externIntegrity` | `(ExternDispatchV2, uint256, uint256) -> (uint256, uint256)` | external pure virtual override | -- | 83 | +| `supportsInterface` | `(bytes4) -> (bool)` | public view virtual override | -- | 112 | +| `opcodeFunctionPointers` | `() -> (bytes memory)` | internal view virtual | -- | 121 | +| `integrityFunctionPointers` | `() -> (bytes memory)` | internal pure virtual | -- | 128 | + +#### Errors (imported) + +| Error | Parameters | Source | +|---|---|---| +| `ExternOpcodeOutOfRange` | `uint256 opcode, uint256 fsCount` | `src/error/ErrExtern.sol` | +| `ExternPointersMismatch` | `uint256 opcodeCount, uint256 integrityCount` | `src/error/ErrExtern.sol` | +| `ExternOpcodePointersEmpty` | -- | `src/error/ErrExtern.sol` | + +--- + +### BaseRainterpreterSubParser.sol + +**Contract**: `BaseRainterpreterSubParser` (line 78), abstract, inherits `ERC165`, `ISubParserV4`, `IDescribedByMetaV1`, `IParserToolingV1`, `ISubParserToolingV1` + +#### Imports + +| Import | Source | Line | +|---|---|---| +| `ERC165` | `openzeppelin-contracts/contracts/utils/introspection/ERC165.sol` | 5 | +| `LibBytes`, `Pointer` | `rain.solmem/lib/LibBytes.sol` | 6 | +| `ISubParserV4`, `AuthoringMetaV2` | `rain.interpreter.interface/interface/ISubParserV4.sol` | 10 | +| `LibSubParse`, `ParseState` | `../lib/parse/LibSubParse.sol` | 11 | +| `CMASK_RHS_WORD_TAIL` | `rain.string/lib/parse/LibParseCMask.sol` | 12 | +| `LibParse`, `OperandV2` | `../lib/parse/LibParse.sol` | 13 | +| `LibParseMeta` | `rain.interpreter.interface/lib/parse/LibParseMeta.sol` | 14 | +| `LibParseOperand` | `../lib/parse/LibParseOperand.sol` | 15 | +| `IDescribedByMetaV1` | `rain.metadata/interface/IDescribedByMetaV1.sol` | 16 | +| `IParserToolingV1` | `rain.sol.codegen/interface/IParserToolingV1.sol` | 17 | +| `ISubParserToolingV1` | `rain.sol.codegen/interface/ISubParserToolingV1.sol` | 18 | +| `SubParserIndexOutOfBounds` | `../error/ErrSubParse.sol` | 19 | + +#### Using Directives + +| Using | For | Line | +|---|---|---| +| `LibBytes` | `bytes` | 85 | +| `LibParse` | `ParseState` | 86 | +| `LibParseMeta` | `ParseState` | 87 | +| `LibParseOperand` | `ParseState` | 88 | + +#### File-level Constants + +| Constant | Type | Value | Line | +|---|---|---|---| +| `SUB_PARSER_WORD_PARSERS` | `bytes` | `hex""` | 26 | +| `SUB_PARSER_PARSE_META` | `bytes` | `hex""` | 32 | +| `SUB_PARSER_OPERAND_HANDLERS` | `bytes` | `hex""` | 36 | +| `SUB_PARSER_LITERAL_PARSERS` | `bytes` | `hex""` | 40 | + +#### Functions + +| Function | Signature | Visibility | Modifiers | Line | +|---|---|---|---|---| +| `subParserParseMeta` | `() -> (bytes memory)` | internal pure virtual | -- | 93 | +| `subParserWordParsers` | `() -> (bytes memory)` | internal pure virtual | -- | 100 | +| `subParserOperandHandlers` | `() -> (bytes memory)` | internal pure virtual | -- | 107 | +| `subParserLiteralParsers` | `() -> (bytes memory)` | internal pure virtual | -- | 114 | +| `matchSubParseLiteralDispatch` | `(uint256, uint256) -> (bool, uint256, bytes32)` | internal view virtual | -- | 139 | +| `subParseLiteral2` | `(bytes memory) -> (bool, bytes32)` | external view virtual | -- | 159 | +| `subParseWord2` | `(bytes memory) -> (bool, bytes memory, bytes32[] memory)` | external pure virtual | -- | 188 | +| `supportsInterface` | `(bytes4) -> (bool)` | public view virtual override | -- | 215 | + +#### Errors (imported) + +| Error | Parameters | Source | +|---|---|---| +| `SubParserIndexOutOfBounds` | `uint256 index, uint256 length` | `src/error/ErrSubParse.sol` | + +--- + +## Security Analysis + +### A01: Extern Dispatch Safety + +#### Mod-wrapping in `extern()` (line 76) + +The `extern()` function uses `mod(opcode, fsCount)` to bound the opcode index before using it to load a function pointer. This is deliberate and well-documented in the code comments (lines 55-65): it mirrors how the main eval loop handles opcode dispatch, and is cheaper than a bounds check. The integrity check (`externIntegrity`) separately enforces that opcodes are in range at parse time, reverting with `ExternOpcodeOutOfRange` (line 98-100). + +The tradeoff is that a direct external call to `extern()` with an out-of-range opcode silently wraps to a different valid opcode rather than reverting. The comments correctly document this design choice and its rationale. + +**Conclusion**: The mod-wrapping is intentional and correctly implemented. The integrity check provides the parse-time safety net. No finding. + +#### Bounds check in `externIntegrity()` (lines 98-100) + +The integrity function uses an explicit `if (opcode >= fsCount) revert` check rather than mod-wrapping. This is correct for the integrity path, which runs at parse time and should reject invalid opcodes rather than silently wrapping them. + +**Conclusion**: Correct design. No finding. + +### A01: Constructor Validation + +The constructor (lines 34-43) enforces: +1. `opcodeFunctionPointers().length != 0` -- prevents zero-length table +2. `opcodeFunctionPointers().length == integrityFunctionPointers().length` -- ensures 1:1 correspondence + +Both checks use raw byte lengths. See finding A01-1 for an edge case. + +### A01: Assembly Memory Safety + +#### `extern()` assembly blocks (lines 68-70, 75-77) + +Block 1 computes `fPointersStart` by adding 0x20 to skip the bytes length prefix. No allocation, no writes. Safe. + +Block 2 reads a function pointer from the computed offset within the `fPointers` array. The `mod(opcode, fsCount)` ensures the index is within `[0, fsCount)`, so the read starts within the array bounds. The `mload` reads 32 bytes, extending past the array boundary for small tables, but `shr(0xf0, ...)` isolates only the first 2 bytes (the actual function pointer). No memory corruption. + +**Conclusion**: Both blocks are correctly marked `memory-safe`. + +#### `externIntegrity()` assembly blocks (lines 94-96, 104-106) + +Same pattern as `extern()`. The explicit bounds check at line 98-100 ensures `opcode < fsCount` before the assembly access, so the read is within the array. + +**Conclusion**: Correctly marked `memory-safe`. + +### A02: Sub Parser Bounds Checking + +#### `subParseLiteral2()` (lines 159-178) + +After `matchSubParseLiteralDispatch` returns `(true, index, ...)`, the code checks `index >= parsersLength` at line 168-170 and reverts with `SubParserIndexOutOfBounds`. Only then does the assembly block at lines 171-173 load the function pointer. + +**Conclusion**: Bounds checking is correct and precedes the unsafe memory access. + +#### `subParseWord2()` (lines 188-212) + +After `lookupWord` returns `(true, index)`, the code checks `index >= parsersLength` at lines 202-204. Only then does the assembly block at lines 205-207 load the function pointer. + +**Conclusion**: Bounds checking is correct and precedes the unsafe memory access. + +### A02: Assembly Memory Safety + +#### `subParseLiteral2()` assembly (lines 171-173) + +```solidity +assembly ("memory-safe") { + subParser := and(mload(add(localSubParserLiteralParsers, mul(add(index, 1), 2))), 0xFFFF) +} +``` + +The offset `(index + 1) * 2` from the base of the bytes array implicitly skips the 32-byte length prefix and then indexes to the correct 2-byte pointer. With the bounds check `index < parsersLength`, the maximum offset is `parsersLength * 2 = localSubParserLiteralParsers.length`, which is within the array. The `mload` reads 32 bytes, but `and(..., 0xFFFF)` masks to the lowest 16 bits, which contain exactly the target function pointer. Bytes read beyond the array are discarded. + +**Conclusion**: Correctly marked `memory-safe`. + +#### `subParseWord2()` assembly (lines 205-207) + +Identical pattern to `subParseLiteral2()`. Same analysis applies. + +**Conclusion**: Correctly marked `memory-safe`. + +### A01/A02: ERC165 Support + +Both contracts correctly override `supportsInterface` and chain to `super.supportsInterface(interfaceId)`. The reference extern (`RainterpreterReferenceExtern`) correctly resolves the diamond inheritance ambiguity by overriding both with a single `super.supportsInterface()` call that traverses the C3 linearization. + +**Conclusion**: No finding. + +--- + +## Findings + +### A01-1 -- LOW: Constructor allows odd-length function pointer tables + +**Location**: `BaseRainterpreterExtern.sol`, lines 34-43 + +**Description**: The constructor validates that `opcodeFunctionPointers().length` is non-zero and equals `integrityFunctionPointers().length`. However, it does not validate that these lengths are even. Since each function pointer is 2 bytes, an odd byte length means the last byte is orphaned and `fsCount` (computed as `length / 2` using integer division) is one less than expected. + +Critically, a 1-byte pointer table (e.g., `hex"00"`) passes the non-zero check but produces `fsCount = 0`. At runtime, `mod(opcode, 0)` in `extern()` causes an EVM panic (division by zero), and `externIntegrity()` always reverts with `ExternOpcodeOutOfRange` (since any opcode >= 0). The contract becomes permanently non-functional despite successful deployment. + +While this requires a misconfiguration by the inheriting contract, the constructor's purpose is to catch exactly such misconfigurations. Adding an even-length check would make the validation complete. + +**Severity**: LOW -- requires inheritor misconfiguration; the contract is non-functional rather than exploitable. + +### A02-1 -- INFO: No constructor validation in BaseRainterpreterSubParser + +**Location**: `BaseRainterpreterSubParser.sol`, entire contract + +**Description**: Unlike `BaseRainterpreterExtern` which validates pointer table consistency at construction time, `BaseRainterpreterSubParser` performs no constructor validation. The bounds checks in `subParseLiteral2` and `subParseWord2` provide runtime safety, but misconfigurations (e.g., a word parsers table shorter than the parse meta index space) are only caught when specific words are looked up. A constructor could validate that `subParserWordParsers().length / 2` is consistent with the parse meta, and that `subParserLiteralParsers()` is non-empty if `matchSubParseLiteralDispatch` can return `true`. + +However, the sub parser design is inherently more flexible than the extern: the parse meta is a bloom filter that may map to indices handled by different code paths, and the `matchSubParseLiteralDispatch` is a virtual function whose behavior cannot be predicted at construction time. The runtime bounds checks are the correct safety net for this architecture. + +**Severity**: INFO -- architectural observation, runtime checks are appropriate here. + +### A01-2 -- INFO: Inconsistent assembly idioms for function pointer extraction + +**Location**: `BaseRainterpreterExtern.sol` lines 75-77; `BaseRainterpreterSubParser.sol` lines 171-173, 205-207 + +**Description**: Two different assembly idioms extract 16-bit function pointers from packed `bytes` arrays: + +- **Extern**: `shr(0xf0, mload(add(fPointersStart, mul(opcode, 2))))` -- pre-computes base past the length prefix, then right-shifts to isolate the top 16 bits of the loaded word. +- **SubParser**: `and(mload(add(base, mul(add(index, 1), 2))), 0xFFFF)` -- offsets from the array base using `(index + 1) * 2` to implicitly skip the length prefix, then masks the bottom 16 bits. + +Both are correct. The difference arises from whether the 0x20 skip is factored into the base address or the index calculation. This is a stylistic inconsistency, not a bug. + +**Severity**: INFO + +### A01-3 -- INFO: `extern()` mod-wrapping is by design + +**Location**: `BaseRainterpreterExtern.sol`, lines 53-79 + +**Description**: A direct external call to `extern()` with an out-of-range opcode silently wraps via `mod(opcode, fsCount)` to a different valid opcode. This is by design: the code comments (lines 55-65) explain the rationale (cheaper than bounds check, mirrors main eval loop, integrity check provides parse-time safety). Without the mod, out-of-range opcodes would read arbitrary memory and jump to arbitrary code, which is worse. + +The security model relies on the integrity check running during parsing. Any path that invokes `extern()` without a prior integrity check (e.g., hand-crafted bytecode or direct external calls) will get mod-wrapped behavior rather than a revert. + +**Severity**: INFO -- documented design decision with correct rationale. + +--- + +## Summary + +No CRITICAL, HIGH, or MEDIUM findings. One LOW finding (A01-1) identifies a missing even-length validation in the extern constructor that could cause a deployment to be permanently non-functional. Three INFO findings document architectural observations and design decisions. Both contracts use assembly correctly, with proper `memory-safe` annotations. Bounds checking is consistently applied before unsafe memory access. The extern's mod-wrapping dispatch and the sub parser's explicit bounds checks are both appropriate for their respective threat models. diff --git a/audit/2026-03-01-01/pass1/ErrAll.md b/audit/2026-03-01-01/pass1/ErrAll.md new file mode 100644 index 000000000..335140852 --- /dev/null +++ b/audit/2026-03-01-01/pass1/ErrAll.md @@ -0,0 +1,220 @@ +# A03 Pass 1 Audit: Error Definition Files + +**Agent:** A03 +**Date:** 2026-03-01 +**Scope:** `src/error/Err*.sol` (10 files) + +## Files Reviewed + +All 10 error definition files were read in full. + +## Evidence of Thorough Reading + +### `src/error/ErrBitwise.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 13 | `UnsupportedBitwiseShiftAmount` | `uint256 shiftAmount` | +| 19 | `TruncatedBitwiseEncoding` | `uint256 startBit, uint256 length` | +| 23 | `ZeroLengthBitwiseEncoding` | (none) | + +- Line 6: Empty workaround contract `ErrBitwise` +- Line 5: Import: none beyond the Foundry workaround +- Lines 22-23: `ZeroLengthBitwiseEncoding` doc block lacks `@notice` tag (other errors in file use it) + +### `src/error/ErrDeploy.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 11 | `UnknownDeploymentSuite` | `bytes32 suite` | + +- Line 6: Empty workaround contract `ErrDeploy` + +### `src/error/ErrEval.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 11 | `InputsLengthMismatch` | `uint256 expected, uint256 actual` | +| 15 | `ZeroFunctionPointers` | (none) | + +- Line 6: Empty workaround contract `ErrEval` +- Lines 13-15: `ZeroFunctionPointers` doc block lacks `@notice` tag (`InputsLengthMismatch` uses it) + +### `src/error/ErrExtern.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 14 | `ExternOpcodeOutOfRange` | `uint256 opcode, uint256 fsCount` | +| 20 | `ExternPointersMismatch` | `uint256 opcodeCount, uint256 integrityCount` | +| 25 | `BadOutputsLength` | `uint256 expectedLength, uint256 actualLength` | +| 28 | `ExternOpcodePointersEmpty` | (none) | + +- Line 5: Import of `NotAnExternContract` from `rain.interpreter.interface/error/ErrExtern.sol` (re-export) +- Line 7: Empty workaround contract `ErrExtern` +- Lines 27-28: `ExternOpcodePointersEmpty` doc block lacks `@notice` tag (other errors in file use it) + +### `src/error/ErrIntegrity.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 12 | `StackUnderflow` | `uint256 opIndex, uint256 stackIndex, uint256 calculatedInputs` | +| 18 | `StackUnderflowHighwater` | `uint256 opIndex, uint256 stackIndex, uint256 stackHighwater` | +| 24 | `StackAllocationMismatch` | `uint256 stackMaxIndex, uint256 bytecodeAllocation` | +| 29 | `StackOutputsMismatch` | `uint256 stackIndex, uint256 bytecodeOutputs` | +| 35 | `OutOfBoundsConstantRead` | `uint256 opIndex, uint256 constantsLength, uint256 constantRead` | +| 41 | `OutOfBoundsStackRead` | `uint256 opIndex, uint256 stackTopIndex, uint256 stackRead` | +| 47 | `CallOutputsExceedSource` | `uint256 sourceOutputs, uint256 outputs` | +| 53 | `OpcodeOutOfRange` | `uint256 opIndex, uint256 opcodeIndex, uint256 fsCount` | + +- Line 6: Empty workaround contract `ErrIntegrity` +- All errors have `@notice` and `@param` tags -- consistent NatSpec + +### `src/error/ErrOpList.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 12 | `BadDynamicLength` | `uint256 dynamicLength, uint256 standardOpsLength` | + +- Line 6: Empty workaround contract `ErrOpList` +- NatSpec complete + +### `src/error/ErrParse.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 10 | `UnexpectedOperand` | (none) | +| 14 | `UnexpectedOperandValue` | (none) | +| 18 | `ExpectedOperand` | (none) | +| 23 | `OperandValuesOverflow` | `uint256 offset` | +| 27 | `UnclosedOperand` | `uint256 offset` | +| 31 | `UnsupportedLiteralType` | `uint256 offset` | +| 35 | `StringTooLong` | `uint256 offset` | +| 40 | `UnclosedStringLiteral` | `uint256 offset` | +| 44 | `HexLiteralOverflow` | `uint256 offset` | +| 48 | `ZeroLengthHexLiteral` | `uint256 offset` | +| 52 | `OddLengthHexLiteral` | `uint256 offset` | +| 56 | `MalformedHexLiteral` | `uint256 offset` | +| 60 | `MissingFinalSemi` | `uint256 offset` | +| 64 | `UnexpectedLHSChar` | `uint256 offset` | +| 68 | `UnexpectedRHSChar` | `uint256 offset` | +| 73 | `ExpectedLeftParen` | `uint256 offset` | +| 77 | `UnexpectedRightParen` | `uint256 offset` | +| 81 | `UnclosedLeftParen` | `uint256 offset` | +| 85 | `UnexpectedComment` | `uint256 offset` | +| 89 | `UnclosedComment` | `uint256 offset` | +| 93 | `MalformedCommentStart` | `uint256 offset` | +| 98 | `DuplicateLHSItem` | `uint256 offset` | +| 102 | `ExcessLHSItems` | `uint256 offset` | +| 106 | `NotAcceptingInputs` | `uint256 offset` | +| 110 | `ExcessRHSItems` | `uint256 offset` | +| 114 | `WordSize` | `string word` | +| 118 | `UnknownWord` | `string word` | +| 121 | `MaxSources` | (none) | +| 124 | `DanglingSource` | (none) | +| 127 | `ParserOutOfBounds` | (none) | +| 131 | `ParseStackOverflow` | (none) | +| 134 | `ParseStackUnderflow` | (none) | +| 138 | `ParenOverflow` | (none) | +| 142 | `NoWhitespaceAfterUsingWordsFrom` | `uint256 offset` | +| 146 | `InvalidSubParser` | `uint256 offset` | +| 150 | `UnclosedSubParseableLiteral` | `uint256 offset` | +| 154 | `SubParseableMissingDispatch` | `uint256 offset` | +| 159 | `BadSubParserResult` | `bytes bytecode` | +| 163 | `OpcodeIOOverflow` | `uint256 offset` | +| 166 | `OperandOverflow` | (none) | +| 171 | `ParseMemoryOverflow` | `uint256 freeMemoryPointer` | +| 175 | `SourceItemOpsOverflow` | (none) | +| 179 | `ParenInputOverflow` | (none) | +| 183 | `LineRHSItemsOverflow` | (none) | + +- Line 7: Empty workaround contract `ErrParse` +- Mixed NatSpec: some errors have `@notice`/`@param`, others have untagged `///` only + +### `src/error/ErrRainType.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 12 | `NotAnAddress` | `uint256 value` | + +- Line 6: Empty workaround contract `ErrRainType` +- NatSpec complete + +### `src/error/ErrStore.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 10 | `OddSetLength` | `uint256 length` | + +- Line 6: Empty workaround contract `ErrStore` +- NatSpec complete + +### `src/error/ErrSubParse.sol` + +| Line | Error | Parameters | +|------|-------|------------| +| 11 | `ExternDispatchConstantsHeightOverflow` | `uint256 constantsHeight` | +| 16 | `ConstantOpcodeConstantsHeightOverflow` | `uint256 constantsHeight` | +| 21 | `ContextGridOverflow` | `uint256 column, uint256 row` | +| 27 | `SubParserIndexOutOfBounds` | `uint256 index, uint256 length` | + +- Line 7: Empty workaround contract `ErrSubParse` +- NatSpec complete + +## Usage Verification + +All errors were searched for references in `src/` (excluding their definition files). Results: + +| Error | Used In | +|-------|---------| +| `UnsupportedBitwiseShiftAmount` | `LibOpShiftBitsLeft.sol`, `LibOpShiftBitsRight.sol` | +| `TruncatedBitwiseEncoding` | `LibOpEncodeBits.sol` | +| `ZeroLengthBitwiseEncoding` | `LibOpEncodeBits.sol` | +| `UnknownDeploymentSuite` | `script/Deploy.sol` only (not in `src/`) | +| `InputsLengthMismatch` | `LibEval.sol` | +| `ZeroFunctionPointers` | `Rainterpreter.sol` | +| `ExternOpcodeOutOfRange` | `BaseRainterpreterExtern.sol` | +| `ExternPointersMismatch` | `BaseRainterpreterExtern.sol` | +| `BadOutputsLength` | `LibOpExtern.sol` | +| `ExternOpcodePointersEmpty` | `BaseRainterpreterExtern.sol` | +| `StackUnderflow` | `LibIntegrityCheck.sol` | +| `StackUnderflowHighwater` | `LibIntegrityCheck.sol` | +| `StackAllocationMismatch` | `LibIntegrityCheck.sol` | +| `StackOutputsMismatch` | `LibIntegrityCheck.sol` | +| `OutOfBoundsConstantRead` | `LibOpConstant.sol` | +| `OutOfBoundsStackRead` | `LibOpStack.sol` | +| `CallOutputsExceedSource` | `LibOpCall.sol` | +| `OpcodeOutOfRange` | `LibIntegrityCheck.sol` | +| `BadDynamicLength` | `LibAllStandardOps.sol`, `RainterpreterReferenceExtern.sol` | +| `NotAnAddress` | 10 ERC op libraries | +| `OddSetLength` | `RainterpreterStore.sol`, `Rainterpreter.sol` | +| `ExternDispatchConstantsHeightOverflow` | `LibSubParse.sol` | +| `ConstantOpcodeConstantsHeightOverflow` | `LibSubParse.sol` | +| `ContextGridOverflow` | `LibSubParse.sol` | +| `SubParserIndexOutOfBounds` | `BaseRainterpreterSubParser.sol` | +| All 38 ErrParse errors | Various parse libraries (verified individually) | + +All error selectors were computed and confirmed unique -- no 4-byte selector collisions. + +## Checks Performed + +1. **Unused error definitions**: No errors are defined without any reference in the codebase. +2. **String message reverts**: None found. All files use custom error types exclusively. +3. **Parameter type issues**: No ABI encoding problems identified. All parameter types are appropriate for their purpose. + +## Findings + +### A03-01 (INFORMATIONAL): Inconsistent NatSpec tagging across error files + +**Files:** +- `src/error/ErrBitwise.sol` line 22 (`ZeroLengthBitwiseEncoding`) +- `src/error/ErrEval.sol` line 13 (`ZeroFunctionPointers`) +- `src/error/ErrExtern.sol` line 27 (`ExternOpcodePointersEmpty`) +- `src/error/ErrParse.sol` lines 8, 12, 16, 120, 123, 126, 129, 133, 136, 165, 173, 177, 181 (13 errors total) + +**Description:** Some errors use `@notice` and `@param` NatSpec tags while other errors in the same file use plain untagged `///` comments. Per Solidity NatSpec rules, untagged `///` lines are implicitly `@notice` when no tags are present in the block, so this is not a correctness issue. However, the inconsistency within files reduces readability and could lead to mistakes if a tagged line (e.g. `@param`) is later added to an untagged block without also adding `@notice`. + +**Severity:** INFORMATIONAL -- no functional impact, purely documentation consistency. + +--- + +No LOW or higher severity findings were identified in the error definition files. These files define only custom error types with no logic, no storage, no external calls, and no access control. The security surface is minimal. All errors are used in the codebase, all use custom error types (no string reverts), and all parameter types are appropriate. diff --git a/audit/2026-03-01-01/pass1/LibDeployRegistry.md b/audit/2026-03-01-01/pass1/LibDeployRegistry.md new file mode 100644 index 000000000..6a2ce6ef9 --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibDeployRegistry.md @@ -0,0 +1,191 @@ +# Pass 1 (Security) -- DISPaiRegistry, IDISPaiRegistry, LibInterpreterDeploy, LibAllStandardOps + +**Files:** +- `src/concrete/RainterpreterDISPaiRegistry.sol` (Agent A46) +- `src/interface/IDISPaiRegistry.sol` (Agent A46) +- `src/lib/deploy/LibInterpreterDeploy.sol` (Agent A13) +- `src/lib/op/LibAllStandardOps.sol` (Agent A04) + +**Auditor:** Claude Opus 4.6 +**Date:** 2026-03-01 + +--- + +## Evidence of Thorough Reading + +### RainterpreterDISPaiRegistry.sol (40 lines) + +**Contract:** `RainterpreterDISPaiRegistry is IDISPaiRegistry, ERC165` + +| Function | Line | Visibility | +|---|---|---| +| `supportsInterface(bytes4)` | 17 | public view override | +| `expressionDeployerAddress()` | 22 | external pure override | +| `interpreterAddress()` | 27 | external pure override | +| `storeAddress()` | 32 | external pure override | +| `parserAddress()` | 37 | external pure override | + +- Imports: `LibInterpreterDeploy`, `IDISPaiRegistry`, `ERC165` (OpenZeppelin) +- No errors, events, structs, assembly, storage, or state-mutating logic +- ERC165 correctly reports `type(IDISPaiRegistry).interfaceId` and delegates to `super.supportsInterface` +- All four address getters delegate to `LibInterpreterDeploy` constants + +### IDISPaiRegistry.sol (25 lines) + +**Interface:** `IDISPaiRegistry` + +| Function | Line | +|---|---| +| `expressionDeployerAddress()` | 12 | +| `interpreterAddress()` | 16 | +| `storeAddress()` | 20 | +| `parserAddress()` | 24 | + +- All functions are `external pure returns (address)` +- No errors, events, or structs +- NatSpec uses `///` with `@return` tags + +### LibInterpreterDeploy.sol (66 lines) + +**Library:** `LibInterpreterDeploy` + +| Constant | Line | Type | +|---|---|---| +| `PARSER_DEPLOYED_ADDRESS` | 14 | `address` | +| `PARSER_DEPLOYED_CODEHASH` | 20-21 | `bytes32` | +| `STORE_DEPLOYED_ADDRESS` | 25 | `address` | +| `STORE_DEPLOYED_CODEHASH` | 31-32 | `bytes32` | +| `INTERPRETER_DEPLOYED_ADDRESS` | 36 | `address` | +| `INTERPRETER_DEPLOYED_CODEHASH` | 42-43 | `bytes32` | +| `EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS` | 47 | `address` | +| `EXPRESSION_DEPLOYER_DEPLOYED_CODEHASH` | 53-54 | `bytes32` | +| `DISPAIR_REGISTRY_DEPLOYED_ADDRESS` | 58 | `address` | +| `DISPAIR_REGISTRY_DEPLOYED_CODEHASH` | 64-65 | `bytes32` | + +- No functions, errors, events, or assembly +- Constants-only library + +### LibAllStandardOps.sol (739 lines) + +**Library:** `LibAllStandardOps` + +| Function | Line | +|---|---| +| `authoringMetaV2()` | 121 | +| `literalParserFunctionPointers()` | 330 | +| `operandHandlerFunctionPointers()` | 363 | +| `integrityFunctionPointers()` | 535 | +| `opcodeFunctionPointers()` | 639 | + +- Constant: `ALL_STANDARD_OPS_LENGTH = 72` (line 106) +- Import: `BadDynamicLength` error from `ErrOpList.sol` +- 66 opcode library imports, 4 literal parser imports, `LibParseOperand`, `LibConvert` + +--- + +## DISPaiRegistry Access Control Analysis + +`RainterpreterDISPaiRegistry` has zero mutable state. All five functions are either `pure` or `view` (the `view` on `supportsInterface` comes from the OpenZeppelin ERC165 base). There is no storage, no `msg.sender` dependence, no external calls, and no assembly. Registry data cannot be tampered with because there is no write path -- all values are compile-time constants embedded in the bytecode. + +The contract is deployed via deterministic Zoltu deploy, so its address is a function of its creation code. Any change to the returned constants would change the bytecode, which would change the deploy address, making tampering self-evident. + +--- + +## LibInterpreterDeploy Hardcoded Constants Verification + +All five codehash constants were cross-referenced against the generated pointers files in `src/generated/`: + +| Component | LibInterpreterDeploy | Generated BYTECODE_HASH | Match | +|---|---|---|---| +| Parser | `0x0a82033a...453d615` | `0x0a82033a...453d615` | Yes | +| Store | `0x0504fb20...854210` | `0x0504fb20...854210` | Yes | +| Interpreter | `0x66a2e1c7...cbce0fa` | `0x66a2e1c7...cbce0fa` | Yes | +| ExpressionDeployer | `0xf9e6af09...484893e` | `0xf9e6af09...484893e` | Yes | +| DISPaiRegistry | `0x79edc50d...7a1191` | N/A (no generated file) | N/A | + +The DISPaiRegistry has no generated pointers file, which is expected -- it has no opcode dispatch. + +Tests in `test/src/lib/deploy/LibInterpreterDeploy.t.sol` verify: +- Zoltu deploy addresses match for all 5 components (fork tests) +- Codehash matches for all 5 components (local deploy tests) +- No CBOR metadata in any component bytecode +- No metamorphic risk opcodes in any component bytecode + +All address constants use EIP-55 checksummed format, which the Solidity compiler validates at compile time. + +--- + +## LibAllStandardOps Four-Array Ordering Consistency + +All four parallel arrays were verified entry-by-entry. Each contains exactly 72 entries matching `ALL_STANDARD_OPS_LENGTH`. The ordering is consistent: + +- **Positions 1-4:** stack, constant, extern, context (fixed well-known parser indexes) +- **Positions 5-11:** bitwise (and, or, ctpop, decode, encode, shift-left, shift-right) +- **Position 12:** call +- **Position 13:** hash +- **Positions 14-16:** uint256-erc20 (allowance, balance-of, total-supply) +- **Positions 17-19:** erc20 (allowance, balance-of, total-supply) +- **Positions 20-22:** erc721 (uint256-balance-of, balance-of, owner-of) +- **Position 23:** erc5313-owner +- **Positions 24-27:** evm (block-number, chain-id, block-timestamp, now) +- **Positions 28-39:** logic (any, conditions, ensure, equal-to, binary-equal-to, every, greater-than, greater-than-or-equal-to, if, is-zero, less-than, less-than-or-equal-to) +- **Positions 40-41:** growth (exponential, linear) +- **Positions 42-47:** uint256 math (max-value, add, div, mul, power, sub) +- **Positions 48-70:** float math (abs, add, avg, ceil, div, e, exp, exp2, floor, frac, gm, headroom, inv, max, max-negative-value, max-positive-value, min, min-negative-value, min-positive-value, mul, power, sqrt, sub) +- **Positions 71-72:** store (get, set) + +Position 27 ("now") correctly aliases position 26 ("block-timestamp") using `LibOpTimestamp.integrity` and `LibOpTimestamp.run` in both arrays. + +--- + +## Findings + +### A04-LOW-01: `sub` operand handler should be `handleOperandDisallowed`, not `handleOperandSingleFull` + +**Severity:** LOW + +**Location:** `src/lib/op/LibAllStandardOps.sol` line 512 + +**Description:** The `sub` opcode at position 70 in `operandHandlerFunctionPointers()` uses `LibParseOperand.handleOperandSingleFull`, while all other variable-arity float math ops (`add`, `mul`, `div`) use `LibParseOperand.handleOperandDisallowed`. + +`handleOperandSingleFull` allows the user to provide a single operand value in angle brackets (e.g., `sub<5>(a b)`), which writes the value into the low 16 bits of the operand. However, `LibOpSub.run` and `LibOpSub.integrity` only read from `(operand >> 0x10) & 0x0F` (bits 16-19), which is the high byte written by the parser's paren-closing logic. The low 16 bits set by `handleOperandSingleFull` are never read. + +This means `sub<42>(1 2)` parses successfully and executes identically to `sub(1 2)` -- the explicit operand `42` is silently ignored. By contrast, `add<42>(1 2)`, `mul<42>(1 2)`, and `div<42>(1 2)` all correctly reject the explicit operand with `UnexpectedOperand`. + +The existing test `testOpSubEvalTwoOperandsDisallowed` (line 118 of `LibOpSub.t.sol`) only tests rejection of two-value operands (`sub<0 0>`, `sub<0 1>`, `sub<1 0>`), which `handleOperandSingleFull` rejects via `UnexpectedOperandValue`. It does not test that single-value operands like `sub<1>` are rejected, because they are not rejected -- they are silently accepted and ignored. + +**Impact:** A Rainlang author writing `sub(...)` would expect the operand to have some effect (since the parser accepts it without error), but it is silently discarded. This is a usability/correctness issue, not a security vulnerability, because the operand value cannot influence runtime behavior. However, it violates the principle that accepted syntax should be meaningful. + +--- + +### A46-INFO-01: IDISPaiRegistry NatSpec has untagged lines alongside explicit tags + +**Severity:** INFO + +**Location:** `src/interface/IDISPaiRegistry.sol` lines 10-11, 14-15, 18-19, 22-23 + +**Description:** Each function's NatSpec block has an untagged first line followed by an explicit `@return` tag. For example: + +```solidity +/// Returns the deterministic deploy address of the expression deployer. +/// @return The expression deployer address. +``` + +Per the project's NatSpec convention (CLAUDE.md): "when a doc block contains any explicit tag (e.g. `@return`), all entries must be explicitly tagged." The first line should use `@notice` explicitly: + +```solidity +/// @notice Returns the deterministic deploy address of the expression deployer. +/// @return The expression deployer address. +``` + +**Impact:** Cosmetic. The Solidity compiler treats untagged lines as `@notice` when they precede any explicit tag, so the generated documentation is correct. This is a style consistency issue. + +--- + +## Summary + +One LOW finding and one INFO finding across the four files. + +The DISPaiRegistry is a minimal read-only contract with zero attack surface. LibInterpreterDeploy constants are verified to match generated pointers and are covered by comprehensive deployment tests. The four parallel arrays in LibAllStandardOps are consistently ordered with all 72 entries matching across all four arrays. + +The one substantive finding (A04-LOW-01) is that `sub` incorrectly uses `handleOperandSingleFull` instead of `handleOperandDisallowed`, allowing users to specify an operand value that is silently ignored at runtime. This is inconsistent with the other float math ops and should be fixed. diff --git a/audit/2026-03-01-01/pass1/LibEval.md b/audit/2026-03-01-01/pass1/LibEval.md new file mode 100644 index 000000000..ee982e96a --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibEval.md @@ -0,0 +1,177 @@ +# Pass 1 (Security) -- LibEval.sol + +**File:** `src/lib/eval/LibEval.sol` +**Agent:** A05 +**Date:** 2026-03-01 + +## Evidence of Thorough Reading + +### Library Name + +`library LibEval` -- line 15 + +### Functions + +| Function | Line | Visibility | +|----------|------|------------| +| `evalLoop(InterpreterState memory state, uint256 parentSourceIndex, Pointer stackTop, Pointer stackBottom) returns (Pointer)` | 41 | `internal view` | +| `eval2(InterpreterState memory state, StackItem[] memory inputs, uint256 maxOutputs) returns (StackItem[] memory, bytes32[] memory)` | 191 | `internal view` | + +### Imports + +| Import | Source | Line | +|--------|--------|------| +| `LibInterpreterState`, `InterpreterState` | `../state/LibInterpreterState.sol` | 5 | +| `LibMemCpy` | `rain.solmem/lib/LibMemCpy.sol` | 7 | +| `LibMemoryKV`, `MemoryKV` | `rain.lib.memkv/lib/LibMemoryKV.sol` | 8 | +| `LibBytecode` | `rain.interpreter.interface/lib/bytecode/LibBytecode.sol` | 9 | +| `Pointer` | `rain.solmem/lib/LibPointer.sol` | 10 | +| `OperandV2`, `StackItem` | `rain.interpreter.interface/interface/IInterpreterV4.sol` | 11 | +| `InputsLengthMismatch` | `../../error/ErrEval.sol` | 13 | + +### Using Declarations + +- `LibMemoryKV for MemoryKV` -- line 16 + +### Errors Referenced + +- `InputsLengthMismatch(uint256 expected, uint256 actual)` -- used at line 213, defined in `src/error/ErrEval.sol` + +### Types Used + +- `InterpreterState` (struct) -- parameter to both functions +- `Pointer` -- stack pointer type, parameter and return value +- `StackItem` -- stack value type, used in `eval2` input/output arrays +- `OperandV2` -- opcode operand type, used in eval loop dispatch +- `MemoryKV` -- ephemeral key-value store, accessed via `state.stateKV` + +### Constants/Errors Defined in This File + +None. All error types and constants are imported. + +--- + +## Security Findings + +### A05-1: `sourceIndex` Not Bounds-Checked in `evalLoop` + +**Severity: LOW** + +In `evalLoop` (lines 46-86), `state.sourceIndex` is masked to 16 bits (line 59) and used to index into the bytecode header to locate the source pointer, ops count, and start position. No bounds check is performed against the actual source count in the bytecode. + +The NatSpec at lines 25-33 explicitly documents this trust assumption. The two callers validate `sourceIndex` before calling: + +- `eval2` (line 231): Validates via `LibBytecode.sourceInputsOutputsLength` (line 200-201), which calls `sourceRelativeOffset`, which reverts with `SourceIndexOutOfBounds` if `sourceIndex >= sourceCount`. +- `LibOpCall.run` (line 153): Relies on the integrity check at deploy time. `LibOpCall.integrity` calls `LibBytecode.sourceInputsOutputsLength` which reverts for invalid indices. + +If `evalLoop` were called from a new code path that omits validation, the assembly at line 67 would compute `sourcesPointer` from whatever bytes happen to be at `cursor + sourceIndex * 2`, causing the cursor to land at an arbitrary position relative to `sourcesStart`. The `mod`-based function pointer lookup (line 100) constrains dispatch to real opcode handlers, but with arbitrary operands and an arbitrary opcode sequence, the result would be unpredictable stack manipulation within the allocated stack region. + +This is a documented trust assumption, not a bug in the current code. It becomes a risk only if the code is modified to add new callers without following the documented contract. + +### A05-2: Division-By-Zero Risk if `state.fs` is Empty + +**Severity: LOW** + +At line 53: `uint256 fsCount = state.fs.length / 2;` + +If `state.fs` is empty (length 0), `fsCount` is 0. The EVM `MOD` opcode returns 0 when the divisor is 0 (it does not revert). Every `mod(byte(..., word), 0)` in the dispatch assembly (lines 100, 107, 114, 121, 128, 135, 142, 149, 166) would return 0. The function pointer lookup at `fPointersStart + 0` would read 2 bytes starting at `add(fPointers, 0x20)`, which is past the end of the empty `fPointers` bytes array. These 2 bytes would be whatever happens to follow in memory, interpreted as an internal function pointer. This could cause a jump to an arbitrary internal function. + +**Mitigating factor:** The `Rainterpreter` constructor (line 39 of `Rainterpreter.sol`) checks `opcodeFunctionPointers().length == 0` and reverts with `ZeroFunctionPointers()`. This prevents the standard interpreter from being deployed with empty function pointers. The `unsafeDeserialize` function passes through the `fs` argument from the caller without validation. + +The risk is system-level: `evalLoop` as a library function does not self-protect. Any contract integrating `LibEval` directly (rather than through `Rainterpreter`) must ensure `state.fs` is non-empty. + +### A05-3: Modulo-Based Dispatch Wraps Out-of-Range Opcode Indices + +**Severity: INFO** + +The eval loop uses `mod(byte(..., word), fsCount)` (e.g., line 100) to bound opcode indices into the function pointer table. An opcode byte value exceeding `fsCount` wraps around via modulo rather than reverting. For example, if `fsCount` is 50 and the byte is 200, opcode `200 % 50 = 0` is dispatched. + +This is by design. The integrity check (`LibIntegrityCheck.sol`) performs strict bounds checks at deploy time, and the expression deployer verifies bytecode hashes to prevent tampering. The `mod` is a cheaper runtime bound than a conditional revert, and only corrupted bytecode (which cannot pass hash verification) could trigger unintended wraparound. + +No action required. + +### A05-4: `eval2` Entire Body in `unchecked` Block + +**Severity: INFO** + +The entire `eval2` function body (lines 196-248) is in an `unchecked` block. Arithmetic operations: + +- Line 222: `stackTop := sub(stackTop, mul(mload(inputs), 0x20))` -- assembly, unchecked regardless. `inputs.length` is validated against `sourceInputs` (a single byte, max 255) at line 212, and the stack was allocated with `sourceInputs` slots. No underflow risk. +- Line 240: `maxOutputs < sourceOutputs ? maxOutputs : sourceOutputs` -- min operation, no overflow. +- Line 243: `stack := sub(stackTop, 0x20)` -- `stackTop` is a valid memory pointer well above 0. + +All values are constrained by bytecode structure and integrity checks. The `unchecked` block is appropriate. + +### A05-5: In-Place Output Array Construction Shares Memory With Stack + +**Severity: INFO** + +At lines 242-245: +```solidity +assembly ("memory-safe") { + stack := sub(stackTop, 0x20) + mstore(stack, outputs) +} +``` + +This constructs a `StackItem[]` by pointing `stack` to 32 bytes before `stackTop` and writing `outputs` as the array length. The returned array aliases the stack's memory region. The NatSpec at lines 233-239 correctly documents that both `stack` and the original stack array must be treated as immutable after this point. + +This is safe because `eval2` returns immediately after this construction. The returned array is a read-only view of the stack. If any future modification to the code wrote to the returned array or the stack after this point, it would corrupt both. + +### A05-6: `stackTrace` Transient Memory Mutation + +**Severity: INFO** + +`LibInterpreterState.stackTrace` (called at line 174 from `evalLoop`) temporarily writes to `sub(stackTop, 0x20)` -- the 32-byte word immediately before the stack top. It saves the original value, overwrites it with packed source indices, makes a `staticcall` to a non-existent address, then restores the original value. + +This is safe because: +1. The `staticcall` to a codeless address cannot have side effects. +2. The 63/64ths gas rule ensures the caller retains sufficient gas for the `mstore` restore. +3. The address is derived from `keccak256("rain.interpreter.stack-tracer.0")`, making collision with a real contract address infeasible. + +The only subtlety: `sub(stackTop, 0x20)` points to memory that was either part of the stack allocation or (if `stackTop == stackBottom`, i.e., no values were pushed) the word immediately before the stack allocation. In the `stackBottom == stackTop` case, this reads/writes the stack's length prefix word, which is safely restored. In the `LibOpCall.run` path, `stackTop == stackBottom` when a called source pushes no values beyond its inputs. The length prefix or a previously pushed value is transiently overwritten and restored. + +### A05-7: `memory-safe` Annotations Are Correct + +**Severity: INFO** + +All assembly blocks in `evalLoop` (lines 57, 92, 99, 106, 113, 120, 127, 134, 141, 148, 164) are marked `memory-safe`. These blocks only: +- Read from `cursor` (within bytecode), `fPointersStart` (within function pointer table), and `word` (a stack variable). +- Write to stack variables (`cursor`, `end`, `m`, `fPointersStart`, `sourceIndex`, `f`, `operand`, `word`). + +No memory allocation or modification occurs in these blocks. + +In `eval2`, assembly blocks at lines 220-224 and 242-244 write to memory that was allocated by Solidity (stack arrays), which satisfies the `memory-safe` definition. + +### A05-8: Remainder Loop Cursor Arithmetic + +**Severity: INFO** + +At line 161: `cursor -= 0x1c;` (28 bytes back) + +After the main 8-at-a-time loop, the cursor is adjusted backwards so that `mload(cursor)` places the next opcode in bytes [28-31] of the loaded 32-byte word, aligning with `byte(28, word)` and `and(word, 0xFFFFFF)` used in the remainder loop (lines 165-168). + +Edge cases verified: +- **`opsLength == 0`:** `m = 0`, `end == cursor` after main loop (which doesn't execute). `cursor -= 0x1c` adjusts back, but `end = cursor + 0 = cursor` for the remainder loop, so it doesn't execute. Correct. +- **`opsLength < 8`:** Main loop doesn't execute (`end == cursor`). `cursor -= 0x1c`, then `end = cursor + opsLength * 4`. Remainder loop processes all opcodes one by one. Correct. +- **`opsLength` is exact multiple of 8:** Main loop processes all opcodes. `m = 0`, so `end = cursor + 0 = cursor` for remainder loop. Doesn't execute. Correct. +- **General case:** Main loop processes `opsLength - m` opcodes. Remainder processes the last `m` opcodes (1-7). Cursor positions are consistent. + +--- + +## Summary + +No CRITICAL or HIGH severity findings. `LibEval.sol` relies on a layered trust model where upstream components (integrity checks at deploy time, `sourceInputsOutputsLength` at eval time, `ZeroFunctionPointers` constructor guard) ensure the eval loop operates on well-formed inputs. The key security property -- that the eval loop cannot jump to arbitrary code -- is maintained by: + +1. Function pointer indices bounded by `mod(..., fsCount)`, limiting dispatch to real opcode handlers. +2. Integrity checks at deploy time verify all opcode indices are in range. +3. Expression deployer verifies bytecode hashes, preventing post-deployment tampering. +4. `ZeroFunctionPointers` constructor check prevents mod-by-zero in the standard interpreter. + +| Severity | Count | +|----------|-------| +| CRITICAL | 0 | +| HIGH | 0 | +| MEDIUM | 0 | +| LOW | 2 | +| INFO | 6 | diff --git a/audit/2026-03-01-01/pass1/LibExtern.md b/audit/2026-03-01-01/pass1/LibExtern.md new file mode 100644 index 000000000..9ae72dd11 --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibExtern.md @@ -0,0 +1,156 @@ +# Pass 1 (Security) -- LibExtern.sol & LibOpExtern.sol + +Agents: A06 (LibExtern), A21 (LibOpExtern) + +## Files + +- `src/lib/extern/LibExtern.sol` +- `src/lib/op/00/LibOpExtern.sol` + +## Evidence of Thorough Reading + +### LibExtern.sol + +**Library:** `library LibExtern` (line 17) + +**Functions:** + +| Function | Line | Visibility | Mutability | +|----------|------|------------|------------| +| `encodeExternDispatch(uint256 opcode, OperandV2 operand)` | 27 | `internal` | `pure` | +| `decodeExternDispatch(ExternDispatchV2 dispatch)` | 35 | `internal` | `pure` | +| `encodeExternCall(IInterpreterExternV4 extern, ExternDispatchV2 dispatch)` | 56 | `internal` | `pure` | +| `decodeExternCall(EncodedExternDispatchV2 dispatch)` | 70 | `internal` | `pure` | + +**Errors/Events/Structs/Constants:** None defined in this file. + +**Imports:** +- `IInterpreterExternV4`, `ExternDispatchV2`, `EncodedExternDispatchV2` from `rain.interpreter.interface/interface/IInterpreterExternV4.sol` (lines 5-9) +- `OperandV2`, `StackItem` from `rain.interpreter.interface/interface/IInterpreterV4.sol` (line 12) + +**User-defined types (from interface):** +- `ExternDispatchV2 is bytes32` +- `EncodedExternDispatchV2 is bytes32` +- `OperandV2 is bytes32` + +### LibOpExtern.sol + +**Library:** `library LibOpExtern` (line 23) + +**Functions:** + +| Function | Line | Visibility | Mutability | +|----------|------|------------|------------| +| `integrity(IntegrityCheckState memory, OperandV2)` | 29 | `internal` | `view` | +| `run(InterpreterState memory, OperandV2, Pointer)` | 49 | `internal` | `view` | +| `referenceFn(InterpreterState memory, OperandV2, StackItem[] memory)` | 102 | `internal` | `view` | + +**Errors/Events/Structs/Constants:** None defined. Errors imported: +- `NotAnExternContract` from `../../../error/ErrExtern.sol` (line 5, originally from `rain.interpreter.interface/error/ErrExtern.sol`) +- `BadOutputsLength` from `../../../error/ErrExtern.sol` (line 19) + +**Imports:** +- `NotAnExternContract` from `../../../error/ErrExtern.sol` (line 5) +- `IntegrityCheckState` from `../../integrity/LibIntegrityCheck.sol` (line 6) +- `OperandV2` from `rain.interpreter.interface/interface/IInterpreterV4.sol` (line 7) +- `InterpreterState` from `../../state/LibInterpreterState.sol` (line 8) +- `Pointer` from `rain.solmem/lib/LibPointer.sol` (line 9) +- `IInterpreterExternV4`, `ExternDispatchV2`, `EncodedExternDispatchV2`, `StackItem` from `rain.interpreter.interface/interface/IInterpreterExternV4.sol` (lines 10-15) +- `LibExtern` from `../../extern/LibExtern.sol` (line 16) +- `LibBytes32Array` from `rain.solmem/lib/LibBytes32Array.sol` (line 17) +- `ERC165Checker` from `openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol` (line 18) +- `BadOutputsLength` from `../../../error/ErrExtern.sol` (line 19) + +## Security Analysis + +### Encoding/Decoding Safety (LibExtern.sol) + +**ExternDispatchV2 encoding** (line 28): `bytes32(opcode) << 0x10 | OperandV2.unwrap(operand)` +- Bits [0,16): operand low 16 bits +- Bits [16,32): opcode low 16 bits +- Bits above 32: any overflow from opcode > 16 bits or operand bits above 15 +- No masking or validation. Documented as caller responsibility (lines 22-23). + +**ExternDispatchV2 decoding** (lines 36-40): +- Opcode: `uint256(dispatch >> 0x10)` -- returns full 240 bits above bit 16, no mask to 16 bits. +- Operand: `dispatch & bytes32(uint256(0xFFFF))` -- properly masked to 16 bits. +- The unmasked opcode decode is not a vulnerability because: (a) the actual consumer in `BaseRainterpreterExtern.extern()` applies its own `& bytes32(uint256(type(uint16).max))` mask (line 71 of BaseRainterpreterExtern.sol), and (b) `decodeExternDispatch` is only used in tests. + +**EncodedExternDispatchV2 encoding** (lines 61-63): `bytes32(uint256(uint160(address))) | dispatch << 160` +- Bits [0,160): extern address (safe, `uint160` truncation enforced by Solidity) +- Bits [160,256): dispatch (when correctly encoded, only bits [160,192) used) +- Roundtrip correct when dispatch uses only low 32 bits. + +**EncodedExternDispatchV2 decoding** (lines 75-78): +- Address: `uint160(uint256(unwrap(dispatch)))` -- extracts low 160 bits cleanly. +- Dispatch: `unwrap(dispatch) >> 160` -- recovers bits [160,256). +- Roundtrip correct. + +### Malformed Dispatch / Out-of-Bounds Access + +If a malformed `EncodedExternDispatchV2` is stored in the constants array, `decodeExternCall` will extract whatever address and dispatch bits are present. The extern address could be an EOA or non-contract address. In `run()`, the call to `extern.extern()` on an EOA would revert (no code). In `integrity()`, the ERC165 check would fail and revert with `NotAnExternContract`. No out-of-bounds memory access is possible from malformed dispatch values. + +The `encodedExternDispatchIndex` (low 16 bits of operand) is used to index `state.constants[]`. Solidity's built-in bounds checking applies. An index beyond the constants array causes a panic revert. + +### ERC165 Check: Integrity vs Run + +`integrity()` (line 35) checks `ERC165Checker.supportsInterface(address(extern), type(IInterpreterExternV4).interfaceId)`. `run()` does not repeat this check. This is by design: integrity runs at deploy time through the expression deployer, and the constants (including the extern address) are immutable after deployment. Re-checking at runtime would waste gas with no security benefit under the invariant that constants are immutable. If a proxy-based extern changed implementation post-deploy, the `extern.extern()` call would revert naturally if the interface is no longer supported. + +### External Call Safety + +Both `extern.extern()` calls (in `run` at line 71, `referenceFn` at line 113) occur within `internal view` functions. The top-level `eval4()` is `external view`, so all external calls are `staticcall`. State modifications are impossible, eliminating reentrancy concerns. The extern cannot call back into the interpreter to modify state. + +### Return Data Validation + +`run()` (line 72) checks `outputsLength != outputs.length` and reverts with `BadOutputsLength`. `referenceFn()` (line 114) does the same check. The Solidity ABI decoder handles malformed return data -- if the extern returns data that cannot be decoded as `StackItem[] memory`, the decoder reverts. + +### Stack Manipulation Safety (LibOpExtern.run) + +The assembly in `run()` (lines 59-93): +1. **Input array construction** (lines 59-70): Temporarily overwrites `sub(stackTop, 0x20)` to build an array length field. Original value saved in `head` and restored at line 79. The mutation window spans only the `extern.extern()` staticcall, which cannot observe or modify the caller's memory. +2. **Stack pointer adjustment** (line 80): `stackTop := add(stackTop, mul(inputsLength, 0x20))` pops inputs. Safe because integrity guarantees sufficient stack depth. +3. **Output reverse copy** (lines 82-92): Iterates outputs forward, writes to stack backward. Each iteration decrements stackTop by 0x20 and writes one output. The loop is bounded by `outputsLength` which was validated against `outputs.length`. Final stackTop = original + inputsLength*32 - outputsLength*32. Stack allocation was pre-computed during integrity to accommodate this. + +All three assembly blocks are correctly annotated `"memory-safe"`: +- Block 1 (lines 59-70): Writes to stack memory within allocated region, saves/restores the overwritten word. +- Block 2 (lines 76-92): Restores saved word, reads from ABI-decoded array (allocated at free pointer), writes within pre-allocated stack. +- Block 3 in `referenceFn` (lines 119-121): Type-punning cast only, no memory access. + +### Operand Bit Layout Consistency + +Both `integrity()` and `run()` extract the same three fields identically: +- `encodedExternDispatchIndex`: `OperandV2.unwrap(operand) & bytes32(uint256(0xFFFF))` -- 16 bits [0,16) +- `inputsLength` (integrity line 38, run line 51): `(operand >> 0x10) & 0x0F` -- 4 bits [16,20) +- `outputsLength` (integrity line 39, run line 52): `(operand >> 0x14) & 0x0F` -- 4 bits [20,24) + +`referenceFn()` extracts the same `encodedExternDispatchIndex` and `outputsLength` but not `inputsLength` (receives inputs as an array parameter). Consistent. + +## Findings + +No new security findings above INFO severity. + +Both files are unchanged since the previous audit (commit `31a6799f`). The previous audit's findings (A06-1 through A06-3 for LibExtern, A21-LOW-1 and A21-LOW-2 for LibOpExtern) were triaged and either fixed or dismissed. No new code has been introduced. + +### A06-INFO-1: No assembly blocks in LibExtern.sol + +All operations are pure Solidity bitwise operations on `bytes32` user-defined value types. No memory safety concerns, no pointer arithmetic, no unchecked blocks. + +### A06-INFO-2: Encoding functions document lack of input validation + +`encodeExternDispatch` (line 22-23) and `encodeExternCall` (lines 49-52) explicitly document that they do not validate input widths. This is appropriate for internal pure functions where the caller is responsible for providing correctly-sized values. The only production caller is the sub-parser path which provides compile-time constants. + +### A21-INFO-1: Reentrancy impossible due to view/staticcall context + +The `extern.extern()` calls in `run()` (line 71) and `referenceFn()` (line 113) execute as `staticcall` because the entire eval chain is `view`. State modification and reentrancy are structurally impossible. + +### A21-INFO-2: Constants array access is bounds-checked by Solidity + +`state.constants[encodedExternDispatchIndex]` at lines 33, 54, and 110 uses standard Solidity array indexing with automatic bounds checking. The maximum possible index is 65535 (16-bit operand field). An out-of-bounds index causes a Solidity panic revert. + +### A21-INFO-3: Assembly memory-safe annotations are correct + +All three assembly blocks in LibOpExtern are annotated `"memory-safe"` and verified to only read/write within previously allocated memory regions. The temporary mutation of the word before `stackTop` is saved/restored within the same function, and the mutation window spans only a `staticcall` which cannot observe the caller's memory. + +### A21-INFO-4: Custom errors used correctly + +All error paths use custom errors (`NotAnExternContract`, `BadOutputsLength`). No string revert messages. Consistent with project conventions. diff --git a/audit/2026-03-01-01/pass1/LibIntegrityCheck.md b/audit/2026-03-01-01/pass1/LibIntegrityCheck.md new file mode 100644 index 000000000..0920ad418 --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibIntegrityCheck.md @@ -0,0 +1,172 @@ +# A12 -- Pass 1 (Security) -- LibIntegrityCheck.sol + +**File:** `src/lib/integrity/LibIntegrityCheck.sol` (207 lines) +**Agent:** A12 +**Date:** 2026-03-01 + +--- + +## Evidence of Thorough Reading + +### Library Name + +- `LibIntegrityCheck` (library, line 40) + +### Struct Definitions + +- `IntegrityCheckState` (lines 31-38): fields `stackIndex` (uint256), `stackMaxIndex` (uint256), `readHighwater` (uint256), `constants` (bytes32[]), `opIndex` (uint256), `bytecode` (bytes) + +### Function Names and Line Numbers + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `newState` | 52 | internal | pure | +| `integrityCheck2` | 87 | internal | view | + +### Errors Used (all imported, none defined locally) + +From `src/error/ErrIntegrity.sol`: +- `OpcodeOutOfRange` (used line 153) +- `StackAllocationMismatch` (used line 196) +- `StackOutputsMismatch` (used line 201) +- `StackUnderflow` (used line 167) +- `StackUnderflowHighwater` (used line 173) + +From `rain.interpreter.interface/error/ErrIntegrity.sol`: +- `BadOpInputsLength` (used line 160) +- `BadOpOutputsLength` (used line 163) + +### Imports + +- `Pointer` from `rain.solmem/lib/LibPointer.sol` (line 5) +- `LibBytecode` from `rain.interpreter.interface/lib/bytecode/LibBytecode.sol` (line 15) +- `OperandV2` from `rain.interpreter.interface/interface/IInterpreterV4.sol` (line 16) + +### Constants/Events Defined + +None. + +### Using-for Directives + +- `LibIntegrityCheck for IntegrityCheckState` (line 41) + +--- + +## Analysis + +### Structural Integrity Delegation + +`integrityCheck2` delegates structural validation to `LibBytecode.checkNoOOBPointers` (line 108) before iterating over opcodes. This call validates source count, relative offsets, ops count per source, contiguity, and that `inputs <= outputs <= stackAllocation`. This is the correct ordering -- structural integrity is confirmed before any per-opcode iteration. + +### Assembly Block Analysis + +1. **Lines 97-100** -- Reads `fPointers` length and computes data start. Read-only, memory-safe. Correct. + +2. **Lines 112-114** -- Reads `io` array data pointer. The `io` array was just allocated at line 110 with length `sourceCount * 2`. Reading its data start at `io + 0x20` is correct. + +3. **Lines 124-128** -- Writes `inputsLength` and `outputsLength` into `io` via `mstore8`. Each source writes exactly 2 bytes. The cursor starts at `io + 0x20` and advances by 2 for each of the `sourceCount` iterations, so writes stay within the `sourceCount * 2` allocation. Both `inputsLength` and `outputsLength` come from `LibBytecode.sourceInputsOutputsLength` which reads single bytes from bytecode headers, so they fit in a byte. Correct. + +4. **Lines 143-151** -- Reads opcode fields from `cursor`. The `mload(cursor)` reads 32 bytes. The opcode fields are extracted using `byte(28, word)` for opcodeIndex, `byte(29, word)` for ioByte, and `and(word, 0xFFFFFF)` for the 3-byte operand. Since `cursor = sourcePointer - 0x18` and `sourcePointer` points to the 4-byte header, `cursor + 28 = sourcePointer + 4` which is the first byte of the first opcode. Each subsequent `cursor += 4` correctly advances to the next opcode. The bounds are validated by `checkNoOOBPointers`. Correct. + +5. **Lines 155-157** -- Reads a 2-byte function pointer from the function pointer table. `opcodeIndex < fsCount` is enforced at line 152. The maximum byte offset is `(fsCount - 1) * 2`, within the `fPointers` data region. `shr(0xf0, mload(...))` shifts the loaded 32-byte word right by 240 bits, isolating the top 2 bytes. Correct. + +### Stack Tracking Correctness + +The stack tracking logic in `integrityCheck2` follows this sequence per opcode: + +1. Call integrity function `f(state, operand)` to get `calcOpInputs, calcOpOutputs` (line 158) +2. Verify `calcOpInputs == bytecodeOpInputs` and `calcOpOutputs == bytecodeOpOutputs` (lines 159-164) +3. Check `calcOpInputs <= state.stackIndex` (underflow check, line 166) +4. Subtract inputs from stack: `state.stackIndex -= calcOpInputs` (line 169) +5. Check `state.stackIndex >= state.readHighwater` (highwater check, line 172) +6. Add outputs to stack: `state.stackIndex += calcOpOutputs` (line 178) +7. Update `stackMaxIndex` if needed (lines 181-183) +8. Advance highwater for multi-output opcodes (lines 186-188) + +This sequence is correct. The underflow check precedes the subtraction, and the highwater check occurs after subtraction but before addition, which is the right point to verify the stack hasn't been consumed below the protected region. + +### Operand Encoding Verification + +The operand is extracted as `and(word, 0xFFFFFF)` (line 147), giving a 3-byte value stored in the low bytes of a `bytes32` (since `OperandV2` is `bytes32`). This encoding means: +- Low 16 bits (bits 0-15): operand data field (e.g., stack index, constant index) +- Bits 16-19: input count for call/extern opcodes +- Bits 20-23: output count for call/extern opcodes + +The ioByte is separately extracted at `byte(29, word)` (line 148), which is the second byte of the 4-byte opcode. The inputs are `and(ioByte, 0x0F)` (low nibble, max 15) and outputs are `shr(4, ioByte)` (high nibble, max 15). + +### Post-Loop Validations + +After processing all opcodes in a source: +- `stackMaxIndex` must equal the bytecode's declared `stackAllocation` (line 195) +- `stackIndex` must equal the declared `outputsLength` (line 200) + +Both are strict equality checks, meaning the bytecode cannot over-allocate or under-report outputs. + +--- + +## Findings + +### A12-2 | LOW | `readHighwater` NatSpec describes it as "Lowest stack index that opcodes are allowed to read from" but it is not enforced as a read floor + +**File:** `src/lib/integrity/LibIntegrityCheck.sol`, lines 23-24, 172-174, 186-188 + +The `IntegrityCheckState.readHighwater` field's NatSpec (lines 23-24) states: "Lowest stack index that opcodes are allowed to read from. Advances past multi-output regions to prevent aliasing reads." + +However, `readHighwater` is not enforced as a read floor. It is enforced only as a *consumption floor* via the check at line 172: `state.stackIndex < state.readHighwater`. The `LibOpStack.integrity` function (in `src/lib/op/00/LibOpStack.sol` line 24) checks `readIndex >= state.stackIndex` for bounds but does NOT check `readIndex` against `readHighwater`. It only *advances* the highwater (line 29-31), never enforces it as a constraint on reads. + +This means an opcode like `stack` can read values below the highwater -- values produced by a multi-output opcode that have since been "protected" by the highwater. This may be intentional (stack reads are copies, not consumptions, so aliasing doesn't apply), but the NatSpec is misleading. + +If the intent is that reads below the highwater should be allowed (since copying doesn't create aliasing), the NatSpec should say "Lowest stack index that the stack pointer is allowed to drop to after consuming inputs" rather than "Lowest stack index that opcodes are allowed to read from." + +### A12-3 | INFO | Assembly memory safety annotations are correct + +All five assembly blocks in this file are annotated `("memory-safe")`. Each block was verified: +- Lines 97-100: read-only access to `fPointers` length field +- Lines 112-114: read-only access to freshly allocated `io` array +- Lines 124-128: writes to `io` via `mstore8`, within allocated bounds +- Lines 143-151: read-only access to bytecode via `mload`, within validated bounds +- Lines 155-157: read-only access to function pointer table, within bounds-checked range + +No issues found. + +### A12-4 | INFO | Unchecked arithmetic is safe due to preceding guards + +The entire `integrityCheck2` body is wrapped in `unchecked` (line 92). All arithmetic operations are protected: + +- **Line 169** (`state.stackIndex -= calcOpInputs`): guarded by the `calcOpInputs > state.stackIndex` check at line 166. +- **Line 134** (`Pointer.unwrap(...) - 0x18`): `sourcePointer` returns at minimum `bytecode + 0x20 + 3 + 0` (for a single source with offset 0), which is always >> `0x18`. +- **Line 135** (`cursor + sourceOpsCount * 4`): ops count is a byte (max 255), so `255 * 4 = 1020`, which cannot overflow when added to a memory pointer. +- **Line 178** (`state.stackIndex += calcOpOutputs`): `calcOpOutputs` is verified equal to `bytecodeOpOutputs` which is at most 15 (4-bit). With max 255 opcodes and max 15 outputs each, theoretical max is `255 + 255 * 15 = 4080`, well within uint256 range. +- **Line 110** (`sourceCount * 2`): `sourceCount` is a byte (max 255), so `255 * 2 = 510`, safe. + +### A12-5 | INFO | Function pointer table bounds check is correct + +Line 152 checks `opcodeIndex >= fsCount` before using `opcodeIndex` at line 156. The `fsCount` is `mload(fPointers) / 2` (number of 2-byte entries). The table access at `fPointersStart + opcodeIndex * 2` stays within bounds since `opcodeIndex < fsCount` is guaranteed. The `shr(0xf0, ...)` isolates the correct 2-byte pointer from the 32-byte word. + +### A12-6 | INFO | Integrity-calculated IO vs bytecode-declared IO comparison prevents bypass + +The checks at lines 159-164 compare the values returned by each opcode's integrity function against the bytecode-declared IO byte. Since both must match, a malicious integrity function cannot cause the stack tracker to use different values than what the bytecode declares. The bytecode IO values are 4-bit packed (max 15 each), so any integrity function returning values > 15 will necessarily fail the equality check. This prevents integrity bypass via fabricated IO values. + +### A12-7 | INFO | `opIndex` is informational only + +`state.opIndex` (incremented at line 190) is used exclusively in error messages (lines 153, 160, 163, 167, 173). Loop control is via `cursor < end` (line 137), which derives from `checkNoOOBPointers`-validated source boundaries. No control flow dependency on `opIndex`. + +### A12-8 | INFO | Zero source count is handled correctly + +When `sourceCount` is 0, the loop at line 120 does not execute, and `io` is allocated as `new bytes(0)` (line 110). `checkNoOOBPointers` handles the zero-source case by checking for trailing bytes (lines 169-176 in `LibBytecode.sol`). The function returns an empty `io` byte array, which is correct. + +--- + +## Summary + +| ID | Severity | Title | +|----|----------|-------| +| A12-2 | LOW | `readHighwater` NatSpec is misleading -- describes read floor but only enforced as consumption floor | +| A12-3 | INFO | Assembly memory safety annotations verified correct | +| A12-4 | INFO | Unchecked arithmetic is safe due to preceding guards | +| A12-5 | INFO | Function pointer table bounds check is correct | +| A12-6 | INFO | Integrity IO comparison prevents bypass | +| A12-7 | INFO | `opIndex` is informational only | +| A12-8 | INFO | Zero source count handled correctly | + +**Total findings: 7** (0 CRITICAL, 0 HIGH, 0 MEDIUM, 1 LOW, 6 INFO) diff --git a/audit/2026-03-01-01/pass1/LibInterpreterState.md b/audit/2026-03-01-01/pass1/LibInterpreterState.md new file mode 100644 index 000000000..f183b9b15 --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibInterpreterState.md @@ -0,0 +1,260 @@ +# Pass 1 (Security) -- LibInterpreterState.sol & LibInterpreterStateDataContract.sol + +**Auditor**: A14/A15 +**Date**: 2026-03-01 +**Files**: +- `src/lib/state/LibInterpreterState.sol` (143 lines) +- `src/lib/state/LibInterpreterStateDataContract.sol` (143 lines) + +## Evidence of Thorough Reading + +### LibInterpreterState.sol + +**Constant**: `STACK_TRACER` (line 17) -- `address(uint160(uint256(keccak256("rain.interpreter.stack-tracer.0"))))`, deterministic non-contract address used as staticcall target for debug traces. + +**Struct**: `InterpreterState` (line 42-53) -- 9 fields: + +| Field | Type | Line | +|---|---|---| +| `stackBottoms` | `Pointer[]` | 43 | +| `constants` | `bytes32[]` | 44 | +| `sourceIndex` | `uint256` | 45 | +| `stateKV` | `MemoryKV` | 47 | +| `namespace` | `FullyQualifiedNamespace` | 48 | +| `store` | `IInterpreterStoreV3` | 49 | +| `context` | `bytes32[][]` | 50 | +| `bytecode` | `bytes` | 51 | +| `fs` | `bytes` | 52 | + +**Library**: `LibInterpreterState` (line 55) + +**Functions**: + +| Function | Signature | Visibility | Line | +|---|---|---|---| +| `stackBottoms` | `(StackItem[][] memory) -> (Pointer[] memory)` | internal pure | 62 | +| `stackTrace` | `(uint256, uint256, Pointer, Pointer) -> ()` | internal view | 126 | + +### LibInterpreterStateDataContract.sol + +**Library**: `LibInterpreterStateDataContract` (line 14) + +**Using directive**: `LibBytes for bytes` (line 15) + +**Imports**: + +| Import | Source | Line | +|---|---|---| +| `MemoryKV` | `rain.lib.memkv/lib/LibMemoryKV.sol` | 5 | +| `Pointer` | `rain.solmem/lib/LibPointer.sol` | 6 | +| `LibMemCpy` | `rain.solmem/lib/LibMemCpy.sol` | 7 | +| `LibBytes` | `rain.solmem/lib/LibBytes.sol` | 8 | +| `FullyQualifiedNamespace` | `rain.interpreter.interface/interface/IInterpreterV4.sol` | 9 | +| `IInterpreterStoreV3` | `rain.interpreter.interface/interface/IInterpreterStoreV3.sol` | 10 | +| `InterpreterState` | `./LibInterpreterState.sol` | 12 | + +**Functions**: + +| Function | Signature | Visibility | Line | +|---|---|---|---| +| `serializeSize` | `(bytes memory, bytes32[] memory) -> (uint256)` | internal pure | 26 | +| `unsafeSerialize` | `(Pointer, bytes memory, bytes32[] memory) -> ()` | internal pure | 39 | +| `unsafeDeserialize` | `(bytes memory, uint256, FullyQualifiedNamespace, IInterpreterStoreV3, bytes32[][] memory, bytes memory) -> (InterpreterState memory)` | internal pure | 69 | + +--- + +## Security Analysis + +### 1. `stackBottoms` Assembly Correctness (LibInterpreterState.sol lines 62-79) + +The function allocates a `Pointer[]` array of length `stacks.length`, then iterates over each `StackItem[]` in the input array, computing `stackBottom = stack + 0x20 * (length + 1)` for each. + +**Loop invariants**: +- `cursor` starts at `stacks + 0x20` (first element pointer), ends at `stacks + 0x20 + stacks.length * 0x20` +- `bottomsCursor` starts at `bottoms + 0x20` (first element slot), advances in lockstep +- For each stack: `mload(cursor)` loads the pointer to the inner `StackItem[]`, then `mload(stack)` reads its length + +**Edge case -- empty stacks**: When `stacks.length == 0`, `end == cursor` at init, the loop body never executes, and an empty `Pointer[]` is returned. Correct. + +**Memory safety**: The `bottoms` array is allocated via `new Pointer[](stacks.length)` before the assembly block, so Solidity manages the free memory pointer. The assembly only writes within the allocated bounds. Marked `memory-safe` correctly. + +**Conclusion**: Correct. No issues found. + +### 2. `stackTrace` Memory Mutation (LibInterpreterState.sol lines 126-142) + +This function temporarily overwrites memory at `stackTop - 0x20` with the packed `(parentSourceIndex, sourceIndex)` selector, calls `STACK_TRACER` via `staticcall`, then restores the original value. + +**Save/restore correctness**: Line 135 saves `mload(sub(stackTop, 0x20))` into `before`. Line 136 overwrites it. Line 140 restores it. The pattern is correct -- the value is always restored regardless of the staticcall outcome. + +**Masking**: Both `parentSourceIndex` and `sourceIndex` are masked to 16 bits with `and(..., 0xFFFF)` before being packed. Upper bits are correctly discarded. + +**Calldata region**: The staticcall reads from `sub(stackTop, 4)` with length `sub(stackBottom, stackTop) + 4`. This correctly selects the 4-byte packed selector plus the stack data from top to bottom. + +**Edge case -- empty stack** (`stackTop == stackBottom`): The staticcall size is `4` (just the selector). The memory at `stackTop - 0x20` is still within valid memory because `stackTop` points to `stackBottom`, which is computed as `stack_array + 0x20 * (length + 1)`. The word at `stackTop - 0x20` is either the last stack slot or, for a zero-length stack, the length prefix of the array. The save/restore ensures no corruption. + +**Edge case -- calldata extends into packed selector bytes**: The 4-byte selector is written at `stackTop - 0x20` but the `staticcall` reads from `sub(stackTop, 4)`. Since `mstore` writes a full 32-byte word at the target, the low 4 bytes of the 32-byte word at `stackTop - 0x20` will be at address `stackTop - 4`. The packed `or(shl(0x10, masked_parent), masked_source)` places the two uint16 values in the high 4 bytes of the 32-byte word. But `mstore(beforePtr, ...)` writes the full word, placing the 4-byte value in the high bytes at `beforePtr`. Reading from `sub(stackTop, 4)` reads bytes at `stackTop - 4` through `stackTop - 4 + 31`. The relevant 4 bytes at `stackTop - 4` correspond to bytes 28-31 of the word stored at `stackTop - 0x20`, which would be zeros from the `or(shl(0x10, ...), ...)` because the value is only 4 bytes wide and stored in the high bytes of the word. + +Wait -- let me re-check this. The value written is `or(shl(0x10, and(parentSourceIndex, 0xFFFF)), and(sourceIndex, 0xFFFF))`. This is a 32-bit value (4 bytes). `mstore(beforePtr, value)` stores it left-aligned in the 32-byte word at `beforePtr`. So bytes at `beforePtr + 0` through `beforePtr + 3` contain the 4-byte selector. `beforePtr = stackTop - 0x20`. So the selector is at addresses `stackTop - 0x20` through `stackTop - 0x1d`. The `staticcall` reads from `sub(stackTop, 4)` = `stackTop - 4`, which is addresses `stackTop - 4` through `stackTop - 4 + calldata_size - 1`. + +These two ranges do not overlap when `stackTop - 0x20 + 3 < stackTop - 4`, i.e., `0x20 - 3 > 4`, i.e., `29 > 4` -- true. So the staticcall reads from a different region than where the selector was written. + +This is a potential issue. The selector bytes are written at `stackTop - 0x20`, but the calldata starts at `stackTop - 4`. The 4 bytes of calldata at `stackTop - 4` through `stackTop - 1` are NOT the selector -- they are whatever was in those bytes before the function was called (the low bytes of the overwritten word and/or the high bytes of the first stack item). + +Actually, let me re-read the assembly more carefully: + +```solidity +mstore(beforePtr, or(shl(0x10, and(parentSourceIndex, 0xFFFF)), and(sourceIndex, 0xFFFF))) +``` + +`beforePtr = sub(stackTop, 0x20)`. This stores: the value `(parentSourceIndex & 0xFFFF) << 16 | (sourceIndex & 0xFFFF)` is a uint256 with only the top 32 bits set (since shl(0x10, ...) shifts left by 16 bits, not bytes). Wait, no. `shl` operates on the full 256-bit value. `shl(0x10, x)` shifts x left by 16 bits. If x is `0xABCD`, the result is `0xABCD0000`. Then `or` with `sourceIndex & 0xFFFF`. So the result is a value like `0xABCD1234` (32 bits, fitting in the high bytes of the 256-bit word when stored). + +When `mstore(beforePtr, value)` stores this 256-bit value, the big-endian representation places the most significant bytes first. So the byte at `beforePtr + 0` would be `0x00`, `beforePtr + 1` = `0x00`, ..., up to `beforePtr + 28` = `0xAB`, `beforePtr + 29` = `0xCD`, `beforePtr + 30` = `0x12`, `beforePtr + 31` = `0x34`. + +The `staticcall` reads from `sub(stackTop, 4)`. Since `beforePtr = stackTop - 0x20`, `sub(stackTop, 4) = beforePtr + 0x1c = beforePtr + 28`. + +So the staticcall reads bytes starting at `beforePtr + 28`, which is exactly `0xAB, 0xCD, 0x12, 0x34` -- the 4-byte selector! + +OK, so the encoding IS correct. The 4-byte packed value lands in the low 4 bytes of the 256-bit word, and the staticcall reads exactly those 4 bytes as the start of its calldata, followed by the stack data. + +**Conclusion**: `stackTrace` is correct. The memory mutation pattern is safe. + +### 3. `serializeSize` Unchecked Overflow (LibInterpreterStateDataContract.sol lines 26-31) + +```solidity +unchecked { + size = bytecode.length + constants.length * 0x20 + 0x40; +} +``` + +The `constants.length * 0x20` can overflow if `constants.length >= 2^251` (producing a length field that, when multiplied by 32, wraps around). The addition can also overflow. The NatSpec explicitly documents this: "the caller MUST ensure the in-memory length fields of bytecode and constants are not corrupt." + +In practice, the only caller (`RainterpreterExpressionDeployer.parse2`) receives these from the parser, which produces Solidity-allocated arrays. Solidity's `new` operator bounds array sizes by available memory/gas, making overflow impossible in practice. + +**Conclusion**: The unchecked arithmetic is safe given the documented precondition and the actual calling context. The NatSpec correctly warns callers. + +### 4. `unsafeSerialize` Assembly Cursor Mutation (LibInterpreterStateDataContract.sol lines 39-54) + +The assembly block modifies the `cursor` parameter (a stack variable of type `Pointer`) in-place. After the loop completes, `cursor` has been advanced past the constants data (length prefix + all elements). The subsequent Solidity call on line 52 uses this updated `cursor` value for the bytecode copy. + +**Correctness of cursor advancement**: +- Initial: `constantsCursor = constants` (length prefix), `cursor` = destination start +- Loop iterates `constants.length + 1` times (length word + each element) +- Post-loop: `cursor = initial_cursor + 0x20 * (constants.length + 1)` = exactly past constants region +- Line 52 copies `bytecode.length + 0x20` bytes (length prefix + data) starting at the new cursor position + +**Memory safety**: The caller (`RainterpreterExpressionDeployer.parse2`) allocates `serializeSize` bytes before calling `unsafeSerialize`. The total bytes written are `0x20 * (constants.length + 1) + bytecode.length + 0x20 = constants.length * 0x20 + 0x40 + bytecode.length`, which equals `serializeSize`. Correct. + +**Conclusion**: Correct. No issues found. + +### 5. `unsafeDeserialize` Memory Safety (LibInterpreterStateDataContract.sol lines 69-142) + +This is the most complex function. It deserializes a flat byte array into an `InterpreterState` struct by creating in-place references and allocating stacks. + +**Constants reference** (lines 84-88): `constants := cursor` makes `constants` point directly into the serialized data. `cursor` advances by `0x20 * (mload(cursor) + 1)`, correctly skipping the length prefix and all elements. + +**Bytecode reference** (lines 91-93): `bytecode := cursor` makes `bytecode` point directly into the serialized data at the position after constants. + +**Stack allocation** (lines 98-135): +- `cursor` advances past bytecode's length word (`add(cursor, 0x20)`) +- `stacksLength = byte(0, mload(cursor))` reads first byte of bytecode data (source count) +- `cursor` advances by 1 +- `sourcesStart = cursor + stacksLength * 2` -- start of source data, past the relative pointer table +- `stackBottoms` is allocated at free memory pointer with length `stacksLength` +- Free memory pointer is advanced by `(stacksLength + 1) * 0x20` + +**Per-source stack allocation loop**: +- `sourcePointer = sourcesStart + (mload(cursor) >> 0xf0)` -- reads 2-byte relative offset from cursor, shifts right by 240 bits to extract the uint16 +- `stackSize = byte(1, mload(sourcePointer))` -- reads second byte of source prefix (stack allocation) +- Allocates a new array at free memory pointer with length `stackSize` +- Sets `stackBottom = stack + (stackSize + 1) * 0x20` -- just past last element +- Advances free memory pointer to `stackBottom` +- Stores `stackBottom` in the `stackBottoms` array + +**Zero stackSize edge case**: If `stackSize == 0`, the array has length 0 and `stackBottom = stack + 0x20`. Only the length word is allocated. This is valid. + +**Memory safety annotation**: The large assembly block at lines 98-136 is marked `memory-safe`. It allocates memory by reading and writing `mload(0x40)` / `mstore(0x40, ...)` correctly. Each allocation advances the free memory pointer past the allocated region. No memory is read or written outside allocated regions. + +**Conclusion**: The assembly is correct for well-formed serialized input. No memory corruption occurs. + +### 6. Malformed Serialized Input to `unsafeDeserialize` + +The function name contains `unsafe`, indicating no validation of the serialized data. If a caller passes crafted data: + +- A corrupt `constants.length` (first word of serialized data) could cause `cursor` to advance past the end of the serialized byte array, causing `bytecode` and subsequent reads to reference zeroed or unrelated memory. +- A corrupt source relative offset could make `sourcePointer` reference arbitrary memory, reading a garbage `stackSize`, potentially allocating an enormous stack (bounded only by gas). +- A corrupt `stacksLength` could cause the loop to allocate many stacks, consuming all available gas. + +**Mitigating factors**: +1. The only production caller is `Rainterpreter.eval4`, which is `view` -- no state changes are possible regardless of what happens in memory. +2. The caller provides the bytecode -- a malicious caller can only affect their own call's return value. +3. Memory is bounded by gas -- excessive allocations simply run out of gas and revert. +4. The expression deployer runs integrity checks before producing serialized bytecode, so well-behaved callers always provide valid data. + +**Conclusion**: The lack of validation is by design. The `unsafe` prefix correctly signals the precondition. No exploitable vulnerability exists because the function runs in a `view` context and callers provide their own data. + +### 7. `stackTrace` Memory Region for Empty or Single-Element Stack + +When `stackTop == stackBottom - 0x20` (single stack item): `beforePtr = stackTop - 0x20 = stackBottom - 0x40`. This is within the stack array allocation (the length prefix or a prior stack slot). The save/restore is safe. + +When `stackTop > stackBottom` (underflow -- more consumed than allocated): This would be a logic error elsewhere (integrity check failure). The function would read `beforePtr` below the stack allocation. However, this scenario is prevented by the integrity check at deploy time, which ensures stack balance. + +**Conclusion**: Safe under the documented precondition that integrity checks pass. + +--- + +## Findings + +### A14-1 -- INFO: `stackTrace` temporarily overwrites memory outside allocated stack bounds + +**Location**: `src/lib/state/LibInterpreterState.sol` lines 131-140 + +**Description**: The `stackTrace` function writes to `sub(stackTop, 0x20)`, which is always 32 bytes below the current stack top. When the stack is empty (`stackTop == stackBottom`), this writes to the word immediately before the stack's data region -- typically the stack array's length prefix. The value is correctly saved and restored, so no corruption occurs. The `memory-safe` annotation is technically correct because the mutation is transient and fully reversed. + +**Severity**: INFO + +### A15-1 -- INFO: `serializeSize` unchecked arithmetic relies on caller precondition + +**Location**: `src/lib/state/LibInterpreterStateDataContract.sol` lines 26-31 + +**Description**: The `unchecked` block in `serializeSize` can overflow if `constants.length * 0x20` wraps around a 256-bit boundary. This is documented in the NatSpec ("the caller MUST ensure the in-memory length fields of bytecode and constants are not corrupt"). The sole production caller (`RainterpreterExpressionDeployer.parse2`) always provides Solidity-allocated arrays, making overflow impossible in practice. + +**Severity**: INFO + +### A15-2 -- INFO: `unsafeDeserialize` performs no validation of serialized data structure + +**Location**: `src/lib/state/LibInterpreterStateDataContract.sol` lines 69-142 + +**Description**: The function trusts that the serialized byte array has the correct internal structure (valid constants length, valid bytecode format, valid source count and offsets). Malformed data would cause reads from arbitrary memory positions and potentially allocate extremely large stacks. This is by design (the `unsafe` prefix documents the precondition), and the production caller (`Rainterpreter.eval4`) is a `view` function, so no state corruption is possible. Callers provide their own bytecode, so they can only affect their own return values. + +**Severity**: INFO + +### A15-3 -- LOW: `unsafeDeserialize` free memory pointer gap when `stacksLength` is 0 + +**Location**: `src/lib/state/LibInterpreterStateDataContract.sol` lines 106-108 + +**Description**: When `stacksLength` is 0 (no sources in bytecode), the code allocates a `stackBottoms` array: + +```solidity +stackBottoms := mload(0x40) +mstore(stackBottoms, stacksLength) // writes 0 +mstore(0x40, add(stackBottoms, mul(add(stacksLength, 1), 0x20))) // advances by 0x20 +``` + +This allocates a `Pointer[]` of length 0 (just the length prefix word). The loop body never executes, so no stacks are allocated. This is correct behavior, but the resulting `InterpreterState` has an empty `stackBottoms` array. If `eval2` is subsequently called with `state.sourceIndex >= 0` (which it always is), the array access `state.stackBottoms[state.sourceIndex]` at `LibEval.sol` line 206 will revert with an out-of-bounds panic. This means zero-source bytecode always reverts at eval time, which is correct but the revert path is indirect -- the failure occurs during eval rather than at deserialization. + +**Severity**: LOW -- The behavior is correct (zero-source bytecode is invalid), but the error path is indirect. A defensive check at deserialization time would provide a clearer revert reason. + +--- + +## Summary + +No CRITICAL, HIGH, or MEDIUM findings. Both files implement low-level memory operations using inline assembly with correct patterns: + +- `stackBottoms` correctly computes array bottom pointers with proper loop bounds +- `stackTrace` correctly saves/restores memory during its transient mutation +- `unsafeSerialize` correctly advances the cursor through constants and bytecode +- `unsafeDeserialize` correctly creates in-place references and allocates stacks with proper free memory pointer management + +The `unsafe` prefix on serialize/deserialize functions correctly documents that callers must provide valid data. The production calling context (`view` function, caller-provided data) means malformed input cannot cause state corruption or cross-user impact. + +One LOW finding (A15-3) identifies an indirect error path for zero-source bytecode that could benefit from an explicit check, though the current behavior is not exploitable. diff --git a/audit/2026-03-01-01/pass1/LibOpBitwise.md b/audit/2026-03-01-01/pass1/LibOpBitwise.md new file mode 100644 index 000000000..4dbdb611f --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibOpBitwise.md @@ -0,0 +1,277 @@ +# Pass 1 (Security) -- Bitwise Opcodes + +**Auditor**: A16 +**Date**: 2026-03-01 +**Files**: +- `src/lib/op/bitwise/LibOpBitwiseAnd.sol` (45 lines) +- `src/lib/op/bitwise/LibOpBitwiseOr.sol` (45 lines) +- `src/lib/op/bitwise/LibOpCtPop.sol` (55 lines) +- `src/lib/op/bitwise/LibOpDecodeBits.sol` (83 lines) +- `src/lib/op/bitwise/LibOpEncodeBits.sol` (104 lines) +- `src/lib/op/bitwise/LibOpShiftBitsLeft.sol` (58 lines) +- `src/lib/op/bitwise/LibOpShiftBitsRight.sol` (58 lines) + +Supporting file: `src/error/ErrBitwise.sol` (23 lines) + +--- + +## Evidence of Thorough Reading + +### LibOpBitwiseAnd.sol + +**Library**: `LibOpBitwiseAnd` (line 12) + +**Imports**: + +| Import | Source | Line | +|---|---|---| +| `IntegrityCheckState` | `../../integrity/LibIntegrityCheck.sol` | 5 | +| `OperandV2`, `StackItem` | `rain.interpreter.interface/interface/IInterpreterV4.sol` | 6 | +| `InterpreterState` | `../../state/LibInterpreterState.sol` | 7 | +| `Pointer` | `rain.solmem/lib/LibPointer.sol` | 8 | + +**Functions**: + +| Function | Signature | Line | Inputs/Outputs | +|---|---|---|---| +| `integrity` | `(IntegrityCheckState memory, OperandV2) -> (uint256, uint256)` | 16 | returns (2, 1) | +| `run` | `(InterpreterState memory, OperandV2, Pointer stackTop) -> (Pointer)` | 24 | assembly: AND of top two stack items | +| `referenceFn` | `(InterpreterState memory, OperandV2, StackItem[] memory) -> (StackItem[] memory)` | 36 | allocates new array, returns `inputs[0] & inputs[1]` | + +**Assembly block** (lines 26-29): reads `mload(stackTop)` and `mload(stackTopAfter)` where `stackTopAfter = stackTop + 0x20`, writes AND result to `stackTopAfter`. Consumes 2 items, produces 1. Stack grows upward (higher addresses = deeper). Correctly marked `memory-safe`. + +### LibOpBitwiseOr.sol + +**Library**: `LibOpBitwiseOr` (line 12) + +**Imports**: identical to `LibOpBitwiseAnd`. + +**Functions**: structurally identical to `LibOpBitwiseAnd`, using `or` instead of `and` in both assembly (line 28) and Solidity (line 42, `|` operator). + +### LibOpCtPop.sol + +**Library**: `LibOpCtPop` (line 18) + +**Imports**: + +| Import | Source | Line | +|---|---|---| +| `Pointer` | `rain.solmem/lib/LibPointer.sol` | 5 | +| `OperandV2`, `StackItem` | `rain.interpreter.interface/interface/IInterpreterV4.sol` | 6 | +| `InterpreterState` | `../../state/LibInterpreterState.sol` | 7 | +| `IntegrityCheckState` | `../../integrity/LibIntegrityCheck.sol` | 8 | +| `LibCtPop` | `rain.math.binary/lib/LibCtPop.sol` | 9 | + +**Functions**: + +| Function | Signature | Line | Inputs/Outputs | +|---|---|---|---| +| `integrity` | `(IntegrityCheckState memory, OperandV2) -> (uint256, uint256)` | 22 | returns (1, 1) | +| `run` | `(InterpreterState memory, OperandV2, Pointer stackTop) -> (Pointer)` | 30 | reads value, calls `LibCtPop.ctpop`, writes back in-place | +| `referenceFn` | `(InterpreterState memory, OperandV2, StackItem[] memory) -> (StackItem[] memory)` | 47 | uses `ctpopSlow`, mutates inputs in-place | + +**Assembly blocks** (lines 32-34, 38-40): reads from and writes to `stackTop`. In-place modification. Stack pointer unchanged (1 in, 1 out). Correctly marked `memory-safe`. + +**Note**: `run` uses `LibCtPop.ctpop` (optimized) while `referenceFn` uses `LibCtPop.ctpopSlow` (naive loop). This is intentional: the reference implementation uses a trivially-correct slow path to validate the optimized path. + +### LibOpDecodeBits.sol + +**Library**: `LibOpDecodeBits` (line 14) + +**Imports**: + +| Import | Source | Line | +|---|---|---| +| `IntegrityCheckState` | `../../integrity/LibIntegrityCheck.sol` | 5 | +| `OperandV2`, `StackItem` | `rain.interpreter.interface/interface/IInterpreterV4.sol` | 6 | +| `InterpreterState` | `../../state/LibInterpreterState.sol` | 7 | +| `Pointer` | `rain.solmem/lib/LibPointer.sol` | 8 | +| `LibOpEncodeBits` | `./LibOpEncodeBits.sol` | 9 | + +**Functions**: + +| Function | Signature | Line | Inputs/Outputs | +|---|---|---|---| +| `integrity` | `(IntegrityCheckState memory, OperandV2) -> (uint256, uint256)` | 20 | delegates to `LibOpEncodeBits.integrity`, returns (1, 1) | +| `run` | `(InterpreterState memory, OperandV2, Pointer stackTop) -> (Pointer)` | 33 | decodes bits from value using operand-specified start/length | +| `referenceFn` | `(InterpreterState memory, OperandV2, StackItem[] memory) -> (StackItem[] memory)` | 65 | same logic using `2 ** length` | + +**Operand layout**: bits [0..7] = `startBit`, bits [8..15] = `length`. Extracted via `& bytes32(uint256(0xFF))` and `>> 8 & bytes32(uint256(0xFF))`. + +**Integrity delegation** (lines 20-27): calls `LibOpEncodeBits.integrity(state, operand)` which validates `length != 0` and `startBit + length <= 256`, then discards the return values (encode returns (2,1), decode returns (1,1)). Slither disable comment on line 23 suppresses unused-return warning. + +**Run logic** (lines 34-58): inside `unchecked` block. `mask = (1 << length) - 1`. `value = (value >> startBit) & mask`. Assembly reads/writes `stackTop` in-place. + +**ReferenceFn logic** (lines 65-82): `mask = (2 ** length) - 1`. Result = `(value >> startBit) & mask`. `2 ** length` and `1 << length` are equivalent for `length` in [1, 255]. + +### LibOpEncodeBits.sol + +**Library**: `LibOpEncodeBits` (line 13) + +**Imports**: + +| Import | Source | Line | +|---|---|---| +| `ZeroLengthBitwiseEncoding`, `TruncatedBitwiseEncoding` | `../../../error/ErrBitwise.sol` | 5 | +| `IntegrityCheckState` | `../../integrity/LibIntegrityCheck.sol` | 6 | +| `OperandV2`, `StackItem` | `rain.interpreter.interface/interface/IInterpreterV4.sol` | 7 | +| `InterpreterState` | `../../state/LibInterpreterState.sol` | 8 | +| `Pointer` | `rain.solmem/lib/LibPointer.sol` | 9 | + +**Functions**: + +| Function | Signature | Line | Inputs/Outputs | +|---|---|---|---| +| `integrity` | `(IntegrityCheckState memory, OperandV2) -> (uint256, uint256)` | 19 | validates operand, returns (2, 1) | +| `run` | `(InterpreterState memory, OperandV2, Pointer stackTop) -> (Pointer)` | 36 | encodes source bits into target | +| `referenceFn` | `(InterpreterState memory, OperandV2, StackItem[] memory) -> (StackItem[] memory)` | 76 | same logic, allocates new output array | + +**Integrity checks** (lines 19-30): +1. `length == 0` -> reverts `ZeroLengthBitwiseEncoding()` (line 24) +2. `startBit + length > 256` -> reverts `TruncatedBitwiseEncoding(startBit, length)` (line 27) + +**Run logic** (lines 36-69): inside `unchecked` block. +1. Reads `source` from `stackTop`, advances to read `target` from `stackTop + 0x20` (lines 40-44) +2. Builds mask: `(1 << length) - 1` (line 57) +3. Clears target bits: `target &= ~(mask << startBit)` (line 60) +4. Inserts source bits: `target |= (source & mask) << startBit` (line 63) +5. Writes result to `stackTop + 0x20` (line 66) + +**Assembly blocks** (lines 40-44, 65-67): reads two stack items, writes one. Correctly consumes 2, produces 1. Correctly marked `memory-safe`. + +### LibOpShiftBitsLeft.sol + +**Library**: `LibOpShiftBitsLeft` (line 14) + +**Imports**: + +| Import | Source | Line | +|---|---|---| +| `IntegrityCheckState` | `../../integrity/LibIntegrityCheck.sol` | 5 | +| `OperandV2`, `StackItem` | `rain.interpreter.interface/interface/IInterpreterV4.sol` | 6 | +| `InterpreterState` | `../../state/LibInterpreterState.sol` | 7 | +| `Pointer` | `rain.solmem/lib/LibPointer.sol` | 8 | +| `UnsupportedBitwiseShiftAmount` | `../../../error/ErrBitwise.sol` | 9 | + +**Functions**: + +| Function | Signature | Line | Inputs/Outputs | +|---|---|---|---| +| `integrity` | `(IntegrityCheckState memory, OperandV2) -> (uint256, uint256)` | 19 | validates shift amount, returns (1, 1) | +| `run` | `(InterpreterState memory, OperandV2, Pointer stackTop) -> (Pointer)` | 38 | SHL in assembly | +| `referenceFn` | `(InterpreterState memory, OperandV2, StackItem[] memory) -> (StackItem[] memory)` | 49 | Solidity `<<` operator | + +**Operand layout**: bits [0..15] = `shiftAmount`. Extracted via `& bytes32(uint256(0xFFFF))` in integrity (line 20) and `and(operand, 0xFFFF)` in run (line 40). + +**Integrity check** (lines 22-28): reverts if `shiftAmount > 255` or `shiftAmount == 0`. + +**Run assembly** (line 40): `shl(and(operand, 0xFFFF), mload(stackTop))`. Since integrity guarantees shiftAmount is in [1, 255], the `and(operand, 0xFFFF)` mask is safe. The EVM `SHL` opcode handles shifts >= 256 by returning 0, but integrity prevents this. + +### LibOpShiftBitsRight.sol + +**Library**: `LibOpShiftBitsRight` (line 14) + +Structurally identical to `LibOpShiftBitsLeft`, using `shr` (line 40) and `>>` (line 55) instead of `shl`/`<<`. + +### ErrBitwise.sol + +**Errors**: + +| Error | Parameters | Line | +|---|---|---| +| `UnsupportedBitwiseShiftAmount` | `uint256 shiftAmount` | 13 | +| `TruncatedBitwiseEncoding` | `uint256 startBit, uint256 length` | 19 | +| `ZeroLengthBitwiseEncoding` | (none) | 23 | + +--- + +## Security Analysis + +### 1. Operand Validation (startBit + length overflow) + +**LibOpEncodeBits.integrity** (line 26): checks `startBit + length > 256`. Since both `startBit` and `length` are masked to `uint8` range (0-255), their sum is at most 510, which cannot overflow `uint256`. The check is correct and sufficient. + +**LibOpDecodeBits.integrity** (lines 20-27): delegates to `LibOpEncodeBits.integrity`, which performs the same validation. The delegation pattern is correct -- the return values (2, 1) from encode's integrity are discarded, and decode returns (1, 1). + +**Zero-length check**: `LibOpEncodeBits.integrity` line 23 checks `length == 0` before the overflow check. This prevents a zero-length encoding which would produce a zero mask (no-op for decode, or data corruption for encode). + +### 2. Shift Amount Bounds + +**LibOpShiftBitsLeft.integrity** and **LibOpShiftBitsRight.integrity** (lines 22-28 in both): validate `shiftAmount > type(uint8).max || shiftAmount == 0`. The operand extracts 16 bits (`0xFFFF`), so `shiftAmount` can be 0-65535. The check correctly rejects 0 (no-op) and anything > 255 (always-zero result for shift left, or always-zero for shift right on uint256). + +The `run` functions use `and(operand, 0xFFFF)` which matches the integrity extraction. Since integrity restricts to [1, 255], the shift is always valid at runtime. + +### 3. Assembly Safety + +All 7 libraries use `assembly ("memory-safe")` annotations. Verified each: + +- **BitwiseAnd/BitwiseOr**: reads two adjacent stack slots, writes to the higher one (consuming 2, producing 1). Only accesses pre-existing stack memory. +- **CtPop**: reads and writes same stack slot (1 in, 1 out). In-place modification. +- **DecodeBits**: reads and writes same stack slot (1 in, 1 out). In-place modification. +- **EncodeBits**: reads two stack slots, writes to higher one (2 in, 1 out). Same pattern as AND/OR. +- **ShiftBitsLeft/ShiftBitsRight**: reads and writes same stack slot (1 in, 1 out). Single-instruction pattern. + +All annotations are correct. No memory allocation occurs in any assembly block. All reads and writes are within the stack bounds guaranteed by the integrity check. + +### 4. Integrity/Run Consistency + +| Opcode | Integrity I/O | Run stack delta | Consistent | +|---|---|---|---| +| BitwiseAnd | (2, 1) | consumes 2, produces 1 (+0x20 to stackTop) | Yes | +| BitwiseOr | (2, 1) | consumes 2, produces 1 (+0x20 to stackTop) | Yes | +| CtPop | (1, 1) | consumes 1, produces 1 (stackTop unchanged) | Yes | +| DecodeBits | (1, 1) | consumes 1, produces 1 (stackTop unchanged) | Yes | +| EncodeBits | (2, 1) | consumes 2, produces 1 (+0x20 to stackTop) | Yes | +| ShiftBitsLeft | (1, 1) | consumes 1, produces 1 (stackTop unchanged) | Yes | +| ShiftBitsRight | (1, 1) | consumes 1, produces 1 (stackTop unchanged) | Yes | + +### 5. Operand Extraction Consistency + +For encode/decode bits, the operand extraction uses `& bytes32(uint256(0xFF))` for low byte and `>> 8 & bytes32(uint256(0xFF))` for second byte. This is consistent across `integrity`, `run`, and `referenceFn` in both libraries. The `run` function operates in Solidity (not assembly) for operand extraction, so there is no `bytes32` vs `uint256` representation concern. + +For shift ops, integrity uses `uint256(OperandV2.unwrap(operand) & bytes32(uint256(0xFFFF)))` and run uses `and(operand, 0xFFFF)`. Since `OperandV2` is `type OperandV2 is bytes32`, and `bytes32`/`uint256` have identical EVM stack representation (both are 256-bit words), these are equivalent. The `referenceFn` uses the same Solidity extraction as integrity. Consistent. + +### 6. Mask Computation Correctness + +**Encode/Decode**: `(1 << length) - 1` where `length` is in [1, 255] (guaranteed by integrity). For `length = 255`: `1 << 255` is a valid `uint256` value, and `(1 << 255) - 1` produces a 255-bit mask. For `length = 1`: produces mask `1`. All values are correct. + +The `referenceFn` uses `2 ** length - 1` which is mathematically equivalent. Operator precedence: `**` binds tighter than `-`, so this is `(2 ** length) - 1`. For all valid lengths, `2 ** length` fits in `uint256` (max is `2 ** 255`). + +### 7. Unchecked Block Safety + +**LibOpDecodeBits.run** (lines 34-58): the entire function body is `unchecked`. The mask computation `(1 << length) - 1` cannot underflow because `length >= 1` (guaranteed by integrity), so `1 << length >= 2`, and `2 - 1 = 1`. The shift and AND operations cannot overflow by nature. + +**LibOpEncodeBits.run** (lines 37-69): same `unchecked` block. The mask computation is safe for the same reason. The bitwise operations (`&=`, `|=`, `~`, `<<`) cannot overflow by nature. The `add(stackTop, 0x20)` in assembly (line 42) is an address increment that cannot practically overflow (would require `stackTop` near `2^256 - 32`). + +--- + +## Findings + +### A16-5: Missing `@notice` tag on `ZeroLengthBitwiseEncoding` error NatSpec [INFORMATIONAL] + +**File**: `src/error/ErrBitwise.sol`, lines 21-23 + +The `ZeroLengthBitwiseEncoding` error doc block uses bare `///` without a `@notice` tag: + +```solidity +/// Thrown during integrity check when the length of a bitwise (en|de)coding +/// would be 0. +error ZeroLengthBitwiseEncoding(); +``` + +The other two errors in the same file (`UnsupportedBitwiseShiftAmount` at line 8, `TruncatedBitwiseEncoding` at line 15) both use explicit `/// @notice`. While bare `///` is implicitly `@notice` when no other tags are present (which is the case here), this is inconsistent with the adjacent error declarations in the same file. + +**Severity**: INFORMATIONAL -- no functional or security impact. + +--- + +## Summary + +No LOW or higher severity findings were identified. The bitwise opcode implementations are well-structured: + +- Operand validation is thorough: `startBit + length` overflow is checked, zero-length is rejected, shift amounts are bounded to [1, 255]. +- Assembly blocks are minimal, correctly annotated as `memory-safe`, and only access stack memory within bounds guaranteed by integrity checks. +- Integrity input/output declarations match actual run-time stack behavior in all 7 opcodes. +- Operand extraction is consistent between `integrity`, `run`, and `referenceFn` across all files. +- `unchecked` blocks contain only operations that cannot overflow given the integrity-enforced constraints. +- The `referenceFn` implementations use equivalent but independent computation paths (`2 ** length` vs `1 << length`, `ctpopSlow` vs `ctpop`) which strengthens the testing strategy. diff --git a/audit/2026-03-01-01/pass1/LibOpCoreOps.md b/audit/2026-03-01-01/pass1/LibOpCoreOps.md new file mode 100644 index 000000000..2483b91cc --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibOpCoreOps.md @@ -0,0 +1,311 @@ +# Pass 1 (Security) -- LibOpCall, LibOpConstant, LibOpContext, LibOpStack + +Agent IDs: A17 (LibOpCall), A18 (LibOpConstant), A19 (LibOpContext), A26 (LibOpStack) + +## Evidence of Thorough Reading + +### LibOpCall.sol (src/lib/op/call/LibOpCall.sol, 171 lines) + +**Library**: `LibOpCall` (line 69) + +**Functions**: +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 85 | `internal` | `pure` | +| `run` | 122 | `internal` | `view` | + +**Imports**: +- `OperandV2` from `rain.interpreter.interface/interface/IInterpreterV4.sol` (line 5) +- `InterpreterState` from `../../state/LibInterpreterState.sol` (line 6) +- `IntegrityCheckState` from `../../integrity/LibIntegrityCheck.sol` (line 7) +- `Pointer` from `rain.solmem/lib/LibPointer.sol` (line 8) +- `LibBytecode` from `rain.interpreter.interface/lib/bytecode/LibBytecode.sol` (line 9) +- `LibEval` from `../../eval/LibEval.sol` (line 10) +- `CallOutputsExceedSource` from `../../../error/ErrIntegrity.sol` (line 11) + +**Errors imported**: `CallOutputsExceedSource` + +**Operand bit layout (24-bit operand)**: +| Bits | Field | Width | +|---|---|---| +| 0-15 | `sourceIndex` | 16 bits | +| 16-19 | `inputs` | 4 bits | +| 20-23 | `outputs` | 4 bits | + +Confirmed by `integrity` (lines 86-87), `run` (lines 124-126), and `LibOperand.build` in test helpers. + +--- + +### LibOpConstant.sol (src/lib/op/00/LibOpConstant.sol, 61 lines) + +**Library**: `LibOpConstant` (line 15) + +**Functions**: +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 21 | `internal` | `pure` | +| `run` | 37 | `internal` | `pure` | +| `referenceFn` | 52 | `internal` | `pure` | + +**Imports**: +- `OutOfBoundsConstantRead` from `../../../error/ErrIntegrity.sol` (line 5) +- `IntegrityCheckState` from `../../integrity/LibIntegrityCheck.sol` (line 6) +- `OperandV2`, `StackItem` from `rain.interpreter.interface/interface/IInterpreterV4.sol` (line 7) +- `InterpreterState` from `../../state/LibInterpreterState.sol` (line 8) +- `Pointer` from `rain.solmem/lib/LibPointer.sol` (line 9) + +**Errors imported**: `OutOfBoundsConstantRead` + +**Operand**: low 16 bits encode the constant index. + +--- + +### LibOpContext.sol (src/lib/op/00/LibOpContext.sol, 63 lines) + +**Library**: `LibOpContext` (line 12) + +**Functions**: +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 16 | `internal` | `pure` | +| `run` | 28 | `internal` | `pure` | +| `referenceFn` | 47 | `internal` | `pure` | + +**Imports**: +- `Pointer` from `rain.solmem/lib/LibPointer.sol` (line 5) +- `OperandV2`, `StackItem` from `rain.interpreter.interface/interface/IInterpreterV4.sol` (line 6) +- `InterpreterState` from `../../state/LibInterpreterState.sol` (line 7) +- `IntegrityCheckState` from `../../integrity/LibIntegrityCheck.sol` (line 8) + +**Errors imported**: None (relies on Solidity's built-in `Panic(0x32)` for array OOB). + +**Operand**: low 8 bits = row index `i`, bits 8-15 = column index `j`. + +--- + +### LibOpStack.sol (src/lib/op/00/LibOpStack.sol, 73 lines) + +**Library**: `LibOpStack` (line 15) + +**Functions**: +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 21 | `internal` | `pure` | +| `run` | 41 | `internal` | `pure` | +| `referenceFn` | 58 | `internal` | `pure` | + +**Imports**: +- `Pointer` from `rain.solmem/lib/LibPointer.sol` (line 5) +- `InterpreterState` from `../../state/LibInterpreterState.sol` (line 6) +- `IntegrityCheckState` from `../../integrity/LibIntegrityCheck.sol` (line 7) +- `OperandV2`, `StackItem` from `rain.interpreter.interface/interface/IInterpreterV4.sol` (line 8) +- `OutOfBoundsStackRead` from `../../../error/ErrIntegrity.sol` (line 9) + +**Errors imported**: `OutOfBoundsStackRead` + +**Operand**: low 16 bits encode the stack read index. + +--- + +## Findings + +No CRITICAL, HIGH, MEDIUM, or LOW findings identified across the four files. + +The analysis below documents the security-relevant design decisions and confirms their correctness. + +--- + +### A17-INFO-01: LibOpCall -- No runtime bounds check on `sourceIndex` for `stackBottoms` access + +**Severity**: INFO + +**File**: `src/lib/op/call/LibOpCall.sol` +**Location**: `run`, line 136 + +**Description**: The `run` function accesses `stackBottoms[sourceIndex]` via assembly pointer arithmetic without a Solidity bounds check: + +```solidity +evalStackBottom := mload(add(stackBottoms, mul(add(sourceIndex, 1), 0x20))) +``` + +If `sourceIndex >= stackBottoms.length`, this reads arbitrary memory. The `sourceIndex` is extracted from the operand as `operand & 0xFFFF` (line 124), giving a range of 0 to 65535. + +**Analysis**: The integrity check at deploy time validates `sourceIndex` via `LibBytecode.sourceInputsOutputsLength(state.bytecode, sourceIndex)` (line 90), which calls `sourcePointer` -> `sourceRelativeOffset`, which reverts with `SourceIndexOutOfBounds` for out-of-range indices. Bytecode is immutable after serialization. The `stackBottoms` array is constructed from the same bytecode source count, so a valid `sourceIndex` is always in bounds. This is the standard trust model: deploy-time integrity guarantees runtime safety. The NatSpec documents this explicitly (lines 111-116). + +--- + +### A17-INFO-02: LibOpCall -- Output copy loop uses unconventional Yul for-loop structure + +**Severity**: INFO + +**File**: `src/lib/op/call/LibOpCall.sol` +**Location**: `run`, lines 159-167 + +**Description**: The output copy loop places the `mstore` in the body and pointer increments in the update clause: + +```solidity +for {} lt(evalStackTop, end) { + cursor := add(cursor, 0x20) + evalStackTop := add(evalStackTop, 0x20) +} { mstore(cursor, mload(evalStackTop)) } +``` + +This is correct: the body executes first with the initial pointer values (`cursor == stackTop`, `evalStackTop` == callee's final stack top), then the update increments both pointers. The loop copies `outputs` items in order. Verified: the first store writes `cursor[0] = evalStackTop[0]`, then the update advances both. The final state is `outputs` values copied from callee stack to caller stack. + +The input copy loop (lines 139-142) uses a different structure (incrementing `stackTop` in the body, decrementing `evalStackTop` before the store). The asymmetry is intentional: inputs are copied in reverse order (caller's top becomes callee's bottom), outputs are copied in forward order. + +--- + +### A17-INFO-03: LibOpCall -- Recursion terminates only via gas exhaustion + +**Severity**: INFO + +**File**: `src/lib/op/call/LibOpCall.sol` +**Location**: `run`, lines 122-170 + +**Description**: No explicit recursion guard exists. Direct recursion (source 0 calling source 0) or indirect recursion (source 0 -> source 1 -> source 0) will consume all gas and revert. The NatSpec documents this at lines 50-53. Test coverage confirms this behavior (`testOpCallRunRecursive` in `LibOpCall.t.sol`). + +**Analysis**: Gas exhaustion guarantees a revert, preventing infinite execution. No state corruption or fund loss is possible. Expression authors who accidentally create recursive expressions lose gas. This is documented and tested. + +--- + +### A17-INFO-04: LibOpCall -- `integrity` validates `sourceIndex` and `outputs` but not `inputs` + +**Severity**: INFO + +**File**: `src/lib/op/call/LibOpCall.sol` +**Location**: `integrity`, lines 85-97 + +**Description**: The `integrity` function extracts `sourceIndex` and `outputs` from the operand but does not extract the `inputs` field. Instead, it returns `sourceInputs` from `LibBytecode.sourceInputsOutputsLength`. The framework (`LibIntegrityCheck.integrityCheck2`, line 159) compares the returned `calcOpInputs` against `bytecodeOpInputs` and reverts with `BadOpInputsLength` if they differ. + +**Analysis**: This is the correct pattern. The integrity function declares the canonical IO, and the framework enforces consistency with the bytecode. Validation is not missing; it is delegated to the framework layer. Test `testOpCallRunInputsMismatch` in `LibOpCall.t.sol` confirms the revert. + +--- + +### A17-INFO-05: LibOpCall -- Assembly blocks correctly marked `memory-safe` + +**Severity**: INFO + +**File**: `src/lib/op/call/LibOpCall.sol` +**Location**: `run`, lines 135, 159 + +**Description**: Both assembly blocks read from and write to pre-allocated memory regions (caller stack, callee stack, `stackBottoms` array). No new memory is allocated. The input copy writes within the callee's pre-allocated stack (from `evalStackBottom` downward, bounded by `stackAllocation`). The output copy writes within the caller's pre-allocated stack (at `stackTop`, bounded by the caller's allocation). Both annotations are correct. + +--- + +### A18-INFO-01: LibOpConstant -- `run` relies entirely on integrity for bounds safety + +**Severity**: INFO + +**File**: `src/lib/op/00/LibOpConstant.sol` +**Location**: `run`, lines 37-46 + +**Description**: The `run` function reads from the constants array in assembly without a bounds check: + +```solidity +let value := mload(add(constants, mul(add(and(operand, 0xFFFF), 1), 0x20))) +``` + +The comment on line 39 states: "Skip index OOB check and rely on integrity check for that." The `integrity` function (line 24) validates `constantIndex < state.constants.length`, reverting with `OutOfBoundsConstantRead` if violated. Since the deployer enforces integrity at deploy time and bytecode is immutable, the runtime index is guaranteed valid. + +**Analysis**: Standard trust model. Tests cover both the happy path (`testOpConstantRun`, `testOpConstantEval`) and the OOB revert (`testOpConstantIntegrityOOBConstants`, `testOpConstantIntegrityMaxIndex`, `testOpConstantEvalZeroConstants`). + +--- + +### A18-INFO-02: LibOpConstant -- Operand extraction is consistent across all three functions + +**Severity**: INFO + +**File**: `src/lib/op/00/LibOpConstant.sol` +**Location**: Lines 23, 41, 57 + +**Description**: All three functions extract the constant index from the low 16 bits of the operand: +- `integrity` (line 23): `uint256(OperandV2.unwrap(operand) & bytes32(uint256(0xFFFF)))` +- `run` (line 41): `and(operand, 0xFFFF)` (assembly, raw bytes32 value) +- `referenceFn` (line 57): `uint256(OperandV2.unwrap(operand) & bytes32(uint256(0xFFFF)))` + +Since `OperandV2` is `type OperandV2 is bytes32`, in assembly the raw bytes32 value's low 16 bits match `OperandV2.unwrap(operand) & bytes32(uint256(0xFFFF))`. These are equivalent. + +--- + +### A19-INFO-01: LibOpContext -- Integrity cannot validate context bounds at compile time + +**Severity**: INFO + +**File**: `src/lib/op/00/LibOpContext.sol` +**Location**: `integrity`, line 16 + +**Description**: The `integrity` function returns `(0, 1)` without validating the operand indices against context dimensions. The comment on lines 18-19 explains this is intentional: context shape is unknown at deploy time. Expressions that pass integrity can still revert at runtime with `Panic(0x32)` if `i` or `j` exceed the actual context dimensions. + +**Analysis**: This is a known design limitation. The Solidity-level array access at line 35 (`state.context[i][j]`) provides runtime bounds checking via compiler-generated code. Tests confirm OOB reverts: `testOpContextRunOOBi`, `testOpContextRunOOBj`, `testOpContextEvalOOBi`, `testOpContextEvalOOBj`, `testOpContextEvalEmptyInnerArray`. + +--- + +### A19-INFO-02: LibOpContext -- Runtime OOB produces Panic rather than custom error + +**Severity**: INFO + +**File**: `src/lib/op/00/LibOpContext.sol` +**Location**: `run`, line 35 + +**Description**: When `state.context[i][j]` is out of bounds, Solidity emits `Panic(0x32)` rather than a custom error. The project convention is custom errors, but replacing this would require manual bounds checking in assembly, which would sacrifice the automatic bounds checking the code explicitly relies on (comment on lines 31-34). + +**Analysis**: Pragmatic tradeoff. The Panic revert is still a safe revert (transaction rolls back), just not as informative as a custom error. The cost of a manual check with custom error would add gas to every context access, which is a hot path. + +--- + +### A26-INFO-01: LibOpStack -- `run` relies entirely on integrity for bounds safety + +**Severity**: INFO + +**File**: `src/lib/op/00/LibOpStack.sol` +**Location**: `run`, lines 41-49 + +**Description**: The `run` function reads from the stack via assembly without bounds checking: + +```solidity +let stackBottom := mload(add(mload(state), mul(0x20, add(sourceIndex, 1)))) +let stackValue := mload(sub(stackBottom, mul(0x20, add(and(operand, 0xFFFF), 1)))) +``` + +The first line loads `state.stackBottoms[sourceIndex]` via pointer arithmetic on the struct's first field. The second line reads from `stackBottom - (readIndex + 1) * 0x20`. If `readIndex` exceeds the stack depth, this reads arbitrary memory before the stack. + +**Analysis**: The `integrity` function (line 24) validates `readIndex < state.stackIndex`, which ensures the read is within the currently computed stack depth. The `LibIntegrityCheck` framework additionally enforces that the stack index never drops below the `readHighwater` (line 172 of `LibIntegrityCheck.sol`), and `LibOpStack.integrity` advances `readHighwater` when needed (line 29-31). This prevents stack reads from being invalidated by subsequent pops. Standard trust model. + +--- + +### A26-INFO-02: LibOpStack -- `readHighwater` tracking prevents dangling stack reads + +**Severity**: INFO + +**File**: `src/lib/op/00/LibOpStack.sol` +**Location**: `integrity`, lines 29-31 + +**Description**: When `readIndex > state.readHighwater`, the integrity function updates `readHighwater` to `readIndex`. The `LibIntegrityCheck` framework (line 172) then reverts with `StackUnderflowHighwater` if any subsequent opcode would consume stack below this mark. This ensures that a stack position read by the `stack` opcode cannot later be consumed and overwritten by another opcode. + +**Analysis**: This is a correctness-critical invariant. Without the highwater tracking, the `stack` opcode could read a position that a later opcode pops, leading to the `run` function reading stale or overwritten data. The mechanism is correctly implemented. Note that the check uses `<` (strict less-than), meaning the highwater mark position itself is protected -- it cannot be popped. + +--- + +### A26-INFO-03: LibOpStack -- `referenceFn` uses Solidity bounds-checked access + +**Severity**: INFO + +**File**: `src/lib/op/00/LibOpStack.sol` +**Location**: `referenceFn`, lines 58-72 + +**Description**: The reference function uses `state.stackBottoms[state.sourceIndex]` (Solidity-level bounds-checked array access, line 66) and Solidity's checked arithmetic for `stackBottom - (readIndex + 1) * 0x20` (line 67). This provides a safer implementation for differential testing against `run`. The only assembly is writing the loaded value into the output array (line 70). The `testOpStackRunReferenceFnParity` test confirms parity between `run` and `referenceFn`. + +--- + +## Summary + +Across all four files, no CRITICAL, HIGH, MEDIUM, or LOW severity issues were identified. + +All four opcodes follow the same architectural pattern: +1. **Integrity at deploy time** validates operand fields against structural limits (source count, constants length, stack depth). +2. **Runtime (`run`)** uses unchecked assembly for gas efficiency, relying on the integrity guarantees. +3. The expression deployer enforces that integrity checks pass before any expression can be evaluated. +4. Bytecode is immutable after deployment, so integrity-validated invariants cannot become stale. + +The assembly blocks are correctly annotated as `memory-safe`. All reverts use custom errors (except `LibOpContext`, which relies on Solidity's built-in `Panic(0x32)` for array bounds checking -- an intentional tradeoff documented in the code). Operand extraction is consistent between `integrity`, `run`, and `referenceFn` across all four files. Test coverage is thorough, including happy paths, OOB error paths, boundary conditions, and differential testing against reference implementations. diff --git a/audit/2026-03-01-01/pass1/LibOpERC20.md b/audit/2026-03-01-01/pass1/LibOpERC20.md new file mode 100644 index 000000000..db01b0fe6 --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibOpERC20.md @@ -0,0 +1,229 @@ +# Pass 1 (Security) -- ERC20 Opcode Libraries + +Agent: A20 + +## Files Reviewed + +1. `src/lib/op/erc20/LibOpERC20Allowance.sol` +2. `src/lib/op/erc20/LibOpERC20BalanceOf.sol` +3. `src/lib/op/erc20/LibOpERC20TotalSupply.sol` +4. `src/lib/op/erc20/uint256/LibOpUint256ERC20Allowance.sol` +5. `src/lib/op/erc20/uint256/LibOpUint256ERC20BalanceOf.sol` +6. `src/lib/op/erc20/uint256/LibOpUint256ERC20TotalSupply.sol` + +--- + +## Evidence of Thorough Reading + +### 1. LibOpERC20Allowance.sol (124 lines) + +- **Library name**: `LibOpERC20Allowance` (line 17) +- **Imports**: `IERC20`, `Pointer`, `IntegrityCheckState`, `OperandV2`, `InterpreterState`, `LibDecimalFloat`/`Float`, `LibTOFUTokenDecimals`, `StackItem`, `NotAnAddress` (lines 5-13) +- **Functions**: + - `integrity(IntegrityCheckState memory, OperandV2)` -- line 21, `internal pure`, returns `(3, 1)` + - `run(InterpreterState memory, OperandV2, Pointer stackTop)` -- line 30, `internal view` + - Reads 3 stack values: `token`, `owner`, `spender` (lines 34-38) + - Validates all three via `NotAnAddress` (lines 44, 47, 50) + - Calls `IERC20.allowance` (line 55), then `safeDecimalsForTokenReadOnly` (line 60) + - Uses `fromFixedDecimalLossyPacked` (line 72) -- lossy, with comment explaining infinite approvals (lines 62-70) + - Stack pointer advances by 0x40 (line 37), writes result at new `stackTop` (line 75) + - `referenceFn(InterpreterState memory, OperandV2, StackItem[] memory inputs)` -- line 83, `internal view` + - Same address validations (lines 88-99) + - Calls `safeDecimalsForTokenReadOnly` first (line 113), then `allowance` (line 114) -- **different order from `run`** + - Uses same `fromFixedDecimalLossyPacked` (line 117) +- **NatSpec**: `@title`, `@notice`, `@param`, `@return` all present where appropriate + +### 2. LibOpERC20BalanceOf.sol (99 lines) + +- **Library name**: `LibOpERC20BalanceOf` (line 17) +- **Imports**: Same set as Allowance minus one StackItem re-import (lines 5-13) +- **Functions**: + - `integrity` -- line 21, returns `(2, 1)` + - `run` -- line 30, `internal view` + - Reads 2 stack values: `token`, `account` (lines 33-36) + - Validates both via `NotAnAddress` (lines 42, 45) + - Calls `IERC20.balanceOf` (line 49), then `safeDecimalsForTokenReadOnly` (line 54) + - Uses `fromFixedDecimalLosslessPacked` (line 56) + - Stack pointer advances by 0x20 (line 35), writes at new `stackTop` (line 59) + - `referenceFn` -- line 67, `internal view` + - Same validations (lines 72-79) + - Calls `balanceOf` first (line 89), then `safeDecimalsForTokenReadOnly` (line 91) -- **same order as `run`** +- **NatSpec**: Complete with `@notice`, `@param`, `@return` + +### 3. LibOpERC20TotalSupply.sol (84 lines) + +- **Library name**: `LibOpERC20TotalSupply` (line 17) +- **Functions**: + - `integrity` -- line 21, returns `(1, 1)` + - `run` -- line 30, `internal view` + - Reads 1 stack value: `token` (lines 32-34) + - Validates via `NotAnAddress` (line 39) + - Calls `IERC20.totalSupply` (line 43), then `safeDecimalsForTokenReadOnly` (line 48) + - Uses `fromFixedDecimalLosslessPacked` (line 50) + - `stackTop` unchanged (1 in, 1 out), writes at same position (line 53) + - `referenceFn` -- line 61, `internal view` + - Same validation (lines 66-69) + - Calls `totalSupply` first (line 74), then `safeDecimalsForTokenReadOnly` (line 76) -- **same order as `run`** +- **NatSpec**: Complete + +### 4. LibOpUint256ERC20Allowance.sol (95 lines) + +- **Library name**: `LibOpUint256ERC20Allowance` (line 14) +- **Imports**: No `LibDecimalFloat`, `LibTOFUTokenDecimals`, or `Float` (lines 5-10) -- correct, no float conversion needed +- **Functions**: + - `integrity` -- line 16, returns `(3, 1)`. NatSpec lacks `@notice` and `@return` tags. + - `run` -- line 25, `internal view` + - Reads 3 stack values (lines 29-33), validates all three (lines 39-45) + - Calls `IERC20.allowance` (line 50), stores raw uint256 + - Stack pointer advances by 0x40 (line 32), writes at new `stackTop` (line 52) + - `referenceFn` -- line 60, `internal view` + - Same validations (lines 65-76), calls `allowance` (line 89) + - Wraps result as `StackItem.wrap(bytes32(tokenAllowance))` (line 91) +- **NatSpec**: `@notice` missing on `integrity`, `@return` missing on `integrity` + +### 5. LibOpUint256ERC20BalanceOf.sol (81 lines) + +- **Library name**: `LibOpUint256ERC20BalanceOf` (line 14) +- **Functions**: + - `integrity` -- line 16, returns `(2, 1)`. NatSpec lacks `@notice` and `@return` tags. + - `run` -- line 25, `internal view` + - Reads 2 stack values (lines 28-31), validates both (lines 37, 40) + - Calls `IERC20.balanceOf` (line 44), stores raw uint256 + - Stack pointer advances by 0x20 (line 30), writes at new `stackTop` (line 46) + - `referenceFn` -- line 54, `internal view` + - Same validations (lines 59-66), calls `balanceOf` (line 75) + - Wraps result as `StackItem.wrap(bytes32(tokenBalance))` (line 77) +- **NatSpec**: `@notice` missing on `integrity`, `@return` missing on `integrity` + +### 6. LibOpUint256ERC20TotalSupply.sol (67 lines) + +- **Library name**: `LibOpUint256ERC20TotalSupply` (line 14) +- **Functions**: + - `integrity` -- line 16, returns `(1, 1)`. NatSpec lacks `@notice` and `@return` tags. + - `run` -- line 25, `internal view` + - Reads 1 stack value (lines 27-29), validates (line 34) + - Calls `IERC20.totalSupply` (line 38), stores raw uint256 + - `stackTop` unchanged, writes at same position (line 40) + - `referenceFn` -- line 48, `internal view` + - Same validation (lines 53-56), calls `totalSupply` (line 61) + - Wraps result as `StackItem.wrap(bytes32(totalSupply))` (line 63) +- **NatSpec**: `@notice` missing on `integrity`, `@return` missing on `integrity` + +--- + +## Security Findings + +### A20-1 -- INFO: External calls to untrusted ERC20 tokens are view-only by design + +**Files**: All six files + +**Description**: Every `run` function makes external calls (`allowance`, `balanceOf`, `totalSupply`) to token addresses supplied by the Rainlang author on the stack. The float variants additionally call `safeDecimalsForTokenReadOnly`, which internally calls `decimals()` via `staticcall`. Since `eval4` is `external view`, all external calls are executed as `staticcall` at the EVM level, preventing state mutation. Reentrancy resulting in state changes is not possible. A malicious token can return arbitrary values or revert, but cannot alter interpreter state. + +**Severity**: INFO + +--- + +### A20-2 -- INFO: Address validation via NotAnAddress is correct + +**Files**: All six files + +**Description**: All six files validate that each address-typed stack input fits in 160 bits: `if (token != uint256(uint160(token))) revert NotAnAddress(token)`. This correctly detects non-address values (e.g., floats, hashes, arithmetic results) whose upper 96 bits are non-zero. The check runs before any external call, preventing calls to truncated (wrong) addresses. Both `run` and `referenceFn` apply the same validation. + +**Severity**: INFO + +--- + +### A20-3 -- INFO: Stack pointer arithmetic is correct across all six files + +**Files**: All six files + +**Description**: Verified stack pointer arithmetic matches declared integrity: + +- **3-input opcodes** (Allowance variants): Read from `stackTop`, `stackTop+0x20`, `stackTop+0x40`. Advance `stackTop` by `0x40`. Write 1 output at new `stackTop`. Net: 3 consumed, 1 produced. Matches `(3, 1)`. +- **2-input opcodes** (BalanceOf variants): Read from `stackTop`, `stackTop+0x20`. Advance by `0x20`. Write 1 output at new `stackTop`. Net: 2 consumed, 1 produced. Matches `(2, 1)`. +- **1-input opcodes** (TotalSupply variants): Read from `stackTop`. No advance. Write 1 output at same `stackTop`. Net: 1 consumed, 1 produced. Matches `(1, 1)`. + +All assembly blocks are correctly annotated `"memory-safe"` -- they only read/write within the stack region owned by the opcode. + +**Severity**: INFO + +--- + +### A20-4 -- INFO: Lossy float conversion for allowance is intentional and correctly documented + +**File**: `src/lib/op/erc20/LibOpERC20Allowance.sol` (lines 62-72) + +**Description**: `erc20-allowance` uses `fromFixedDecimalLossyPacked` while `erc20-balance-of` and `erc20-total-supply` use `fromFixedDecimalLosslessPacked`. The comment at lines 62-70 explains that `type(uint256).max` (infinite approval) cannot be represented losslessly in a decimal float, so the lossy variant is required to avoid bricking evaluations. The second return value (the `lossless` flag) is intentionally discarded. The `referenceFn` uses the same lossy conversion. This is correct. + +**Severity**: INFO + +--- + +### A20-5 -- INFO: `decimals()` revert on non-ERC20Metadata tokens handled by TOFU library + +**Files**: `LibOpERC20Allowance.sol`, `LibOpERC20BalanceOf.sol`, `LibOpERC20TotalSupply.sol` + +**Description**: The float-converting variants call `LibTOFUTokenDecimals.safeDecimalsForTokenReadOnly`, which internally calls `decimals()` via `staticcall` in the TOFU implementation. If `decimals()` is not implemented (e.g., MKR, SAI), the `staticcall` returns `success = false`, which causes `safeDecimalsForTokenReadOnly` to revert with `TokenDecimalsReadFailure`. This is a design decision: the uint256 variants exist as alternatives for tokens that do not implement `decimals()`. The TOFU library also validates that the returned value fits in `uint8` and that `returndatasize >= 0x20`. + +**Severity**: INFO + +--- + +### A20-6 -- LOW: Call order discrepancy between `run` and `referenceFn` in LibOpERC20Allowance + +**File**: `src/lib/op/erc20/LibOpERC20Allowance.sol` + +**Description**: In `run` (lines 51-60), the call order is: +1. `IERC20(token).allowance(owner, spender)` (line 55) +2. `LibTOFUTokenDecimals.safeDecimalsForTokenReadOnly(token)` (line 60) + +In `referenceFn` (lines 113-114), the call order is reversed: +1. `LibTOFUTokenDecimals.safeDecimalsForTokenReadOnly(token)` (line 113) +2. `IERC20(token).allowance(owner, spender)` (line 114) + +The other two float variants (`LibOpERC20BalanceOf`, `LibOpERC20TotalSupply`) maintain consistent call ordering between `run` and `referenceFn` (ERC20 call first, then decimals). + +Because both functions are `view`, the result is identical for well-behaved tokens. However, the `referenceFn` is intended as a differential testing oracle. If a token's `decimals()` reverts, `referenceFn` will revert before calling `allowance`, while `run` will call `allowance` first and revert on `decimals()` afterward. This means the differential test may not exercise the `allowance` call path when `decimals()` fails. More importantly, the `referenceFn` should mirror the `run` implementation as closely as possible to serve its purpose as a correctness oracle. + +**Severity**: LOW + +--- + +### A20-7 -- INFO: NatSpec inconsistency on uint256 variant `integrity` functions + +**Files**: `LibOpUint256ERC20Allowance.sol` (line 15), `LibOpUint256ERC20BalanceOf.sol` (line 15), `LibOpUint256ERC20TotalSupply.sol` (line 15) + +**Description**: The `integrity` function NatSpec in all three uint256 variants uses a bare `///` comment without `@notice` or `@return` tags: +``` +/// `uint256-erc20-allowance` integrity check. Requires 3 inputs and produces 1 output. +``` + +The corresponding float variants use explicit tags: +``` +/// @notice `erc20-allowance` integrity check. Requires 3 inputs and produces 1 output. +/// @return The number of inputs. +/// @return The number of outputs. +``` + +Since the function doc block itself does not contain any explicit tags, the untagged text defaults to `@notice` per NatSpec rules. However, the `@return` tags are absent, making the return value semantics less discoverable. This is a documentation consistency issue, not a security concern. + +**Severity**: INFO + +--- + +## Summary + +No CRITICAL, HIGH, or MEDIUM findings. One LOW finding: + +| ID | Severity | File | Description | +|----|----------|------|-------------| +| A20-6 | LOW | LibOpERC20Allowance.sol | Call order discrepancy between `run` and `referenceFn` (decimals vs allowance) | + +Key security properties verified: + +1. **Reentrancy**: Not exploitable -- `eval4` is `view`, all external calls are `staticcall`. +2. **Address validation**: All address inputs checked for upper-96-bit cleanliness via `NotAnAddress` before external calls. +3. **Stack safety**: All assembly pointer arithmetic matches declared integrity counts. +4. **Memory safety**: All assembly blocks correctly annotated and stay within owned stack memory. +5. **Return value handling**: Solidity high-level calls automatically revert on failure. The TOFU library handles `decimals()` failure via manual `staticcall` with `returndatasize` and `uint8` range checks. +6. **Float conversion**: Lossy for allowance (handles infinite approvals), lossless for balance/supply. diff --git a/audit/2026-03-01-01/pass1/LibOpERC721EVM.md b/audit/2026-03-01-01/pass1/LibOpERC721EVM.md new file mode 100644 index 000000000..f56caf9af --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibOpERC721EVM.md @@ -0,0 +1,262 @@ +# Pass 1 (Security) -- ERC721/ERC5313/EVM/Crypto Ops + +**Auditor**: A22 +**Date**: 2026-03-01 + +## Files Reviewed + +1. `src/lib/op/erc721/LibOpERC721BalanceOf.sol` (89 lines) +2. `src/lib/op/erc721/LibOpERC721OwnerOf.sol` (71 lines) +3. `src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol` (76 lines) +4. `src/lib/op/erc5313/LibOpERC5313Owner.sol` (67 lines) +5. `src/lib/op/evm/LibOpBlockNumber.sol` (48 lines) +6. `src/lib/op/evm/LibOpChainId.sol` (48 lines) +7. `src/lib/op/evm/LibOpTimestamp.sol` (48 lines) +8. `src/lib/op/crypto/LibOpHash.sol` (49 lines) + +--- + +## Evidence of Thorough Reading + +### LibOpERC721BalanceOf.sol + +**Library**: `LibOpERC721BalanceOf` (line 15) + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 19 | internal | pure | +| `run` | 28 | internal | view | +| `referenceFn` | 60 | internal | view | + +**Imports**: `InterpreterState` (line 5), `OperandV2`, `StackItem` (line 6), `Pointer` (line 7), `IERC721` (line 8), `LibDecimalFloat`, `Float` (line 9), `IntegrityCheckState` (line 10), `NotAnAddress` (line 11). + +**Integrity**: Returns (2, 1). Two inputs (token, account), one output (balance as float). + +**Run logic**: Reads `token` and `account` from stack (lines 31-35). Validates both are valid addresses via `NotAnAddress` (lines 40, 43). Calls `IERC721(token).balanceOf(account)` (line 47). Converts result to `Float` via `fromFixedDecimalLosslessPacked(tokenBalance, 0)` (line 49). Writes float to stack (line 52). + +**referenceFn**: Same logic using `StackItem[]` array access. Validates addresses, calls `balanceOf`, converts to float. Returns via `StackItem[]` array. + +### LibOpERC721OwnerOf.sol + +**Library**: `LibOpERC721OwnerOf` (line 14) + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 18 | internal | pure | +| `run` | 27 | internal | view | +| `referenceFn` | 53 | internal | view | + +**Imports**: `IERC721` (line 5), `Pointer` (line 6), `IntegrityCheckState` (line 7), `OperandV2`, `StackItem` (line 8), `InterpreterState` (line 9), `NotAnAddress` (line 10). + +**Integrity**: Returns (2, 1). Two inputs (token, tokenId), one output (owner address). + +**Run logic**: Reads `token` and `tokenId` from stack (lines 30-33). Validates `token` is a valid address (line 39). Does NOT validate `tokenId` (raw uint256 pass-through to `ownerOf`). Calls `IERC721(token).ownerOf(tokenId)` (line 43). Writes owner address to stack (line 45). + +**referenceFn**: Same logic. Validates `token` address, passes `tokenId` directly. Returns owner wrapped as `bytes32(uint256(uint160(owner)))`. + +### LibOpUint256ERC721BalanceOf.sol + +**Library**: `LibOpUint256ERC721BalanceOf` (line 14) + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 16 | internal | pure | +| `run` | 25 | internal | view | +| `referenceFn` | 52 | internal | view | + +**Imports**: `IERC721` (line 5), `Pointer` (line 6), `IntegrityCheckState` (line 7), `OperandV2`, `StackItem` (line 8), `InterpreterState` (line 9), `NotAnAddress` (line 10). + +**Integrity**: Returns (2, 1). Two inputs (token, account), one output (raw uint256 balance). + +**Run logic**: Same as `LibOpERC721BalanceOf` except result is stored as raw `uint256` instead of float. No `fromFixedDecimalLosslessPacked` conversion. + +**referenceFn**: Validates addresses, calls `balanceOf`, wraps raw balance as `bytes32(tokenBalance)`. + +### LibOpERC5313Owner.sol + +**Library**: `LibOpERC5313Owner` (line 14) + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 18 | internal | pure | +| `run` | 27 | internal | view | +| `referenceFn` | 50 | internal | view | + +**Imports**: `IERC5313` (line 5), `Pointer` (line 6), `IntegrityCheckState` (line 7), `OperandV2`, `StackItem` (line 8), `InterpreterState` (line 9), `NotAnAddress` (line 10). + +**Integrity**: Returns (1, 1). One input (contract address), one output (owner address). + +**Run logic**: Reads `account` from stack (line 30). Validates address (line 36). Calls `IERC5313(account).owner()` (line 40). Writes owner to stack (line 42). + +**referenceFn**: Same logic, validates address, calls `owner()`, returns wrapped address. + +### LibOpBlockNumber.sol + +**Library**: `LibOpBlockNumber` (line 13), `using LibDecimalFloat for Float` (line 14) + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 19 | internal | pure | +| `run` | 26 | internal | view | +| `referenceFn` | 39 | internal | view | + +**Integrity**: Returns (0, 1). No inputs, one output. + +**Run logic**: Assembly pushes `number()` (EVM block number opcode) to stack by decrementing `stackTop` by 0x20 and writing value (lines 27-30). + +**referenceFn**: Uses `LibDecimalFloat.fromFixedDecimalLosslessPacked(block.number, 0)` and wraps as `StackItem`. Comment documents that `run` stores raw value directly as gas optimization, and the reference function verifies `fromFixedDecimalLosslessPacked(value, 0)` is identity (lines 35-37). + +### LibOpChainId.sol + +**Library**: `LibOpChainId` (line 13), `using LibDecimalFloat for Float` (line 14) + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 19 | internal | pure | +| `run` | 26 | internal | view | +| `referenceFn` | 39 | internal | view | + +Structurally identical to `LibOpBlockNumber` but uses `chainid()` instead of `number()`. + +### LibOpTimestamp.sol + +**Library**: `LibOpTimestamp` (line 13), `using LibDecimalFloat for Float` (line 14) + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 19 | internal | pure | +| `run` | 26 | internal | view | +| `referenceFn` | 39 | internal | view | + +Structurally identical to `LibOpBlockNumber` but uses `timestamp()` instead of `number()`. + +### LibOpHash.sol + +**Library**: `LibOpHash` (line 12) + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `integrity` | 17 | internal | pure | +| `run` | 28 | internal | pure | +| `referenceFn` | 41 | internal | pure | + +**Imports**: `Pointer` (line 5), `OperandV2`, `StackItem` (line 6), `InterpreterState` (line 7), `IntegrityCheckState` (line 8). + +**Integrity**: Extracts input count from operand bits 16-19 via `(OperandV2.unwrap(operand) >> 0x10) & 0x0F`, yielding 0-15. Returns `(inputs, 1)`. + +**Run logic**: Assembly extracts same input count. Computes `keccak256(stackTop, length)` where `length = inputs * 0x20`. Adjusts stackTop: `sub(add(stackTop, length), 0x20)` consumes `inputs` slots and produces 1 output. Writes hash to new stackTop. + +**referenceFn**: Uses `keccak256(abi.encodePacked(inputs))`. Since `StackItem` is `bytes32`, `abi.encodePacked` produces the same byte sequence as the contiguous stack memory. + +--- + +## Security Analysis + +### External Call Safety + +All four libraries making external calls (`LibOpERC721BalanceOf`, `LibOpERC721OwnerOf`, `LibOpUint256ERC721BalanceOf`, `LibOpERC5313Owner`) use typed Solidity interface calls (`IERC721(addr).balanceOf(...)`, `IERC721(addr).ownerOf(...)`, `IERC5313(addr).owner()`). These are compiled to `STATICCALL` (all functions are `view`) with: + +1. ABI-encoded calldata generated by the compiler. +2. Automatic revert propagation on call failure. +3. ABI-decoded return value with implicit length/type validation. + +If the target address is an EOA or non-contract, the STATICCALL succeeds with empty returndata, and Solidity's ABI decoder reverts when attempting to decode the expected return type from empty bytes. + +**Conclusion**: External calls are safe. Return values are implicitly validated by Solidity's ABI decoder. + +### Address Validation + +All address inputs are validated with `NotAnAddress`: +- `LibOpERC721BalanceOf`: validates `token` (line 40) and `account` (line 43) +- `LibOpERC721OwnerOf`: validates `token` (line 39) +- `LibOpUint256ERC721BalanceOf`: validates `token` (line 35) and `account` (line 38) +- `LibOpERC5313Owner`: validates `account` (line 36) + +The validation pattern `if (value != uint256(uint160(value))) revert NotAnAddress(value)` correctly detects values with non-zero upper 96 bits. + +`LibOpERC721OwnerOf` does NOT validate `tokenId` -- this is correct because `tokenId` is a raw `uint256`, not an address. + +**Conclusion**: Address validation is complete and correct. + +### Assembly Memory Safety + +All assembly blocks are annotated `"memory-safe"`. Analysis of each: + +1. **ERC721/ERC5313 stack reads** (e.g., lines 31-35 of `LibOpERC721BalanceOf`): Read from `stackTop` and `stackTop + 0x20`. These are within the interpreter's allocated stack region (bounds enforced by the integrity check). No free memory pointer modification. + +2. **ERC721/ERC5313 stack writes** (e.g., lines 51-53 of `LibOpERC721BalanceOf`): Write to `stackTop`, which is within the consumed stack region. No allocation. + +3. **EVM opcode pushes** (e.g., lines 27-30 of `LibOpBlockNumber`): `sub(stackTop, 0x20)` moves the pointer to the slot immediately below the current top, then writes. This is the standard push pattern. The stack region was pre-allocated by the interpreter with sufficient space (sized by the integrity check's max-stack analysis). + +4. **Hash opcode** (lines 29-35 of `LibOpHash`): Reads `inputs * 0x20` bytes from `stackTop`. Writes 1 value to `stackTop + length - 0x20`, which is within the consumed stack region. Correctly bounded. + +**Conclusion**: All assembly is memory-safe. + +### Integrity/Run Consistency + +All eight opcodes have consistent integrity declarations and runtime behavior: + +| Opcode | Integrity | Run net stack effect | +|---|---|---| +| `erc721-balance-of` | (2, 1) | Consumes 2 (token, account), produces 1 (float balance) | +| `erc721-owner-of` | (2, 1) | Consumes 2 (token, tokenId), produces 1 (owner address) | +| `uint256-erc721-balance-of` | (2, 1) | Consumes 2 (token, account), produces 1 (raw balance) | +| `erc5313-owner` | (1, 1) | Consumes 1 (contract), produces 1 (owner address) | +| `block-number` | (0, 1) | Consumes 0, produces 1 (raw block number) | +| `chain-id` | (0, 1) | Consumes 0, produces 1 (raw chain id) | +| `block-timestamp` | (0, 1) | Consumes 0, produces 1 (raw timestamp) | +| `hash` | (N, 1) where N=0..15 | Consumes N, produces 1 (keccak256 hash) | + +**Conclusion**: All integrity declarations match runtime behavior. + +### referenceFn Consistency + +All `referenceFn` implementations produce the same outputs as `run` for the same inputs: + +- **ERC721/ERC5313 ops**: Same external calls, same address validation, same return value handling. +- **EVM ops**: Use `LibDecimalFloat.fromFixedDecimalLosslessPacked(value, 0)` where `run` stores `value` directly. The NatSpec documents this is because `fromFixedDecimalLosslessPacked(value, 0)` is identity for these values -- confirmed by the `opReferenceCheck` fuzz tests. +- **Hash op**: `keccak256(abi.encodePacked(inputs))` produces the same bytes as `keccak256(stackTop, length)` because `StackItem` is `bytes32` and `abi.encodePacked` on a `bytes32[]` concatenates elements without padding. + +**Conclusion**: All reference functions are consistent with runtime functions. + +### Reentrancy + +The external calls in ERC721/ERC5313 opcodes are `STATICCALL` (all `run` functions are `view`). `STATICCALL` prevents the callee from modifying state, eliminating reentrancy risk. + +**Conclusion**: No reentrancy risk. + +--- + +## Findings + +### A22-1 -- INFO: `LibOpUint256ERC721BalanceOf.integrity` NatSpec missing `@notice` tag + +**Severity**: INFO + +**Location**: `src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol`, line 15 + +**Description**: The `integrity` function's NatSpec comment on line 15 reads `/// \`uint256-erc721-balance-of\` integrity check...` without an explicit `@notice` tag. All other `integrity` functions across the seven other files in this review use explicit `@notice` tags. While technically valid Solidity NatSpec (implicit `@notice` when no tags are present in a doc block), it is inconsistent with the project's convention. + +### A22-2 -- INFO: EVM ops store raw values relying on `fromFixedDecimalLosslessPacked(v, 0)` being identity + +**Severity**: INFO + +**Location**: `src/lib/op/evm/LibOpBlockNumber.sol` line 29, `LibOpChainId.sol` line 29, `LibOpTimestamp.sol` line 29 + +**Description**: The three EVM opcode `run` functions store raw `number()`, `chainid()`, and `timestamp()` values directly on the stack without float conversion, while their `referenceFn` implementations use `LibDecimalFloat.fromFixedDecimalLosslessPacked(value, 0)`. The NatSpec explicitly documents this optimization and states that `fromFixedDecimalLosslessPacked(value, 0)` is identity for these values. The `opReferenceCheck` fuzz tests validate this invariant. This is noted for completeness -- the optimization is correct and well-documented. + +### A22-3 -- INFO: Hash opcode input count limited to 15 by 4-bit mask + +**Severity**: INFO + +**Location**: `src/lib/op/crypto/LibOpHash.sol`, lines 20 and 30 + +**Description**: The input count is extracted with `& 0x0F`, limiting the hash opcode to at most 15 inputs (480 bytes of data). This is a shared design constraint across all multi-input opcodes in the codebase and is explicitly documented in the comment on line 19. Rainlang authors needing to hash more data would need to chain multiple hash operations. + +--- + +## Summary + +No CRITICAL, HIGH, MEDIUM, or LOW findings across all eight files. + +All external calls use typed Solidity interface calls compiled to `STATICCALL`, with implicit return value validation via the ABI decoder. Address inputs are consistently validated with `NotAnAddress`. All assembly blocks are correctly annotated `"memory-safe"` and operate within the interpreter's managed stack region. Integrity declarations match runtime behavior in all cases. Reference functions are semantically equivalent to runtime functions, confirmed by fuzz tests. diff --git a/audit/2026-03-01-01/pass1/LibOpLogic.md b/audit/2026-03-01-01/pass1/LibOpLogic.md new file mode 100644 index 000000000..0dd9e41ea --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibOpLogic.md @@ -0,0 +1,66 @@ +# Pass 1 Audit: Logic Opcodes + +**Agent**: A23 +**Date**: 2026-03-01 +**Scope**: `src/lib/op/logic/` (12 files) + +## Files Reviewed + +| File | Lines | Assembly Blocks | Operand-Driven Inputs | +|------|-------|-----------------|----------------------| +| `LibOpAny.sol` | 77 | 2 (run) | Yes (4-bit, clamped >= 1 in integrity) | +| `LibOpBinaryEqualTo.sol` | 47 | 1 (run) | No (fixed 2 inputs) | +| `LibOpConditions.sol` | 107 | 3 (run) | Yes (4-bit, clamped >= 2 in integrity) | +| `LibOpEnsure.sol` | 60 | 1 (run) | No (fixed 2 inputs) | +| `LibOpEqualTo.sol` | 63 | 2 (run) | No (fixed 2 inputs) | +| `LibOpEvery.sol` | 76 | 2 (run) | Yes (4-bit, clamped >= 1 in integrity) | +| `LibOpGreaterThan.sol` | 57 | 2 (run) | No (fixed 2 inputs) | +| `LibOpGreaterThanOrEqualTo.sol` | 58 | 2 (run) | No (fixed 2 inputs) | +| `LibOpIf.sol` | 55 | 2 (run) | No (fixed 3 inputs) | +| `LibOpIsZero.sol` | 50 | 2 (run) | No (fixed 1 input) | +| `LibOpLessThan.sol` | 57 | 2 (run) | No (fixed 2 inputs) | +| `LibOpLessThanOrEqualTo.sol` | 58 | 2 (run) | No (fixed 2 inputs) | + +## Evidence of Thorough Reading + +### Operand Decoding +- `LibOpAny`, `LibOpEvery`, `LibOpConditions` all extract input count from operand bits `[16:19]` via `(OperandV2.unwrap(operand) >> 0x10) & 0x0F`, yielding 0-15. +- Integrity clamps: Any/Every >= 1, Conditions >= 2. The `run()` functions read the raw operand without clamping. +- Verified via `LibIntegrityCheck.integrityCheck2` (line 159) that the bytecode IO byte must match the integrity function's return value. Since integrity clamps the operand but the IO byte must match the clamped value, the parser must produce a consistent operand+IO byte pair or integrity rejects at deploy time. + +### Stack Pointer Arithmetic +- Fixed-input ops (2-input comparisons): read `mload(stackTop)`, advance `stackTop += 0x20`, read/write second slot. Net: 2 in, 1 out. Verified for all 6 comparison ops. +- `LibOpIf`: reads condition at `stackTop`, advances by `0x40` (skipping condition and then-value), then conditionally reads from `stackTop` (else) or `stackTop - 0x20` (then). Net: 3 in, 1 out. +- `LibOpEnsure`: reads 2 items, advances by `0x40`. Net: 2 in, 0 out. +- `LibOpIsZero`: reads 1 item, overwrites in-place. Net: 1 in, 1 out. +- `LibOpAny`/`LibOpEvery`: `length = 0x20 * inputs`, `end = stackTop + length`, `stackTop = end - 0x20`. Loop iterates `[stackTop, end)`. Net: N in, 1 out. +- `LibOpConditions`: pairs iteration from cursor to end, with optional odd trailing reason. Stacktop computed as `end - 0x20` (even) or `end` (odd). Net: N in, 1 out. + +### Assembly `memory-safe` Annotations +- All assembly blocks are annotated `memory-safe`. Verified that each block only reads/writes within the stack region bounded by `stackTop` through `stackTop + inputs * 0x20`. The stack is Solidity-allocated memory (via `LibInterpreterState`), so these annotations are correct. + +### Float vs. Binary Equality +- `LibOpBinaryEqualTo` uses EVM `eq()` (bitwise). `LibOpEqualTo` uses `Float.eq()` (semantic float equality, handling different representations of the same value). Both are intentional per their naming and NatSpec. + +### Boolean Output Encoding +- All comparison and boolean ops (`equal-to`, `binary-equal-to`, `greater-than`, `greater-than-or-equal-to`, `less-than`, `less-than-or-equal-to`, `is-zero`) output raw `0` or `1` (not float-encoded). This is consistent across all ops. Conditional ops (`any`, `every`, `if`, `conditions`, `ensure`) use `Float.isZero()` to check truthiness, which checks whether the lower 224 bits are zero. Raw `1` has nonzero lower 224 bits, so it is correctly treated as truthy. + +### String Reverts +- `LibOpConditions` and `LibOpEnsure` use `revert(string)` -- confirmed these are documented exceptions (user-facing revert reason feature). No other logic ops use string reverts. + +### Reference Functions +- Each op has a `referenceFn` for testing. Verified these match the `run()` semantics for each op. `LibOpConditions.referenceFn` has both even and odd input handling matching `run()`. + +### `LibOpIf` Conditional Selection Logic +- Traced the expression `mload(sub(stackTop, mul(0x20, iszero(isZero))))`: + - Condition nonzero: `isZero = false (0)`, `iszero(0) = 1`, reads `stackTop - 0x20` = then-value. Correct. + - Condition zero: `isZero = true (1)`, `iszero(1) = 0`, reads `stackTop` = else-value. Correct. + +### `LibOpConditions` Default Variable Initialization +- `conditionIsZero` declared as `bool` without initializer, defaults to `false`. With the integrity minimum of 2 inputs, the loop always executes at least once, so `conditionIsZero` is always explicitly set before the line-72 check. No issue with the default value. + +## Findings + +No findings at LOW or above. + +The logic opcode implementations are correct. The integrity/run operand-clamping divergence (integrity clamps minimum inputs, `run()` reads raw operand) is structurally unreachable because the bytecode integrity check at deploy time ensures the IO byte matches the clamped value, and the parser produces consistent operand+IO byte pairs. The eval loop dispatches `run()` with the same operand that integrity validated, so the clamped minimum is always respected at runtime. diff --git a/audit/2026-03-01-01/pass1/LibOpMath.md b/audit/2026-03-01-01/pass1/LibOpMath.md new file mode 100644 index 000000000..85f06c6bc --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibOpMath.md @@ -0,0 +1,267 @@ +# Pass 1 (Security) -- Math Opcodes + +**Auditors**: A24 (decimal float math), A29 (growth/uint256 math) +**Date**: 2026-03-01 +**Scope**: All math opcode libraries in `src/lib/op/math/`, `src/lib/op/math/growth/`, `src/lib/op/math/uint256/` + +--- + +## Evidence of Thorough Reading + +### Decimal Float Math (`src/lib/op/math/`) + +| Library | Functions (line numbers) | Lines | +|---|---|---| +| `LibOpAbs` | `integrity` (19), `run` (28), `referenceFn` (44) | 53 | +| `LibOpAdd` | `integrity` (22), `run` (33), `referenceFn` (76) | 98 | +| `LibOpAvg` | `integrity` (19), `run` (28), `referenceFn` (47) | 61 | +| `LibOpCeil` | `integrity` (19), `run` (28), `referenceFn` (44) | 55 | +| `LibOpDiv` | `integrity` (21), `run` (33), `referenceFn` (74) | 107 | +| `LibOpE` | `integrity` (17), `run` (24), `referenceFn` (35) | 44 | +| `LibOpExp` | `integrity` (19), `run` (28), `referenceFn` (44) | 56 | +| `LibOpExp2` | `integrity` (19), `run` (28), `referenceFn` (45) | 57 | +| `LibOpFloor` | `integrity` (19), `run` (28), `referenceFn` (44) | 53 | +| `LibOpFrac` | `integrity` (19), `run` (28), `referenceFn` (44) | 53 | +| `LibOpGm` | `integrity` (21), `run` (31), `referenceFn` (55) | 74 | +| `LibOpHeadroom` | `integrity` (20), `run` (30), `referenceFn` (49) | 65 | +| `LibOpInv` | `integrity` (19), `run` (28), `referenceFn` (44) | 53 | +| `LibOpMax` | `integrity` (20), `run` (32), `referenceFn` (67) | 79 | +| `LibOpMaxNegativeValue` | `integrity` (19), `run` (26), `referenceFn` (37) | 46 | +| `LibOpMaxPositiveValue` | `integrity` (19), `run` (26), `referenceFn` (37) | 46 | +| `LibOpMin` | `integrity` (20), `run` (32), `referenceFn` (68) | 84 | +| `LibOpMinNegativeValue` | `integrity` (19), `run` (26), `referenceFn` (37) | 46 | +| `LibOpMinPositiveValue` | `integrity` (19), `run` (26), `referenceFn` (37) | 46 | +| `LibOpMul` | `integrity` (21), `run` (32), `referenceFn` (74) | 101 | +| `LibOpPow` | `integrity` (19), `run` (28), `referenceFn` (47) | 60 | +| `LibOpSqrt` | `integrity` (19), `run` (28), `referenceFn` (44) | 56 | +| `LibOpSub` | `integrity` (21), `run` (33), `referenceFn` (75) | 101 | + +### Growth (`src/lib/op/math/growth/`) + +| Library | Functions (line numbers) | Lines | +|---|---|---| +| `LibOpExponentialGrowth` | `integrity` (18), `run` (26), `referenceFn` (47) | 60 | +| `LibOpLinearGrowth` | `integrity` (18), `run` (26), `referenceFn` (48) | 60 | + +### Uint256 (`src/lib/op/math/uint256/`) + +| Library | Functions (line numbers) | Lines | +|---|---|---| +| `LibOpMaxUint256` | `integrity` (14), `run` (21), `referenceFn` (31) | 40 | +| `LibOpUint256Add` | `integrity` (17), `run` (30), `referenceFn` (64) | 80 | +| `LibOpUint256Div` | `integrity` (18), `run` (30), `referenceFn` (65) | 82 | +| `LibOpUint256Mul` | `integrity` (17), `run` (30), `referenceFn` (64) | 80 | +| `LibOpUint256Pow` | `integrity` (17), `run` (30), `referenceFn` (64) | 80 | +| `LibOpUint256Sub` | `integrity` (17), `run` (30), `referenceFn` (64) | 81 | + +--- + +## Security Analysis + +### 1. Assembly Memory Safety + +All assembly blocks across all 29 files are marked `"memory-safe"`. The patterns used are: + +**Stack read pattern** (1-input opcodes: abs, ceil, floor, frac, headroom, inv, exp, exp2, sqrt): +```solidity +assembly ("memory-safe") { + a := mload(stackTop) +} +``` +Reads from the current stack top. Correct -- `stackTop` is always a valid memory pointer managed by the interpreter loop. + +**Stack read pattern** (2-input opcodes: avg, gm, pow): +```solidity +assembly ("memory-safe") { + a := mload(stackTop) + stackTop := add(stackTop, 0x20) + b := mload(stackTop) +} +``` +Reads two values and advances `stackTop` by one slot (net consumption of 1, since the result is written back to the current `stackTop`). Correct. + +**N-ary stack read pattern** (add, sub, mul, div, max, min): +```solidity +assembly ("memory-safe") { + a := mload(stackTop) + b := mload(add(stackTop, 0x20)) + stackTop := add(stackTop, 0x40) +} +``` +Initial read of two values, then loop reads additional values. Final write-back: +```solidity +assembly ("memory-safe") { + stackTop := sub(stackTop, 0x20) + mstore(stackTop, a) +} +``` +This pops N items and pushes 1, for a net consumption of N-1. Correct. + +**Stack push pattern** (0-input opcodes: e, max-uint256, constant value opcodes): +```solidity +assembly ("memory-safe") { + stackTop := sub(stackTop, 0x20) + mstore(stackTop, value) +} +``` +Pushes one value onto the stack. Correct. + +All assembly blocks only touch the stack pointer region. No free memory pointer manipulation, no scratch space use. All reads and writes are within the bounds managed by the interpreter's stack allocation. + +**Conclusion**: Assembly memory safety is correctly maintained across all files. + +### 2. Integrity/Run Consistency + +For each opcode, the `integrity` function declares how many stack items are consumed and produced. The `run` function must match this exactly. + +| Opcode | integrity inputs | integrity outputs | run pops | run pushes | Match | +|---|---|---|---|---|---| +| abs | 1 | 1 | 1 (read+overwrite) | 1 | Yes | +| add | N (min 2) | 1 | N | 1 | Yes | +| avg | 2 | 1 | 2 | 1 | Yes | +| ceil | 1 | 1 | 1 | 1 | Yes | +| div | N (min 2) | 1 | N | 1 | Yes | +| e | 0 | 1 | 0 | 1 | Yes | +| exp | 1 | 1 | 1 | 1 | Yes | +| exp2 | 1 | 1 | 1 | 1 | Yes | +| floor | 1 | 1 | 1 | 1 | Yes | +| frac | 1 | 1 | 1 | 1 | Yes | +| gm | 2 | 1 | 2 | 1 | Yes | +| headroom | 1 | 1 | 1 | 1 | Yes | +| inv | 1 | 1 | 1 | 1 | Yes | +| max | N (min 2) | 1 | N | 1 | Yes | +| max-negative-value | 0 | 1 | 0 | 1 | Yes | +| max-positive-value | 0 | 1 | 0 | 1 | Yes | +| min | N (min 2) | 1 | N | 1 | Yes | +| min-negative-value | 0 | 1 | 0 | 1 | Yes | +| min-positive-value | 0 | 1 | 0 | 1 | Yes | +| mul | N (min 2) | 1 | N | 1 | Yes | +| pow | 2 | 1 | 2 | 1 | Yes | +| sqrt | 1 | 1 | 1 | 1 | Yes | +| sub | N (min 2) | 1 | N | 1 | Yes | +| exponential-growth | 3 | 1 | 3 | 1 | Yes | +| linear-growth | 3 | 1 | 3 | 1 | Yes | +| max-uint256 | 0 | 1 | 0 | 1 | Yes | +| uint256-add | N (min 2) | 1 | N | 1 | Yes | +| uint256-div | N (min 2) | 1 | N | 1 | Yes | +| uint256-mul | N (min 2) | 1 | N | 1 | Yes | +| uint256-pow | N (min 2) | 1 | N | 1 | Yes | +| uint256-sub | N (min 2) | 1 | N | 1 | Yes | + +**Conclusion**: All integrity declarations match their run implementations exactly. + +### 3. Arithmetic Overflow/Underflow + +**Decimal float opcodes**: All arithmetic is delegated to `LibDecimalFloat` / `LibDecimalFloatImplementation` functions (`add`, `sub`, `mul`, `div`, `pow`, `sqrt`, etc.). These are external library functions that handle overflow internally -- either by reverting (`ExponentOverflow`) or by lossy packing (`packLossy`). The opcode wrappers do not perform any arithmetic on the float values themselves (except `unchecked { i++; }` in loops, analyzed below). + +**Uint256 opcodes**: The main arithmetic operations (`a += b`, `a -= b`, `a *= b`, `a /= b`, `a = a ** b`) are in checked Solidity 0.8.25 context, which will automatically revert on overflow/underflow. + +**Loop counter `unchecked { i++; }`**: All N-ary opcodes use `unchecked { i++; }` for the loop counter. The counter `i` starts at 2 and is bounded by `inputs`, which is masked to 4 bits (`& 0x0F`), giving a maximum value of 15. The increment from 14 to 15 is the maximum, which cannot overflow a `uint256`. Safe. + +**Conclusion**: No overflow or underflow vulnerabilities. + +### 4. Division by Zero + +**LibOpDiv (float)**: Division by zero handling is delegated to `LibDecimalFloatImplementation.div()`, which reverts on division by zero. The `referenceFn` (line 92) explicitly checks `if (b.isZero())` and bails out with a sentinel value, confirming the real implementation is expected to revert. + +**LibOpUint256Div**: Uses Solidity's `/=` operator, which reverts on division by zero via the compiler's built-in check. + +**LibOpInv (float)**: Delegates to `Float.inv()` which computes `1/x`. Division by zero when `x == 0` is handled by the underlying `LibDecimalFloat.div()`. + +**Conclusion**: Division by zero is correctly handled (reverts) in all cases. + +### 5. packLossy Precision Loss + +The N-ary float opcodes (`add`, `sub`, `mul`, `div`) unpack inputs, perform multi-step arithmetic on raw `(signedCoefficient, exponent)` pairs, and then call `packLossy` once at the end to re-pack the result. This is the correct pattern -- it maximizes precision by keeping values in unpacked form during intermediate calculations. + +The single-step float opcodes (`abs`, `ceil`, `floor`, `frac`, `avg`, `headroom`, `inv`) use the high-level `Float` methods (e.g., `a.add(b)`, `a.ceil()`) which call `packLossy` internally after each operation. For `avg` (line 36 of LibOpAvg.sol), this means `packLossy` is called twice: once after `a.add(b)` and once after `.div(FLOAT_TWO)`. This is the expected behavior for these convenience methods, and the precision loss is bounded by the float format (int224 coefficient, int32 exponent). + +**Conclusion**: `packLossy` usage is consistent and precision loss is bounded by the float format. No unbounded or undocumented precision loss. + +### 6. N-ary Operand Parsing + +All N-ary opcodes extract the input count with: +```solidity +uint256 inputs = uint256(OperandV2.unwrap(operand) >> 0x10) & 0x0F; +inputs = inputs > 1 ? inputs : 2; +``` + +The 4-bit mask limits inputs to 0-15. Values 0 and 1 are clamped to 2 (minimum). Both `integrity` and `run` use identical extraction logic, so they always agree on the input count. + +**Conclusion**: Operand parsing is consistent between integrity and run for all N-ary opcodes. + +### 7. Uint256 Overflow Protection + +All uint256 opcodes (`uint256-add`, `uint256-sub`, `uint256-mul`, `uint256-pow`) use checked Solidity arithmetic. The `referenceFn` implementations use `unchecked` blocks intentionally, so that test harnesses can verify the real implementation throws overflow errors while the reference produces a different (wrapped) result. + +`uint256-div` relies on the compiler's built-in division-by-zero check. + +**Conclusion**: Uint256 opcodes correctly leverage Solidity 0.8.x checked arithmetic. + +### 8. Growth Opcodes + +**LibOpExponentialGrowth**: Computes `base * (1 + rate)^t` (line 36). Uses `rate.add(FLOAT_ONE).pow(t, LOG_TABLES_ADDRESS)` then multiplies by `base`. Delegates all arithmetic to `LibDecimalFloat`. No direct arithmetic in the opcode itself. The `pow` function requires `LOG_TABLES_ADDRESS` to be deployed, making this a `view` function. + +**LibOpLinearGrowth**: Computes `base + rate * t` (line 37). Uses `base.add(rate.mul(t))`. Pure function. No issues. + +**Conclusion**: Growth opcodes correctly delegate to float library. No arithmetic bugs. + +--- + +## Findings + +### A24-1 -- INFO: NatSpec missing `@notice` tag on integrity functions in growth opcodes + +**Location**: `LibOpExponentialGrowth.sol` line 17, `LibOpLinearGrowth.sol` line 17 + +**Description**: The `integrity` function NatSpec uses a bare `///` comment without any tag: +```solidity +/// `exponential-growth` integrity check. Requires exactly 3 inputs and produces 1 output. +``` +Per the project's NatSpec convention: when no explicit tags are used in a doc block, the text is implicitly `@notice`. Since no other tags are present in this doc block, this is technically valid Solidity NatSpec. However, it is inconsistent with all other math opcodes which use explicit `@notice` tags. This inconsistency is also present in `LibOpMaxUint256.sol` line 13. + +**Severity**: INFO + +### A24-2 -- INFO: NatSpec missing `@notice` tag on integrity and referenceFn in LibOpMaxUint256 + +**Location**: `LibOpMaxUint256.sol` line 13, line 30 + +**Description**: Same pattern as A24-1. The `integrity` function (line 13) and `referenceFn` (line 30) use bare `///` without `@notice`. This is consistent with the growth opcodes but inconsistent with the majority of math opcodes. + +**Severity**: INFO + +### A24-3 -- INFO: Intermediate packLossy in LibOpAvg vs N-ary opcodes + +**Location**: `LibOpAvg.sol` line 36 + +**Description**: `LibOpAvg.run()` computes `a.add(b).div(FLOAT_TWO)` using high-level `Float` methods. This results in two `packLossy` calls (one inside `add`, one inside `div`), whereas the N-ary opcodes (add, sub, mul, div) use unpacked intermediate arithmetic with a single final `packLossy`. The extra packing step in `avg` can cause slightly different rounding compared to computing the average using unpacked values. This is by design -- `avg` uses the convenience API for simplicity and the precision loss is bounded by the float format. The `referenceFn` uses the same code path, so tests will validate consistency. + +**Severity**: INFO + +### A24-4 -- INFO: Unchecked loop counters in N-ary opcodes are provably safe + +**Location**: All N-ary opcodes (LibOpAdd line 57-59, LibOpSub line 57-59, LibOpMul line 56-58, LibOpDiv line 57-59, LibOpMax line 51-53, LibOpMin line 52-54, LibOpUint256Add line 49-51, LibOpUint256Div line 49-51, LibOpUint256Mul line 49-51, LibOpUint256Pow line 49-51, LibOpUint256Sub line 49-51) + +**Description**: All N-ary opcodes use `unchecked { i++; }` for loop counter increments. The counter `i` starts at 2 and is bounded by `inputs`, which is extracted as `(operand >> 0x10) & 0x0F`, giving a maximum value of 15. The maximum increment is from 14 to 15, which cannot overflow `uint256`. This is safe. + +**Severity**: INFO + +### A24-5 -- INFO: LibOpHeadroom returns 1 for integer inputs + +**Location**: `LibOpHeadroom.sol` lines 35-38 + +**Description**: The headroom opcode computes `ceil(x) - x`, and when the result is zero (i.e., `x` is already an integer), it returns `FLOAT_ONE` instead of zero. This behavior is documented in the NatSpec (line 28: "except when `x` is already an integer (headroom would be zero), in which case it returns 1"). This is intentional design -- it ensures headroom is always positive, which is useful for expressions that multiply by headroom to scale a value. Documented and correct. + +**Severity**: INFO + +--- + +## Summary + +No CRITICAL, HIGH, MEDIUM, or LOW findings across all 29 math opcode files. + +The math opcodes are thin wrappers around `LibDecimalFloat` / `LibDecimalFloatImplementation` (for float opcodes) and Solidity checked arithmetic (for uint256 opcodes). Each opcode follows a consistent pattern: integrity declares input/output counts, run performs stack manipulation via assembly and delegates arithmetic to the library, referenceFn provides a test oracle using the same library functions. + +All assembly is correctly marked `memory-safe` and only operates on the interpreter's stack pointer region. N-ary operand parsing is consistent between integrity and run. Division by zero is handled by the underlying libraries. Overflow/underflow is caught by either the float library's `ExponentOverflow` revert or Solidity 0.8.x checked arithmetic. The `packLossy` precision loss is bounded by the float format and consistently applied. + +The only findings are informational: minor NatSpec inconsistencies (bare `///` vs explicit `@notice`) in three files, and documentation of design decisions (avg intermediate packing, headroom returning 1, safe unchecked counters). diff --git a/audit/2026-03-01-01/pass1/LibOpStore.md b/audit/2026-03-01-01/pass1/LibOpStore.md new file mode 100644 index 000000000..f47f4e635 --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibOpStore.md @@ -0,0 +1,77 @@ +# Pass 1: LibOpGet.sol and LibOpSet.sol + +## Files Audited + +- `src/lib/op/store/LibOpGet.sol` (93 lines) +- `src/lib/op/store/LibOpSet.sol` (56 lines) + +## Evidence of Thorough Reading + +### LibOpGet.sol + +- Lines 1-9: SPDX license (`LicenseRef-DCL-1.0`), pragma `^0.8.25`, five imports: `LibMemoryKV`/`MemoryKVKey`/`MemoryKVVal`/`MemoryKV` from `rain.lib.memkv`, `OperandV2`/`StackItem` from `IInterpreterV4`, `Pointer` from `rain.solmem`, `InterpreterState` from `LibInterpreterState`, `IntegrityCheckState` from `LibIntegrityCheck`. +- Lines 11-14: `library LibOpGet` with `using LibMemoryKV for MemoryKV`. +- Lines 16-23: `integrity()` -- pure, ignores both parameters, returns `(1, 1)` meaning 1 input (key) and 1 output (value). +- Lines 25-62: `run()` -- marked `view` because of the external `state.store.get()` call on cache miss. Assembly block at line 34 reads key from `stackTop`. Line 37 attempts cache lookup via `state.stateKV.get()`. On miss (lines 40-52): fetches from external store, caches fetched value (including zero) into `stateKV`, writes value to `stackTop`. On hit (lines 55-58): writes cached value to `stackTop`. Returns `stackTop` unchanged (1 input consumed, 1 output produced at same location). +- Lines 44-48: Comment documents deliberate design choice: read-only keys are cached and therefore will be persisted to the on-chain store at eval end, paying an unnecessary SSTORE. This is a gas-cost tradeoff, not a correctness issue. +- Lines 64-92: `referenceFn()` -- array-based mirror of `run` used for differential testing. Same cache-miss/hit logic using `StackItem[]` inputs/outputs instead of raw pointers. + +### LibOpSet.sol + +- Lines 1-9: Same license/pragma pattern, imports `LibMemoryKV`/`MemoryKV`/`MemoryKVKey`/`MemoryKVVal`, `IntegrityCheckState`, `OperandV2`/`StackItem`, `InterpreterState`, `Pointer`. +- Lines 11-14: `library LibOpSet` with `using LibMemoryKV for MemoryKV`. +- Lines 16-23: `integrity()` -- pure, ignores parameters, returns `(2, 0)` meaning 2 inputs (key, value) and 0 outputs. +- Lines 25-40: `run()` -- marked `pure` (no external calls needed). Assembly block reads key from `stackTop`, value from `stackTop + 0x20`, advances `stackTop` by `0x40` (consuming both inputs). Line 38 stores to `stateKV`. +- Lines 42-55: `referenceFn()` -- array-based mirror for testing, returns empty `StackItem[]`. + +## Security Analysis + +### State KV Caching Safety + +The `get` opcode caches every value it reads from the external store into `stateKV`, including cache misses that return zero. The `stateKV` is then exported as a `bytes32[]` by `LibEval.eval2` (line 247) and returned to the caller. The caller (not the interpreter) is responsible for calling `store.set()` with these KV pairs. + +Caching zero values on miss means that a `get` on an unset key will produce a `(key, 0)` entry in the final KV writes. When the caller applies these writes, it will call `store.set()` with key=X, value=0. This is semantically a no-op on a fresh store (default is already 0), but it costs an SSTORE. This is documented in the code comments and is a known gas tradeoff, not a correctness bug. + +### Assembly Memory Safety + +Both files use `assembly ("memory-safe")` blocks. Analysis: + +**LibOpGet.run:** +- Line 34-36: `key := mload(stackTop)` -- reads from a pre-validated stack pointer. The integrity check ensures exactly 1 input exists on the stack before `run` is called. Memory-safe: reads only. +- Lines 50-52: `mstore(stackTop, storeValue)` -- writes the fetched value back into the same stack slot that held the key. This is within the stack's allocated memory region. Memory-safe: overwrites own stack slot. +- Lines 56-58: `mstore(stackTop, value)` -- same pattern for cache hit. Memory-safe. + +**LibOpSet.run:** +- Lines 32-36: Reads key from `stackTop`, value from `stackTop + 0x20`, advances `stackTop` by `0x40`. Both reads are within the stack's allocated region (integrity ensures 2 inputs exist). The pointer arithmetic is correct: consuming 2 words = advancing by `0x40` bytes. + +All assembly blocks are correctly marked `memory-safe` and do not violate their claimed safety: they only read/write within the stack region that is guaranteed to be allocated by the integrity check. + +### Namespace Isolation + +The `get` opcode reads from the external store using `state.namespace`, which is a `FullyQualifiedNamespace`. This namespace is set during deserialization in `LibInterpreterStateDataContract.unsafeDeserialize` from whatever the caller of `eval4` provides. The `set` opcode only writes to the in-memory `stateKV` and does not interact with the namespace directly. + +Namespace qualification is the responsibility of the interpreter's `eval4` caller and the store's `set()` function (which qualifies by `msg.sender`). This is outside the scope of LibOpGet/LibOpSet themselves. + +### Can get/set corrupt the KV store? + +No. Both opcodes interact with `stateKV` exclusively through `LibMemoryKV.get()` and `LibMemoryKV.set()`, which are the canonical accessor functions for the linked-list-based in-memory KV store. The `MemoryKV` type is a value type (`uint256`) that is reassigned on every `set` (the `stateKV` field on `InterpreterState` is updated with the return value). This prevents stale-pointer issues. + +The only path to KV corruption would be through `LibMemoryKV` itself (e.g., the `MemoryKVOverflow` error if a pointer exceeds `0xFFFF`), which is handled by the library and not specific to these opcodes. + +## Findings + +### A28-1 [INFO] Read-only `get` keys are persisted to on-chain store, paying unnecessary SSTORE gas + +**File:** `src/lib/op/store/LibOpGet.sol`, lines 42-48 + +When a `get` encounters a cache miss, it fetches from the external store and caches the result in `stateKV`. This cached value is then included in the KV writes returned by `eval2`, meaning the caller will call `store.set()` with the read-only value. For keys that are only read and never written, this pays an SSTORE for a value that is already present in storage. + +This is documented in the code comments at lines 43-47 as a deliberate design tradeoff. The gas savings from caching repeated reads within a single eval are expected to outweigh the SSTORE cost in practice. No fix needed -- this is informational only, documenting a known design decision. + +### A28-2 [INFO] NatSpec for `integrity` functions uses `@return` without parameter names + +**File:** `src/lib/op/store/LibOpGet.sol`, lines 16-18; `src/lib/op/store/LibOpSet.sol`, lines 16-18 + +Both `integrity` functions have `@return` tags that describe inputs/outputs but do not name the return values. The function signatures use unnamed returns `(uint256, uint256)`. This is consistent with the codebase style for integrity functions and is not a defect, but named returns would improve documentation clarity. + +No fix needed -- informational only. diff --git a/audit/2026-03-01-01/pass1/LibParse.md b/audit/2026-03-01-01/pass1/LibParse.md new file mode 100644 index 000000000..30f27409b --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibParse.md @@ -0,0 +1,121 @@ +# Audit Pass 1 -- LibParse.sol + +**Agent:** A30 +**File:** `src/lib/parse/LibParse.sol` (449 lines) +**Date:** 2026-03-01 + +## Evidence of Thorough Reading + +### Library Name +`LibParse` (line 68) + +### Constants +| Name | Value | Line | +|------|-------|------| +| `SUB_PARSER_BYTECODE_HEADER_SIZE` | `5` | 59 | + +### Functions +| Name | Line | Visibility | +|------|------|------------| +| `parseWord` | 100 | `internal pure` | +| `parseLHS` | 136 | `internal pure` | +| `parseRHS` | 204 | `internal view` | +| `parse` | 425 | `internal view` | + +### Imported Types/Errors/Constants +**Types:** `Pointer`, `ParseState`, `OperandV2` + +**Errors (from ErrParse.sol):** +`UnexpectedRHSChar`, `UnexpectedRightParen`, `WordSize`, `DuplicateLHSItem`, `ParserOutOfBounds`, `ExpectedLeftParen`, `UnexpectedLHSChar`, `MissingFinalSemi`, `UnexpectedComment`, `ParenOverflow` + +**Constants (from LibParseState.sol):** +`FSM_YANG_MASK`, `FSM_DEFAULT`, `FSM_ACTIVE_SOURCE_MASK`, `FSM_WORD_END_MASK`, `PARSE_STATE_PAREN_TRACKER0_OFFSET` + +**Libraries used via `using...for`:** +`LibPointer`, `LibParseStackName`, `LibParseState`, `LibParseInterstitial`, `LibParseError`, `LibParseMeta`, `LibParsePragma`, `LibParse`, `LibParseOperand`, `LibSubParse`, `LibBytes`, `LibUint256Array`, `LibBytes32Array` + +--- + +## Findings + +### A30-1: parseOperand reads memory at cursor without bounds check when cursor == end [INFO] + +**Location:** Called from `parseRHS` lines 228, 261 -> `LibParseOperand.parseOperand` line 37-39 + +**Description:** +After `parseWord` returns in `parseRHS`, the cursor may equal `end` (e.g., when the last character of the input is the last character of a word). `parseOperand` is then called with `cursor == end`. At line 37-39 of `LibParseOperand.sol`, `mload(cursor)` reads 32 bytes starting at `end`, which is past the data boundary. + +Since this is a memory read (not calldata), it reads whatever happens to be at that memory address rather than reverting. In the common case the garbage byte does not match `CMASK_OPERAND_START` and the function returns cursor unchanged. In the unlikely case the garbage byte does match, the `while (cursor < end)` loop at line 63 exits immediately, `success` remains false, and the function reverts with `UnclosedOperand`. + +**Impact:** The behavior is correct in all cases (either no-op or clean revert). The read is from Solidity-managed memory so no out-of-bounds revert occurs. The only theoretical concern is a misleading error message (`UnclosedOperand` instead of a more descriptive error) in the edge case where post-data garbage matches `<`. + +**Severity:** INFO + +**Recommendation:** No code change required. The current behavior is safe. If clarity is desired, a `cursor >= end` guard before the `mload` in `parseOperand` would make the intent explicit. + +--- + +### A30-2: Sub-parser bytecode construction uses unaligned memory allocation [INFO] + +**Location:** `parseRHS` lines 271-284 + +**Description:** +The sub-parser bytecode allocation at line 275: +```solidity +mstore(0x40, add(subParserBytecode, add(subParserBytecodeLength, 0x20))) +``` +is explicitly documented as "NOT an aligned allocation." This is intentional -- the sub-parser bytecode is treated as raw bytes passed to external sub-parser contracts, not as a Solidity `bytes` variable that would need 32-byte alignment for ABI encoding. + +Later code in `pushOpToSource` stores a pointer to this bytecode as the operand of `OPCODE_UNKNOWN`. When `subParseWordSlice` dereferences it, the pointer is used to build the `data` variable passed to `subParser.subParseWord2(data)`, which ABI-encodes it for the external call. + +**Impact:** The unaligned allocation does not cause issues because the bytecode is consumed through explicit pointer manipulation, not through Solidity's ABI decoder on the allocating side. The external call ABI-encodes the data regardless of alignment. + +**Severity:** INFO + +**Recommendation:** No change needed. The existing comment at line 274 documents the intentional non-alignment. + +--- + +### A30-3: parseWord mload reads up to 31 bytes past data end [INFO] + +**Location:** `parseWord` line 111 + +**Description:** +`word := mload(cursor)` always reads 32 bytes from the cursor position, even when less than 32 bytes remain between cursor and end. The `iEnd` variable correctly limits the loop to only check bytes within bounds (`iEnd = min(remaining, 0x20)`), and the scrub operation zeros out bytes beyond the word length. The extra bytes read are from Solidity-managed memory. + +**Impact:** None. The over-read is from heap memory, not calldata or storage. The scrubbed bytes are discarded. This is a standard pattern in Solidity assembly for processing packed data. + +**Severity:** INFO + +--- + +### A30-4: checkParseMemoryOverflow is post-hoc, not preventive [INFO] + +**Location:** `LibParseState.checkParseMemoryOverflow` (line 1044 of LibParseState.sol), called as modifier in `RainterpreterParser.sol` + +**Description:** +The 16-bit pointer system used throughout the parser (active source slots, paren tracker, sources builder, constants builder, stack names) requires all memory pointers to fit in 16 bits (< 0x10000). `checkParseMemoryOverflow` validates this constraint after the entire parse operation completes. If memory crosses the 0x10000 boundary during parsing, linked list pointers are silently truncated before the check runs. + +The NatSpec in `pushSubParser` (line 303-305 of LibParseState.sol) explicitly documents this dependency: "this function relies on `checkParseMemoryOverflow` keeping the free memory pointer below `0x10000`. If that invariant is violated, the tail pointer will be silently truncated and the linked list corrupted." + +**Impact:** If memory overflows during parsing, the resulting bytecode is corrupted. However, the post-hoc check will revert the transaction, so corrupted bytecode never escapes the parser. The risk is zero for external callers since the modifier ensures the revert. An internal library caller that skips the check could produce corrupted output, but all entry points in `RainterpreterParser.sol` use the modifier. + +**Severity:** INFO + +**Recommendation:** No change needed. The design is sound -- the post-hoc check catches any overflow and reverts the entire transaction, preventing corrupted output from being returned. The NatSpec correctly documents the invariant dependency. + +--- + +## Summary + +No LOW or higher findings were identified in `LibParse.sol`. The code demonstrates careful attention to: + +1. **Paren tracking bounds:** The `ParenOverflow` check at line 341 correctly prevents the phantom write in `pushOpToSource` from corrupting `lineTracker`. The boundary of 59 (rejecting 60+) accounts for the +4 phantom write offset. + +2. **Cursor advancement:** All cursor advances are bounded by `end`. The `while (cursor < end)` loops in `parseLHS`, `parseRHS`, and `parse` prevent processing past the data boundary. The `parseWord` function correctly limits iteration via `iEnd = min(remaining, 0x20)`. + +3. **Memory safety:** Assembly blocks are correctly marked `memory-safe`. The `mload` operations that read past data boundaries are from Solidity heap memory and produce either correct no-ops or clean reverts. + +4. **Sub-parser bytecode construction:** The allocation, header population, word copy, and operand values copy are correctly sequenced with non-overlapping memory regions. + +5. **Right paren handling:** The paren offset decrement, input counter write-back, and pointer dereference are all correct for the valid range of paren offsets (0 to 57 in multiples of 3). diff --git a/audit/2026-03-01-01/pass1/LibParseLiteral.md b/audit/2026-03-01-01/pass1/LibParseLiteral.md new file mode 100644 index 000000000..101751538 --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibParseLiteral.md @@ -0,0 +1,281 @@ +# Pass 1 (Security) -- Literal Parsing Libraries + +**Audit date:** 2026-03-01 +**Files:** +- `src/lib/parse/literal/LibParseLiteral.sol` (A33) +- `src/lib/parse/literal/LibParseLiteralDecimal.sol` (A34) +- `src/lib/parse/literal/LibParseLiteralHex.sol` (A35) +- `src/lib/parse/literal/LibParseLiteralString.sol` (A37) +- `src/lib/parse/literal/LibParseLiteralSubParseable.sol` (A38) + +--- + +## Evidence of Thorough Reading + +### LibParseLiteral.sol (A33) + +**Library:** `LibParseLiteral` (line 23) + +**Constants:** + +| Constant | Line | Value | +|---|---|---| +| `LITERAL_PARSERS_LENGTH` | 16 | 4 | +| `LITERAL_PARSER_INDEX_HEX` | 18 | 0 | +| `LITERAL_PARSER_INDEX_DECIMAL` | 19 | 1 | +| `LITERAL_PARSER_INDEX_STRING` | 20 | 2 | +| `LITERAL_PARSER_INDEX_SUB_PARSE` | 21 | 3 | + +**Functions:** + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `selectLiteralParserByIndex` | 33 | internal | pure | +| `parseLiteral` | 55 | internal | view | +| `tryParseLiteral` | 77 | internal | view | + +**Imports:** `CMASK_STRING_LITERAL_HEAD`, `CMASK_LITERAL_HEX_DISPATCH`, `CMASK_NUMERIC_LITERAL_HEAD`, `CMASK_SUB_PARSEABLE_LITERAL_HEAD` from `rain.string`; `UnsupportedLiteralType` from `ErrParse.sol`; `ParseState`; `LibParseError`. + +**Assembly blocks:** Lines 42-44 (selectLiteralParserByIndex, reads function pointer from packed bytes), lines 86-89 (tryParseLiteral, reads head byte), lines 96-98 (tryParseLiteral, reads disambiguate byte). All marked `memory-safe`, all read-only. + +**Dispatch logic verified:** Head character mask check determines literal type. Hex dispatch requires `(head | disambiguate) == CMASK_LITERAL_HEX_DISPATCH` which is `CMASK_ZERO | CMASK_LOWER_X`, meaning only `0x` (lowercase) routes to hex. Other numeric heads route to decimal. `"` routes to string. `[` routes to sub-parseable. All other characters return `(false, cursor, 0)`. + +--- + +### LibParseLiteralDecimal.sol (A34) + +**Library:** `LibParseLiteralDecimal` (line 10) + +**Functions:** + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `parseDecimalFloatPacked` | 20 | internal | pure | + +**Imports:** `ParseState`, `LibParseError`, `LibParseDecimalFloat`, `Float`, `LibDecimalFloat`. + +**Using-for:** `LibParseError for ParseState` (line 11). + +**No assembly blocks.** No `unchecked` blocks. Single function that delegates to `LibParseDecimalFloat.parseDecimalFloatInline` and `LibDecimalFloat.packLossless`. Error propagation via `handleErrorSelector`. + +**Delegation chain verified:** `parseDecimalFloatInline` returns `(errorSelector, cursor, signedCoefficient, exponent)`. If `errorSelector != 0`, `handleErrorSelector` reverts with the selector and offset. Otherwise `packLossless` packs the value, reverting with `CoefficientOverflow` if lossy. + +--- + +### LibParseLiteralHex.sol (A35) + +**Library:** `LibParseLiteralHex` (line 20) + +**Functions:** + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `boundHex` | 31 | internal | pure | +| `parseHex` | 63 | internal | pure | + +**Imports:** `ParseState`; errors `MalformedHexLiteral`, `OddLengthHexLiteral`, `ZeroLengthHexLiteral`, `HexLiteralOverflow` from `ErrParse.sol`; masks `CMASK_UPPER_ALPHA_A_F`, `CMASK_LOWER_ALPHA_A_F`, `CMASK_NUMERIC_0_9`, `CMASK_HEX` from `rain.string`; `LibParseError`. + +**Assembly blocks:** Lines 40-45 (boundHex, forward scan with hex mask, read-only), lines 84-86 (parseHex, reads hex char byte, read-only). Both marked `memory-safe`. + +**Unchecked block:** Lines 64-121, entire `parseHex` body. Verified arithmetic safety: +- `hexEnd - hexStart`: safe because `hexEnd >= hexStart` (boundHex only increments). +- `hexEnd - 1`: safe because `hexLength >= 2` at this point. +- `cursor--` at line 115: when `cursor == hexStart`, decrements to `hexStart - 1`. Since `hexStart` is a memory pointer (always >> 0), `hexStart - 1 < hexStart`, so loop condition `cursor >= hexStart` becomes false. No underflow to `type(uint256).max`. +- `valueOffset += 4`: max 64 iterations * 4 = 256, but shift at line 113 uses `valueOffset` before increment, so max shift is 252. Correct. + +**Overflow check verified:** `hexLength > 0x40` (line 71) correctly limits to 64 hex digits = 32 bytes = `uint256` max. + +**Backward loop verified:** Iterates from `hexEnd - 1` down to `hexStart` inclusive, processing `hexLength` nybbles. Each nybble is shifted into `value` at the correct bit offset. Produces right-aligned value in `bytes32`, consistent with `uint256` encoding. + +--- + +### LibParseLiteralString.sol (A37) + +**Library:** `LibParseLiteralString` (line 13) + +**Functions:** + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `boundString` | 26 | internal | pure | +| `parseString` | 88 | internal | pure | + +**Imports:** `ParseState`; `IntOrAString`, `LibIntOrAString` from `rain.intorastring`; `UnclosedStringLiteral`, `StringTooLong` from `ErrParse.sol`; `CMASK_STRING_LITERAL_END`, `CMASK_STRING_LITERAL_TAIL` from `rain.string`; `LibParseError`. + +**Assembly blocks:** Lines 39-51 (boundString, scans string characters), lines 58-59 (boundString, reads final char), lines 100-105 (parseString, fabricates string memory layout), lines 107-109 (parseString, restores memory). All marked `memory-safe`. + +**Unchecked block:** Lines 31-74, entire `boundString` body. Key analysis: +- `cursor + 1` (line 32): cursor is a memory pointer, cannot overflow. +- `sub(end, innerStart)` in assembly (line 40): if `innerStart > end`, this underflows in EVM to a very large number. But `max` is clamped via `if lt(distanceFromEnd, 0x20) { max := distanceFromEnd }`. When underflow produces a huge value, `lt(huge, 0x20)` is false, so `max = 0x20`. However, this case is unreachable because the caller guarantees `cursor < end` (literal head character was already checked), so `innerStart = cursor + 1 <= end`. +- When `innerStart == end`, `distanceFromEnd = 0`, `max = 0`, loop doesn't execute, `i = 0`, `innerEnd = end`, `end == innerEnd` triggers `UnclosedStringLiteral`. Correct. + +**Memory mutation in `parseString` verified:** Saves word at `str = stringStart - 0x20`, writes length, calls `fromStringV3` (which only uses scratch space 0x00-0x3f), then restores. No reentrancy possible (pure function). Correctly bracketed. + +**String length limit:** Max 31 bytes (loop runs at most 0x1F iterations before hitting a terminator; reaching 0x20 reverts with `StringTooLong`). Consistent with `IntOrAString` 5-bit length field. + +--- + +### LibParseLiteralSubParseable.sol (A38) + +**Library:** `LibParseLiteralSubParseable` (line 14) + +**Functions:** + +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `parseSubParseable` | 35 | internal | view | + +**Imports:** `ParseState`; `LibParse`; `UnclosedSubParseableLiteral`, `SubParseableMissingDispatch` from `ErrParse.sol`; `CMASK_WHITESPACE`, `CMASK_SUB_PARSEABLE_LITERAL_END` from `rain.string`; `LibParseInterstitial`; `LibParseError`; `LibSubParse`; `LibParseChar`. + +**Using-for:** `LibParse for ParseState` (line 15, unused in this file), `LibParseInterstitial for ParseState` (line 16), `LibParseError for ParseState` (line 17), `LibSubParse for ParseState` (line 18). + +**Assembly block:** Lines 73-76 (reads final char for bracket check). Marked `memory-safe`, read-only. + +**Unchecked block:** Lines 40-86, entire function body. Key analysis: +- `++cursor` (line 44): cursor is a memory pointer, cannot overflow. +- `++cursor` (line 83): same. +- Guard at line 68 (`cursor >= end`) correctly prevents out-of-bounds read before the assembly block at line 73-76. This was a prior audit finding (A38-1) that has been fixed. + +**Dispatch/body parsing verified:** +1. Skip opening `[` (line 44). +2. Scan non-whitespace, non-`]` chars for dispatch (line 49). If empty, revert `SubParseableMissingDispatch` (line 53). +3. Skip whitespace (line 57). +4. Scan non-`]` chars for body (line 65). +5. If `cursor >= end`, revert `UnclosedSubParseableLiteral` (lines 68-69). +6. Verify char at cursor is `]` (lines 72-79). +7. Skip closing `]` (line 83). +8. Delegate to `subParseLiteral` (line 85). + +--- + +## Security Findings + +### A35-1: Dead code -- `MalformedHexLiteral` revert is unreachable + +**Severity:** LOW + +**Location:** `src/lib/parse/literal/LibParseLiteralHex.sol`, line 110 + +**Description:** + +The `parseHex` function calls `boundHex` internally (line 68) to determine the range `[hexStart, hexEnd)` of hex digits. `boundHex` scans forward from `cursor + 2` using `CMASK_HEX` (which is `CMASK_NUMERIC_0_9 | CMASK_LOWER_ALPHA_A_F | CMASK_UPPER_ALPHA_A_F`), stopping at the first non-hex character. This guarantees every byte between `hexStart` and `hexEnd` matches `CMASK_HEX`. + +The backward parsing loop in `parseHex` (lines 82-116) then checks each character against `CMASK_NUMERIC_0_9`, `CMASK_LOWER_ALPHA_A_F`, and `CMASK_UPPER_ALPHA_A_F` individually. Since these three masks are the exact components of `CMASK_HEX`, every character validated by `boundHex` will match one of the three branches. The `else` branch at line 109-111 that reverts with `MalformedHexLiteral` can never execute. + +Dead code is a maintenance concern: it provides a false sense of coverage (the revert path appears tested-for but cannot actually be triggered), and it adds bytecode size without providing any runtime benefit. More importantly, if a future change alters `boundHex` to accept different characters, the dead code might create a false assumption that `parseHex` independently validates characters, when in fact the validation is redundant under current invariants. + +**Recommendation:** Either: +(a) Remove the `else` branch and `MalformedHexLiteral` import, since `boundHex` guarantees only hex characters are in the range, or +(b) Add a comment documenting that the branch is a defensive guard that is currently unreachable due to the `boundHex` invariant. + +--- + +### A38-2: Unused `using LibParse for ParseState` declaration + +**Severity:** INFO + +**Location:** `src/lib/parse/literal/LibParseLiteralSubParseable.sol`, line 15 + +**Description:** + +The `using LibParse for ParseState` declaration is present but no function from `LibParse` is called anywhere in this file. This was noted in the prior audit. The unused declaration adds a small amount of compilation overhead and reduces readability by suggesting a dependency that does not exist. + +--- + +### A33-1: No bounds check in `selectLiteralParserByIndex` + +**Severity:** INFO + +**Location:** `src/lib/parse/literal/LibParseLiteral.sol`, lines 33-46 + +**Description:** + +The function reads a 2-byte function pointer from the `literalParsers` array at position `2 + index * 2` without bounds checking. The comment documents this as intentional. All call sites (line 122) pass an index from `tryParseLiteral`'s dispatch logic, which only produces values 0-3. For a `literalParsers` array with `LITERAL_PARSERS_LENGTH * 2 = 8` bytes of data, index 3 reads at offset `literalParsers + 8`, which `mload` loads bytes [8..39] and masks to the lowest 16 bits (bytes [38..39] -- but actually `mload(literalParsers + 8)` reads bytes at memory positions `literalParsers+8` through `literalParsers+39`; the lowest 16 bits correspond to positions `literalParsers+38` and `literalParsers+39`, which are the data bytes at index [6..7], i.e., the 4th function pointer). This is correct. + +No external caller can invoke this function (it is `internal`). The risk is limited to future code changes that might pass an out-of-range index. + +--- + +### A37-1: `mload` reads past logical end of data in `boundString` + +**Severity:** INFO + +**Location:** `src/lib/parse/literal/LibParseLiteralString.sol`, lines 58-59 + +**Description:** + +When the string scanning loop terminates at `innerEnd == end` (no closing quote found before end of data), `mload(innerEnd)` reads 32 bytes starting from `end`, which extends past the logical end of the parse data. The read is safe because: +1. EVM `mload` at any valid memory address is safe (memory is zero-initialized beyond the free memory pointer). +2. The `end == innerEnd` guard on line 66 catches this case and reverts with `UnclosedStringLiteral`, so the read result is never used to make a correctness-affecting decision. +3. Even if the byte at `end` happens to be `"` (0x22), the `end == innerEnd` condition still triggers the revert. + +No action required. + +--- + +### A34-1: Security depends on `rain.math.float` library + +**Severity:** INFO + +**Location:** `src/lib/parse/literal/LibParseLiteralDecimal.sol`, lines 25-28 + +**Description:** + +`parseDecimalFloatPacked` is a thin wrapper that delegates all parsing logic to `LibParseDecimalFloat.parseDecimalFloatInline` and all packing logic to `LibDecimalFloat.packLossless`, both from the `rain.math.float` submodule. The wrapper correctly propagates all error selectors via `handleErrorSelector` and does not suppress any return values. Any parsing or precision bugs would originate in the submodule, not in this file. + +The error types propagated include: `ParseEmptyDecimalString`, `MalformedDecimalPoint`, `MalformedExponentDigits`, `ParseDecimalPrecisionLoss`, and `CoefficientOverflow`. All are custom errors. + +No action required at this layer. + +--- + +### A35-2: Assembly blocks correctly annotated `memory-safe` + +**Severity:** INFO + +**Location:** `src/lib/parse/literal/LibParseLiteralHex.sol`, lines 40 and 84 + +**Description:** + +Both assembly blocks perform only `mload` (read) operations. Neither writes to memory. The `memory-safe` annotations are accurate. + +--- + +### A33-2: All assembly blocks correctly annotated `memory-safe` + +**Severity:** INFO + +**Location:** `src/lib/parse/literal/LibParseLiteral.sol`, lines 42, 86, 96 + +**Description:** + +All three assembly blocks perform read-only operations (`mload`, `byte`, `shl`). None write to memory. Annotations are accurate. + +--- + +### A37-2: Temporary memory mutation correctly bracketed + +**Severity:** INFO + +**Location:** `src/lib/parse/literal/LibParseLiteralString.sol`, lines 100-109 + +**Description:** + +`parseString` temporarily writes a length prefix at `str = stringStart - 0x20` to fabricate a `string memory`. The original word is saved to `memSnapshot` before writing and restored immediately after `fromStringV3` returns. `fromStringV3` only writes to scratch space (addresses 0x00-0x3f) via `mstore(0, ...)` and `mcopy(sub(0x20, ...), ...)`. The function is `pure`, so no reentrancy is possible. The save-restore bracket is correct. + +--- + +## Summary + +| File | Severity | Count | +|---|---|---| +| LibParseLiteral.sol (A33) | INFO | 2 | +| LibParseLiteralDecimal.sol (A34) | INFO | 1 | +| LibParseLiteralHex.sol (A35) | LOW | 1 | +| LibParseLiteralHex.sol (A35) | INFO | 1 | +| LibParseLiteralString.sol (A37) | INFO | 2 | +| LibParseLiteralSubParseable.sol (A38) | INFO | 1 | + +**Total:** 1 LOW, 7 INFO. No CRITICAL, HIGH, or MEDIUM findings. + +The literal parsing subsystem is well-structured with proper bounds checking, correct use of custom errors, safe assembly annotations, and sound arithmetic. The single LOW finding (dead code in the hex parser) is a code quality issue rather than a correctness or exploitability concern. The prior audit's A38-1 finding (out-of-bounds read in sub-parseable parser) has been fixed with the `cursor >= end` guard at line 68. diff --git a/audit/2026-03-01-01/pass1/LibParseState.md b/audit/2026-03-01-01/pass1/LibParseState.md new file mode 100644 index 000000000..ffafeab32 --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibParseState.md @@ -0,0 +1,144 @@ +# Pass 1 Audit: LibParseState.sol + +**Agent**: A43 +**File**: `src/lib/parse/LibParseState.sol` +**Commit**: `79903569` + +## Evidence of Thorough Reading + +### Library Name +- `LibParseState` (line 185) + +### Struct / Type Definitions +- `ParseState` (line 155) -- 18-field struct holding all parser state + +### Constants Defined +| Constant | Line | Value | +|---|---|---| +| `EMPTY_ACTIVE_SOURCE` | 31 | `0x20` | +| `FSM_YANG_MASK` | 35 | `1` | +| `FSM_WORD_END_MASK` | 38 | `1 << 1` | +| `FSM_ACCEPTING_INPUTS_MASK` | 41 | `1 << 2` | +| `FSM_ACTIVE_SOURCE_MASK` | 45 | `1 << 3` | +| `FSM_DEFAULT` | 51 | `FSM_ACCEPTING_INPUTS_MASK` | +| `OPERAND_VALUES_LENGTH` | 62 | `4` | +| `PARSE_STATE_TOP_LEVEL0_OFFSET` | 66 | `0x20` | +| `PARSE_STATE_TOP_LEVEL0_DATA_OFFSET` | 70 | `0x21` | +| `PARSE_STATE_PAREN_TRACKER0_OFFSET` | 74 | `0x60` | +| `PARSE_STATE_LINE_TRACKER_OFFSET` | 78 | `0xa0` | + +### Errors (imported from ErrParse.sol) +`DanglingSource`, `MaxSources`, `ParseMemoryOverflow`, `ParseStackOverflow`, +`UnclosedLeftParen`, `ExcessRHSItems`, `ExcessLHSItems`, `NotAcceptingInputs`, +`UnsupportedLiteralType`, `InvalidSubParser`, `OpcodeIOOverflow`, +`SourceItemOpsOverflow`, `ParenInputOverflow`, `LineRHSItemsOverflow` + +### Functions (all `internal pure` unless noted) +| Function | Line | Visibility | Mutability | +|---|---|---|---| +| `newActiveSourcePointer` | 201 | internal | pure | +| `resetSource` | 222 | internal | pure | +| `newState` | 248 | internal | pure | +| `pushSubParser` | 309 | internal | pure | +| `exportSubParsers` | 329 | internal | pure | +| `snapshotSourceHeadToLineTracker` | 358 | internal | pure | +| `endLine` | 393 | internal | pure | +| `highwater` | 519 | internal | pure | +| `constantValueBloom` | 547 | internal | pure | +| `pushConstantValue` | 555 | internal | pure | +| `pushLiteral` | 585 | internal | view | +| `pushOpToSource` | 660 | internal | pure | +| `endSource` | 767 | internal | pure | +| `buildBytecode` | 900 | internal | pure | +| `buildConstants` | 994 | internal | pure | +| `checkParseMemoryOverflow` | 1044 | internal | pure | + +--- + +## Findings + +### A43-1: Source prefix ops-count byte overflow when total ops exceed 255 (MEDIUM) + +**Location**: `endSource()`, lines 871-878 + +**Description**: The source prefix written by `endSource` encodes the opcodes count in a single byte (bits 24-31 of a 4-byte prefix): + +```solidity +let prefixWritePointer := add(source, 4) +mstore( + prefixWritePointer, + or( + and(mload(prefixWritePointer), not(0xFFFFFFFF)), + or(shl(0x18, sub(div(length, 4), 1)), stackTracker) + ) +) +``` + +The value `sub(div(length, 4), 1)` is the total number of ops across ALL top-level items in the source. This value is shifted left by 24 bits (`shl(0x18, ...)`) and OR'd into the result. However, if the total ops count exceeds 255 (0xFF), the shifted value has bits set above bit 31. + +The `mstore` writes 32 bytes to `prefixWritePointer = source + 4`. The low 32 bits of the written word correspond to memory addresses `source + 0x20` through `source + 0x23` (the 4-byte source prefix). Bits above 31 correspond to earlier memory addresses overlapping `source + 0x04` through `source + 0x1F`, which is the interior of the source's length word (previously written by `mstore(source, length)` at line 868). The mask `and(mload(prefixWritePointer), not(0xFFFFFFFF))` preserves these bytes, but the `or` with the overflowed shift value corrupts them, writing non-zero bits into the length word. + +This causes two distinct problems: +1. The source's `bytes` length is corrupted, causing `buildBytecode` to compute incorrect total bytecode sizes +2. `LibBytecode.sourceOpsCount` reads only `byte(0, mload(pointer))` (1 byte), so the ops count is silently truncated to its low 8 bits, causing the integrity checker and eval loop to process only a fraction of the source + +**Reachability**: The per-item ops counter overflows at 0xFF (255 ops), checked by `SourceItemOpsOverflow`. But the TOTAL ops across all items in a source has no separate check. With up to 62 top-level items (bounded by `ParseStackOverflow` at `newStackRHSOffset >= 0x3f`) and up to 255 ops per item, the total can reach 62 * 255 = 15,810. Even modestly, 2 top-level items with 128 ops each = 256 total ops would trigger this. + +The `checkParseMemoryOverflow` check bounds total memory to 0x10000, but this allows thousands of ops within that budget (each op consumes roughly 4.6 bytes of memory in the linked-list structure). + +**Impact**: Corrupted source length word and truncated ops count in the source prefix. The corrupted length causes `buildBytecode` to produce malformed bytecode. The truncated ops count causes the integrity checker to examine too few ops and the evaluation engine to execute an incomplete source. + +**Severity**: MEDIUM -- requires a source with more than 255 total ops across all top-level items (achievable with moderately complex expressions, e.g. 2 items with 128 nested ops each), but `checkParseMemoryOverflow` limits the practical magnitude. + +--- + +### A43-2: `newActiveSourcePointer(0)` writes to scratch space at memory address 0 (INFO) + +**Location**: `newActiveSourcePointer()`, line 213; called from `resetSource()` at line 223 + +**Description**: When `resetSource` calls `newActiveSourcePointer(0)`, line 213 executes: + +```solidity +mstore(oldActiveSourcePointer, or(and(mload(oldActiveSourcePointer), not(0xFFFF)), activeSourcePtr)) +``` + +With `oldActiveSourcePointer = 0`, this writes to memory address 0, which is Solidity's scratch space. The written value combines whatever was in scratch space with the new pointer. + +**Analysis**: This is benign. The node at address 0 is never traversed by the linked list -- `endSource` walks backward via `shr(0x10, mload(...))` and stops when the tail pointer is 0. The initial slot (returned by `newActiveSourcePointer(0)`) has a tail pointer of 0 (from `shl(0x10, 0)`), so the traversal correctly terminates without following the scratch space write. No subsequent code depends on the value at address 0 being preserved. + +**Severity**: INFO -- functionally harmless side effect, but worth documenting. A future refactor could skip the write when `oldActiveSourcePointer == 0`. + +--- + +### A43-3: `buildConstants` loop body comment references "fingerprint" that does not exist (INFO) + +**Location**: `buildConstants()`, lines 1020-1023 + +**Description**: The comment reads: + +```solidity +// tail pointer in tail keys is the low 16 bits under the +// fingerprint, which is different from the tail pointer in +// the constants builder, where it sits above the constants +// height. +``` + +The word "fingerprint" is misleading. In `pushConstantValue` (lines 559-568), the first word of each constant entry stores the raw tail pointer, and the second word stores the constant value. There is no fingerprint stored in the linked-list nodes. The comment may be a remnant from an earlier design or a different data structure. + +**Impact**: None -- the code is correct. The comment may confuse future auditors or maintainers. + +**Severity**: INFO + +--- + +### A43-4: `checkParseMemoryOverflow` is a post-condition check, not a pre-condition guard (INFO) + +**Location**: `checkParseMemoryOverflow()`, lines 1044-1052; `RainterpreterParser.sol`, lines 46-49 + +**Description**: The `checkParseMemoryOverflow` function runs AFTER the entire parse operation completes (via a modifier in `RainterpreterParser`). All linked-list pointer truncation, if any occurred during parsing, would have already happened by the time this check runs. The check works because if it reverts, the entire transaction is rolled back, so no truncated state persists. + +However, this means that during parsing itself, code executes with potentially-truncated pointers for some operations before the revert. If any parsing code has a conditional branch that depends on a truncated pointer (e.g., a bloom filter check or a dedup lookup), it could take the wrong branch. Since the transaction reverts regardless, the consequence is limited to a potentially misleading revert reason (e.g., reverting with `InvalidSubParser` instead of `ParseMemoryOverflow` if the pointer truncation caused a different error to fire first). + +**Impact**: At worst, a confusing revert reason. No persistent state corruption. + +**Severity**: INFO -- the transaction-level atomicity makes this safe. diff --git a/audit/2026-03-01-01/pass1/LibParseUtilities.md b/audit/2026-03-01-01/pass1/LibParseUtilities.md new file mode 100644 index 000000000..a19b6ba19 --- /dev/null +++ b/audit/2026-03-01-01/pass1/LibParseUtilities.md @@ -0,0 +1,116 @@ +# Pass 1 Audit: Parse Utility Libraries + +Audited files: +- `src/lib/parse/LibParseOperand.sol` (A39) +- `src/lib/parse/LibParseInterstitial.sol` (A32) +- `src/lib/parse/LibParsePragma.sol` (A40) +- `src/lib/parse/LibParseStackName.sol` (A41) +- `src/lib/parse/LibParseStackTracker.sol` (A42) +- `src/lib/parse/LibSubParse.sol` (A44) +- `src/lib/parse/LibParseError.sol` (A31) + +## Evidence of Thorough Reading + +### LibParseOperand.sol (A39, 348 lines) +- `parseOperand`: reads char via `shl(byte(0, mload(cursor)), 1)` bitmask pattern; resets `operandValues` length to 0 via assembly; loops parsing literals between `<` and `>` delimiters; checks `OPERAND_VALUES_LENGTH` (4) bound on `i`; writes values via `mstore(add(operandValues, add(0x20, mul(i, 0x20))), value)` bypassing Solidity bounds checks against current length; toggles FSM_YANG_MASK for whitespace-separated literal sequences. +- `handleOperand`: reads 2-byte function pointer from `operandHandlers` at `add(handlers, add(2, mul(wordIndex, 2)))`, no bounds check (documented as intentional -- parser-internal index). +- `handleOperandDisallowed`, `handleOperandDisallowedAlwaysOne`: revert on non-empty values. +- `handleOperandSingleFull`, `handleOperandSingleFullNoDefault`: Float-to-integer conversion via `toFixedDecimalLossless`; uint16 range check. +- `handleOperandDoublePerByteNoDefault`: two uint8 values packed `a | (b << 8)`. +- `handleOperand8M1M1`: uint8 + two 1-bit flags packed `a | (b << 8) | (c << 9)`. +- `handleOperandM1M1`: two optional 1-bit flags packed `a | (b << 1)`. + +### LibParseInterstitial.sol (A32, 128 lines) +- `skipComment`: checks `cursor + 4 > end`; validates `/*` start sequence via `shr(0xf0, mload(cursor))`; skips to cursor+3; scans for `*/` by checking `byte(0, mload(cursor)) == CMASK_COMMENT_END_SEQUENCE_END` (which is `/`, value 0x2F); on match, reads 2-byte sequence at `cursor-1` via `shr(0xf0, mload(sub(cursor, 1)))` and compares to `COMMENT_END_SEQUENCE`; sets FSM_YANG_MASK. +- `skipWhitespace`: clears FSM_YANG_MASK; delegates to `LibParseChar.skipMask`. +- `parseInterstitial`: loops skipping whitespace and comments until non-interstitial char. + +### LibParsePragma.sol (A40, 92 lines) +- Constants: `PRAGMA_KEYWORD_BYTES = "using-words-from"` (16 bytes); mask zeroes low `(32-16)*8 = 128` bits. +- `parsePragma`: loads 32 bytes at cursor; masks and compares to keyword; requires at least one whitespace char after keyword; loops calling `parseInterstitial` then `tryParseLiteral`; calls `pushSubParser` for each address. + +### LibParseStackName.sol (A41, 89 lines) +- Node packing: bits [255:32] = 224-bit fingerprint, bits [23:16] = stack index, bits [15:0] = next-node pointer. +- `pushStackName`: hashes word via `keccak256(0, 0x20)` after `mstore(0, word)`; computes `fingerprint := and(hash, not(0xFFFFFFFF))` (top 224 bits, bottom 32 zeroed); allocates node at free pointer; stores `fingerprint | (stackLHSIndex << 0x10) | ptr`. +- `stackNameIndex`: computes `fingerprint := shr(0x20, hash)` (top 224 bits shifted to [223:0]); bloom key = `and(fingerprint, 0xFF)` (bits [39:32] of original hash); bloom = `shl(bloomKey, 1)`; on bloom hit, walks linked list comparing `eq(fingerprint, shr(0x20, stackNames))`; extracts index as `and(shr(0x10, stackNames), 0xFFFF)`. +- Bloom filter always updated (line 87) even on miss, ensuring future lookups hit. +- Fingerprint comparison verified consistent between push and lookup (both derive same 224-bit value, just stored/compared differently). + +### LibParseStackTracker.sol (A42, 77 lines) +- Type: `ParseStackTracker` is `uint256` with packing: bits [7:0] = current height, bits [15:8] = inputs, bits [255:16] = high watermark (max). +- `pushInputs`: calls `push(n)` then adds `n` to inputs byte; checks `inputs > 0xFF`. +- `push`: extracts current, inputs, max; `current += n` unchecked; checks `current > 0xFF`; updates max if exceeded; repacks. +- `pop`: extracts current; checks `current < n`; subtracts `n` directly from packed word (safe because `n <= current <= 0xFF` prevents borrow into higher bytes). + +### LibSubParse.sol (A44, 450 lines) +- `subParserContext`: bounds-checks column/row to uint8; unaligned 4-byte bytecode allocation; individual `mstore8` for each byte; empty constants array. +- `subParserConstant`: bounds-checks `constantsHeight` to uint16; writes constants height via `mstore(add(bytecode, 4), constantsHeight)` (32-byte write that zeroes IO/opcode area, then individual `mstore8` for IO byte and opcode); single-element constants array. +- `subParserExtern`: bounds-checks `constantsHeight`; builds extern dispatch via `LibExtern.encodeExternCall`; same bytecode construction pattern. +- `subParseWordSlice`: iterates bytecode 4 bytes at a time; for `OPCODE_UNKNOWN` (0xFF), walks sub-parser linked list; extracts operand as data pointer (`and(shr(0xe0, memoryAtCursor), 0xFFFF)`); writes 3-byte header (constants height + IO byte) into data; calls `subParseWord2`; validates result length == 4; copies result over unknown op; pushes sub-constants. If no sub-parser succeeds, constructs error string from subparse data and reverts `UnknownWord`. +- `subParseWords`: iterates all sources via `LibBytecode`; calls `subParseWordSlice` for each. +- `subParseLiteral`: builds data with 2-byte dispatch length prefix; copies dispatch and body; walks sub-parser list calling `subParseLiteral2`; reverts `UnsupportedLiteralType` if none succeed. +- `consumeSubParseWordInputData`: extracts constants height (16-bit at data+2), IO byte (8-bit at data+3), string length (16-bit at data+5); advances data pointer past header; creates new `ParseState`. +- `consumeSubParseLiteralInputData`: extracts dispatch length and computes pointers. + +### LibParseError.sol (A31, 36 lines) +- `parseErrorOffset`: computes `cursor - (data + 0x20)` via assembly. +- `handleErrorSelector`: if selector non-zero, packs selector + offset into scratch and reverts with 36 bytes. + +## Findings + +### A44-1: Unaligned Free Memory Pointer in `subParseLiteral` (LOW) + +**File:** `src/lib/parse/LibSubParse.sol`, line 367 + +**Description:** The `subParseLiteral` function allocates a `bytes memory` region for the sub-parser payload with: +```solidity +mstore(0x40, add(data, add(dataLength, 0x20))) +``` + +`dataLength = 2 + dispatchLength + bodyLength`, which can be any value. The free memory pointer is advanced to `data + 0x20 + dataLength`, which is not guaranteed to be 32-byte aligned. Subsequent Solidity-generated code (including the ABI encoding for the `subParser.subParseLiteral2(data)` external call) expects the free memory pointer to be 32-byte aligned. + +**Impact:** In practice, the Solidity ABI encoder tolerates unaligned free memory pointers in current compiler versions (0.8.25). However, this violates the Solidity memory model's alignment invariant. A future compiler version or optimizer pass that relies on this invariant could produce incorrect ABI encoding, causing malformed external calls to sub-parsers. + +Additionally, the same pattern exists in `subParserContext`, `subParserConstant`, and `subParserExtern` (lines 65, 114, 185) where `add(bytecode, 0x24)` is not 32-byte aligned. These are documented as intentionally unaligned and the bytecode is only used via direct memory copy (not passed to Solidity ABI encoding), so they are less concerning. However, `subParseLiteral` does pass the unaligned allocation to an ABI-encoded external call. + +**Severity:** LOW -- no known exploit path in solc 0.8.25; the risk is future compiler incompatibility. The `checkParseMemoryOverflow` modifier would catch gross memory corruption but not misalignment. + +### A42-1: `pop` Does Not Validate `n` Upper Bound (INFORMATIONAL) + +**File:** `src/lib/parse/LibParseStackTracker.sol`, line 68 + +**Description:** The `pop` function accepts `uint256 n` and checks `current < n` to prevent underflow. If `n > 0xFF`, the check correctly reverts (since `current <= 0xFF`). However, unlike `push` which documents "MUST be <= 0xFF" for `n`, `pop`'s NatSpec does not impose an upper bound on `n`. The NatSpec comment at lines 60-63 explains why the direct subtraction shortcut is safe ("n <= current <= 0xFF") but this is an invariant that follows from the underflow check, not a precondition on `n`. + +No bug here -- the code is correct. The NatSpec could be clearer that `n` has no precondition requirement because the underflow check implicitly constrains it to `<= current <= 0xFF`. + +**Severity:** INFORMATIONAL -- documentation clarity only. + +### A41-1: Stack Name Fingerprint Collision Causes Silent Misresolution (INFORMATIONAL) + +**File:** `src/lib/parse/LibParseStackName.sol`, lines 31-52 + +**Description:** Stack name identity is determined by a 224-bit fingerprint derived from `keccak256(word)`. If two distinct LHS names produce the same 224-bit fingerprint, `pushStackName` would find the first name via `stackNameIndex`, return `exists = true`, and skip allocating a new node. The second name would silently resolve to the first name's stack index. + +A 224-bit collision requires ~2^112 attempts (birthday bound), which is computationally infeasible. The bloom filter adds no additional collision risk beyond what the fingerprint already has -- bloom false positives just trigger a linked-list traversal that still compares the full 224-bit fingerprint. + +**Severity:** INFORMATIONAL -- astronomically unlikely; standard hash-based identity approach. + +### A44-2: Sub-Parser Header Mutation Persists Across Iterations (INFORMATIONAL) + +**File:** `src/lib/parse/LibSubParse.sol`, lines 243-259 + +**Description:** In `subParseWordSlice`, the header bytes (constants height + IO byte) are written into the `data` region pointed to by the unknown opcode's operand. This `data` region is shared across all sub-parser attempts in the while loop (line 224). The header is written before each `subParseWord2` call, but since `constantsBuilder` and `memoryAtCursor` do not change between iterations, the re-write is idempotent. + +If a sub-parser implementation were to mutate the `data` bytes it receives (which is allowed since it's an external call that copies the data into its own memory), it would not affect the next sub-parser's view because external calls use calldata copies. But if a sub-parser were called via `delegatecall` instead, it could corrupt the shared data. Currently only `call` is used (via the interface's `external` function), so this is not an issue. + +**Severity:** INFORMATIONAL -- no current exploit path; the external call boundary provides isolation. + +### A32-1: Comment End Detection Scans Single Byte Before Checking Sequence (INFORMATIONAL) + +**File:** `src/lib/parse/LibParseInterstitial.sol`, lines 63-76 + +**Description:** The comment end detection first checks if the current byte equals `CMASK_COMMENT_END_SEQUENCE_END` (which is `0x2F`, the `/` character -- the low byte of `*/`). On match, it then loads the 2-byte sequence at `cursor - 1` to verify the full `*/`. The `mload(sub(cursor, 1))` reads from `cursor - 1`, which is safe because `cursor` starts at `original_cursor + 3` and only increments, so `cursor - 1 >= original_cursor + 2 >= data_start`. + +The single-byte pre-check (`/`) is correct as a fast path. A standalone `/` inside a comment triggers the 2-byte verification but does not falsely close the comment. The sequence `*/` is correctly detected. The cursor advancement (`++cursor` at line 73) moves past the final `/`, which is correct. + +**Severity:** INFORMATIONAL -- no issue found; documenting the analysis for completeness. diff --git a/audit/2026-03-01-01/pass1/Rainterpreter.md b/audit/2026-03-01-01/pass1/Rainterpreter.md new file mode 100644 index 000000000..9e4ad9ab3 --- /dev/null +++ b/audit/2026-03-01-01/pass1/Rainterpreter.md @@ -0,0 +1,139 @@ +# Pass 1 (Security) -- Rainterpreter.sol + +**File:** `src/concrete/Rainterpreter.sol` +**Agent:** A45 +**Date:** 2026-03-01 + +## Evidence of Thorough Reading + +### Contract Name + +`contract Rainterpreter is IInterpreterV4, IOpcodeToolingV1, ERC165` -- line 32 + +### Functions + +| Function | Line | Visibility | +|----------|------|------------| +| `constructor()` | 38 | N/A (constructor) | +| `opcodeFunctionPointers() returns (bytes memory)` | 45 | `internal view virtual` | +| `eval4(EvalV4 calldata eval) returns (StackItem[] memory, bytes32[] memory)` | 50 | `external view virtual override` | +| `supportsInterface(bytes4 interfaceId) returns (bool)` | 73 | `public view virtual override` | +| `buildOpcodeFunctionPointers() returns (bytes memory)` | 78 | `public view virtual override` | + +### Imports + +| Import | Source | Line | +|--------|--------|------| +| `ERC165` | `openzeppelin-contracts/contracts/utils/introspection/ERC165.sol` | 5 | +| `LibMemoryKV`, `MemoryKVKey`, `MemoryKVVal` | `rain.lib.memkv/lib/LibMemoryKV.sol` | 6 | +| `LibEval` | `../lib/eval/LibEval.sol` | 8 | +| `LibInterpreterStateDataContract` | `../lib/state/LibInterpreterStateDataContract.sol` | 9 | +| `InterpreterState` | `../lib/state/LibInterpreterState.sol` | 10 | +| `LibAllStandardOps` | `../lib/op/LibAllStandardOps.sol` | 11 | +| `IInterpreterV4`, `SourceIndexV2`, `EvalV4`, `StackItem` | `rain.interpreter.interface/interface/IInterpreterV4.sol` | 12-17 | +| `BYTECODE_HASH` (as `INTERPRETER_BYTECODE_HASH`), `OPCODE_FUNCTION_POINTERS` | `../generated/Rainterpreter.pointers.sol` | 18-24 | +| `IOpcodeToolingV1` | `rain.sol.codegen/interface/IOpcodeToolingV1.sol` | 25 | +| `OddSetLength` | `../error/ErrStore.sol` | 26 | +| `ZeroFunctionPointers` | `../error/ErrEval.sol` | 27 | + +### Using Declarations + +- `LibEval for InterpreterState` -- line 33 +- `LibInterpreterStateDataContract for bytes` -- line 34 + +### Types/Errors/Constants Defined in This File + +None defined. All types, errors, and constants are imported. + +### Exported Constants + +- `INTERPRETER_BYTECODE_HASH` -- re-exported from generated pointers (line 22) + +--- + +## Security Analysis + +### 1. Function Pointer Dispatch Safety in `eval4` + +`eval4` passes `opcodeFunctionPointers()` to `unsafeDeserialize`, which stores it as `state.fs`. The eval loop in `LibEval.evalLoop` uses `mod(byte(..., word), fsCount)` to index into this table. The modulo ensures all lookups stay within the table's bounds. The constructor guard (`ZeroFunctionPointers`) prevents `fsCount == 0` which would be a division-by-zero. This is covered in detail by the A05 audit of `LibEval.sol`. + +### 2. Can the Eval Loop Jump to Arbitrary Code? + +No. The function pointer table is built at compile time from `OPCODE_FUNCTION_POINTERS` (a `bytes constant`). Each 2-byte entry is an internal function pointer to a known opcode handler. The `mod` operation constrains all opcode bytes to valid indices. A crafted bytecode can only alias to existing opcode handlers (not arbitrary code addresses). The `view` modifier on `eval4` further ensures that even if something unexpected occurred, no state changes persist. + +### 3. Stack Safety + +Stack allocation happens in `unsafeDeserialize` based on the bytecode's declared `stackSize` per source. Stack overflow/underflow protection relies on the integrity check at deploy time (via `RainterpreterExpressionDeployer`). The `eval4` function in `Rainterpreter.sol` itself validates `inputs.length` (via `eval2` -> `InputsLengthMismatch` check). The `view` modifier prevents any persistent damage from a corrupted stack. + +### 4. State Overlay Security + +The `stateOverlay` loop (lines 59-66) validates even-length (`OddSetLength` revert) and applies key-value pairs to the in-memory `stateKV`. Each pair is applied via `LibMemoryKV.set`. The overlay only affects the current `eval` call's ephemeral state; no persistent storage writes occur during `eval4` (which is `view`). The overlay is applied BEFORE the eval loop, so evaluated logic can override overlay values via `set`. + +### 5. Assembly Memory Safety + +`Rainterpreter.sol` itself contains no assembly blocks. All assembly is in the libraries it delegates to (`LibEval`, `LibInterpreterStateDataContract`, `LibInterpreterState`). The `memory-safe` annotations in those libraries have been verified by the A05 audit of `LibEval.sol`. + +--- + +## Security Findings + +### A45-7: `eval4` Does Not Call `checkNoOOBPointers` on Caller-Supplied Bytecode + +**Severity: INFO** + +`eval4` receives `bytecode` in calldata and passes it directly to `unsafeDeserialize` without calling `LibBytecode.checkNoOOBPointers`. The `IInterpreterV4` interface NatSpec (line 112) states implementations "SHOULD" validate bytecode structure, using advisory rather than mandatory language. + +Malformed bytecode with out-of-bounds relative offsets could cause the deserialization to read from arbitrary memory positions beyond the bytecode array. However, the security model explicitly allows this (IInterpreterV4.sol lines 88-99): the interpreter "MAY return garbage or exhibit undefined behaviour or error during an eval, _provided that no state changes are persisted_." Since `eval4` is `view`, the EVM enforces this at the call boundary via `STATICCALL`. + +In practice, bytecode reaches `eval4` through the expression deployer, which runs `checkNoOOBPointers` during the integrity check. Direct callers constructing bytecode manually bear the risk, but the worst case is a reverted or garbage-returning `view` call. + +No action required. + +### A45-8: `stateOverlay` Loop Operates on `calldata` Array Without Gas Bounding + +**Severity: INFO** + +The `stateOverlay` loop at lines 62-66 iterates over the caller-supplied `eval.stateOverlay` array. Each iteration calls `LibMemoryKV.set`, which allocates 3 words (96 bytes) of memory for each new key. A caller could pass a very large `stateOverlay` to cause quadratic memory expansion costs (each allocation moves the free memory pointer, expanding the memory cost quadratically per the EVM memory cost formula). + +This is not exploitable because: +1. `eval4` is `view` -- no state changes persist. +2. The caller pays for their own gas. +3. The gas cost is borne entirely by the transaction sender, who chose to call with that large overlay. + +No action required. + +### A45-9: `opcodeFunctionPointers()` is `virtual` -- Subclass Override Could Bypass Constructor Check + +**Severity: LOW** + +The `opcodeFunctionPointers()` function (line 45) is `virtual`, allowing subclasses to override it. The constructor (line 39) calls this function and reverts if it returns empty bytes. However, a subclass could override `opcodeFunctionPointers()` to return a non-empty value during construction and a different value at runtime (e.g., by reading from mutable storage). + +If the runtime override returned empty bytes, `fsCount` would be 0, and the EVM `MOD` instruction would return 0 for all dispatch lookups. The function pointer read at `fPointersStart + 0` would read 2 bytes from whatever memory follows the empty `fs` bytes array, interpreting it as an internal function pointer. This could cause a jump to an arbitrary internal function. + +**Mitigating factors:** +- This requires a deliberately malicious subclass. The base `Rainterpreter` contract is not affected. +- Any subclass deployed via the standard deployer would have its bytecode hash checked, preventing unauthorized modifications. +- The `view` modifier on `eval4` prevents persistent state damage even in the worst case. + +This is a theoretical concern for downstream integrators who subclass `Rainterpreter` with dynamic `opcodeFunctionPointers()` implementations. + +--- + +## Summary + +`Rainterpreter.sol` is a thin orchestration layer that delegates all heavy lifting to `LibEval`, `LibInterpreterStateDataContract`, and `LibMemoryKV`. The contract's security model is well-designed: + +1. **`view` modifier on `eval4`**: The most important security property. Even with arbitrary/malicious bytecode, no persistent state changes occur. The EVM enforces this at the call boundary. +2. **Constructor guard**: Prevents deployment with an empty function pointer table, avoiding mod-by-zero in the eval loop. +3. **`OddSetLength` check**: Validates the `stateOverlay` array has even length before processing. +4. **`InputsLengthMismatch` check**: Delegated to `eval2`, ensures caller-supplied inputs match the source's declared input count, preventing stack corruption from mismatched input sizes. + +The contract correctly delegates bytecode validation to upstream components (the expression deployer's integrity check) and relies on the `view` modifier as the ultimate safety net. + +| Severity | Count | +|----------|-------| +| CRITICAL | 0 | +| HIGH | 0 | +| MEDIUM | 0 | +| LOW | 1 | +| INFO | 2 | diff --git a/audit/2026-03-01-01/pass1/RainterpreterExpressionDeployer.md b/audit/2026-03-01-01/pass1/RainterpreterExpressionDeployer.md new file mode 100644 index 000000000..a192cf122 --- /dev/null +++ b/audit/2026-03-01-01/pass1/RainterpreterExpressionDeployer.md @@ -0,0 +1,139 @@ +# Pass 1 (Security) -- RainterpreterExpressionDeployer.sol (A47) + +## Evidence of Thorough Reading + +### Contract + +`RainterpreterExpressionDeployer` (line 26), inheriting from `IDescribedByMetaV1`, `IParserV2`, `IParserPragmaV1`, `IIntegrityToolingV1`, `ERC165`. + +### Functions + +| Function | Line | Visibility | Mutability | Modifiers | +|---|---|---|---|---| +| `supportsInterface(bytes4)` | 34 | public | view | virtual override | +| `parse2(bytes memory)` | 41 | external | view | virtual override | +| `parsePragma1(bytes calldata)` | 66 | external | view | virtual override | +| `buildIntegrityFunctionPointers()` | 73 | external | view | virtual | +| `describedByMetaV1()` | 78 | external | pure | override | + +### Types/Errors/Constants Defined + +None defined directly in this file. All errors are defined transitively: +- From `LibIntegrityCheck`: `OpcodeOutOfRange`, `BadOpInputsLength`, `BadOpOutputsLength`, `StackUnderflow`, `StackUnderflowHighwater`, `StackAllocationMismatch`, `StackOutputsMismatch` +- From `RainterpreterParser`: parse errors, `ParseMemoryOverflow` + +### Constants Imported + +- `INTEGRITY_FUNCTION_POINTERS` -- packed 2-byte function pointers for integrity check (line 16) +- `DESCRIBED_BY_META_HASH` -- hash of the CBOR-encoded meta describing this contract (line 17) + +### Imports (lines 5--21) + +- `ERC165`, `IERC165` from OpenZeppelin +- `Pointer` from `rain.solmem` +- `IParserV2` from `rain.interpreter.interface` +- `IParserPragmaV1`, `PragmaV1` from `rain.interpreter.interface` +- `IDescribedByMetaV1` from `rain.metadata` +- `LibIntegrityCheck` (internal, `src/lib/integrity/`) +- `LibInterpreterStateDataContract` (internal, `src/lib/state/`) +- `LibAllStandardOps` (internal, `src/lib/op/`) +- `INTEGRITY_FUNCTION_POINTERS`, `DESCRIBED_BY_META_HASH` from generated pointers +- `IIntegrityToolingV1` from `rain.sol.codegen` +- `RainterpreterParser` (internal, `src/concrete/`) +- `LibInterpreterDeploy` (internal, `src/lib/deploy/`) + +--- + +## Security Analysis + +### Pipeline walkthrough: `parse2` + +1. **Parse**: Calls `RainterpreterParser(PARSER_DEPLOYED_ADDRESS).unsafeParse(data)` (line 43). This is an external `view` call to a deterministic Zoltu-deployed address. Returns `(bytes memory bytecode, bytes32[] memory constants)`. + +2. **Serialize**: Computes `serializeSize(bytecode, constants)` (line 45), allocates memory via inline assembly (lines 48--53), then writes via `unsafeSerialize(cursor, bytecode, constants)` (line 54). The serialized format is `[constants_length][constants_data...][bytecode_length][bytecode_data...]`. + +3. **Integrity check**: Calls `integrityCheck2(INTEGRITY_FUNCTION_POINTERS, bytecode, constants)` (line 56). This walks every opcode in every source, calling each opcode's integrity function. Reverts on any structural mismatch (stack underflow, opcode out of range, allocation mismatch, etc.). + +4. **Return**: Returns the serialized bytes. The integrity check result (`io`) is discarded (line 57--58) because `IParserV2` does not use it. + +### Ordering: serialize before integrity check + +The deployer serializes the bytecode before running the integrity check. If the integrity check reverts, the serialized data is never returned. If the integrity check passes, the serialized data is valid. Both `serializeSize` and `unsafeSerialize` are `pure` with no side effects. The `checkNoOOBPointers` validation in `integrityCheck2` guards against structurally malformed bytecode before opcode iteration. The ordering is safe. + +### No runtime codehash verification + +The deployer calls `PARSER_DEPLOYED_ADDRESS` without verifying `extcodehash`. The code hash constants in `LibInterpreterDeploy` exist for build-time/test verification only. This is an architectural decision: deterministic Zoltu deployment provides the trust anchor. If no contract exists at the address, the external call reverts (empty code -> revert on ABI decode). If a different contract exists (which cannot happen with deterministic deployment unless the chain has a different genesis), it would need to conform to the ABI of `unsafeParse`, and any structurally invalid output would be caught by `integrityCheck2`. + +### Assembly block in `parse2` (lines 48--53) + +```solidity +assembly ("memory-safe") { + serialized := mload(0x40) + mstore(0x40, add(serialized, add(0x20, size))) + mstore(serialized, size) + cursor := add(serialized, 0x20) +} +``` + +This allocates `size + 0x20` bytes at the free memory pointer, writes the length, and sets the cursor past the length word. The `"memory-safe"` annotation is correct: it only reads/writes the free memory region and updates `0x40`. + +### `virtual` modifiers + +All public/external functions are `virtual`. This allows subclassing, but any subclass would be a different contract at a different address with a different codehash. The deterministic deployment model ensures that `EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS` points to the exact unmodified `RainterpreterExpressionDeployer` bytecode. Subclasses cannot impersonate the canonical deployer. + +### Access control + +All functions are permissionless. This is by design: `parse2` and `parsePragma1` are `view` (no state changes), `buildIntegrityFunctionPointers` is `view` (returns a constant), and `describedByMetaV1` is `pure`. There is no state to protect. + +--- + +## Findings + +### A47-1: `serializeSize` unchecked overflow in `parse2` context -- LOW + +**Location:** Line 45 (deployer), calling `LibInterpreterStateDataContract.serializeSize` (line 26--31 of `src/lib/state/LibInterpreterStateDataContract.sol`) + +**Description:** `serializeSize` uses `unchecked` arithmetic: `size = bytecode.length + constants.length * 0x20 + 0x40`. The multiplication `constants.length * 0x20` can overflow if `constants.length >= 2^251`. The subsequent addition can also overflow. + +**Impact:** If overflow occurred, `size` would be smaller than the actual data. The assembly block (lines 48--53) would allocate a too-small buffer and `mstore(0x40, ...)` would set the free memory pointer too low. `unsafeSerialize` would then write past the allocated region, corrupting the free memory pointer and subsequent memory allocations. Depending on what follows, this could corrupt the integrity check state or the returned data. + +**Mitigating factors:** +1. The `constants` array is produced by `unsafeParse`, which is subject to the parser's `checkParseMemoryOverflow` modifier (reverts if free memory pointer reaches `0x10000`). This means `constants.length` is bounded by `~0x10000 / 0x20 = 2048` in practice. +2. Even without the parser constraint, allocating an array of length `2^251` would require `2^256` bytes of memory, which would exhaust gas immediately. +3. The NatSpec on `serializeSize` documents this precondition. + +**Severity:** LOW -- the overflow is mathematically possible but practically unreachable due to EVM memory/gas constraints and the parser's memory overflow check. + +### A47-2: No `@notice` tag on contract-level NatSpec -- INFO + +**Location:** Lines 23--25 + +**Description:** The contract has a `@title` tag (line 23) followed by a `@notice` tag (line 24). According to the codebase convention, when any explicit tag is present, all entries must be explicitly tagged. The current NatSpec is correct: `@title` on line 23 and `@notice` on lines 24--25. No issue here -- confirming correctness. + +**Severity:** INFO -- NatSpec is correct. + +### A47-3: `describedByMetaV1` lacks `virtual` unlike other functions -- INFO + +**Location:** Line 78 + +**Description:** All other public/external functions in the contract have the `virtual` modifier, but `describedByMetaV1` uses only `override` (no `virtual`). This means a subclass cannot override the meta hash. This is inconsistent with the other functions but not a security concern -- the meta hash is a compile-time constant and there is no reason for a subclass to override it. + +**Severity:** INFO -- stylistic inconsistency, not a security issue. + +--- + +## Summary + +No CRITICAL, HIGH, or MEDIUM findings. The `RainterpreterExpressionDeployer` is a small, focused contract that orchestrates parsing, serialization, and integrity checking. The pipeline is correctly ordered (serialize is side-effect-free; integrity check reverts on invalid bytecode). The assembly memory allocation is correct and properly annotated. The security model relies on deterministic Zoltu deployment rather than runtime hash verification, which is appropriate for the architecture. + +Previous audit findings from `2026-02-17-03` have been addressed: +- Test coverage gaps (A47-1, A47-2 from pass 2) are now covered by `RainterpreterExpressionDeployer.parse2.t.sol` and `RainterpreterExpressionDeployer.parsePragma1.t.sol`. +- NatSpec issues (A47-1, A47-2 from pass 1 triage) have been fixed. + +| Severity | Count | +|---|---| +| CRITICAL | 0 | +| HIGH | 0 | +| MEDIUM | 0 | +| LOW | 1 | +| INFO | 2 | diff --git a/audit/2026-03-01-01/pass1/RainterpreterParser.md b/audit/2026-03-01-01/pass1/RainterpreterParser.md new file mode 100644 index 000000000..a86246170 --- /dev/null +++ b/audit/2026-03-01-01/pass1/RainterpreterParser.md @@ -0,0 +1,115 @@ +# Pass 1 Audit: RainterpreterParser.sol + +**File:** `src/concrete/RainterpreterParser.sol` +**Agent:** A48 +**Date:** 2026-03-01 + +## Evidence of Thorough Reading + +### Contract/Library +- `contract RainterpreterParser is ERC165, IParserToolingV1` (line 36) + +### Functions (with line numbers) +| Function | Line | Visibility | Modifiers | +|---|---|---|---| +| `checkParseMemoryOverflow` (modifier) | 46 | N/A | N/A | +| `unsafeParse(bytes memory data)` | 57 | external view | `checkParseMemoryOverflow` | +| `supportsInterface(bytes4 interfaceId)` | 71 | public view virtual override | none | +| `parsePragma1(bytes memory data)` | 79 | external view virtual | `checkParseMemoryOverflow` | +| `parseMeta()` | 92 | internal pure virtual | none | +| `operandHandlerFunctionPointers()` | 97 | internal pure virtual | none | +| `literalParserFunctionPointers()` | 102 | internal pure virtual | none | +| `buildOperandHandlerFunctionPointers()` | 107 | external pure override | none | +| `buildLiteralParserFunctionPointers()` | 112 | external pure override | none | + +### Types/Errors/Constants Defined in This File +- None defined directly; all types, errors, and constants are imported. + +### Imported Types/Constants Used +- `ParseState` struct (from `LibParseState`) +- `PragmaV1` struct (from `IParserPragmaV1`) +- `Pointer` (from `rain.solmem`) +- `LITERAL_PARSER_FUNCTION_POINTERS`, `BYTECODE_HASH`, `OPERAND_HANDLER_FUNCTION_POINTERS`, `PARSE_META`, `PARSE_META_BUILD_DEPTH` (from generated pointers) +- `LibParse`, `LibParseState`, `LibParsePragma`, `LibAllStandardOps`, `LibBytes`, `LibParseInterstitial` + +### `using` Directives +- `LibParse for ParseState` (line 37) +- `LibParseState for ParseState` (line 38) +- `LibParsePragma for ParseState` (line 39) +- `LibParseInterstitial for ParseState` (line 40) +- `LibBytes for bytes` (line 41) + +## Security Analysis + +### Bytecode Hash Verification +The contract itself does not verify its own bytecode hash. The `BYTECODE_HASH` constant is exported for convenience (re-exported as `PARSER_BYTECODE_HASH`) and is used by `RainterpreterExpressionDeployer` to verify the parser's identity at deploy time. This is the intended design: the parser is not self-verifying; the deployer enforces hash checks. + +### Memory Overflow Check +The `checkParseMemoryOverflow` modifier runs `LibParseState.checkParseMemoryOverflow()` after the function body. This checks that the free memory pointer (0x40) has not reached or exceeded 0x10000, which would corrupt the 16-bit packed pointer structures used throughout the parser's linked lists. Both `unsafeParse` and `parsePragma1` apply this modifier. + +### Assembly Memory Safety +No assembly blocks exist directly in this contract. All assembly is in the library code (`LibParseState`, `LibParse`, etc.) which is audited separately. + +### Input Validation +- `unsafeParse`: Accepts arbitrary `bytes memory data`. The `LibParse.parse()` function handles zero-length data (returns empty bytecode with source count 0). Non-ASCII bytes (>0x7F) will not match any character mask and will be rejected as unexpected characters during parsing. +- `parsePragma1`: Same input treatment. The function parses interstitial content and then the pragma. The cursor is not validated against `end` after pragma parsing (line 87 silences the unused variable warning), but this is correct behavior: the function only needs to extract the pragma, and any remaining data after the pragma is intentionally ignored. + +### Access Control +Both `unsafeParse` and `parsePragma1` are `external view`, meaning anyone can call them. This is by design: the parser is a pure transformation from text to bytecode with no state modifications. The `view` modifier means no storage writes occur. The name `unsafeParse` communicates that integrity checks are NOT performed by this function -- the deployer is responsible for those. + +### Virtual Functions +Three internal functions (`parseMeta`, `operandHandlerFunctionPointers`, `literalParserFunctionPointers`) are `virtual`, allowing overriding in derived contracts. Since the parser is deployed to a deterministic address and verified by bytecode hash, any override would produce a different bytecode hash that the deployer would reject. Additionally, `parsePragma1` and `supportsInterface` are `virtual`, which is standard inheritance practice. + +## Findings + +### A48-1: NatSpec Missing `@notice` on Internal Virtual Functions (INFO) + +**Location:** Lines 91-104 + +**Description:** The three internal virtual functions (`parseMeta`, `operandHandlerFunctionPointers`, `literalParserFunctionPointers`) use bare `///` NatSpec comments without explicit tags. Per project convention (CLAUDE.md): "when a doc block contains any explicit tag, all entries must be explicitly tagged." These blocks do not contain any explicit tags so the implicit `@notice` rule applies, which means they are technically compliant. However, for consistency with the rest of the contract where `@notice` is explicitly used, these could be made explicit. + +**Severity:** INFO + +**Recommendation:** Add explicit `@notice` tags for consistency, or leave as-is since the implicit rule applies. + +### A48-2: NatSpec Missing `@notice` on External Build Functions (INFO) + +**Location:** Lines 106-114 + +**Description:** `buildOperandHandlerFunctionPointers` and `buildLiteralParserFunctionPointers` use bare `///` comments without explicit tags. Same situation as A48-1: technically compliant under the implicit rule, but inconsistent with the `@notice`-tagged functions above them. + +**Severity:** INFO + +**Recommendation:** Add explicit `@notice` tags for consistency. + +### A48-3: `checkParseMemoryOverflow` Modifier Runs After Function Body Completes (INFO) + +**Location:** Lines 46-49 + +**Description:** The modifier places `_;` before the overflow check, meaning the check runs after the entire parse has already completed and returned results. If the free memory pointer exceeded 0x10000 at any point during parsing but was somehow below 0x10000 when the check runs, the check would pass despite corruption having occurred. In practice, the free memory pointer in the EVM only moves forward (Solidity never decreases 0x40 during a `view` call), so this ordering is safe. The check is effectively "did parsing consume too much memory," which is the correct question. This is noted for documentation completeness. + +**Severity:** INFO + +**Recommendation:** No change needed. The monotonic growth of the free memory pointer makes the post-execution check equivalent to a continuous check. + +### A48-4: NatSpec Missing on `checkParseMemoryOverflow` Modifier (INFO) + +**Location:** Lines 43-49 + +**Description:** The modifier's NatSpec comment uses a bare `///` without an explicit `@notice` tag. This is compliant under the implicit rule but inconsistent with the function-level documentation style used elsewhere in the contract. + +**Severity:** INFO + +**Recommendation:** Add explicit `@notice` for consistency. + +## Summary + +RainterpreterParser.sol is a thin wrapper contract that delegates all parsing logic to library code. The contract itself is well-structured with appropriate safety measures: + +1. Memory overflow protection via the `checkParseMemoryOverflow` modifier on both parse entry points. +2. No direct assembly -- all assembly is in the library layer. +3. `view` visibility prevents state corruption. +4. Bytecode hash enforcement is delegated to the deployer (by design). +5. Virtual functions are protected by the bytecode hash verification at the deployer level. + +No CRITICAL, HIGH, MEDIUM, or LOW findings were identified. Four INFO-level suggestions relate to NatSpec consistency. diff --git a/audit/2026-03-01-01/pass1/RainterpreterReferenceExtern.md b/audit/2026-03-01-01/pass1/RainterpreterReferenceExtern.md new file mode 100644 index 000000000..fe2b7100b --- /dev/null +++ b/audit/2026-03-01-01/pass1/RainterpreterReferenceExtern.md @@ -0,0 +1,223 @@ +# Pass 1 (Security) -- RainterpreterReferenceExtern.sol and Extern Op Libraries + +**Auditor**: A49 +**Date**: 2026-03-01 + +## Files Reviewed + +1. `src/concrete/extern/RainterpreterReferenceExtern.sol` (427 lines) +2. `src/lib/extern/reference/op/LibExternOpContextCallingContract.sol` (23 lines) +3. `src/lib/extern/reference/op/LibExternOpContextRainlen.sol` (22 lines) +4. `src/lib/extern/reference/op/LibExternOpContextSender.sol` (20 lines) +5. `src/lib/extern/reference/op/LibExternOpIntInc.sol` (67 lines) +6. `src/lib/extern/reference/op/LibExternOpStackOperand.sol` (31 lines) +7. `src/lib/extern/reference/literal/LibParseLiteralRepeat.sol` (73 lines) + +## Evidence of Thorough Reading + +### RainterpreterReferenceExtern.sol + +**Library**: `LibRainterpreterReferenceExtern` (line 84) +**Contract**: `RainterpreterReferenceExtern` (line 157), inherits `BaseRainterpreterSubParser`, `BaseRainterpreterExtern` + +**Constants (file-level)**: + +| Constant | Line | Value | +|---|---|---| +| `SUB_PARSER_WORD_PARSERS_LENGTH` | 46 | `5` | +| `SUB_PARSER_LITERAL_PARSERS_LENGTH` | 49 | `1` | +| `SUB_PARSER_LITERAL_REPEAT_KEYWORD` | 53 | `bytes("ref-extern-repeat-")` | +| `SUB_PARSER_LITERAL_REPEAT_KEYWORD_BYTES32` | 58 | `bytes32(SUB_PARSER_LITERAL_REPEAT_KEYWORD)` | +| `SUB_PARSER_LITERAL_REPEAT_KEYWORD_BYTES_LENGTH` | 61 | `18` | +| `SUB_PARSER_LITERAL_REPEAT_KEYWORD_MASK` | 65 | Computed from keyword length | +| `SUB_PARSER_LITERAL_REPEAT_INDEX` | 71 | `0` | +| `OPCODE_FUNCTION_POINTERS_LENGTH` | 77 | `1` | + +**Error**: `InvalidRepeatCount()` (line 74) + +**Functions in `LibRainterpreterReferenceExtern`**: + +| Function | Line | Visibility | +|---|---|---| +| `authoringMetaV2()` | 93 | internal pure | + +**Functions in `RainterpreterReferenceExtern`**: + +| Function | Line | Visibility | +|---|---|---| +| `describedByMetaV1()` | 161 | external pure override | +| `subParserParseMeta()` | 168 | internal pure virtual override | +| `subParserWordParsers()` | 175 | internal pure override | +| `subParserOperandHandlers()` | 182 | internal pure override | +| `subParserLiteralParsers()` | 189 | internal pure override | +| `opcodeFunctionPointers()` | 196 | internal pure override | +| `integrityFunctionPointers()` | 203 | internal pure override | +| `buildLiteralParserFunctionPointers()` | 209 | external pure | +| `matchSubParseLiteralDispatch(uint256, uint256)` | 232 | internal pure virtual override | +| `buildOperandHandlerFunctionPointers()` | 275 | external pure override | +| `buildSubParserWordParsers()` | 318 | external pure | +| `buildOpcodeFunctionPointers()` | 358 | external pure | +| `buildIntegrityFunctionPointers()` | 390 | external pure | +| `supportsInterface(bytes4)` | 418 | public view virtual override | + +**Imports**: Verified all imports including generated pointers (DESCRIBED_BY_META_HASH, SUB_PARSER_PARSE_META, SUB_PARSER_WORD_PARSERS, OPERAND_HANDLER_FUNCTION_POINTERS, LITERAL_PARSER_FUNCTION_POINTERS, INTEGRITY_FUNCTION_POINTERS, OPCODE_FUNCTION_POINTERS), LibDecimalFloat, Float, and all extern op libraries. + +**Assembly blocks**: 6 instances of fixed-to-dynamic array reinterpretation (lines 120-123, 219-221, 297-298, 338-339, 370-371, 402-403), plus `mload(cursor)` in `matchSubParseLiteralDispatch` (line 243). + +--- + +### LibExternOpContextCallingContract.sol + +**Library**: `LibExternOpContextCallingContract` (line 15) + +| Function | Line | Visibility | +|---|---|---| +| `subParser(uint256, uint256, OperandV2)` | 19 | internal pure | + +**Imports**: `OperandV2`, `LibSubParse`, `CONTEXT_BASE_COLUMN` (=0), `CONTEXT_BASE_ROW_CALLING_CONTRACT` (=1) from `LibContext.sol`. + +**Behavior**: Delegates to `LibSubParse.subParserContext(0, 1)`, producing a context opcode referencing column 0 row 1. + +--- + +### LibExternOpContextRainlen.sol + +**Library**: `LibExternOpContextRainlen` (line 14) + +| Function | Line | Visibility | +|---|---|---| +| `subParser(uint256, uint256, OperandV2)` | 18 | internal pure | + +**Constants (file-level)**: + +| Constant | Line | Value | +|---|---|---| +| `CONTEXT_CALLER_CONTEXT_COLUMN` | 8 | `1` | +| `CONTEXT_CALLER_CONTEXT_ROW_RAINLEN` | 9 | `0` | + +**Behavior**: Delegates to `LibSubParse.subParserContext(1, 0)`, producing a context opcode referencing column 1 row 0. + +--- + +### LibExternOpContextSender.sol + +**Library**: `LibExternOpContextSender` (line 13) + +| Function | Line | Visibility | +|---|---|---| +| `subParser(uint256, uint256, OperandV2)` | 17 | internal pure | + +**Imports**: `OperandV2`, `LibSubParse`, `CONTEXT_BASE_COLUMN` (=0), `CONTEXT_BASE_ROW_SENDER` (=0) from `LibContext.sol`. + +**Behavior**: Delegates to `LibSubParse.subParserContext(0, 0)`, producing a context opcode referencing column 0 row 0. + +--- + +### LibExternOpIntInc.sol + +**Library**: `LibExternOpIntInc` (line 18) + +**Constants**: `OP_INDEX_INCREMENT = 0` (line 13) + +| Function | Line | Visibility | +|---|---|---| +| `run(OperandV2, StackItem[] memory inputs)` | 27 | internal pure | +| `integrity(OperandV2, uint256 inputs, uint256)` | 44 | internal pure | +| `subParser(uint256, uint256, OperandV2)` | 57 | internal view | + +**Behavior**: `run` increments each input by Float(1) using `LibDecimalFloat.add`. `integrity` returns `(inputs, inputs)`. `subParser` delegates to `LibSubParse.subParserExtern(address(this), ...)`. + +--- + +### LibExternOpStackOperand.sol + +**Library**: `LibExternOpStackOperand` (line 14) + +| Function | Line | Visibility | +|---|---|---| +| `subParser(uint256, uint256, OperandV2)` | 23 | internal pure | + +**Behavior**: Delegates to `LibSubParse.subParserConstant(constantsHeight, OperandV2.unwrap(operand))`, pushing the operand value as a constant at eval time. + +--- + +### LibParseLiteralRepeat.sol + +**Library**: `LibParseLiteralRepeat` (line 45) + +**Constants**: `MAX_REPEAT_LITERAL_LENGTH = 78` (line 34) + +**Errors**: `RepeatLiteralTooLong(uint256)` (line 39), `RepeatDispatchNotDigit(uint256)` (line 43) + +| Function | Line | Visibility | +|---|---|---| +| `parseRepeat(uint256, uint256, uint256)` | 53 | internal pure | + +**Behavior**: Validates `dispatchValue <= 9`, validates `length < 78`, then computes `sum(dispatchValue * 10^i for i in 0..length-1)`. + +--- + +## Security Findings + +### A49-6: `matchSubParseLiteralDispatch` does not verify `cursor` consumed all bytes up to `end` + +**Severity**: LOW + +**Location**: `src/concrete/extern/RainterpreterReferenceExtern.sol` lines 253-254 + +**Description**: After matching the keyword prefix `ref-extern-repeat-`, the function calls `parseDecimalFloatPacked` starting at `cursor + 18`. The returned `cursor` indicates where the decimal parser stopped, but this value is neither checked against `end` (the end of the dispatch region) nor returned to the caller. If the dispatch region contains trailing bytes after the decimal digit (e.g., `ref-extern-repeat-5xyz`), these bytes are silently ignored. + +In practice, the calling flow (`subParseLiteral2` in `BaseRainterpreterSubParser`) computes `dispatchStart` and `bodyStart` from the input data. The dispatch region boundaries are set by the main parser, and the keyword matching requires `length > 18` (strictly greater), meaning there must be at least one byte after the keyword for the decimal parser to consume. However, if the dispatch region contains additional bytes after the digit (e.g., the dispatch region is `ref-extern-repeat-5x`), the decimal parser would stop after `5` and the `x` would be silently ignored. + +Whether this is exploitable depends on how the main parser constructs the dispatch/body boundary. If the boundary is always set tightly around the keyword + digit, this is a non-issue. But the function itself does not enforce this invariant. + +**Mitigation**: After parsing the decimal float, verify `cursor == end` (or the expected position in the dispatch region), and revert if trailing bytes exist. + +--- + +### A49-7: NatSpec missing `@notice` tag in `matchSubParseLiteralDispatch` and `buildOperandHandlerFunctionPointers` + +**Severity**: LOW + +**Location**: `src/concrete/extern/RainterpreterReferenceExtern.sol` + +**Description**: Two functions use doc blocks with both tagged and untagged lines, creating ambiguity: + +1. `buildOperandHandlerFunctionPointers` (lines 272-274): Has `@notice` tag on line 272 followed by `@inheritdoc IParserToolingV1` on line 274. The description "We haven't implemented any words with meaningful operands yet." on the same `@notice` line is fine, but this is a minor style observation. + +2. `buildSubParserWordParsers` (lines 309-317): Has `@notice` on line 309, then `@inheritdoc ISubParserToolingV1` on line 317. This is correct. + +No actual tag errors were found -- the NatSpec usage is consistent with the project convention that once any explicit tag appears, all entries must be tagged. + +**Mitigation**: No action required. This finding is downgraded to INFO on review. + +--- + +### A49-8: `CONTEXT_CALLER_CONTEXT_COLUMN` and `CONTEXT_CALLER_CONTEXT_ROW_RAINLEN` defined locally instead of from shared library + +**Severity**: LOW + +**Location**: `src/lib/extern/reference/op/LibExternOpContextRainlen.sol` lines 8-9 + +**Description**: The constants `CONTEXT_CALLER_CONTEXT_COLUMN = 1` and `CONTEXT_CALLER_CONTEXT_ROW_RAINLEN = 0` are defined inline in this file. The sibling libraries (`LibExternOpContextSender` and `LibExternOpContextCallingContract`) import their context position constants from `rain.interpreter.interface/lib/caller/LibContext.sol`. + +This inconsistency means that if the canonical context grid layout changes in `LibContext.sol`, the rainlen constants would not be updated automatically. The values in `LibContext.sol` are `CONTEXT_BASE_COLUMN = 0`, `CONTEXT_BASE_ROW_SENDER = 0`, `CONTEXT_BASE_ROW_CALLING_CONTRACT = 1`. The rainlen position (column 1, row 0) is in the "caller context" area rather than the "base context" area, which may explain why it is not defined in `LibContext.sol`. However, the lack of NatSpec on these file-level constants (no `@dev` or `@notice` tags) makes the rationale unclear. + +This was previously identified in audit 2026-02-17-03 (pass 1, LibExternOpContextRainlen.md). I am re-raising it because it has not been addressed. + +**Mitigation**: Either define `CONTEXT_CALLER_CONTEXT_COLUMN` and `CONTEXT_CALLER_CONTEXT_ROW_RAINLEN` in `LibContext.sol` (or a new shared file) and import them, or add NatSpec to the local definitions explaining why they are not in the shared library. + +--- + +## Summary + +No CRITICAL or HIGH severity issues were found across the seven files reviewed. The reference extern implementation is well-structured with consistent patterns for function pointer dispatch, array reinterpretation, and sub-parser delegation. + +Key observations: +- All assembly blocks are marked `memory-safe` and operate within Solidity's memory model +- The `mod`-based dispatch in `BaseRainterpreterExtern.extern()` provides defense against out-of-range opcodes +- Constructor validation ensures pointer table consistency (non-empty, matching lengths) +- All errors are custom error types with no string reverts +- The `parseRepeat` function correctly bounds both the dispatch value (0-9) and body length (<78) to prevent overflow in the `unchecked` block +- Function pointer type erasure between `subParseLiteral2` (typed `function(bytes32, ...)`) and `parseRepeat` (typed `function(uint256, ...)`) is safe because both are 32-byte stack values at the EVM level, and packed Float values for integers 0-9 with exponent 0 equal their integer representation +- The `InvalidRepeatCount` validation in `matchSubParseLiteralDispatch` uses proper Float comparisons (`lt`, `gt`, `frac().isZero()`) before the value reaches `parseRepeat` diff --git a/audit/2026-03-01-01/pass1/RainterpreterStore.md b/audit/2026-03-01-01/pass1/RainterpreterStore.md new file mode 100644 index 000000000..d11667c9c --- /dev/null +++ b/audit/2026-03-01-01/pass1/RainterpreterStore.md @@ -0,0 +1,170 @@ +# Pass 1 (Security) -- RainterpreterStore.sol + +**Auditor**: A50 +**Date**: 2026-03-01 +**File**: `src/concrete/RainterpreterStore.sol` (69 lines) + +## Evidence of Thorough Reading + +**Contract**: `RainterpreterStore` (line 25), inherits `IInterpreterStoreV3`, `ERC165` + +**Using directive**: `LibNamespace` for `StateNamespace` (line 26) + +### Imports + +| Import | Source | Line | +|---|---|---| +| `ERC165` | `openzeppelin-contracts/contracts/utils/introspection/ERC165.sol` | 5 | +| `IInterpreterStoreV3` | `rain.interpreter.interface/interface/IInterpreterStoreV3.sol` | 7 | +| `LibNamespace`, `FullyQualifiedNamespace`, `StateNamespace` | `rain.interpreter.interface/lib/ns/LibNamespace.sol` | 8-12 | +| `BYTECODE_HASH` (as `STORE_BYTECODE_HASH`) | `../generated/RainterpreterStore.pointers.sol` | 16 | +| `OddSetLength` | `../error/ErrStore.sol` | 17 | + +### State Variables + +| Variable | Type | Visibility | Line | +|---|---|---|---| +| `sStore` | `mapping(FullyQualifiedNamespace => mapping(bytes32 => bytes32))` | internal | 40 | + +### Functions + +| Function | Signature | Visibility | Modifiers | Line | +|---|---|---|---|---| +| `supportsInterface` | `(bytes4) -> (bool)` | public view virtual override | -- | 43 | +| `set` | `(StateNamespace, bytes32[] calldata) -> ()` | external virtual | -- | 48 | +| `get` | `(FullyQualifiedNamespace, bytes32) -> (bytes32)` | external view virtual | -- | 66 | + +### Errors (imported) + +| Error | Parameters | Source | Line | +|---|---|---|---| +| `OddSetLength` | `uint256 length` | `src/error/ErrStore.sol` | 17 | + +### Events (from interface) + +| Event | Parameters | +|---|---| +| `Set` | `FullyQualifiedNamespace namespace, bytes32 key, bytes32 value` | + +### Types (from interface) + +| Type | Underlying | Definition | +|---|---|---| +| `StateNamespace` | `uint256` | User-defined value type | +| `FullyQualifiedNamespace` | `uint256` | User-defined value type | + +--- + +## Security Analysis + +### Namespace Isolation + +The `set` function qualifies the caller-provided `StateNamespace` with `msg.sender` at line 55: + +```solidity +FullyQualifiedNamespace fullyQualifiedNamespace = namespace.qualifyNamespace(msg.sender); +``` + +`qualifyNamespace` (in `LibNamespace`) computes `keccak256(stateNamespace, sender)`, producing a 256-bit hash. This makes it computationally infeasible for one `msg.sender` to produce a `FullyQualifiedNamespace` that collides with another sender's namespace. + +**Conclusion**: Write isolation is correctly enforced. One user cannot write to another user's storage. + +### Can One User Read Another User's Storage? + +The `get` function at line 66 accepts a `FullyQualifiedNamespace` directly: + +```solidity +function get(FullyQualifiedNamespace namespace, bytes32 key) external view virtual returns (bytes32) { + return sStore[namespace][key]; +} +``` + +Any address can read any other address's stored values by computing the appropriate `FullyQualifiedNamespace`. This is by design per the `IInterpreterStoreV3` interface spec, which documents: "Technically also allows onchain reads of any set value from any contract." All on-chain storage is publicly readable via `eth_getStorageAt` regardless, so no confidentiality expectation exists. + +**Conclusion**: Read access is intentionally unrestricted. Not a vulnerability. + +### Assembly Memory Safety + +`RainterpreterStore.sol` contains no inline assembly. The `LibNamespace.qualifyNamespace` function called at line 55 uses assembly to write to scratch space (`0x00`-`0x3f`) and compute `keccak256`: + +```solidity +assembly ("memory-safe") { + mstore(0, stateNamespace) + mstore(0x20, sender) + qualifiedNamespace := keccak256(0, 0x40) +} +``` + +This is marked `memory-safe` and correctly uses only the EVM scratch space (bytes 0-63), which Solidity reserves for hashing. It does not allocate or corrupt memory. + +**Conclusion**: No memory safety issues. + +### Storage Collision Risks + +The `sStore` mapping uses `FullyQualifiedNamespace` (a keccak256 hash) as its first key. Solidity storage layout for nested mappings computes the slot as `keccak256(key, keccak256(innerKey, slot))`. Since `FullyQualifiedNamespace` is itself a keccak256 output, the probability of storage slot collision is negligible (2^-256). + +There is only one state variable (`sStore`), so there are no slot overlap concerns with other storage variables. + +**Conclusion**: No storage collision risk. + +### Unchecked Arithmetic + +The `set` function wraps its loop in `unchecked` (lines 54-62): + +- `i += 2`: Bounded by `i < kvs.length`, and `kvs.length` is bounded by calldata size (max ~16M bytes on mainnet, well below `type(uint256).max`). Cannot overflow. +- `i + 1`: Safe because `kvs.length` is verified even at line 51, so when `i` is a valid even index, `i + 1` is always in bounds. +- Calldata array indexing (`kvs[i]`, `kvs[i + 1]`) is bounds-checked by the Solidity compiler regardless of `unchecked`. + +**Conclusion**: Unchecked arithmetic is provably safe. + +### Reentrancy + +`set` performs only storage writes and event emissions -- no external calls. `get` is a pure view function. Neither function has reentrancy risk. + +### Access Control + +`set` is `external virtual` with no access restriction beyond namespace qualification. Any address can call `set` and will write to their own namespace. This is the intended design -- the store is a shared utility contract. + +`get` is `external view virtual` with no access restriction. Read access is unrestricted by design. + +`supportsInterface` is `public view virtual override` and is a standard ERC-165 implementation. + +--- + +## Findings + +### A50-1 -- INFO: `get()` accepts pre-qualified namespace without sender verification + +**Location**: Line 66-68 + +**Description**: The `get` function accepts a `FullyQualifiedNamespace` directly, allowing any caller to read any namespace's data. This is intentional per the interface specification and does not constitute a vulnerability since on-chain storage is publicly observable regardless. + +**Severity**: INFO + +### A50-2 -- INFO: `set()` event emission for duplicate keys + +**Location**: Lines 56-61 + +**Description**: When the same key appears multiple times in the `kvs` array, `Set` is emitted for each occurrence. Only the last value persists in storage, but all events are logged. Event consumers (indexers, off-chain systems) must be aware that for a given key within a single `set` call, only the last emitted `Set` event reflects the final state. The NatSpec at lines 23-24 documents this: "doesn't attempt to do any deduping etc. if the same key appears twice it will be set twice." + +**Severity**: INFO + +### A50-3 -- INFO: Unchecked arithmetic in `set()` is provably safe + +**Location**: Lines 54-62 + +**Description**: The `unchecked` block wraps loop arithmetic (`i += 2`, `i + 1`). The odd-length check at line 51 guarantees `kvs.length` is even, so `i + 1` is always in bounds when `i < kvs.length`. The loop increment `i += 2` cannot overflow because `kvs.length` is bounded by calldata size. + +**Severity**: INFO + +### A50-4 -- INFO: No constructor or initializer + +**Description**: The contract has no constructor or initializer. The `sStore` mapping defaults to zero for all keys, and `ERC165` has no constructor side effects. This is correct -- no initialization is needed. + +**Severity**: INFO + +--- + +## Summary + +No CRITICAL, HIGH, MEDIUM, or LOW findings. `RainterpreterStore` is a 69-line contract with a single state variable, three functions, no assembly, no external calls, and correct namespace isolation via `keccak256(stateNamespace, msg.sender)`. The write path enforces sender isolation; the read path is intentionally unrestricted. The unchecked arithmetic is bounded by the prior parity check. The contract is straightforward and secure. diff --git a/audit/2026-03-01-01/pass1/RustCrates.md b/audit/2026-03-01-01/pass1/RustCrates.md new file mode 100644 index 000000000..6021868d8 --- /dev/null +++ b/audit/2026-03-01-01/pass1/RustCrates.md @@ -0,0 +1,155 @@ +# Pass 1: Rust Crates Audit + +**Agent:** R01 (CLI), R02 (eval), R03 (parser/dispair/bindings/test_fixtures) +**Date:** 2026-03-01 +**Scope:** All `.rs` files in `crates/{cli,eval,parser,dispair,bindings,test_fixtures}/src/` + +--- + +## Files Read (evidence of thorough reading) + +### crates/cli/src/ (R01) + +| File | Lines | Key observations | +|---|---|---| +| `main.rs` (34 lines) | Clap `Parser` derive, tracing-subscriber setup with `EnvFilter`, delegates to `Interpreter::execute()`. Uses `from_env()?` for filter -- propagates error. | +| `lib.rs` (25 lines) | Enum dispatch: `Interpreter::Parse` and `Interpreter::Eval`. Both delegate to `Execute` trait. | +| `execute.rs` (5 lines) | Trait definition: `async fn execute(&self) -> Result<()>`. | +| `output.rs` (29 lines) | Writes bytes to file or stdout. No path sanitization, appropriate for CLI. | +| `fork.rs` (20 lines) | `NewForkedEvmCliArgs` with `fork_url: String` and optional `fork_block_number`. Direct passthrough to `NewForkedEvm`. | +| `commands/mod.rs` (5 lines) | Re-exports `Eval` and `Parse`. | +| `commands/eval.rs` (183 lines) | `ForkEvalCliArgs` with many typed CLI args. `parse_int_or_hex` helper. `TryFrom` impl parses namespace/context. Test at bottom with `LocalEvm`. | +| `commands/parse.rs` (64 lines) | `ForkParseArgsCli` with deployer, rainlang_string, decode_errors. `From` impl. `Execute` delegates to `Forker::fork_parse`. | + +### crates/eval/src/ (R02) + +| File | Lines | Key observations | +|---|---|---| +| `lib.rs` (7 lines) | Module declarations, conditional compilation for wasm. | +| `error.rs` (50 lines) | `ForkCallError` and `ReplayTransactionError` enums with `thiserror`. All variants preserve error chains via `#[from]` or string formatting. | +| `eval.rs` (268 lines) | `ForkEvalArgs`, `ForkParseArgs`, `Forker::fork_parse`, `Forker::fork_eval`. Tests at bottom (150-268). | +| `fork.rs` (799 lines) | Core `Forker` struct. `new()`, `new_with_fork()`, `add_or_select()`, `alloy_call()`, `alloy_call_committing()`, `call()`, `call_committing()`, `roll_fork()`, `replay_transaction()`. Address length validated in `call`/`call_committing`. Tests from line 509. | +| `namespace.rs` (40 lines) | `qualify_namespace` function. Correct byte layout matching Solidity `abi.encodePacked(bytes32, address)`. Test verifies against chisel output. | +| `trace.rs` (575 lines) | `RainSourceTrace`, `RainEvalResult`, trace parsing from EVM call results. `search_trace_by_path` with path parsing. `flattened_trace_path_names`. `RainEvalResults` and `RainEvalResultsTable`. Tests from line 308. | + +### crates/parser/src/ (R03) + +| File | Lines | Key observations | +|---|---|---| +| `lib.rs` (5 lines) | Re-exports `error` and `v2` modules. | +| `error.rs` (10 lines) | `ParserError` enum with `thiserror`. Two variants with `#[from]`. | +| `v2.rs` (266 lines) | `Parser2` trait (dual wasm/non-wasm impls), `ParserV2` struct. `parse`, `parse_pragma`, `parse_text`, `parse_pragma_text`. Tests from line 169. | + +### crates/dispair/src/ (R03) + +| File | Lines | Key observations | +|---|---|---| +| `lib.rs` (42 lines) | `DISPaiR` struct with 4 `Address` fields. `new()` constructor. Test verifies fields. | + +### crates/bindings/src/ (R03) + +| File | Lines | Key observations | +|---|---|---| +| `lib.rs` (35 lines) | 6 `sol!` macro invocations generating Alloy bindings from JSON ABI artifacts. No logic. | + +### crates/test_fixtures/src/ (R03) + +| File | Lines | Key observations | +|---|---|---| +| `lib.rs` (267 lines) | `LocalEvm` struct wrapping Anvil. `new()` deploys all contracts, copies code to Zoltu addresses. `new_with_tokens()`, `deploy_new_token()`, `send_contract_transaction()`, `send_transaction()`, `call_contract()`, `call()`. All `unwrap()` calls are acceptable for test infrastructure. | + +--- + +## Audit Checks + +### 1. Unsafe Code Blocks + +**Result:** No `unsafe` blocks found in any crate. Clean. + +### 2. Unwrap/Panic in Production Code Paths + +**Result:** One production-code `unwrap()` found. All others are in `#[cfg(test)]` blocks or in the test-only `test_fixtures` crate. + +- `crates/eval/src/trace.rs:158` -- `.pop().unwrap()` in `search_trace_by_path`. This is guarded by a `parts.len() < 2` check on line 152, so `pop()` on a vec with 2+ elements cannot fail. **SAFE** -- the invariant is established two lines above. Not a finding. + +### 3. Input Validation on CLI Args + +**Result:** CLI args use Clap's typed parsing (`Address`, `U256`, `u16`, `BlockNumber`). These are validated at parse time by Clap. The `parse_int_or_hex` helper validates format and returns `Result`. Context values are parsed with error propagation. No unvalidated raw string consumption in security-relevant paths. + +### 4. Fork URL Handling (Injection Risks) + +**Result:** The `fork_url` is a raw `String` accepted from CLI (`-i` flag) and passed directly to `EvmOpts::fork_url` and `CreateFork::url`. No URL parsing or sanitization is performed. However, this string is consumed by foundry-evm's `EvmOpts::fork_evm_env()` which internally uses an HTTP/WS client. The trust boundary is the user running the CLI -- they control the fork URL. No injection vector beyond what the user already controls. **No finding.** + +### 5. Error Chain Preservation + +**Result:** Error chains are well-preserved throughout: +- `ForkCallError` uses `#[from]` for `AbiDecodeFailedErrors`, `AbiDecodedErrorType`, `FromUintError`, `eyre::Report`, `ReplayTransactionError`. +- String-based error wrapping in `ExecutorError` and `TypedError` variants loses the original error type but preserves the message via `.to_string()` / `format!`. This is a minor information loss but not a security issue. +- `ParserError` uses `#[from]` for both variants. +- CLI layer uses `anyhow` with `.context()` where appropriate. + +--- + +## Findings + +### R02-RUST-01: Potential Arithmetic Underflow in `replay_transaction` (LOW) + +**File:** `/Users/thedavidmeister/Code/rain.interpreter/crates/eval/src/fork.rs` +**Line:** 451 +**Code:** +```rust +fork_block_number: Some(block_number - 1), +``` + +**Description:** In `Forker::replay_transaction()`, the code computes `block_number - 1` where `block_number` is a `u64` obtained from the transaction's block number. If the transaction is in block 0 (genesis block), this subtraction will underflow in release mode (wrapping to `u64::MAX`) or panic in debug mode. + +**Impact:** In practice, genesis block transactions are rare and unlikely to be replayed through this API. However, the underflow would cause `add_or_select` to request fork at block `u64::MAX`, which would fail with an RPC error from the upstream provider. The error message would be confusing and unhelpful. + +**Severity:** LOW -- The scenario is unlikely (genesis block replay), and the downstream RPC call would fail rather than silently producing wrong results. No data corruption or security impact. + +### R02-RUST-02: Error Context Lost in String-Wrapped Error Variants (INFO) + +**File:** `/Users/thedavidmeister/Code/rain.interpreter/crates/eval/src/fork.rs` +**Lines:** 177, 221, 331, 361, 405 + +**Description:** Several error paths convert typed errors to strings via `.to_string()` before wrapping in `ForkCallError::ExecutorError(String)`: +```rust +.map_err(|e| ForkCallError::ExecutorError(e.to_string())) +``` + +This discards the original error type and its chain of causes. The `Display` representation is preserved, but programmatic error matching and `source()` traversal are lost. + +**Impact:** Debugging is harder when errors propagate through these paths. The error message is still human-readable, but downstream code cannot match on specific error types or access the cause chain. + +**Severity:** INFO -- No security impact. Purely a debuggability concern. + +### R02-RUST-03: `unwrap_or` with `format!` Fallback in `flattened_trace_path_names` (INFO) + +**File:** `/Users/thedavidmeister/Code/rain.interpreter/crates/eval/src/trace.rs` +**Lines:** 292-295 + +**Description:** When building trace path names, the code has a fallback: +```rust +.unwrap_or(format!( + "{}?.{}", + trace.parent_source_index, trace.source_index +)) +``` + +This silently produces path names containing `?` when the parent path cannot be resolved. Consumers of `flattened_trace_path_names` may not expect this format. + +**Impact:** Column names in `RainEvalResultsTable` could contain unexpected `?` characters. This is a cosmetic/documentation issue. + +**Severity:** INFO -- No security impact. The `?` marker is actually a reasonable way to indicate unresolved paths. + +--- + +## Summary + +| ID | Severity | Crate | Description | +|---|---|---|---| +| R02-RUST-01 | LOW | eval | Potential `u64` underflow in `replay_transaction` at block 0 | +| R02-RUST-02 | INFO | eval | Error context lost in string-wrapped error variants | +| R02-RUST-03 | INFO | eval | Silent `?` fallback in trace path names | + +**Overall assessment:** The Rust crates are well-structured with good error handling. No `unsafe` code, no `panic!` calls, no `expect()` in production paths. The single LOW finding is a defensive programming gap in an unlikely edge case. The codebase makes appropriate use of `thiserror` for error hierarchies and `anyhow` in the CLI boundary. diff --git a/audit/2026-03-01-01/pass2/CoreConcrete.md b/audit/2026-03-01-01/pass2/CoreConcrete.md new file mode 100644 index 000000000..ee86555b9 --- /dev/null +++ b/audit/2026-03-01-01/pass2/CoreConcrete.md @@ -0,0 +1,22 @@ +# Pass 2: Test Coverage — Core Concrete Contracts + +**Audit:** 2026-03-01-01 +**Agent IDs:** A45, A46, A47, A48, A13 + +## Findings + +### P2-CC-01 (LOW) `Rainterpreter.supportsInterface` omits `IOpcodeToolingV1` (A45) + +`Rainterpreter` inherits `IOpcodeToolingV1` and implements `buildOpcodeFunctionPointers()`, but its `supportsInterface()` does not return `true` for `IOpcodeToolingV1.interfaceId`. This is inconsistent with `RainterpreterParser` (which includes `IParserToolingV1`) and `RainterpreterExpressionDeployer` (which includes `IIntegrityToolingV1`). The ERC165 test at `test/src/concrete/Rainterpreter.ierc165.t.sol` reflects the current (incomplete) code — it only checks `IERC165` and `IInterpreterV4`. + +### P2-CC-02 (LOW) `RainterpreterExpressionDeployer` missing dedicated pointer consistency test (A47) + +Both `Rainterpreter` and `RainterpreterParser` have dedicated `*.pointers.t.sol` test files. `RainterpreterExpressionDeployer` does not. Its `INTEGRITY_FUNCTION_POINTERS` is only checked as a sanity assertion inside the `RainterpreterExpressionDeployerDeploymentTest` abstract constructor (line 123-128), which produces an unclear revert rather than a named test failure. + +### P2-CC-03 (LOW) Missing direct test for `StateNamespace` isolation (same sender) (A48) + +The namespace isolation tests in `RainterpreterStore.namespaceIsolation.t.sol` verify different `msg.sender` addresses are isolated. The complementary case — same `msg.sender`, different `StateNamespace` — is not directly tested. It's indirectly covered by fuzz tests but not by a named test. + +### P2-CC-04 (INFO) `DISPaiRegistry` does not test that returned addresses are mutually distinct + +Each getter is tested individually to match its constant and be non-zero, but there's no assertion that all four returned addresses are distinct from each other. diff --git a/audit/2026-03-01-01/pass2/ErrAll.md b/audit/2026-03-01-01/pass2/ErrAll.md new file mode 100644 index 000000000..163b917ed --- /dev/null +++ b/audit/2026-03-01-01/pass2/ErrAll.md @@ -0,0 +1,13 @@ +# Pass 2: Test Coverage — Error Definition Files + +**Audit:** 2026-03-01-01 +**Agent ID:** F01 + +## Findings + +No LOW+ findings. All 64 errors across the 10 error definition files were checked. 60 have direct `vm.expectRevert(abi.encodeWithSelector(...))` test coverage. The remaining 4 are all INFO-level: + +- **F-01 (INFO):** `UnknownDeploymentSuite` — script-only error in `script/Deploy.sol`, not a contract runtime error +- **F-02 (INFO):** `BadDynamicLength` — unreachable defensive code guarding compiler memory layout +- **F-03 (INFO):** `MalformedHexLiteral` — unreachable defensive code; `boundHex` pre-validates the character range +- **F-04 (INFO):** `ParserOutOfBounds` — unreachable defensive invariant; all cursor advancement is bounded by end checks diff --git a/audit/2026-03-01-01/pass2/ExternAbstractDeploy.md b/audit/2026-03-01-01/pass2/ExternAbstractDeploy.md new file mode 100644 index 000000000..8625e0328 --- /dev/null +++ b/audit/2026-03-01-01/pass2/ExternAbstractDeploy.md @@ -0,0 +1,30 @@ +# Pass 2: Test Coverage — Extern, Abstract, Deploy, AllStandardOps + +**Audit:** 2026-03-01-01 +**Agent IDs:** A01, A02, A04, A06, A13, A49 + +## Findings + +### P2-EAD-01 (LOW) `BaseRainterpreterSubParser.subParseWord2` missing happy-path and no-match tests (A02) + +The test file `BaseRainterpreterSubParser.subParseWord2.t.sol` only tests the `SubParserIndexOutOfBounds` revert paths. There is no base-level test for: + +1. **Happy path**: A word is found in meta and the corresponding parser function pointer is called, producing valid output. The happy path is only tested indirectly through `RainterpreterReferenceExtern.intInc.t.sol`. The base abstract contract should have its own isolated happy-path test. + +2. **No-match path**: A word is not found in the parse meta, and the function returns `(false, "", new bytes32[](0))`. This path at line 210 is never directly tested at the base level. + +### P2-EAD-02 (LOW) `authoringMetaV2` word names not verified beyond index 3 (A04) + +The `testAuthoringMetaV2Content` test verifies that words[0..3] are "stack", "constant", "extern", "context" and that all words are non-empty. However, it does not verify the names or ordering of opcodes 4 through 71. A subtle typo in a word name or an ordering swap between two entries would not be caught by any test. + +The four parallel arrays (authoring meta, operand handlers, integrity pointers, opcode pointers) must have consistent ordering. Length consistency is tested, but ordering is not. + +**Mitigating factor**: The parse meta is built from the authoring meta at build time and baked into constants. Any ordering change changes the parse meta constant, which is validated by the pointer tests. + +### P2-EAD-03 (INFO) `extern()` mod-wrap not tested at base level with multiple opcodes (A01) + +The `extern()` function uses `mod(opcode, fsCount)` to wrap out-of-range opcodes. This is tested only through `RainterpreterReferenceExtern` which has exactly 1 opcode. No base-level test with multiple opcodes verifies that mod dispatches to the correct function for different opcode values. + +### P2-EAD-04 (INFO) Repeat literal boundary digit 0 not tested through full stack (A49) + +The repeat literal happy-path tests exercise digits 8 and 9. Digit 0 (lower boundary) is not tested through the full parse-and-eval stack, though it is covered at the library level by `LibParseLiteralRepeat.t.sol` fuzz tests. diff --git a/audit/2026-03-01-01/pass2/LibEvalIntegrity.md b/audit/2026-03-01-01/pass2/LibEvalIntegrity.md new file mode 100644 index 000000000..7b35fb67d --- /dev/null +++ b/audit/2026-03-01-01/pass2/LibEvalIntegrity.md @@ -0,0 +1,18 @@ +# Pass 2: Test Coverage — LibEval, LibIntegrityCheck, LibInterpreterState, LibInterpreterStateDataContract + +**Audit:** 2026-03-01-01 +**Agent IDs:** A05, A12, A14, A15 + +## Findings + +### P2-EI-1 (LOW) `eval2` `InputsLengthMismatch` not tested at library level (A05) + +The `InputsLengthMismatch` error at `src/lib/eval/LibEval.sol:212-213` is only tested through the full `Rainterpreter.eval4` integration path. No test in `test/src/lib/eval/` calls `LibEval.eval2` directly with mismatched input lengths. + +### P2-EI-2 (LOW) `integrityCheck2` `BadOpInputsLength` and `BadOpOutputsLength` not tested directly (A12) + +Of 7 revert paths in `integrityCheck2`, five are directly tested. `BadOpInputsLength` (line 160) and `BadOpOutputsLength` (line 163) are only tested indirectly through opcode tests via `OpTest.checkBadOp`. + +### P2-EI-3 (LOW) `evalLoop` remainder-only path (1-7 opcodes) not directly tested (A05) + +Tests exist for 0, 8, 16, and 37 opcodes. No test exercises opcode counts 1-7 where ONLY the remainder loop executes without the main unrolled loop. diff --git a/audit/2026-03-01-01/pass2/LibOpAll.md b/audit/2026-03-01-01/pass2/LibOpAll.md new file mode 100644 index 000000000..a7899cab4 --- /dev/null +++ b/audit/2026-03-01-01/pass2/LibOpAll.md @@ -0,0 +1,43 @@ +# Pass 2: Test Coverage — Opcode Libraries + +**Audit:** 2026-03-01-01 +**Agent IDs:** A07-A29 (all opcode source files) + +## Findings + +### P2-01 (LOW) Missing operand-disallowed tests for 10 logic opcodes + +**Files affected:** +- `test/src/lib/op/logic/LibOpGreaterThan.t.sol` +- `test/src/lib/op/logic/LibOpLessThan.t.sol` +- `test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol` +- `test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol` +- `test/src/lib/op/logic/LibOpEqualTo.t.sol` +- `test/src/lib/op/logic/LibOpBinaryEqualTo.t.sol` +- `test/src/lib/op/logic/LibOpIsZero.t.sol` +- `test/src/lib/op/logic/LibOpIf.t.sol` +- `test/src/lib/op/logic/LibOpAny.t.sol` +- `test/src/lib/op/logic/LibOpEvery.t.sol` + +These 10 logic opcodes do not have a test verifying that the parser rejects an unexpected operand (e.g., `greater-than<0>(1 2)`). Most other opcodes include a `testOp*EvalOperandDisallowed` or `testOp*EvalBadOperand` test. Within the logic category itself, `LibOpConditions` and `LibOpEnsure` DO have this test. + +### P2-02 (LOW) Missing operand-disallowed tests for 5 math opcodes + +**Files affected:** +- `test/src/lib/op/math/LibOpSub.t.sol` +- `test/src/lib/op/math/LibOpMaxPositiveValue.t.sol` +- `test/src/lib/op/math/LibOpMaxNegativeValue.t.sol` +- `test/src/lib/op/math/LibOpMinPositiveValue.t.sol` +- `test/src/lib/op/math/LibOpMinNegativeValue.t.sol` + +These 5 math opcodes do not test that the parser rejects unexpected operands. All other math opcodes DO have this test. `LibOpSub` is an N-ary opcode that should reject operands like `sub<0>(1 2)`. The four value opcodes are 0-input opcodes that should reject operands like `max-positive-value<0>()`. + +### P2-03 (LOW) Missing operand-disallowed test for `LibOpHash` + +**File:** `test/src/lib/op/crypto/LibOpHash.t.sol` + +`LibOpHash` does not test that the parser rejects an unexpected operand (e.g., `hash<0>(0x00)`). The `hash` opcode's input count is driven by the number of parenthesized arguments, not by an explicit operand, so an explicit operand should be rejected. + +## Overall Assessment + +The opcode test suite is comprehensive. Virtually every opcode follows a thorough pattern: fuzz-tested `integrity`, fuzz-tested `run` via `opReferenceCheck`, end-to-end eval tests via parsed Rainlang, `checkBadInputs`/`checkBadOutputs` for incorrect I/O counts, and `checkUnhappyParse`/`checkDisallowedOperand` for operand rejection. The only systematic gap is the missing operand-disallowed tests for the subset identified above. diff --git a/audit/2026-03-01-01/pass2/LibParse.md b/audit/2026-03-01-01/pass2/LibParse.md new file mode 100644 index 000000000..ef3173d1d --- /dev/null +++ b/audit/2026-03-01-01/pass2/LibParse.md @@ -0,0 +1,30 @@ +# Pass 2: Test Coverage — LibParse, LibParseState + +**Audit:** 2026-03-01-01 +**Agent IDs:** A30, A43 + +## Findings + +### A30-P2-1 (MEDIUM) No test for total source ops count > 255 across multiple top-level items (A30) + +Related to Pass 1 finding A43-1. The source code in `endSource()` at lines 871-878 writes the total ops count into a single byte of the source prefix. If the total across all top-level items exceeds 255, the shifted value overflows bit 31 and corrupts the source length word. `LibBytecode.sourceOpsCount` reads only 1 byte (`byte(0, mload(pointer))`), so the count is silently truncated. + +**Test gap**: No test constructs a source with more than 255 total ops across multiple top-level items. Existing tests only cover the per-item limit: +- `testSourceItemOps255NoOverflow` / `testSourceItemOpsOverflow` in `overflow.t.sol` — per-item counter only +- `testPushOpToSourceItemOpsOverflow` in `pushOpToSource.t.sol` — per-item counter only +- `testBuildBytecodeFuzz` caps at 20 ops total per source +- `testEndSourceByteLengthFuzz` caps at 50 ops total per source + +With 2 top-level items of 128 ops each, the per-item counter never overflows (each stays at 128), but the total is 256 — overflowing the prefix byte. + +### A30-P2-2 (LOW) No test for `ParserOutOfBounds` error in `parse()` (A30) + +The `ParserOutOfBounds` error at line 438 in `LibParse.sol` guards against `cursor != end` after the main parse loop. No test triggers this error. This is likely unreachable defensive code (all character-reading paths check `cursor < end`), but defensive code deserves a test documenting the invariant. + +### A30-P2-3 (LOW) `testEndSourceByteLengthFuzz` upper bound too low (A30) + +The fuzz test in `LibParseState.endSource.t.sol` (line 104) bounds `opCount` to `[1, 50]`. This misses multi-slot linked-list edge cases beyond 49 ops (7 ops per slot = 7+ slot transitions) and does not approach the 255 ops prefix limit. Should be extended to at least 200. + +### A30-P2-4 (INFO) No integration test for stack-name-only RHS with paren state + +Stack name references work correctly inside paren groups (tested indirectly via `testParseNamedLHSStackIndex`), but no test explicitly verifies the `highwater()` call path when a stack name appears as a paren input. Low impact — the code path is covered indirectly. diff --git a/audit/2026-03-01-01/pass2/LibParseUtilities.md b/audit/2026-03-01-01/pass2/LibParseUtilities.md new file mode 100644 index 000000000..6234b96c9 --- /dev/null +++ b/audit/2026-03-01-01/pass2/LibParseUtilities.md @@ -0,0 +1,34 @@ +# Pass 2: Test Coverage — Parse Utilities and Literals + +**Audit:** 2026-03-01-01 +**Agent IDs:** A32, A42, A44 + +## Findings + +### A32-1 (LOW) `skipComment` — no test for `UnclosedComment` when comment is well-formed but never closed (A32) + +Source: `src/lib/parse/LibParseInterstitial.sol`, line 83. + +`skipComment` has two distinct revert paths for `UnclosedComment`: +1. Line 40: `cursor + 4 > end` — the comment cannot fit. Tested by `testSkipCommentTooShort` and `testSkipCommentThreeBytes`. +2. Line 83: `!foundEnd` — the `/*` is well-formed, data >= 4 bytes, but `*/` is never found. **Not tested.** + +The existing tests cover the "too short" path (< 4 bytes). There is no test with data like `"/* no end here"` where the comment opens validly but never closes. + +### A32-2 (LOW) `skipComment` — no fuzz test for well-formed comments (A32) + +Source: `src/lib/parse/LibParseInterstitial.sol`, lines 58-79. + +All `skipComment` tests use hardcoded strings (`"/**/"`, `"/* hello world */"`). No fuzz test generates arbitrary comment content to verify the cursor lands correctly. A fuzz test would exercise the byte-scanning loop with diverse content including `*` characters inside comments that don't form `*/`. + +### A42-1 (LOW) `pushInputs` — no test for push-overflow-inside-pushInputs (A42) + +Source: `src/lib/parse/LibParseStackTracker.sol`, line 21. + +`pushInputs` calls `push(n)` internally, which can revert with `ParseStackOverflow` if `current + n > 0xFF`. The test `testPushInputsOverflow` only tests the `inputs` field overflow (line 24), setting up the tracker with `current = 0`. No test exercises the case where `push(n)` overflows because `current` is already non-zero (e.g., `current=200`, `pushInputs(100)` should revert from the inner `push`). + +### A44-1 (LOW) `subParseWordSlice` — no test for no-sub-parsers-registered path (A44) + +Source: `src/lib/parse/LibSubParse.sol`, line 224. + +`subParseWordSlice` iterates over sub parsers in `while (deref != 0)` at line 224. If `state.subParsers` is zero, the loop body never executes and the code falls through to `UnknownWord` revert. All existing unknown-word tests register at least one sub parser. No test covers an `OPCODE_UNKNOWN` opcode with zero registered sub parsers. diff --git a/audit/2026-03-01-01/pass2/RustCrates.md b/audit/2026-03-01-01/pass2/RustCrates.md new file mode 100644 index 000000000..ccb13dc17 --- /dev/null +++ b/audit/2026-03-01-01/pass2/RustCrates.md @@ -0,0 +1,49 @@ +# Pass 2: Test Coverage — Rust Crates + +**Audit:** 2026-03-01-01 +**Agent ID:** R02 + +## Findings + +### R02-PASS2-01 (LOW) No tests for error paths in `Forker` methods (R02) + +**Files:** `crates/eval/src/fork.rs`, `crates/eval/src/error.rs` + +The `Forker` methods (`alloy_call`, `alloy_call_committing`, `call`, `call_committing`, `roll_fork`, `replay_transaction`) have numerous error return paths with only happy paths tested. Key untested paths: + +1. `call()` / `call_committing()` with invalid address length (lines 320-322, 349-351) +2. `alloy_call()` revert with `decode_error: true` (line 245-250) +3. `alloy_call()` non-ok non-revert exit (line 252-254) +4. `alloy_call()` / `alloy_call_committing()` ABI decode failure (lines 256-263, 301-303) +5. `roll_fork()` with no active fork (line 381) +6. `replay_transaction()` error paths: no active fork, tx not found, no block number, DB errors +7. `replay_transaction()` with `TxKind::Create` (line 494-496) + +### R02-PASS2-02 (LOW) `Forker::new()` has no test (R02) + +**File:** `crates/eval/src/fork.rs`, lines 60-68 + +`Forker::new()` creates an empty forker without any fork. No test calls this constructor. The `add_or_select()` `self.forks.is_empty()` branch (line 153) that handles an initially-empty forker is also untested. + +### R02-PASS2-04 (LOW) `RainSourceTrace::from_data()` edge cases untested (R02) + +**File:** `crates/eval/src/trace.rs`, lines 28-55 + +Edge cases with no test: data < 4 bytes (returns `None`), exactly 4 bytes (empty stack), trailing partial word (silently dropped). A trace with 35 bytes (4 header + 31 payload) produces an empty stack, silently losing the partial word. + +### R02-PASS2-07 (LOW) CLI `Parse` command entirely untested (R02) + +**File:** `crates/cli/src/commands/parse.rs` + +No tests at all. Asymmetric with `Eval` command which has `test_execute`. + +## Summary + +| ID | Severity | Crate | Description | +|----|----------|-------|-------------| +| R02-PASS2-01 | LOW | eval | Error paths in `Forker` methods have no tests | +| R02-PASS2-02 | LOW | eval | `Forker::new()` and empty-forks `add_or_select` untested | +| R02-PASS2-04 | LOW | eval | `RainSourceTrace::from_data()` edge cases untested | +| R02-PASS2-07 | LOW | cli | CLI `Parse` command entirely untested | + +**Overall:** 4 LOW, 6 INFO (INFO findings omitted from this report). Happy paths are well-tested. The primary gap is systematic: error paths and edge cases are largely untested, concentrated in the `eval` crate's `Forker` methods. diff --git a/audit/2026-03-01-01/pass3/CoreConcrete.md b/audit/2026-03-01-01/pass3/CoreConcrete.md new file mode 100644 index 000000000..57f14620e --- /dev/null +++ b/audit/2026-03-01-01/pass3/CoreConcrete.md @@ -0,0 +1,165 @@ +# Pass 3: Documentation — Core Concrete Contracts + +**Audit:** 2026-03-01-01 + +## Scope + +| File | Lines | +|---|---| +| `src/concrete/Rainterpreter.sol` | 81 | +| `src/concrete/RainterpreterStore.sol` | 69 | +| `src/concrete/RainterpreterParser.sol` | 115 | +| `src/concrete/RainterpreterExpressionDeployer.sol` | 81 | +| `src/concrete/RainterpreterDISPaiRegistry.sol` | 40 | +| `src/interface/IDISPaiRegistry.sol` | 25 | + +## Evidence of Review + +### Rainterpreter.sol + +- **Contract:** `Rainterpreter` (line 32), inherits `IInterpreterV4`, `IOpcodeToolingV1`, `ERC165` +- **Contract-level NatSpec:** `@title` (line 29), `@notice` (line 30-31). Correct. +- **Constructor:** line 38-40. Doc at line 36-37, untagged `///` only, no explicit tags -- valid (implicit `@notice`). +- **`opcodeFunctionPointers()`** internal view virtual (line 45). Doc at lines 42-44: untagged description (42-43) + `@return` (44). +- **`eval4(EvalV4)`** external view virtual override (line 50). `@inheritdoc IInterpreterV4` (line 49). Correct. +- **`supportsInterface(bytes4)`** public view virtual override (line 73). `@inheritdoc ERC165` (line 72). Correct. +- **`buildOpcodeFunctionPointers()`** public view virtual override (line 78). `@inheritdoc IOpcodeToolingV1` (line 77). Correct. + +### RainterpreterStore.sol + +- **Contract:** `RainterpreterStore` (line 25), inherits `IInterpreterStoreV3`, `ERC165` +- **Contract-level NatSpec:** `@title` (line 19), `@notice` (line 20-24). Correct. +- **`sStore` mapping** (line 40). Doc at lines 28-37, untagged `///` only -- valid (implicit `@notice`). +- **`supportsInterface(bytes4)`** public view virtual override (line 43). `@inheritdoc ERC165` (line 42). Correct. +- **`set(StateNamespace, bytes32[])`** external virtual (line 48). `@inheritdoc IInterpreterStoreV3` (line 47). Correct. +- **`get(FullyQualifiedNamespace, bytes32)`** external view virtual (line 66). `@inheritdoc IInterpreterStoreV3` (line 65). Correct. + +### RainterpreterParser.sol + +- **Contract:** `RainterpreterParser` (line 36), inherits `ERC165`, `IParserToolingV1` +- **Contract-level NatSpec:** `@title` (line 30), `@notice` (line 31), `@dev` (line 32-35). All tagged. Correct. +- **Modifier `checkParseMemoryOverflow`** (line 46). Doc at lines 43-45, untagged `///` only -- valid (implicit `@notice`). +- **`unsafeParse(bytes)`** external view (line 57). Doc at lines 51-56: `@notice` (51-53), `@param data` (54), two `@return` (55-56). Correct. +- **`supportsInterface(bytes4)`** public view virtual override (line 71). `@inheritdoc ERC165` (line 70). Correct. +- **`parsePragma1(bytes)`** external view virtual (line 79). Doc at lines 75-78: `@notice` (75-76), `@param data` (77), `@return` (78). Correct. +- **`parseMeta()`** internal pure virtual (line 92). Doc at line 91, untagged `///` only, no `@return`. +- **`operandHandlerFunctionPointers()`** internal pure virtual (line 97). Doc at line 96, untagged `///` only, no `@return`. +- **`literalParserFunctionPointers()`** internal pure virtual (line 102). Doc at line 101, untagged `///` only, no `@return`. +- **`buildOperandHandlerFunctionPointers()`** external pure override (line 107). Doc at line 106, bare `///`, no `@inheritdoc`. +- **`buildLiteralParserFunctionPointers()`** external pure override (line 112). Doc at line 111, bare `///`, no `@inheritdoc`. + +### RainterpreterExpressionDeployer.sol + +- **Contract:** `RainterpreterExpressionDeployer` (line 26), inherits `IDescribedByMetaV1`, `IParserV2`, `IParserPragmaV1`, `IIntegrityToolingV1`, `ERC165` +- **Contract-level NatSpec:** `@title` (line 23), `@notice` (line 24-25). Correct. +- **`supportsInterface(bytes4)`** public view virtual override (line 34). `@inheritdoc ERC165` (line 33). Correct. +- **`parse2(bytes)`** external view virtual override (line 41). `@inheritdoc IParserV2` (line 40). Correct. +- **`parsePragma1(bytes calldata)`** external view virtual override (line 66). `@notice` (line 63-64) + `@inheritdoc IParserPragmaV1` (line 65). Correct. +- **`buildIntegrityFunctionPointers()`** external view virtual (line 73). `@inheritdoc IIntegrityToolingV1` (line 72). Correct. +- **`describedByMetaV1()`** external pure override (line 78). `@inheritdoc IDescribedByMetaV1` (line 77). Correct. + +### RainterpreterDISPaiRegistry.sol + +- **Contract:** `RainterpreterDISPaiRegistry` (line 15), inherits `IDISPaiRegistry`, `ERC165` +- **Contract-level NatSpec:** `@title` (line 9), `@notice` (line 10-14). Correct. +- **`supportsInterface(bytes4)`** public view override (line 17). `@inheritdoc ERC165` (line 16). Correct. +- **`expressionDeployerAddress()`** external pure override (line 22). `@inheritdoc IDISPaiRegistry` (line 21). Correct. +- **`interpreterAddress()`** external pure override (line 27). `@inheritdoc IDISPaiRegistry` (line 26). Correct. +- **`storeAddress()`** external pure override (line 32). `@inheritdoc IDISPaiRegistry` (line 31). Correct. +- **`parserAddress()`** external pure override (line 37). `@inheritdoc IDISPaiRegistry` (line 36). Correct. + +### IDISPaiRegistry.sol + +- **Interface:** `IDISPaiRegistry` (line 9) +- **Interface-level NatSpec:** `@title` (line 5), `@notice` (line 6-8). Correct. +- **`expressionDeployerAddress()`** (line 12). Doc at lines 10-11: untagged line (10) + `@return` (11). +- **`interpreterAddress()`** (line 16). Doc at lines 14-15: untagged line (14) + `@return` (15). +- **`storeAddress()`** (line 20). Doc at lines 18-19: untagged line (18) + `@return` (19). +- **`parserAddress()`** (line 24). Doc at lines 22-23: untagged line (22) + `@return` (23). + +## Findings + +### P3-CC-01 (LOW) `Rainterpreter.opcodeFunctionPointers` mixed tagged/untagged NatSpec + +**File:** `src/concrete/Rainterpreter.sol`, lines 42-44 + +The doc block for `opcodeFunctionPointers()` has untagged description lines (42-43) followed by a `@return` tag (44). Per project convention, when any explicit tag is present in a doc block, all entries must be explicitly tagged. The untagged lines are treated as continuation of the previous tag or (when first) as `@notice` by the compiler, but the project convention requires an explicit `@notice` tag. + +```solidity +/// Returns the packed 2-byte function pointer table used by the eval loop +/// to dispatch each opcode. Virtual so subclasses can override the table. +/// @return The opcode function pointers for the interpreter. +``` + +Should be: + +```solidity +/// @notice Returns the packed 2-byte function pointer table used by the eval loop +/// to dispatch each opcode. Virtual so subclasses can override the table. +/// @return The opcode function pointers for the interpreter. +``` + +### P3-CC-02 (LOW) `RainterpreterParser` override functions missing `@inheritdoc` + +**File:** `src/concrete/RainterpreterParser.sol`, lines 106-113 + +`buildOperandHandlerFunctionPointers()` (line 107) and `buildLiteralParserFunctionPointers()` (line 112) are `override` implementations of `IParserToolingV1` but use bare `///` comments instead of `@inheritdoc IParserToolingV1`. Every other override function across all six audited contracts consistently uses `@inheritdoc`. The interface `IParserToolingV1` already has full NatSpec for both functions. + +```solidity +/// External function to build the operand handler function pointers. +function buildOperandHandlerFunctionPointers() external pure override returns (bytes memory) { +``` + +Should be: + +```solidity +/// @inheritdoc IParserToolingV1 +function buildOperandHandlerFunctionPointers() external pure override returns (bytes memory) { +``` + +(Same for `buildLiteralParserFunctionPointers`.) + +### P3-CC-03 (LOW) `RainterpreterParser` internal virtual functions missing `@return` tags + +**File:** `src/concrete/RainterpreterParser.sol`, lines 91-103 + +Three internal virtual functions (`parseMeta`, `operandHandlerFunctionPointers`, `literalParserFunctionPointers`) each return `bytes memory` but have only a bare `///` description with no `@return` tag. While they are `internal`, their `virtual` visibility means subclasses must understand the return value contract. + +```solidity +/// Virtual function to return the parse meta. +function parseMeta() internal pure virtual returns (bytes memory) { +``` + +Should be: + +```solidity +/// @notice Virtual function to return the parse meta. +/// @return The parse meta bytes used to initialize parser state. +function parseMeta() internal pure virtual returns (bytes memory) { +``` + +(Same pattern for `operandHandlerFunctionPointers` and `literalParserFunctionPointers`.) + +### P3-CC-04 (LOW) `IDISPaiRegistry` all four functions have mixed tagged/untagged NatSpec + +**File:** `src/interface/IDISPaiRegistry.sol`, lines 10-24 + +All four interface functions (`expressionDeployerAddress`, `interpreterAddress`, `storeAddress`, `parserAddress`) have an untagged description line followed by a `@return` tag. Per project convention, when any explicit tag is present, all entries must be explicitly tagged. + +Example (line 10-11): +```solidity +/// Returns the deterministic deploy address of the expression deployer. +/// @return The expression deployer address. +``` + +Should be: +```solidity +/// @notice Returns the deterministic deploy address of the expression deployer. +/// @return The expression deployer address. +``` + +### P3-CC-05 (INFO) `ZeroFunctionPointers` error inconsistent NatSpec style + +**File:** `src/error/ErrEval.sol`, lines 13-15 + +`ZeroFunctionPointers` uses bare `///` with no `@notice` tag, while `InputsLengthMismatch` in the same file (line 8) uses `@notice`. Both are valid NatSpec (no tags means implicit `@notice`), but the inconsistency within a single file is a style issue. diff --git a/audit/2026-03-01-01/pass3/ErrAll.md b/audit/2026-03-01-01/pass3/ErrAll.md new file mode 100644 index 000000000..525ae223c --- /dev/null +++ b/audit/2026-03-01-01/pass3/ErrAll.md @@ -0,0 +1,169 @@ +# Pass 3: NatSpec Documentation Review -- Error Definition Files + +## Scope + +All 10 error definition files in `src/error/`: + +| File | Errors | Status | +|------|--------|--------| +| `ErrBitwise.sol` | 3 | 1 finding | +| `ErrDeploy.sol` | 1 | Clean | +| `ErrEval.sol` | 2 | 1 finding | +| `ErrExtern.sol` | 4 | 1 finding | +| `ErrIntegrity.sol` | 8 | Clean | +| `ErrOpList.sol` | 1 | Clean | +| `ErrParse.sol` | 36 | 1 finding (13 errors) | +| `ErrRainType.sol` | 1 | Clean | +| `ErrStore.sol` | 1 | Clean | +| `ErrSubParse.sol` | 4 | Clean | + +## Evidence + +### `src/error/ErrBitwise.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 13 | `UnsupportedBitwiseShiftAmount` | `uint256 shiftAmount` | Yes | Yes | +| 19 | `TruncatedBitwiseEncoding` | `uint256 startBit, uint256 length` | Yes | Yes | +| 23 | `ZeroLengthBitwiseEncoding` | (none) | **No** | N/A | + +Line 21-22: Doc block uses `///` without `@notice`. Sibling errors at lines 8 and 15 use `@notice`. + +### `src/error/ErrDeploy.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 11 | `UnknownDeploymentSuite` | `bytes32 suite` | Yes | Yes | + +Clean. + +### `src/error/ErrEval.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 11 | `InputsLengthMismatch` | `uint256 expected, uint256 actual` | Yes | Yes | +| 15 | `ZeroFunctionPointers` | (none) | **No** | N/A | + +Line 13-14: Doc block uses `///` without `@notice`. Sibling error at line 8 uses `@notice`. + +### `src/error/ErrExtern.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 14 | `ExternOpcodeOutOfRange` | `uint256 opcode, uint256 fsCount` | Yes | Yes | +| 20 | `ExternPointersMismatch` | `uint256 opcodeCount, uint256 integrityCount` | Yes | Yes | +| 25 | `BadOutputsLength` | `uint256 expectedLength, uint256 actualLength` | Yes | Yes | +| 28 | `ExternOpcodePointersEmpty` | (none) | **No** | N/A | + +Line 27: Doc block uses `///` without `@notice`. Sibling errors at lines 10, 16, 22 use `@notice`. + +### `src/error/ErrIntegrity.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 12 | `StackUnderflow` | `uint256 opIndex, uint256 stackIndex, uint256 calculatedInputs` | Yes | Yes | +| 18 | `StackUnderflowHighwater` | `uint256 opIndex, uint256 stackIndex, uint256 stackHighwater` | Yes | Yes | +| 24 | `StackAllocationMismatch` | `uint256 stackMaxIndex, uint256 bytecodeAllocation` | Yes | Yes | +| 29 | `StackOutputsMismatch` | `uint256 stackIndex, uint256 bytecodeOutputs` | Yes | Yes | +| 35 | `OutOfBoundsConstantRead` | `uint256 opIndex, uint256 constantsLength, uint256 constantRead` | Yes | Yes | +| 41 | `OutOfBoundsStackRead` | `uint256 opIndex, uint256 stackTopIndex, uint256 stackRead` | Yes | Yes | +| 47 | `CallOutputsExceedSource` | `uint256 sourceOutputs, uint256 outputs` | Yes | Yes | +| 53 | `OpcodeOutOfRange` | `uint256 opIndex, uint256 opcodeIndex, uint256 fsCount` | Yes | Yes | + +Clean. + +### `src/error/ErrOpList.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 12 | `BadDynamicLength` | `uint256 dynamicLength, uint256 standardOpsLength` | Yes | Yes | + +Clean. + +### `src/error/ErrParse.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 10 | `UnexpectedOperand` | (none) | **No** | N/A | +| 14 | `UnexpectedOperandValue` | (none) | **No** | N/A | +| 18 | `ExpectedOperand` | (none) | **No** | N/A | +| 23 | `OperandValuesOverflow` | `uint256 offset` | Yes | Yes | +| 27 | `UnclosedOperand` | `uint256 offset` | Yes | Yes | +| 31 | `UnsupportedLiteralType` | `uint256 offset` | Yes | Yes | +| 35 | `StringTooLong` | `uint256 offset` | Yes | Yes | +| 40 | `UnclosedStringLiteral` | `uint256 offset` | Yes | Yes | +| 44 | `HexLiteralOverflow` | `uint256 offset` | Yes | Yes | +| 48 | `ZeroLengthHexLiteral` | `uint256 offset` | Yes | Yes | +| 52 | `OddLengthHexLiteral` | `uint256 offset` | Yes | Yes | +| 56 | `MalformedHexLiteral` | `uint256 offset` | Yes | Yes | +| 60 | `MissingFinalSemi` | `uint256 offset` | Yes | Yes | +| 64 | `UnexpectedLHSChar` | `uint256 offset` | Yes | Yes | +| 68 | `UnexpectedRHSChar` | `uint256 offset` | Yes | Yes | +| 73 | `ExpectedLeftParen` | `uint256 offset` | Yes | Yes | +| 77 | `UnexpectedRightParen` | `uint256 offset` | Yes | Yes | +| 81 | `UnclosedLeftParen` | `uint256 offset` | Yes | Yes | +| 85 | `UnexpectedComment` | `uint256 offset` | Yes | Yes | +| 89 | `UnclosedComment` | `uint256 offset` | Yes | Yes | +| 93 | `MalformedCommentStart` | `uint256 offset` | Yes | Yes | +| 98 | `DuplicateLHSItem` | `uint256 offset` | Yes | Yes | +| 102 | `ExcessLHSItems` | `uint256 offset` | Yes | Yes | +| 106 | `NotAcceptingInputs` | `uint256 offset` | Yes | Yes | +| 110 | `ExcessRHSItems` | `uint256 offset` | Yes | Yes | +| 114 | `WordSize` | `string word` | Yes | Yes | +| 118 | `UnknownWord` | `string word` | Yes | Yes | +| 121 | `MaxSources` | (none) | **No** | N/A | +| 124 | `DanglingSource` | (none) | **No** | N/A | +| 127 | `ParserOutOfBounds` | (none) | **No** | N/A | +| 131 | `ParseStackOverflow` | (none) | **No** | N/A | +| 134 | `ParseStackUnderflow` | (none) | **No** | N/A | +| 138 | `ParenOverflow` | (none) | **No** | N/A | +| 163 | `OpcodeIOOverflow` | `uint256 offset` | Yes | Yes | +| 166 | `OperandOverflow` | (none) | **No** | N/A | +| 171 | `ParseMemoryOverflow` | `uint256 freeMemoryPointer` | Yes | Yes | +| 175 | `SourceItemOpsOverflow` | (none) | **No** | N/A | +| 179 | `ParenInputOverflow` | (none) | **No** | N/A | +| 183 | `LineRHSItemsOverflow` | (none) | **No** | N/A | + +13 errors use `///` without `@notice` while 23 sibling errors in the same file use `@notice`. + +### `src/error/ErrRainType.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 12 | `NotAnAddress` | `uint256 value` | Yes | Yes | + +Clean. + +### `src/error/ErrStore.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 10 | `OddSetLength` | `uint256 length` | Yes | Yes | + +Clean. + +### `src/error/ErrSubParse.sol` + +| Line | Error | Params | `@notice` | `@param` | +|------|-------|--------|-----------|----------| +| 11 | `ExternDispatchConstantsHeightOverflow` | `uint256 constantsHeight` | Yes | Yes | +| 16 | `ConstantOpcodeConstantsHeightOverflow` | `uint256 constantsHeight` | Yes | Yes | +| 21 | `ContextGridOverflow` | `uint256 column, uint256 row` | Yes | Yes | +| 27 | `SubParserIndexOutOfBounds` | `uint256 index, uint256 length` | Yes | Yes | + +Clean. + +## Findings + +### P3-ERR-1 (LOW): Missing `@notice` tags on 16 errors across 4 files + +**Files and lines:** + +- `src/error/ErrBitwise.sol` line 21: `ZeroLengthBitwiseEncoding` +- `src/error/ErrEval.sol` line 13: `ZeroFunctionPointers` +- `src/error/ErrExtern.sol` line 27: `ExternOpcodePointersEmpty` +- `src/error/ErrParse.sol` lines 8, 12, 16, 120, 123, 126, 129, 133, 136, 165, 173, 177, 181: `UnexpectedOperand`, `UnexpectedOperandValue`, `ExpectedOperand`, `MaxSources`, `DanglingSource`, `ParserOutOfBounds`, `ParseStackOverflow`, `ParseStackUnderflow`, `ParenOverflow`, `OperandOverflow`, `SourceItemOpsOverflow`, `ParenInputOverflow`, `LineRHSItemsOverflow` + +**Convention violated:** Per CLAUDE.md: "when a doc block contains any explicit tag (e.g. `@title`), all entries must be explicitly tagged -- untagged lines continue the previous tag, not implicit `@notice`." While these errors use plain `///` (no tags at all), sibling errors in each of these four files use `@notice`. The mix of tagged and untagged doc styles within a single file is inconsistent and could cause tooling to render the documentation differently depending on which NatSpec interpretation is used. + +**Fix:** Add `@notice` to all 16 doc blocks. See `.fixes/P3-ERR-1.md`. diff --git a/audit/2026-03-01-01/pass3/ExternAbstract.md b/audit/2026-03-01-01/pass3/ExternAbstract.md new file mode 100644 index 000000000..f05dc55bc --- /dev/null +++ b/audit/2026-03-01-01/pass3/ExternAbstract.md @@ -0,0 +1,226 @@ +# Pass 3 — NatSpec Documentation Audit: Extern & Abstract Contracts + +Auditor: Claude Opus 4.6 +Date: 2026-03-01 +Scope: 10 files covering extern abstract bases, reference extern, LibExtern, and reference op/literal libraries. + +--- + +## File Inventory and Evidence of Thorough Reading + +### 1. `src/abstract/BaseRainterpreterExtern.sol` (131 lines) + +**Contract:** `BaseRainterpreterExtern` (abstract) +**Constants (file-level):** +- `OPCODE_FUNCTION_POINTERS` (line 20) — `@dev` documented +- `INTEGRITY_FUNCTION_POINTERS` (line 24) — `@dev` documented + +**Functions:** +- `constructor()` (line 34) — NatSpec present (untagged, defaults to `@notice`) +- `extern(ExternDispatchV2, StackItem[])` (line 46) — `@inheritdoc IInterpreterExternV4` +- `externIntegrity(ExternDispatchV2, uint256, uint256)` (line 83) — `@inheritdoc IInterpreterExternV4` +- `supportsInterface(bytes4)` (line 112) — `@inheritdoc ERC165` +- `opcodeFunctionPointers()` (line 121) — NatSpec present (untagged) +- `integrityFunctionPointers()` (line 128) — NatSpec present (untagged) + +### 2. `src/abstract/BaseRainterpreterSubParser.sol` (220 lines) + +**Contract:** `BaseRainterpreterSubParser` (abstract) +**Constants (file-level):** +- `SUB_PARSER_WORD_PARSERS` (line 26) — `@dev` documented +- `SUB_PARSER_PARSE_META` (line 32) — `@dev` documented +- `SUB_PARSER_OPERAND_HANDLERS` (line 36) — `@dev` documented +- `SUB_PARSER_LITERAL_PARSERS` (line 40) — `@dev` documented + +**Functions:** +- `subParserParseMeta()` (line 93) — NatSpec present (untagged) +- `subParserWordParsers()` (line 100) — NatSpec present (untagged) +- `subParserOperandHandlers()` (line 107) — NatSpec present (untagged) +- `subParserLiteralParsers()` (line 114) — NatSpec present (untagged) +- `matchSubParseLiteralDispatch(uint256, uint256)` (line 139) — `@notice`, `@param cursor`, `@param end`, `@return success`, `@return index`, `@return value` all present +- `subParseLiteral2(bytes)` (line 159) — `@notice` + `@inheritdoc ISubParserV4` +- `subParseWord2(bytes)` (line 188) — `@notice` + `@inheritdoc ISubParserV4` +- `supportsInterface(bytes4)` (line 215) — `@inheritdoc ERC165` + +### 3. `src/concrete/extern/RainterpreterReferenceExtern.sol` (427 lines) + +**Library:** `LibRainterpreterReferenceExtern` +**Contract:** `RainterpreterReferenceExtern` +**Constants (file-level):** +- `SUB_PARSER_WORD_PARSERS_LENGTH` (line 46) — `@dev` documented +- `SUB_PARSER_LITERAL_PARSERS_LENGTH` (line 49) — `@dev` documented +- `SUB_PARSER_LITERAL_REPEAT_KEYWORD` (line 53) — `@dev` documented +- `SUB_PARSER_LITERAL_REPEAT_KEYWORD_BYTES32` (line 58) — `@dev` documented +- `SUB_PARSER_LITERAL_REPEAT_KEYWORD_BYTES_LENGTH` (line 61) — `@dev` documented +- `SUB_PARSER_LITERAL_REPEAT_KEYWORD_MASK` (line 65) — `@dev` documented +- `SUB_PARSER_LITERAL_REPEAT_INDEX` (line 71) — `@dev` documented +- `OPCODE_FUNCTION_POINTERS_LENGTH` (line 77) — `@dev` documented + +**Errors:** +- `InvalidRepeatCount` (line 74) — `@dev` documented + +**Library functions:** +- `LibRainterpreterReferenceExtern.authoringMetaV2()` (line 93) — NatSpec present (untagged) + +**Contract functions:** +- `describedByMetaV1()` (line 161) — `@inheritdoc IDescribedByMetaV1` +- `subParserParseMeta()` (line 168) — NatSpec present (untagged) +- `subParserWordParsers()` (line 175) — NatSpec present (untagged) +- `subParserOperandHandlers()` (line 182) — NatSpec present (untagged) +- `subParserLiteralParsers()` (line 189) — NatSpec present (untagged) +- `opcodeFunctionPointers()` (line 196) — NatSpec present (untagged) +- `integrityFunctionPointers()` (line 203) — NatSpec present (untagged) +- `buildLiteralParserFunctionPointers()` (line 209) — `@notice` + `@inheritdoc IParserToolingV1` +- `matchSubParseLiteralDispatch(uint256, uint256)` (line 232) — `@inheritdoc BaseRainterpreterSubParser` +- `buildOperandHandlerFunctionPointers()` (line 275) — `@notice` + `@inheritdoc IParserToolingV1` +- `buildSubParserWordParsers()` (line 318) — `@notice` + `@inheritdoc ISubParserToolingV1` +- `buildOpcodeFunctionPointers()` (line 358) — NatSpec present (untagged, no `@return`) +- `buildIntegrityFunctionPointers()` (line 390) — NatSpec present (untagged, no `@return`) +- `supportsInterface(bytes4)` (line 418) — `@notice` + `@inheritdoc BaseRainterpreterSubParser` + +### 4. `src/lib/extern/LibExtern.sol` (80 lines) + +**Library:** `LibExtern` +- `@title` and `@notice` present on library + +**Functions:** +- `encodeExternDispatch(uint256, OperandV2)` (line 27) — `@notice`, `@param opcode`, `@param operand`, `@return` all present +- `decodeExternDispatch(ExternDispatchV2)` (line 35) — `@notice`, `@param dispatch`, `@return` x2 present +- `encodeExternCall(IInterpreterExternV4, ExternDispatchV2)` (line 56) — `@notice`, `@param extern`, `@param dispatch`, `@return` all present +- `decodeExternCall(EncodedExternDispatchV2)` (line 70) — `@notice`, `@param dispatch`, `@return` x2 present + +### 5. `src/lib/extern/reference/literal/LibParseLiteralRepeat.sol` (73 lines) + +**Library:** `LibParseLiteralRepeat` +- `@title` and `@notice` present on library + +**Constants:** +- `MAX_REPEAT_LITERAL_LENGTH` (line 34) — `@dev` documented + +**Errors:** +- `RepeatLiteralTooLong(uint256)` (line 39) — `@dev` + `@param` documented +- `RepeatDispatchNotDigit(uint256)` (line 43) — `@dev` + `@param` documented + +**Functions:** +- `parseRepeat(uint256, uint256, uint256)` (line 53) — `@notice`, `@param dispatchValue`, `@param cursor`, `@param end`, `@return` all present + +### 6. `src/lib/extern/reference/op/LibExternOpContextCallingContract.sol` (23 lines) + +**Library:** `LibExternOpContextCallingContract` +- `@title` and `@notice` present + +**Functions:** +- `subParser(uint256, uint256, OperandV2)` (line 19) — NatSpec present (untagged), no `@param` or `@return` + +### 7. `src/lib/extern/reference/op/LibExternOpContextRainlen.sol` (22 lines) + +**Library:** `LibExternOpContextRainlen` +- `@title` and `@notice` present + +**Constants:** +- `CONTEXT_CALLER_CONTEXT_COLUMN` (line 8) — no NatSpec +- `CONTEXT_CALLER_CONTEXT_ROW_RAINLEN` (line 9) — no NatSpec + +**Functions:** +- `subParser(uint256, uint256, OperandV2)` (line 18) — NatSpec present (untagged), no `@param` or `@return` + +### 8. `src/lib/extern/reference/op/LibExternOpContextSender.sol` (21 lines) + +**Library:** `LibExternOpContextSender` +- `@title` and `@notice` present + +**Functions:** +- `subParser(uint256, uint256, OperandV2)` (line 17) — NatSpec present (untagged), no `@param` or `@return` + +### 9. `src/lib/extern/reference/op/LibExternOpIntInc.sol` (67 lines) + +**Library:** `LibExternOpIntInc` +- `@title` and `@notice` present + +**Constants:** +- `OP_INDEX_INCREMENT` (line 13) — `@dev` documented + +**Functions:** +- `run(OperandV2, StackItem[])` (line 27) — `@notice`, `@param inputs`, `@return` present +- `integrity(OperandV2, uint256, uint256)` (line 44) — `@notice`, `@param inputs`, `@return` x2 present +- `subParser(uint256, uint256, OperandV2)` (line 57) — `@notice`, `@param constantsHeight`, `@param ioByte`, `@param operand`, `@return` x3 present + +### 10. `src/lib/extern/reference/op/LibExternOpStackOperand.sol` (31 lines) + +**Library:** `LibExternOpStackOperand` +- `@title` and `@notice` present + +**Functions:** +- `subParser(uint256, uint256, OperandV2)` (line 23) — `@notice`, `@param constantsHeight`, `@param operand`, `@return` x3 present + +--- + +## Findings + +### P3-EA-01 [LOW] — `opcodeFunctionPointers` NatSpec says "word dispatches" instead of "opcode dispatches" + +**File:** `src/abstract/BaseRainterpreterExtern.sol`, line 118-119 +**Description:** The NatSpec for `opcodeFunctionPointers()` reads "Overrideable function to provide the list of function pointers for word dispatches." The term "word dispatches" is the parser-side terminology. This function provides pointers for **opcode** dispatches at eval time (used in `extern()`). The sister function `integrityFunctionPointers` correctly says "integrity checks." Using "word dispatches" here is misleading and could confuse readers about the function's purpose. +**Fix:** Change "word dispatches" to "opcode dispatches" or "extern opcode dispatches." + +### P3-EA-02 [LOW] — `buildOpcodeFunctionPointers` and `buildIntegrityFunctionPointers` missing `@return` tags + +**File:** `src/concrete/extern/RainterpreterReferenceExtern.sol`, lines 350-358 and 382-390 +**Description:** Both `buildOpcodeFunctionPointers()` and `buildIntegrityFunctionPointers()` are external functions returning `bytes memory`. Their NatSpec comments describe their purpose but lack `@return` tags documenting the return value. Per conventions, all public/external functions need `@return` tags. Other build functions on the same contract (`buildLiteralParserFunctionPointers`, `buildOperandHandlerFunctionPointers`, `buildSubParserWordParsers`) use `@inheritdoc` to pull interface documentation which includes return semantics. +**Fix:** Add `@return` tags, or add `@inheritdoc IOpcodeToolingV1` / `@inheritdoc IIntegrityToolingV1` directives. + +### P3-EA-03 [LOW] — `subParser` functions on context op libraries missing `@param` and `@return` tags + +**Files:** +- `src/lib/extern/reference/op/LibExternOpContextCallingContract.sol`, line 19 +- `src/lib/extern/reference/op/LibExternOpContextRainlen.sol`, line 18 +- `src/lib/extern/reference/op/LibExternOpContextSender.sol`, line 17 + +**Description:** The `subParser(uint256, uint256, OperandV2)` function in each of these three libraries has a plain `///` comment but no `@param` or `@return` NatSpec tags. The sibling implementations in `LibExternOpIntInc.subParser` and `LibExternOpStackOperand.subParser` fully document all parameters and return values. These three functions have unnamed parameters which makes `@param` tags impossible without first naming them, but the `@return` tags and parameter naming are both needed for documentation completeness. +**Fix:** Name the parameters (e.g. `constantsHeight`, `ioByte`, `operand` to match the pattern in `LibExternOpIntInc`) and add `@param` and `@return` tags. + +### P3-EA-04 [LOW] — Undocumented constants in `LibExternOpContextRainlen.sol` + +**File:** `src/lib/extern/reference/op/LibExternOpContextRainlen.sol`, lines 8-9 +**Description:** The file-level constants `CONTEXT_CALLER_CONTEXT_COLUMN` and `CONTEXT_CALLER_CONTEXT_ROW_RAINLEN` have no NatSpec documentation. By contrast, the equivalent constants in `LibExternOpContextCallingContract.sol` and `LibExternOpContextSender.sol` are imported from the interface library (`LibContext.sol`) where they are presumably documented. These locally-defined constants should have `@dev` documentation explaining what context grid position they reference. +**Fix:** Add `@dev` NatSpec to both constants. + +### P3-EA-05 [LOW] — Typo in `SUB_PARSER_LITERAL_REPEAT_KEYWORD_MASK` NatSpec + +**File:** `src/concrete/extern/RainterpreterReferenceExtern.sol`, line 63 +**Description:** The NatSpec reads "The mask to apply to the dispatch bytes when parsing to **determin** whether the dispatch is for the repeat literal parser." The word "determin" should be "determine." +**Fix:** Correct the typo. + +### P3-EA-06 [INFO] — `LibRainterpreterReferenceExtern.authoringMetaV2` missing `@return` tag + +**File:** `src/concrete/extern/RainterpreterReferenceExtern.sol`, line 93 +**Description:** The `authoringMetaV2()` function has a descriptive untagged NatSpec comment but no `@return` tag for its `bytes memory` return value. This is an internal function so the convention requirement for `@param`/`@return` applies less strictly, but adding a `@return` tag would improve clarity. + +### P3-EA-07 [INFO] — `ExternOpcodePointersEmpty` error NatSpec lacks `@notice` tag + +**File:** `src/error/ErrExtern.sol`, line 27-28 +**Description:** The `ExternOpcodePointersEmpty` error uses an untagged `///` comment while the other errors in the same file (`ExternOpcodeOutOfRange`, `ExternPointersMismatch`, `BadOutputsLength`) all use explicit `@notice` tags. Since no explicit tags are present in this block, it defaults to `@notice` and is technically correct. However it is inconsistent with the rest of the file. + +### P3-EA-08 [INFO] — `LibParseLiteralRepeat.parseRepeat` `@param dispatchValue` may be misleading + +**File:** `src/lib/extern/reference/literal/LibParseLiteralRepeat.sol`, line 48 +**Description:** The NatSpec says `@param dispatchValue The single decimal digit to repeat (0-9)`. However, the actual value passed to this function through the function pointer dispatch in `BaseRainterpreterSubParser.subParseLiteral2` is a packed decimal float `bytes32` reinterpreted as `uint256` (via assembly function pointer casting). The function's guard `if (dispatchValue > 9)` checks this raw value against 9, which may not produce correct results for packed float representations of digits 0-9. The documentation describes the *intended* semantic (a digit 0-9) but not the actual representation of the value. This is at the boundary of a documentation finding and a correctness finding; flagging here for documentation purposes. + +### P3-EA-09 [INFO] — Internal override functions in `RainterpreterReferenceExtern` lack `@return` tags + +**File:** `src/concrete/extern/RainterpreterReferenceExtern.sol`, lines 168, 175, 182, 189, 196, 203 +**Description:** The six internal override functions (`subParserParseMeta`, `subParserWordParsers`, `subParserOperandHandlers`, `subParserLiteralParsers`, `opcodeFunctionPointers`, `integrityFunctionPointers`) each have untagged NatSpec but no `@return` tags. As internal functions, this is lower priority, but they mirror the pattern of their base class counterparts which also lack `@return` tags. + +--- + +## Summary + +| Severity | Count | +|----------|-------| +| CRITICAL | 0 | +| HIGH | 0 | +| MEDIUM | 0 | +| LOW | 5 | +| INFO | 4 | + +The extern system documentation is generally well-maintained. `LibExtern.sol` is exemplary with complete `@notice`, `@param`, and `@return` tags on all functions. `LibExternOpIntInc` and `LibExternOpStackOperand` also demonstrate good documentation practice. The main gaps are: (1) a misleading "word dispatches" description in the base extern contract, (2) missing `@return` tags on two external build functions, (3) three context op libraries with undocumented `subParser` functions, (4) undocumented local constants, and (5) a typo. diff --git a/audit/2026-03-01-01/pass3/LibEvalIntegrity.md b/audit/2026-03-01-01/pass3/LibEvalIntegrity.md new file mode 100644 index 000000000..07310c0ae --- /dev/null +++ b/audit/2026-03-01-01/pass3/LibEvalIntegrity.md @@ -0,0 +1,136 @@ +# Pass 3: NatSpec Documentation Review — LibEval, LibIntegrityCheck, LibInterpreterState, LibInterpreterStateDataContract, LibInterpreterDeploy + +## Scope + +Files reviewed: +1. `src/lib/eval/LibEval.sol` (251 lines) +2. `src/lib/integrity/LibIntegrityCheck.sol` (207 lines) +3. `src/lib/state/LibInterpreterState.sol` (143 lines) +4. `src/lib/state/LibInterpreterStateDataContract.sol` (143 lines) +5. `src/lib/deploy/LibInterpreterDeploy.sol` (67 lines) + +## Evidence of Thorough Reading + +### LibEval.sol +- Library: `LibEval` (line 15) +- Functions: + - `evalLoop` (line 41) — internal view, 4 params, 1 return + - `eval2` (line 191) — internal view, 3 params, 2 returns +- Imports: `LibInterpreterState`, `InterpreterState`, `LibMemCpy`, `LibMemoryKV`, `MemoryKV`, `LibBytecode`, `Pointer`, `OperandV2`, `StackItem`, `InputsLengthMismatch` +- No errors, structs, events, or constants defined in this file + +### LibIntegrityCheck.sol +- Struct: `IntegrityCheckState` (line 31) — 6 fields: `stackIndex`, `stackMaxIndex`, `readHighwater`, `constants`, `opIndex`, `bytecode` +- Library: `LibIntegrityCheck` (line 40) +- Functions: + - `newState` (line 52) — internal pure, 3 params, 1 return + - `integrityCheck2` (line 87) — internal view, 3 params, 1 return +- Imports: `Pointer`, `OpcodeOutOfRange`, `StackAllocationMismatch`, `StackOutputsMismatch`, `StackUnderflow`, `StackUnderflowHighwater`, `BadOpInputsLength`, `BadOpOutputsLength`, `LibBytecode`, `OperandV2` + +### LibInterpreterState.sol +- Constant: `STACK_TRACER` (line 17) +- Struct: `InterpreterState` (line 42) — 9 fields: `stackBottoms`, `constants`, `sourceIndex`, `stateKV`, `namespace`, `store`, `context`, `bytecode`, `fs` +- Library: `LibInterpreterState` (line 55) +- Functions: + - `stackBottoms` (line 62) — internal pure, 1 param, 1 return + - `stackTrace` (line 126) — internal view, 4 params, 0 returns +- Imports: `Pointer`, `MemoryKV`, `FullyQualifiedNamespace`, `IInterpreterStoreV3`, `StackItem` + +### LibInterpreterStateDataContract.sol +- Library: `LibInterpreterStateDataContract` (line 14) +- Functions: + - `serializeSize` (line 26) — internal pure, 2 params, 1 return + - `unsafeSerialize` (line 39) — internal pure, 3 params, 0 returns + - `unsafeDeserialize` (line 69) — internal pure, 6 params, 1 return +- Imports: `MemoryKV`, `Pointer`, `LibMemCpy`, `LibBytes`, `FullyQualifiedNamespace`, `IInterpreterStoreV3`, `InterpreterState` + +### LibInterpreterDeploy.sol +- Library: `LibInterpreterDeploy` (line 11) — has `@title` and `@notice` +- Constants (10 total): + - `PARSER_DEPLOYED_ADDRESS` (line 14) + - `PARSER_DEPLOYED_CODEHASH` (line 20) + - `STORE_DEPLOYED_ADDRESS` (line 25) + - `STORE_DEPLOYED_CODEHASH` (line 31) + - `INTERPRETER_DEPLOYED_ADDRESS` (line 36) + - `INTERPRETER_DEPLOYED_CODEHASH` (line 42) + - `EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS` (line 47) + - `EXPRESSION_DEPLOYER_DEPLOYED_CODEHASH` (line 53) + - `DISPAIR_REGISTRY_DEPLOYED_ADDRESS` (line 58) + - `DISPAIR_REGISTRY_DEPLOYED_CODEHASH` (line 64) +- No functions, errors, structs, or events + +## NatSpec Completeness Checklist + +| File | Item | Type | @notice | @param | @return | Complete | +|------|------|------|---------|--------|---------|----------| +| LibEval.sol | `evalLoop` | function | Yes (line 18) | 4/4 (lines 34-39) | 1/1 (line 40) | Yes | +| LibEval.sol | `eval2` | function | Yes (line 179) | 3/3 (lines 183-187) | 2/2 (lines 188-190) | Yes | +| LibIntegrityCheck.sol | `IntegrityCheckState` | struct | Yes (line 18) | 6/6 (lines 19-37) | N/A | Yes | +| LibIntegrityCheck.sol | `newState` | function | Yes (line 43) | 3/3 (lines 47-50) | 1/1 (line 51) | Yes | +| LibIntegrityCheck.sol | `integrityCheck2` | function | Yes (line 73) | 3/3 (lines 80-83) | 1/1 (line 84) | Yes | +| LibInterpreterState.sol | `STACK_TRACER` | constant | @dev (line 13) | N/A | N/A | Yes | +| LibInterpreterState.sol | `InterpreterState` | struct | Yes (line 19) | 9/9 (lines 22-41) | N/A | Yes | +| LibInterpreterState.sol | `stackBottoms` | function | Yes (line 56) | 1/1 (line 60) | 1/1 (line 61) | Yes | +| LibInterpreterState.sol | `stackTrace` | function | Yes (line 81) | 4/4 (lines 122-125) | 0/0 | Yes | +| LibInterpreterStateDataContract.sol | `serializeSize` | function | Yes (line 17) | 2/2 (lines 23-24) | 1/1 (line 25) | Yes | +| LibInterpreterStateDataContract.sol | `unsafeSerialize` | function | Yes (line 33) | 3/3 (lines 36-38) | 0/0 | Yes | +| LibInterpreterStateDataContract.sol | `unsafeDeserialize` | function | Yes (line 56) | 6/6 (lines 61-67) | 1/1 (line 68) | Yes | +| LibInterpreterDeploy.sol | library | library | Yes (line 7) | N/A | N/A | Yes | +| LibInterpreterDeploy.sol | all 10 constants | constant | Implicit (untagged) | N/A | N/A | Yes | + +## Documentation Accuracy Review + +### LibEval.sol +- `evalLoop` doc (line 18-40): Claims "up to 8 packed 4-byte opcodes (1 byte opcode index + 3 bytes operand)" -- confirmed in assembly at lines 99-102: `byte(0, word)` extracts the opcode index, `and(shr(0xe0, word), 0xFFFFFF)` extracts 3 bytes as the operand. Note: from the eval loop's perspective the 3 bytes include the IO byte, which is only semantically meaningful during integrity checking but is harmlessly passed as part of the operand to the runtime opcode function. The doc accurately describes the eval loop's view. +- Claims "bounded by modulo" -- confirmed at line 100: `mod(byte(0, word), fsCount)`. +- Claims "Emits a stack trace via `STACK_TRACER` after execution" -- confirmed at line 174: `LibInterpreterState.stackTrace(...)`. +- TRUST block (lines 25-33): Claims `eval2` validates via `LibBytecode.sourceInputsOutputsLength` which reverts with `SourceIndexOutOfBounds` -- confirmed at line 200-201. +- `eval2` doc (line 179-190): Claims inputs length validation, stack construction, output truncation -- all confirmed in implementation (lines 212, 225-226, 240, 243-244). + +### LibIntegrityCheck.sol +- `IntegrityCheckState` struct doc: `readHighwater` described as "Lowest stack index that opcodes are allowed to read from" -- confirmed at line 172: `state.stackIndex < state.readHighwater` triggers revert. "Advances past multi-output regions" -- confirmed at line 186-188. +- `newState` doc: Claims initial values set to `stackIndex` -- confirmed at lines 59-61 where all three (stackIndex, stackMaxIndex, readHighwater) are initialized to the same value. +- `integrityCheck2` doc: Claims "Returns a packed `io` byte array with two bytes per source" -- confirmed at lines 110, 124-127. + +### LibInterpreterState.sol +- `STACK_TRACER` doc: Claims "Derived from a domain-specific keccak hash" -- confirmed: `keccak256("rain.interpreter.stack-tracer.0")`. +- `stackBottoms` doc: Claims "address just past its last element" -- confirmed at line 74: `add(stack, mul(0x20, add(mload(stack), 1)))`. +- `stackTrace` doc: Gas cost analysis at lines 107-115 -- verified arithmetic: tracer ~3158 gas vs events ~14679 gas for 50 items over 5 calls. +- Claims "2 bytes of parent source index followed by 2 bytes of source index" packed as function selector -- confirmed at line 136-138. + +### LibInterpreterStateDataContract.sol +- `serializeSize` doc: Claims layout `[constants length][constants data...][bytecode length][bytecode data...]` -- confirmed at line 29 and in `unsafeSerialize` implementation. +- `unsafeSerialize` doc: Claims "Writes `constants` (with length prefix) then `bytecode` (with length prefix)" -- confirmed at lines 42-52. +- `unsafeDeserialize` doc: Claims "References the constants and bytecode arrays in-place (no copy)" -- confirmed at lines 86 and 93 where assembly sets pointers directly into the serialized buffer. + +### LibInterpreterDeploy.sol +- All constant docs accurately describe what each constant represents (deployed address or code hash for each of the 5 contracts via Zoltu deployer). +- No behavioral claims to verify (pure data). + +## Findings + +### P3-LEI-01 [INFO]: Missing library-level NatSpec on four libraries + +**Files:** +- `src/lib/eval/LibEval.sol` line 15 +- `src/lib/integrity/LibIntegrityCheck.sol` line 40 +- `src/lib/state/LibInterpreterState.sol` line 55 +- `src/lib/state/LibInterpreterStateDataContract.sol` line 14 + +**Description:** Four of the five libraries lack `@title` and/or `@notice` NatSpec at the library declaration level. Only `LibInterpreterDeploy` (line 5-10) has library-level documentation with `@title` and `@notice`. While all individual functions and types within these libraries are well-documented, the library declarations themselves have no doc blocks. This makes it harder for documentation generators and readers to understand the library's purpose at a glance. + +**Impact:** Cosmetic. Does not affect correctness or security. All functions within these libraries have thorough NatSpec. + +### P3-LEI-02 [INFO]: Vague opening line in stackTrace NatSpec + +**File:** `src/lib/state/LibInterpreterState.sol` line 81 + +**Description:** The `@notice` for `stackTrace` opens with "Does something that a full node can easily track in its traces that isn't an event." The phrase "Does something" is imprecise for an API-facing doc tag. A more descriptive opening such as "Emits a stack trace via a staticcall to a deterministic no-code address" would immediately communicate the function's purpose. The rest of the doc block (lines 82-121) is thorough and accurate. + +**Impact:** Cosmetic. The detailed explanation that follows is comprehensive and correct. + +## Summary + +All five files have thorough and accurate NatSpec documentation. Every function has `@notice`, `@param` (for all parameters), and `@return` (for all return values) tags. Both structs (`IntegrityCheckState` and `InterpreterState`) have complete `@param` documentation for all fields. The `STACK_TRACER` constant is documented with `@dev`. All documentation claims verified against their implementations are accurate. + +No CRITICAL, HIGH, MEDIUM, or LOW findings. Two INFO-level observations about missing library-level NatSpec and a vague opening line. diff --git a/audit/2026-03-01-01/pass3/LibOpAll.md b/audit/2026-03-01-01/pass3/LibOpAll.md new file mode 100644 index 000000000..8a9ef9d8e --- /dev/null +++ b/audit/2026-03-01-01/pass3/LibOpAll.md @@ -0,0 +1,601 @@ +# Pass 3 -- NatSpec Documentation Audit: All Opcode Libraries (`src/lib/op/`) + +Audit date: 2026-03-01 +Auditor: Claude Opus 4.6 +Scope: All 68 `.sol` files under `src/lib/op/` + +## Methodology + +Every file under `src/lib/op/` was read in full. For each file, every library +and every function was identified. Each was checked for: + +1. Presence of NatSpec (`@title`, `@notice`, `@param`, `@return`) +2. Completeness of `@param` and `@return` tags against function signatures +3. Accuracy of documentation vs. implementation +4. Conformance to the project NatSpec convention: when a doc block contains any + explicit tag (e.g. `@title`), all entries must be explicitly tagged. + +## Summary + +The overall quality of NatSpec documentation across the opcode libraries is high. +Most files follow a consistent pattern: `@title`/`@notice` on the library, +`@notice` on each function, and `@param`/`@return` tags on function parameters. + +The findings below are deviations from this established pattern. + +--- + +## Findings + +### P3-OPALL-01 [LOW] Missing `@notice` tag on `integrity` in several uint256 / growth files + +**Files and locations:** +- `src/lib/op/erc20/uint256/LibOpUint256ERC20Allowance.sol` line 15: `/// \`uint256-erc20-allowance\` integrity check...` +- `src/lib/op/erc20/uint256/LibOpUint256ERC20BalanceOf.sol` line 15: `/// \`uint256-erc20-balance-of\` integrity check...` +- `src/lib/op/erc20/uint256/LibOpUint256ERC20TotalSupply.sol` line 15: `/// \`uint256-erc20-total-supply\` integrity check...` +- `src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol` line 15: `/// \`uint256-erc721-balance-of\` integrity check...` +- `src/lib/op/math/growth/LibOpExponentialGrowth.sol` line 17: `/// \`exponential-growth\` integrity check...` +- `src/lib/op/math/growth/LibOpLinearGrowth.sol` line 17: `/// \`linear-growth\` integrity check...` +- `src/lib/op/math/uint256/LibOpMaxUint256.sol` line 13: `/// \`max-uint256\` integrity check...` + +**Description:** +These `integrity` functions have a doc comment starting with `///` but without +the `@notice` tag. In files where no explicit tags are used at all in a doc +block, this is acceptable (untagged lines become implicit `@notice`). However, +the pattern established across the entire opcode library set is to explicitly +use `@notice` on every function's doc block. These seven files deviate from +that pattern. + +More importantly, several of these same files DO have `@return` tags on the +corresponding float-variant `integrity` functions (e.g. `LibOpERC20Allowance`), +so the inconsistency makes it unclear whether the missing tags were intentional. + +**Recommendation:** +Add `@notice` tag to each of these `integrity` doc comments and add missing +`@return` tags to match the pattern used by the float-variant opcodes. + +--- + +### P3-OPALL-02 [LOW] Missing `@return` tags on `integrity` in several uint256 / growth files + +**Files and locations (same set as P3-OPALL-01):** +- `src/lib/op/erc20/uint256/LibOpUint256ERC20Allowance.sol` line 15-16 +- `src/lib/op/erc20/uint256/LibOpUint256ERC20BalanceOf.sol` line 15-16 +- `src/lib/op/erc20/uint256/LibOpUint256ERC20TotalSupply.sol` line 15-16 +- `src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol` line 15-16 +- `src/lib/op/math/growth/LibOpExponentialGrowth.sol` line 17-18 +- `src/lib/op/math/growth/LibOpLinearGrowth.sol` line 17-18 +- `src/lib/op/math/uint256/LibOpMaxUint256.sol` line 13-14 + +**Description:** +These `integrity` functions return `(uint256, uint256)` representing +(inputs, outputs). Every other opcode library documents these returns with +`@return The number of inputs.` / `@return The number of outputs.` but these +seven files omit the `@return` tags entirely. + +**Recommendation:** +Add `@return The number of inputs.` and `@return The number of outputs.` to +match the convention used everywhere else. + +--- + +### P3-OPALL-03 [LOW] Missing `@notice` and NatSpec tags on `referenceFn` in `LibOpMaxUint256.sol` + +**File:** `src/lib/op/math/uint256/LibOpMaxUint256.sol` line 30-31 + +**Description:** +The `referenceFn` function has a doc comment `/// Reference implementation of +\`max-uint256\` for testing.` but it is missing the `@notice` tag and has no +`@return` tag. Both the `run` function in the same file and `referenceFn` +functions in other opcode libraries use explicit `@notice` tags. + +**Recommendation:** +Add `@notice` and `@return` tags. + +--- + +### P3-OPALL-04 [LOW] Missing `@notice` tag on `integrity` in `LibOpEnsure.sol` + +**File:** `src/lib/op/logic/LibOpEnsure.sol` line 18-19 + +**Description:** +The `integrity` function's doc block has `@return` tags but is missing the +`@notice` tag. This is the only logic opcode where `integrity` lacks `@notice`. +The `@return` tags without `@notice` means the first `///` line is implicitly +`@notice`, which is correct in isolation, but violates the project convention +that when any explicit tag (here `@return`) is present, all entries should be +explicitly tagged. + +**Recommendation:** +Add `@notice` to be consistent: `/// @notice \`ensure\` integrity check. +Requires exactly 2 inputs and 0 outputs.` + +--- + +### P3-OPALL-05 [INFO] `LibOpCall.sol` has no `referenceFn` + +**File:** `src/lib/op/call/LibOpCall.sol` + +**Description:** +`LibOpCall` does not provide a `referenceFn` like all other opcode libraries. +This is expected because `call` dispatches to another source and cannot +meaningfully be expressed as a simple stack-in/stack-out reference function. +The `run` function delegates to `LibEval.evalLoop`. Documenting the absence +of `referenceFn` is not required but noted for completeness. + +No action needed. + +--- + +### P3-OPALL-06 [INFO] `LibOpCall.integrity` return NatSpec uses single `@return` for tuple + +**File:** `src/lib/op/call/LibOpCall.sol` line 84 + +**Description:** +The `integrity` function's `@return` documents only one tag for a function that +returns `(uint256, uint256)`: `@return The number of inputs and outputs for +stack tracking.` All other opcode libraries use two separate `@return` tags. +However, this is arguably more accurate for `call` specifically because the +return values are dynamically derived from bytecode, not fixed constants. + +This is a minor inconsistency but is defensible as an intentional deviation. + +No action needed. + +--- + +### P3-OPALL-07 [INFO] `LibAllStandardOps.sol` functions lack `@return` tags + +**File:** `src/lib/op/LibAllStandardOps.sol` + +**Description:** +The five functions in `LibAllStandardOps` (`authoringMetaV2`, `literalParserFunctionPointers`, +`operandHandlerFunctionPointers`, `integrityFunctionPointers`, `opcodeFunctionPointers`) +all return `bytes memory` but have no `@return` tag. Their doc comments use +untagged `///` lines which become implicit `@notice` (no explicit tags are +present), so this does not violate the "explicit tag" convention. + +This is an acceptable style for internal builder functions whose return values +are self-evident. Noted for completeness only. + +No action needed. + +--- + +## Evidence Index + +Below is a per-file inventory of every library and function, with NatSpec status. + +### `src/lib/op/LibAllStandardOps.sol` +- Library: `LibAllStandardOps` -- `@title` line 108, `@notice` line 109 +- `authoringMetaV2()` line 121 -- untagged `///` (OK, no explicit tags in block) +- `literalParserFunctionPointers()` line 330 -- untagged `///` (OK) +- `operandHandlerFunctionPointers()` line 363 -- untagged `///` (OK) +- `integrityFunctionPointers()` line 535 -- untagged `///` (OK) +- `opcodeFunctionPointers()` line 639 -- untagged `///` (OK) + +### `src/lib/op/00/LibOpConstant.sol` +- Library: `LibOpConstant` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 21 -- `@notice`, `@param`x2, `@return`x2. COMPLETE. +- `run()` line 37 -- `@notice`, `@param`x3, `@return`. COMPLETE. +- `referenceFn()` line 52 -- `@notice`, `@param`x2, `@return`. COMPLETE. + +### `src/lib/op/00/LibOpContext.sol` +- Library: `LibOpContext` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 16 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`x3, `@return`. COMPLETE. +- `referenceFn()` line 47 -- `@notice`, `@param`x2, `@return`. COMPLETE. + +### `src/lib/op/00/LibOpExtern.sol` +- Library: `LibOpExtern` -- `@title` line 21, `@notice` line 22 +- `integrity()` line 29 -- `@notice`, `@param`x2, `@return`x2. COMPLETE. +- `run()` line 49 -- `@notice`, `@param`x3, `@return`. COMPLETE. +- `referenceFn()` line 102 -- `@notice`, `@param`x3, `@return`. COMPLETE. + +### `src/lib/op/00/LibOpStack.sol` +- Library: `LibOpStack` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 21 -- `@notice`, `@param`x2, `@return`x2. COMPLETE. +- `run()` line 41 -- `@notice`, `@param`x3, `@return`. COMPLETE. +- `referenceFn()` line 58 -- `@notice`, `@param`x2, `@return`. COMPLETE. + +### `src/lib/op/bitwise/LibOpBitwiseAnd.sol` +- Library: `LibOpBitwiseAnd` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 16 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 24 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 36 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/bitwise/LibOpBitwiseOr.sol` +- Library: `LibOpBitwiseOr` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 16 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 24 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 36 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/bitwise/LibOpCtPop.sol` +- Library: `LibOpCtPop` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 22 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 47 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/bitwise/LibOpDecodeBits.sol` +- Library: `LibOpDecodeBits` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 20 -- `@notice`, `@param`x2, `@return`x2. COMPLETE. +- `run()` line 33 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 65 -- `@notice`, `@param`x2, `@return`. COMPLETE. + +### `src/lib/op/bitwise/LibOpEncodeBits.sol` +- Library: `LibOpEncodeBits` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 36 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 76 -- `@notice`, `@param`x2, `@return`. COMPLETE. + +### `src/lib/op/bitwise/LibOpShiftBitsLeft.sol` +- Library: `LibOpShiftBitsLeft` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 38 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 49 -- `@notice`, `@param`x2, `@return`. COMPLETE. + +### `src/lib/op/bitwise/LibOpShiftBitsRight.sol` +- Library: `LibOpShiftBitsRight` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 38 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 49 -- `@notice`, `@param`x2, `@return`. COMPLETE. + +### `src/lib/op/call/LibOpCall.sol` +- Library: `LibOpCall` -- `@title` line 13, `@notice` line 14 +- `integrity()` line 85 -- `@notice`, `@param`x2, `@return`x1 (single for tuple). See P3-OPALL-06. +- `run()` line 122 -- `@notice`, `@param`x3, `@return`. COMPLETE. +- No `referenceFn`. See P3-OPALL-05. + +### `src/lib/op/crypto/LibOpHash.sol` +- Library: `LibOpHash` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 17 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 41 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc20/LibOpERC20Allowance.sol` +- Library: `LibOpERC20Allowance` -- `@title` line 15, `@notice` line 16 +- `integrity()` line 21 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 83 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc20/LibOpERC20BalanceOf.sol` +- Library: `LibOpERC20BalanceOf` -- `@title` line 15, `@notice` line 16 +- `integrity()` line 21 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 67 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc20/LibOpERC20TotalSupply.sol` +- Library: `LibOpERC20TotalSupply` -- `@title` line 15, `@notice` line 16 +- `integrity()` line 21 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 61 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc20/uint256/LibOpUint256ERC20Allowance.sol` +- Library: `LibOpUint256ERC20Allowance` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 16 -- MISSING `@notice`, MISSING `@return`x2. **FINDING P3-OPALL-01/02.** +- `run()` line 25 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 60 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc20/uint256/LibOpUint256ERC20BalanceOf.sol` +- Library: `LibOpUint256ERC20BalanceOf` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 16 -- MISSING `@notice`, MISSING `@return`x2. **FINDING P3-OPALL-01/02.** +- `run()` line 25 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 54 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc20/uint256/LibOpUint256ERC20TotalSupply.sol` +- Library: `LibOpUint256ERC20TotalSupply` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 16 -- MISSING `@notice`, MISSING `@return`x2. **FINDING P3-OPALL-01/02.** +- `run()` line 25 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 48 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc5313/LibOpERC5313Owner.sol` +- Library: `LibOpERC5313Owner` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 18 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 27 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 50 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc721/LibOpERC721BalanceOf.sol` +- Library: `LibOpERC721BalanceOf` -- `@title` line 13, `@notice` line 14 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 60 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc721/LibOpERC721OwnerOf.sol` +- Library: `LibOpERC721OwnerOf` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 18 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 27 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 53 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol` +- Library: `LibOpUint256ERC721BalanceOf` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 16 -- MISSING `@notice`, MISSING `@return`x2. **FINDING P3-OPALL-01/02.** +- `run()` line 25 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 52 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/evm/LibOpBlockNumber.sol` +- Library: `LibOpBlockNumber` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 39 -- `@notice`, `@return`. COMPLETE. + +### `src/lib/op/evm/LibOpChainId.sol` +- Library: `LibOpChainId` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 39 -- `@notice`, `@return`. COMPLETE. + +### `src/lib/op/evm/LibOpTimestamp.sol` +- Library: `LibOpTimestamp` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 39 -- `@notice`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpAny.sol` +- Library: `LibOpAny` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 21 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 33 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 60 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpBinaryEqualTo.sol` +- Library: `LibOpBinaryEqualTo` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 17 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 38 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpConditions.sol` +- Library: `LibOpConditions` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 23 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 40 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 82 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpEnsure.sol` +- Library: `LibOpEnsure` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 20 -- MISSING `@notice`, has `@return`x2. **FINDING P3-OPALL-04.** +- `run()` line 31 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 49 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpEqualTo.sol` +- Library: `LibOpEqualTo` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 21 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 52 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpEvery.sol` +- Library: `LibOpEvery` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 21 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 32 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 58 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpGreaterThan.sol` +- Library: `LibOpGreaterThan` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 20 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 46 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpGreaterThanOrEqualTo.sol` +- Library: `LibOpGreaterThanOrEqualTo` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 20 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 29 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 47 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpIf.sol` +- Library: `LibOpIf` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 20 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 29 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 47 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpIsZero.sol` +- Library: `LibOpIsZero` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 27 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 42 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpLessThan.sol` +- Library: `LibOpLessThan` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 20 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 46 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/logic/LibOpLessThanOrEqualTo.sol` +- Library: `LibOpLessThanOrEqualTo` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 20 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 29 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 47 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/growth/LibOpExponentialGrowth.sol` +- Library: `LibOpExponentialGrowth` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 18 -- MISSING `@notice`, MISSING `@return`x2. **FINDING P3-OPALL-01/02.** +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 47 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/growth/LibOpLinearGrowth.sol` +- Library: `LibOpLinearGrowth` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 18 -- MISSING `@notice`, MISSING `@return`x2. **FINDING P3-OPALL-01/02.** +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 48 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/uint256/LibOpMaxUint256.sol` +- Library: `LibOpMaxUint256` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 14 -- MISSING `@notice`, MISSING `@return`x2. **FINDING P3-OPALL-01/02.** +- `run()` line 21 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 31 -- MISSING `@notice`, MISSING `@return`. **FINDING P3-OPALL-03.** + +### `src/lib/op/math/uint256/LibOpUint256Add.sol` +- Library: `LibOpUint256Add` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 17 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 64 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/uint256/LibOpUint256Div.sol` +- Library: `LibOpUint256Div` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 18 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 65 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/uint256/LibOpUint256Mul.sol` +- Library: `LibOpUint256Mul` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 17 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 64 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/uint256/LibOpUint256Pow.sol` +- Library: `LibOpUint256Pow` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 17 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 64 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/uint256/LibOpUint256Sub.sol` +- Library: `LibOpUint256Sub` -- `@title` line 10, `@notice` line 11 +- `integrity()` line 17 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 64 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpAbs.sol` +- Library: `LibOpAbs` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 44 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpAdd.sol` +- Library: `LibOpAdd` -- `@title` line 13, `@notice` line 14 +- `integrity()` line 22 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 33 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 76 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpAvg.sol` +- Library: `LibOpAvg` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 47 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpCeil.sol` +- Library: `LibOpCeil` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 44 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpDiv.sol` +- Library: `LibOpDiv` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 21 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 33 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 74 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpE.sol` +- Library: `LibOpE` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 17 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 24 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 35 -- `@notice`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpExp.sol` +- Library: `LibOpExp` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 44 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpExp2.sol` +- Library: `LibOpExp2` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 45 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpFloor.sol` +- Library: `LibOpFloor` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 44 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpFrac.sol` +- Library: `LibOpFrac` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 44 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpGm.sol` +- Library: `LibOpGm` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 21 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 31 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 55 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpHeadroom.sol` +- Library: `LibOpHeadroom` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 20 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 30 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 49 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpInv.sol` +- Library: `LibOpInv` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 44 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpMax.sol` +- Library: `LibOpMax` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 20 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 32 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 67 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpMaxNegativeValue.sol` +- Library: `LibOpMaxNegativeValue` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 37 -- `@notice`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpMaxPositiveValue.sol` +- Library: `LibOpMaxPositiveValue` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 37 -- `@notice`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpMin.sol` +- Library: `LibOpMin` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 20 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 32 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 68 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpMinNegativeValue.sol` +- Library: `LibOpMinNegativeValue` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 37 -- `@notice`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpMinPositiveValue.sol` +- Library: `LibOpMinPositiveValue` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 26 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 37 -- `@notice`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpMul.sol` +- Library: `LibOpMul` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 21 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 32 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 74 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpPow.sol` +- Library: `LibOpPow` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 47 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpSqrt.sol` +- Library: `LibOpSqrt` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 28 -- `@notice`, `@param`, `@return`. COMPLETE. +- `referenceFn()` line 44 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/math/LibOpSub.sol` +- Library: `LibOpSub` -- `@title` line 12, `@notice` line 13 +- `integrity()` line 21 -- `@notice`, `@param`, `@return`x2. COMPLETE. +- `run()` line 33 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 75 -- `@notice`, `@param`, `@return`. COMPLETE. + +### `src/lib/op/store/LibOpGet.sol` +- Library: `LibOpGet` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 32 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 68 -- `@notice`, `@param`x2, `@return`. COMPLETE. + +### `src/lib/op/store/LibOpSet.sol` +- Library: `LibOpSet` -- `@title` line 11, `@notice` line 12 +- `integrity()` line 19 -- `@notice`, `@return`x2. COMPLETE. +- `run()` line 29 -- `@notice`, `@param`x2, `@return`. COMPLETE. +- `referenceFn()` line 46 -- `@notice`, `@param`x2, `@return`. COMPLETE. diff --git a/audit/2026-03-01-01/pass3/LibParse.md b/audit/2026-03-01-01/pass3/LibParse.md new file mode 100644 index 000000000..f33eeb643 --- /dev/null +++ b/audit/2026-03-01-01/pass3/LibParse.md @@ -0,0 +1,429 @@ +# Pass 3 — NatSpec Documentation Audit: Parse Libraries + +Audit date: 2026-03-01 +Files reviewed: 14 parse library source files + +--- + +## File Inventory + +### 1. `src/lib/parse/LibParse.sol` (449 lines) + +**Library**: `LibParse` (line 68) +- Library-level: `@title` (line 61) + `@notice` (line 62) -- complete + +**Constants**: +- `SUB_PARSER_BYTECODE_HEADER_SIZE` (line 59): `@dev` -- complete + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `parseWord` | 100 | Yes (83) | `cursor`, `end`, `mask` | 2 returns documented | COMPLETE | +| `parseLHS` | 136 | Yes (128) | `state`, `cursor`, `end` | 1 return documented | COMPLETE | +| `parseRHS` | 204 | Yes (195) | `state`, `cursor`, `end` | 1 return documented | COMPLETE | +| `parse` | 425 | Yes (419) | `state` | `bytecode`, unnamed constants | SEE FINDING P3-LP-01 | + +--- + +### 2. `src/lib/parse/LibParseState.sol` (1053 lines) + +**Library**: `LibParseState` (line 185) +- Library-level: NO NatSpec -- SEE FINDING P3-LPS-01 + +**Struct**: `ParseState` (line 155) +- Struct-level: `@notice` (line 80) + `@param` for all fields -- complete + +**Constants** (all have `@dev` tags): +- `EMPTY_ACTIVE_SOURCE` (line 31): `@dev` -- complete +- `FSM_YANG_MASK` (line 35): `@dev` -- complete +- `FSM_WORD_END_MASK` (line 38): `@dev` -- complete +- `FSM_ACCEPTING_INPUTS_MASK` (line 41): `@dev` -- complete +- `FSM_ACTIVE_SOURCE_MASK` (line 45): `@dev` -- complete +- `FSM_DEFAULT` (line 51): `@dev` -- complete +- `OPERAND_VALUES_LENGTH` (line 62): `@dev` -- complete +- `PARSE_STATE_TOP_LEVEL0_OFFSET` (line 66): `@dev` -- complete +- `PARSE_STATE_TOP_LEVEL0_DATA_OFFSET` (line 70): `@dev` -- complete +- `PARSE_STATE_PAREN_TRACKER0_OFFSET` (line 74): `@dev` -- complete +- `PARSE_STATE_LINE_TRACKER_OFFSET` (line 78): `@dev` -- complete + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `newActiveSourcePointer` | 201 | Yes (192) | `oldActiveSourcePointer` | documented | COMPLETE | +| `resetSource` | 222 | Yes (218) | `state` | void | COMPLETE | +| `newState` | 248 | Yes (238) | `data`, `meta`, `operandHandlers`, `literalParsers` | documented | COMPLETE | +| `pushSubParser` | 309 | Yes (299) | `state`, `cursor`, `subParser` | void | COMPLETE | +| `exportSubParsers` | 329 | Yes (326) | `state` | documented | COMPLETE | +| `snapshotSourceHeadToLineTracker` | 358 | Yes (354) | `state` | void | COMPLETE | +| `endLine` | 393 | Yes (387) | `state`, `cursor` | void | COMPLETE | +| `highwater` | 519 | Yes (514) | `state` | void | COMPLETE | +| `constantValueBloom` | 547 | Yes (543) | `value` | `bloom` | COMPLETE | +| `pushConstantValue` | 555 | Yes (551) | `state`, `value` | void | COMPLETE | +| `pushLiteral` | 585 | Yes (578) | `state`, `cursor`, `end` | documented | COMPLETE | +| `pushOpToSource` | 660 | Yes (650) | `state`, `opcode`, `operand` | void | COMPLETE | +| `endSource` | 767 | Yes (762) | `state` | void | COMPLETE | +| `buildBytecode` | 900 | Yes (895) | `state` | `bytecode` | COMPLETE | +| `buildConstants` | 994 | Yes (988) | `state` | `constants` | COMPLETE | +| `checkParseMemoryOverflow` | 1044 | NO `@notice` tag | none (no params) | void | SEE FINDING P3-LPS-02 | + +--- + +### 3. `src/lib/parse/LibParseError.sol` (37 lines) + +**Library**: `LibParseError` (line 7) +- Library-level: NO NatSpec -- SEE FINDING P3-LPE-01 + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `parseErrorOffset` | 13 | Yes (8) | `state`, `cursor` | `offset` | COMPLETE | +| `handleErrorSelector` | 26 | Yes (20) | `state`, `cursor`, `errorSelector` | void | COMPLETE | + +--- + +### 4. `src/lib/parse/LibParseInterstitial.sol` (128 lines) + +**Library**: `LibParseInterstitial` (line 17) +- Library-level: NO NatSpec -- SEE FINDING P3-LPI-01 + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `skipComment` | 28 | Yes (21) | `state`, `cursor`, `end` | documented | COMPLETE | +| `skipWhitespace` | 96 | Yes (90) | `state`, `cursor`, `end` | documented | COMPLETE | +| `parseInterstitial` | 111 | Yes (104) | `state`, `cursor`, `end` | documented | COMPLETE | + +--- + +### 5. `src/lib/parse/LibParseOperand.sol` (348 lines) + +**Library**: `LibParseOperand` (line 21) +- Library-level: NO NatSpec -- SEE FINDING P3-LPO-01 + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `parseOperand` | 35 | Yes (28) | `state`, `cursor`, `end` | documented | COMPLETE | +| `handleOperand` | 136 | Yes (125) | `state`, `wordIndex` | documented | COMPLETE | +| `handleOperandDisallowed` | 153 | Yes (149) | `values` | documented | COMPLETE | +| `handleOperandDisallowedAlwaysOne` | 164 | Yes (160) | `values` | documented | COMPLETE | +| `handleOperandSingleFull` | 177 | Yes (171) | `values` | `operand` | COMPLETE | +| `handleOperandSingleFullNoDefault` | 201 | Yes (196) | `values` | `operand` | COMPLETE | +| `handleOperandDoublePerByteNoDefault` | 225 | Yes (220) | `values` | `operand` | COMPLETE | +| `handleOperand8M1M1` | 258 | Yes (252) | `values` | `operand` | COMPLETE | +| `handleOperandM1M1` | 310 | Yes (305) | `values` | `operand` | COMPLETE | + +--- + +### 6. `src/lib/parse/LibParsePragma.sol` (92 lines) + +**Library**: `LibParsePragma` (line 20) +- Library-level: NO NatSpec -- SEE FINDING P3-LPP-01 + +**Constants**: +- `PRAGMA_KEYWORD_BYTES` (line 12): No NatSpec -- SEE FINDING P3-LPP-02 +- `PRAGMA_KEYWORD_BYTES32` (line 15): No NatSpec -- SEE FINDING P3-LPP-02 +- `PRAGMA_KEYWORD_BYTES_LENGTH` (line 16): No NatSpec -- SEE FINDING P3-LPP-02 +- `PRAGMA_KEYWORD_MASK` (line 18): No NatSpec -- SEE FINDING P3-LPP-02 + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `parsePragma` | 33 | Yes (26) | `state`, `cursor`, `end` | documented | COMPLETE | + +--- + +### 7. `src/lib/parse/LibParseStackName.sol` (89 lines) + +**Library**: `LibParseStackName` (line 21) +- Library-level: `@title` (line 7) + `@notice` (line 8) -- complete + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `pushStackName` | 31 | Yes (22) | `state`, `word` | `exists`, `index` | COMPLETE | +| `stackNameIndex` | 62 | Yes (54) | `state`, `word` | `exists`, `index` | COMPLETE | + +--- + +### 8. `src/lib/parse/LibParseStackTracker.sol` (77 lines) + +**Library**: `LibParseStackTracker` (line 9) +- Library-level: NO NatSpec -- SEE FINDING P3-LPST-01 + +**Type**: `ParseStackTracker` (line 7): No NatSpec -- SEE FINDING P3-LPST-02 + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `pushInputs` | 19 | Yes (12) | `tracker`, `n` | documented | COMPLETE | +| `push` | 41 | Yes (31) | `tracker`, `n` | documented | COMPLETE | +| `pop` | 68 | Yes (57) | `tracker`, `n` | documented | COMPLETE | + +--- + +### 9. `src/lib/parse/LibSubParse.sol` (450 lines) + +**Library**: `LibSubParse` (line 36) +- Library-level: `@title` (line 25) + `@notice` (line 26) -- complete + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `subParserContext` | 48 | Yes (40) | `column`, `row` | 3 returns documented | COMPLETE | +| `subParserConstant` | 96 | Yes (89) | `constantsHeight`, `value` | 3 returns documented | COMPLETE | +| `subParserExtern` | 161 | Yes (144) | `extern`, `constantsHeight`, `ioByte`, `operand`, `opcodeIndex` | 3 returns documented | COMPLETE | +| `subParseWordSlice` | 215 | Yes (210) | `state`, `cursor`, `end` | void | COMPLETE | +| `subParseWords` | 323 | Yes (316) | `state`, `bytecode` | 2 returns documented | COMPLETE | +| `subParseLiteral` | 349 | Yes (340) | `state`, `dispatchStart`, `dispatchEnd`, `bodyStart`, `bodyEnd` | documented | COMPLETE | +| `consumeSubParseWordInputData` | 407 | Yes (396) | `data`, `meta`, `operandHandlers` | `constantsHeight`, `ioByte`, `state` | COMPLETE | +| `consumeSubParseLiteralInputData` | 438 | Yes (431) | `data` | `dispatchStart`, `bodyStart`, `bodyEnd` | COMPLETE | + +--- + +### 10. `src/lib/parse/literal/LibParseLiteral.sol` (125 lines) + +**Library**: `LibParseLiteral` (line 23) +- Library-level: NO NatSpec -- SEE FINDING P3-LPL-01 + +**Constants**: +- `LITERAL_PARSERS_LENGTH` (line 16): No NatSpec -- SEE FINDING P3-LPL-02 +- `LITERAL_PARSER_INDEX_HEX` (line 18): No NatSpec -- SEE FINDING P3-LPL-02 +- `LITERAL_PARSER_INDEX_DECIMAL` (line 19): No NatSpec -- SEE FINDING P3-LPL-02 +- `LITERAL_PARSER_INDEX_STRING` (line 20): No NatSpec -- SEE FINDING P3-LPL-02 +- `LITERAL_PARSER_INDEX_SUB_PARSE` (line 21): No NatSpec -- SEE FINDING P3-LPL-02 + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `selectLiteralParserByIndex` | 33 | Yes (27) | `state`, `index` | documented | COMPLETE | +| `parseLiteral` | 55 | Yes (48) | `state`, `cursor`, `end` | 2 returns documented | COMPLETE | +| `tryParseLiteral` | 77 | Yes (68) | `state`, `cursor`, `end` | 3 returns documented | COMPLETE | + +--- + +### 11. `src/lib/parse/literal/LibParseLiteralDecimal.sol` (30 lines) + +**Library**: `LibParseLiteralDecimal` (line 10) +- Library-level: NO NatSpec -- SEE FINDING P3-LPLD-01 + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `parseDecimalFloatPacked` | 20 | Yes (13) | `state`, `start`, `end` | 2 returns documented | COMPLETE | + +--- + +### 12. `src/lib/parse/literal/LibParseLiteralHex.sol` (122 lines) + +**Library**: `LibParseLiteralHex` (line 20) +- Library-level: NO NatSpec -- SEE FINDING P3-LPLH-01 + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `boundHex` | 31 | Yes (24) | `cursor`, `end` (note: `state` unused) | 3 returns documented | COMPLETE | +| `parseHex` | 63 | Yes (51) | `state`, `cursor`, `end` | 2 returns documented | COMPLETE | + +--- + +### 13. `src/lib/parse/literal/LibParseLiteralString.sol` (112 lines) + +**Library**: `LibParseLiteralString` (line 13) +- Library-level: `@title` (line 11) + `@notice` (line 12) -- complete + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `boundString` | 26 | Yes (17) | `state`, `cursor`, `end` | 3 returns documented | COMPLETE | +| `parseString` | 88 | Yes (77) | `state`, `cursor`, `end` | 2 returns documented | COMPLETE | + +--- + +### 14. `src/lib/parse/literal/LibParseLiteralSubParseable.sol` (88 lines) + +**Library**: `LibParseLiteralSubParseable` (line 14) +- Library-level: NO NatSpec -- SEE FINDING P3-LPLSP-01 + +**Functions**: +| Function | Line | `@notice` | `@param` | `@return` | Status | +|---|---|---|---|---|---| +| `parseSubParseable` | 35 | Yes (20) | `state`, `cursor`, `end` | 2 returns documented | COMPLETE | + +--- + +## Error NatSpec Audit (ErrParse.sol) + +Errors with `@notice` + `@param` tags (complete): `OperandValuesOverflow`, `UnclosedOperand`, `UnsupportedLiteralType`, `StringTooLong`, `UnclosedStringLiteral`, `HexLiteralOverflow`, `ZeroLengthHexLiteral`, `OddLengthHexLiteral`, `MalformedHexLiteral`, `MissingFinalSemi`, `UnexpectedLHSChar`, `UnexpectedRHSChar`, `ExpectedLeftParen`, `UnexpectedRightParen`, `UnclosedLeftParen`, `UnexpectedComment`, `UnclosedComment`, `MalformedCommentStart`, `DuplicateLHSItem`, `ExcessLHSItems`, `NotAcceptingInputs`, `ExcessRHSItems`, `WordSize`, `UnknownWord`, `NoWhitespaceAfterUsingWordsFrom`, `InvalidSubParser`, `UnclosedSubParseableLiteral`, `SubParseableMissingDispatch`, `BadSubParserResult`, `OpcodeIOOverflow`, `ParseMemoryOverflow`. + +Errors missing `@notice`/`@dev` tag (doc comments exist but no explicit tag): SEE FINDING P3-ERR-01 +- `UnexpectedOperand` (line 8-10) +- `UnexpectedOperandValue` (line 12-14) +- `ExpectedOperand` (line 16-18) +- `MaxSources` (line 120-121) +- `DanglingSource` (line 123-124) +- `ParserOutOfBounds` (line 126-127) +- `ParseStackOverflow` (line 129-131) +- `ParseStackUnderflow` (line 133-134) +- `ParenOverflow` (line 136-138) +- `OperandOverflow` (line 165-166) +- `SourceItemOpsOverflow` (line 173-175) +- `ParenInputOverflow` (line 177-179) +- `LineRHSItemsOverflow` (line 181-183) + +--- + +## Findings + +### P3-LP-01: `parse()` second `@return` tag missing name [INFO] + +**File**: `src/lib/parse/LibParse.sol`, line 424 +**Description**: The `parse` function returns two values: `bytes memory bytecode` (named) and `bytes32[] memory` (unnamed). The NatSpec has `@return bytecode` and `@return The constants array.` but the second return value has no name in the function signature, so `@return` cannot bind to a name. The documentation content is present but the return variable itself is unnamed in the signature. + +This is cosmetic but inconsistent with the first return which is named. + +--- + +### P3-LPS-01: `LibParseState` library missing library-level NatSpec [LOW] + +**File**: `src/lib/parse/LibParseState.sol`, line 185 +**Description**: The `LibParseState` library has no `@title` or `@notice` tag. The `ParseState` struct is well documented, but the library itself (which contains 16 functions) has no introductory documentation. + +--- + +### P3-LPS-02: `checkParseMemoryOverflow` missing `@notice` tag [LOW] + +**File**: `src/lib/parse/LibParseState.sol`, lines 1032-1044 +**Description**: The `checkParseMemoryOverflow` function has a detailed NatSpec comment block (lines 1032-1043) but uses no explicit tag. Since `LibParseState` is a library and other functions in the same library use `@notice`, consistency requires this function to also use `@notice`. The untagged lines default to `@notice` only when no tags are present in the doc block, but as a standalone doc block with no tags, it technically defaults correctly. However, for consistency with all other functions in the file which explicitly use `@notice`, an explicit tag should be added. + +--- + +### P3-LPE-01: `LibParseError` library missing library-level NatSpec [INFO] + +**File**: `src/lib/parse/LibParseError.sol`, line 7 +**Description**: The `LibParseError` library has no `@title` or `@notice` tag. Both functions within it are fully documented. + +--- + +### P3-LPI-01: `LibParseInterstitial` library missing library-level NatSpec [INFO] + +**File**: `src/lib/parse/LibParseInterstitial.sol`, line 17 +**Description**: The `LibParseInterstitial` library has no `@title` or `@notice` tag. All functions within it are fully documented. + +--- + +### P3-LPO-01: `LibParseOperand` library missing library-level NatSpec [INFO] + +**File**: `src/lib/parse/LibParseOperand.sol`, line 21 +**Description**: The `LibParseOperand` library has no `@title` or `@notice` tag. All functions within it are fully documented. + +--- + +### P3-LPP-01: `LibParsePragma` library missing library-level NatSpec [INFO] + +**File**: `src/lib/parse/LibParsePragma.sol`, line 20 +**Description**: The `LibParsePragma` library has no `@title` or `@notice` tag. The single function within it is fully documented. + +--- + +### P3-LPP-02: Pragma constants missing NatSpec [LOW] + +**File**: `src/lib/parse/LibParsePragma.sol`, lines 12-18 +**Description**: Four file-level constants have no NatSpec documentation: +- `PRAGMA_KEYWORD_BYTES` (line 12) +- `PRAGMA_KEYWORD_BYTES32` (line 15) +- `PRAGMA_KEYWORD_BYTES_LENGTH` (line 16) +- `PRAGMA_KEYWORD_MASK` (line 18) + +These define the `using-words-from` pragma keyword matching parameters and should have `@dev` documentation explaining their purpose. + +--- + +### P3-LPST-01: `LibParseStackTracker` library missing library-level NatSpec [INFO] + +**File**: `src/lib/parse/LibParseStackTracker.sol`, line 9 +**Description**: The `LibParseStackTracker` library has no `@title` or `@notice` tag. All functions within it are fully documented. + +--- + +### P3-LPST-02: `ParseStackTracker` type missing NatSpec [LOW] + +**File**: `src/lib/parse/LibParseStackTracker.sol`, line 7 +**Description**: The `ParseStackTracker` user-defined value type (`type ParseStackTracker is uint256`) has no NatSpec. This type encodes three packed fields (current height in bits 0-7, inputs in bits 8-15, max/highwater in bits 16+) that are documented implicitly across the `push`, `pop`, and `pushInputs` functions but never explained as a whole. A `@dev` comment on the type declaration would help readers understand the encoding. + +--- + +### P3-LPL-01: `LibParseLiteral` library missing library-level NatSpec [INFO] + +**File**: `src/lib/parse/literal/LibParseLiteral.sol`, line 23 +**Description**: The `LibParseLiteral` library has no `@title` or `@notice` tag. All functions within it are fully documented. + +--- + +### P3-LPL-02: Literal parser index constants missing NatSpec [LOW] + +**File**: `src/lib/parse/literal/LibParseLiteral.sol`, lines 16-21 +**Description**: Five file-level constants have no NatSpec documentation: +- `LITERAL_PARSERS_LENGTH` (line 16) +- `LITERAL_PARSER_INDEX_HEX` (line 18) +- `LITERAL_PARSER_INDEX_DECIMAL` (line 19) +- `LITERAL_PARSER_INDEX_STRING` (line 20) +- `LITERAL_PARSER_INDEX_SUB_PARSE` (line 21) + +These define the dispatch indices for literal type resolution and should have `@dev` documentation. + +--- + +### P3-LPLD-01: `LibParseLiteralDecimal` library missing library-level NatSpec [INFO] + +**File**: `src/lib/parse/literal/LibParseLiteralDecimal.sol`, line 10 +**Description**: The `LibParseLiteralDecimal` library has no `@title` or `@notice` tag. The single function within it is fully documented. + +--- + +### P3-LPLH-01: `LibParseLiteralHex` library missing library-level NatSpec [INFO] + +**File**: `src/lib/parse/literal/LibParseLiteralHex.sol`, line 20 +**Description**: The `LibParseLiteralHex` library has no `@title` or `@notice` tag. All functions within it are fully documented. + +--- + +### P3-LPLSP-01: `LibParseLiteralSubParseable` library missing library-level NatSpec [INFO] + +**File**: `src/lib/parse/literal/LibParseLiteralSubParseable.sol`, line 14 +**Description**: The `LibParseLiteralSubParseable` library has no `@title` or `@notice` tag. The single function within it is fully documented. + +--- + +### P3-ERR-01: 13 errors in ErrParse.sol missing explicit NatSpec tags [LOW] + +**File**: `src/error/ErrParse.sol` +**Description**: 13 custom errors have NatSpec doc comments but lack explicit tags (`@notice`/`@dev`). While untagged doc comments default to `@notice` when no other tags are present in the block, many peer errors in the same file DO use explicit `@notice`. This inconsistency should be resolved. The affected errors are: + +- `UnexpectedOperand` (line 8-10) +- `UnexpectedOperandValue` (line 12-14) +- `ExpectedOperand` (line 16-18) +- `MaxSources` (line 120-121) +- `DanglingSource` (line 123-124) +- `ParserOutOfBounds` (line 126-127) +- `ParseStackOverflow` (line 129-131) +- `ParseStackUnderflow` (line 133-134) +- `ParenOverflow` (line 136-138) +- `OperandOverflow` (line 165-166) +- `SourceItemOpsOverflow` (line 173-175) +- `ParenInputOverflow` (line 177-179) +- `LineRHSItemsOverflow` (line 181-183) + +--- + +## Summary + +| Severity | Count | +|----------|-------| +| CRITICAL | 0 | +| HIGH | 0 | +| MEDIUM | 0 | +| LOW | 5 (P3-LPS-01, P3-LPS-02, P3-LPP-02, P3-LPST-02, P3-LPL-02, P3-ERR-01) | +| INFO | 9 (P3-LP-01, P3-LPE-01, P3-LPI-01, P3-LPO-01, P3-LPP-01, P3-LPST-01, P3-LPL-01, P3-LPLD-01, P3-LPLH-01, P3-LPLSP-01) | + +All 45 internal functions across the 14 files have `@notice`, `@param`, and `@return` NatSpec tags present and accurate. The findings are limited to missing library-level documentation, missing constant documentation, inconsistent tag usage on errors, and one type definition without NatSpec. diff --git a/audit/2026-03-01-01/pass3/RustCrates.md b/audit/2026-03-01-01/pass3/RustCrates.md new file mode 100644 index 000000000..826710e40 --- /dev/null +++ b/audit/2026-03-01-01/pass3/RustCrates.md @@ -0,0 +1,258 @@ +# Pass 3: Rust Crates Documentation Audit + +Audit date: 2026-03-01 +Auditor: Claude Opus 4.6 +Scope: All `.rs` files in `crates/cli/src/`, `crates/dispair/src/`, `crates/eval/src/`, `crates/parser/src/` + +--- + +## Inventory of Public Items + +### crates/cli/src/ + +| File | Item | Line | Has Doc? | +|------|------|------|----------| +| commands/eval.rs | `pub struct ForkEvalCliArgs` | 15 | NO (has clap help attrs) | +| commands/eval.rs | `pub struct Eval` | 105 | NO | +| commands/mod.rs | `pub use Eval` | 4 | NO | +| commands/mod.rs | `pub use Parse` | 5 | NO | +| commands/parse.rs | `pub struct ForkParseArgsCli` | 13 | NO | +| commands/parse.rs | `pub struct Parse` | 25 | NO | +| execute.rs | `pub trait Execute` | 3 | NO | +| execute.rs | `fn execute` | 4 | NO | +| fork.rs | `pub struct NewForkedEvmCliArgs` | 6 | NO | +| lib.rs | `pub enum Interpreter` | 13 | NO | +| lib.rs | `pub async fn execute` | 19 | NO | +| output.rs | `pub enum SupportedOutputEncoding` | 5 | NO | +| output.rs | `pub fn output` | 10 | NO | + +Crate-level `//!` docs: MISSING + +### crates/dispair/src/ + +| File | Item | Line | Has Doc? | +|------|------|------|----------| +| lib.rs | `pub struct DISPaiR` | 6 | YES (redundant name) | +| lib.rs | `pub fn new` | 14 | NO | + +Crate-level `//!` docs: MISSING + +### crates/eval/src/ + +| File | Item | Line | Has Doc? | +|------|------|------|----------| +| error.rs | `pub enum ForkCallError` | 8 | NO | +| error.rs | `pub enum ReplayTransactionError` | 31 | NO | +| eval.rs | `pub struct ForkEvalArgs` | 10 | YES | +| eval.rs | `pub struct ForkParseArgs` | 35 | YES | +| eval.rs | `pub async fn fork_parse` | 66 | YES | +| eval.rs | `pub async fn fork_eval` | 95 | YES | +| fork.rs | `pub struct Forker` | 26 | YES | +| fork.rs | `pub struct ForkTypedReturn` | 31 | NO | +| fork.rs | `pub struct NewForkedEvm` | 37 | NO | +| fork.rs | `pub fn new` | 60 | YES | +| fork.rs | `pub async fn new_with_fork` | 93 | YES (inaccurate) | +| fork.rs | `pub async fn add_or_select` | 148 | YES | +| fork.rs | `pub async fn alloy_call` | 232 | YES (incomplete) | +| fork.rs | `pub async fn alloy_call_committing` | 275 | YES (incomplete) | +| fork.rs | `pub fn call` | 314 | YES | +| fork.rs | `pub fn call_committing` | 342 | YES | +| fork.rs | `pub fn roll_fork` | 372 | YES | +| fork.rs | `pub async fn replay_transaction` | 413 | YES | +| namespace.rs | `pub fn qualify_namespace` | 6 | YES | +| trace.rs | `pub const RAIN_TRACER_ADDRESS` | 14 | NO | +| trace.rs | `pub struct RainSourceTrace` | 20 | YES | +| trace.rs | `pub struct RainEvalResult` | 60 | YES | +| trace.rs | `pub enum RainEvalResultFromRawCallResultError` | 101 | NO | +| trace.rs | `pub enum TraceSearchError` | 141 | NO | +| trace.rs | `pub fn search_trace_by_path` | 149 | NO | +| trace.rs | `pub struct RainEvalResultsTable` | 220 | NO | +| trace.rs | `pub struct RainEvalResults` | 229 | NO | +| trace.rs | `pub fn into_flattened_table` | 240 | NO | +| trace.rs | `pub fn flattened_trace_path_names` | 272 | YES | + +Crate-level `//!` docs: MISSING + +### crates/parser/src/ + +| File | Item | Line | Has Doc? | +|------|------|------|----------| +| error.rs | `pub enum ParserError` | 5 | NO | +| v2.rs | `pub trait Parser2` | 9 | NO | +| v2.rs | `fn parse_text` | 11 | YES | +| v2.rs | `fn parse` | 24 | YES | +| v2.rs | `fn parse_pragma` | 32 | YES | +| v2.rs | `fn parse_pragma_text` | 39 | YES | +| v2.rs | `pub struct ParserV2` | 103 | YES (redundant name) | +| v2.rs | `pub fn new` | 124 | NO | + +Crate-level `//!` docs: MISSING + +--- + +## Findings + +### P3-RC-01: No crate-level documentation on any Rust crate [LOW] + +**Files:** +- `crates/cli/src/lib.rs` +- `crates/dispair/src/lib.rs` +- `crates/eval/src/lib.rs` +- `crates/parser/src/lib.rs` + +None of the four Rust crates have crate-level `//!` documentation. Crate-level docs are the +primary entry point for `cargo doc` and describe the crate's purpose, usage, and key types. + +**Fix:** `.fixes/P3-RC-01.md` + +--- + +### P3-RC-02: cli crate has zero doc comments on all 13 public items [LOW] + +**File:** All files under `crates/cli/src/` + +Every public struct, enum, trait, and function in the `cli` crate lacks `///` doc comments: +- `ForkEvalCliArgs` (commands/eval.rs:15) +- `Eval` (commands/eval.rs:105) +- `ForkParseArgsCli` (commands/parse.rs:13) +- `Parse` (commands/parse.rs:25) +- `Execute` trait (execute.rs:3) +- `Execute::execute` (execute.rs:4) +- `NewForkedEvmCliArgs` (fork.rs:6) +- `Interpreter` enum (lib.rs:13) +- `Interpreter::execute` (lib.rs:19) +- `SupportedOutputEncoding` (output.rs:5) +- `output` fn (output.rs:10) + +While some structs use clap `#[arg(help = "...")]` annotations, those are CLI help text, not +rustdoc documentation. The two serve different audiences. + +**Fix:** `.fixes/P3-RC-02.md` + +--- + +### P3-RC-03: eval crate has 10 undocumented public items [LOW] + +**Files:** `crates/eval/src/error.rs`, `crates/eval/src/fork.rs`, `crates/eval/src/trace.rs` + +The following public items lack doc comments: +- `ForkCallError` (error.rs:8) -- public error enum, no overview doc +- `ReplayTransactionError` (error.rs:31) -- public error enum, no overview doc +- `ForkTypedReturn` (fork.rs:31) -- key return type, no doc +- `NewForkedEvm` (fork.rs:37) -- configuration struct, no doc +- `RAIN_TRACER_ADDRESS` (trace.rs:14) -- magic constant, no doc +- `RainEvalResultFromRawCallResultError` (trace.rs:101) -- error type, no doc +- `TraceSearchError` (trace.rs:141) -- error type, no doc +- `search_trace_by_path` (trace.rs:149) -- public method, no doc +- `RainEvalResultsTable` (trace.rs:220) -- public struct, no doc +- `RainEvalResults` (trace.rs:229) -- public struct, no doc +- `into_flattened_table` (trace.rs:240) -- public method, no doc + +**Fix:** `.fixes/P3-RC-03.md` + +--- + +### P3-RC-04: parser crate has 3 undocumented public items [LOW] + +**Files:** `crates/parser/src/error.rs`, `crates/parser/src/v2.rs` + +- `ParserError` (error.rs:5) -- public error enum, no doc +- `Parser2` trait (v2.rs:9) -- the trait itself has no doc, though its methods do +- `ParserV2::new` (v2.rs:124) -- constructor, no doc + +**Fix:** `.fixes/P3-RC-04.md` + +--- + +### P3-RC-05: dispair crate `DISPaiR::new` has no doc comment [LOW] + +**File:** `crates/dispair/src/lib.rs:14` + +The only constructor for the main type in this crate lacks a doc comment. + +**Fix:** `.fixes/P3-RC-05.md` + +--- + +### P3-RC-06: "Rainalang" typo in `ForkEvalArgs` field doc [LOW] + +**File:** `crates/eval/src/eval.rs:11` + +```rust +/// The Rainalang string to evaluate +pub rainlang_string: String, +``` + +"Rainalang" should be "Rainlang" -- every other reference in the codebase uses "Rainlang" +(including the field name itself). + +**Fix:** `.fixes/P3-RC-06.md` + +--- + +### P3-RC-07: `new_with_fork` doc describes params that don't match signature [LOW] + +**File:** `crates/eval/src/fork.rs:70-92` + +The `# Arguments` section documents `fork_url` and `fork_block_number` as separate parameters, +but the actual signature takes `args: NewForkedEvm`. The doc describes fields of the struct as +if they were direct function parameters. + +```rust +/// * `fork_url` - The URL of the fork to connect to. +/// * `fork_block_number` - Optional fork block number to start from. +/// * `env` - Optional fork environment. +/// * `gas_limit` - Optional fork gas limit. +``` + +Actual signature: +```rust +pub async fn new_with_fork( + args: NewForkedEvm, + env: Option, + gas_limit: Option, +) -> Result +``` + +**Fix:** `.fixes/P3-RC-07.md` + +--- + +### P3-RC-08: `alloy_call` and `alloy_call_committing` docs omit `decode_error` parameter [LOW] + +**File:** `crates/eval/src/fork.rs:225-231, 267-274` + +Both functions have a `decode_error: bool` parameter that is not listed in their `# Arguments` +sections. This parameter controls whether revert errors are decoded via the openchain.xyz +selector registry, which is significant behavior. + +**Fix:** `.fixes/P3-RC-08.md` + +--- + +### P3-RC-09: Redundant struct-name-as-first-line in doc comments [INFO] + +**Files:** +- `crates/dispair/src/lib.rs:3-4`: `/// DISPaiR\n/// Struct representing...` +- `crates/parser/src/v2.rs:100-101`: `/// ParserV2\n/// Struct representing ParserV2 instances.` + +Rustdoc already shows the struct name. The first line of a doc comment should be a summary +sentence describing what the type does, not repeating its name. + +--- + +## Summary + +| Severity | Count | +|----------|-------| +| CRITICAL | 0 | +| HIGH | 0 | +| MEDIUM | 0 | +| LOW | 8 | +| INFO | 1 | +| **Total** | **9** | + +All findings are documentation quality issues. The codebase has reasonable documentation on the +core `eval` crate's main types and methods, but the `cli` crate is entirely undocumented, and +several key types across other crates lack doc comments. No crate has crate-level `//!` +documentation. diff --git a/audit/2026-03-01-01/pass4/CoreConcrete.md b/audit/2026-03-01-01/pass4/CoreConcrete.md new file mode 100644 index 000000000..1e3bd4a08 --- /dev/null +++ b/audit/2026-03-01-01/pass4/CoreConcrete.md @@ -0,0 +1,395 @@ +# Pass 4: Code Quality -- Core Concrete Contracts + +Audit date: 2026-03-01 +Auditor: Claude Opus 4.6 + +## Scope + +| File | Lines | +|------|-------| +| `src/concrete/Rainterpreter.sol` | 81 | +| `src/concrete/RainterpreterStore.sol` | 69 | +| `src/concrete/RainterpreterParser.sol` | 115 | +| `src/concrete/RainterpreterExpressionDeployer.sol` | 81 | +| `src/concrete/RainterpreterDISPaiRegistry.sol` | 40 | +| `src/interface/IDISPaiRegistry.sol` | 25 | +| `src/lib/deploy/LibInterpreterDeploy.sol` | 66 | + +--- + +## Evidence of Thorough Reading + +### 1. Rainterpreter.sol (81 lines) + +**Contract:** `Rainterpreter` (line 32), inherits `IInterpreterV4`, `IOpcodeToolingV1`, `ERC165` + +**Imports:** +- `ERC165` from openzeppelin (line 5) +- `LibMemoryKV`, `MemoryKVKey`, `MemoryKVVal` from rain.lib.memkv (line 6) +- `LibEval` (line 8) +- `LibInterpreterStateDataContract` (line 9) +- `InterpreterState` (line 10) +- `LibAllStandardOps` (line 11) +- `IInterpreterV4`, `SourceIndexV2`, `EvalV4`, `StackItem` (lines 12-17) +- `BYTECODE_HASH` (aliased `INTERPRETER_BYTECODE_HASH`), `OPCODE_FUNCTION_POINTERS` (lines 18-24) +- `IOpcodeToolingV1` (line 25) +- `OddSetLength` (line 26) +- `ZeroFunctionPointers` (line 27) + +**Using directives:** +- `LibEval for InterpreterState` (line 33) +- `LibInterpreterStateDataContract for bytes` (line 34) + +**Functions:** + +| Function | Line | Visibility | Mutability | Keywords | +|----------|------|-----------|------------|----------| +| `constructor` | 38 | N/A | N/A | -- | +| `opcodeFunctionPointers` | 45 | internal | view | virtual | +| `eval4` | 50 | external | view | virtual override | +| `supportsInterface` | 73 | public | view | virtual override | +| `buildOpcodeFunctionPointers` | 78 | public | view | virtual override | + +**Constants used:** `OPCODE_FUNCTION_POINTERS` (line 46), `type(uint256).max` (line 69) + +--- + +### 2. RainterpreterStore.sol (69 lines) + +**Contract:** `RainterpreterStore` (line 25), inherits `IInterpreterStoreV3`, `ERC165` + +**Imports:** +- `ERC165` from openzeppelin (line 5) +- `IInterpreterStoreV3` (line 7) +- `LibNamespace`, `FullyQualifiedNamespace`, `StateNamespace` (lines 8-12) +- `BYTECODE_HASH` (aliased `STORE_BYTECODE_HASH`) (line 16) +- `OddSetLength` (line 17) + +**Using directives:** +- `LibNamespace for StateNamespace` (line 26) + +**State variables:** +- `sStore` (line 40): `mapping(FullyQualifiedNamespace => mapping(bytes32 => bytes32))`, internal + +**Functions:** + +| Function | Line | Visibility | Mutability | Keywords | +|----------|------|-----------|------------|----------| +| `supportsInterface` | 43 | public | view | virtual override | +| `set` | 48 | external | (mutating) | virtual | +| `get` | 66 | external | view | virtual | + +--- + +### 3. RainterpreterParser.sol (115 lines) + +**Contract:** `RainterpreterParser` (line 36), inherits `ERC165`, `IParserToolingV1` + +**Imports:** +- `ERC165` from openzeppelin (line 5) +- `LibParse` (line 7) +- `PragmaV1` (line 9) +- `LibParseState`, `ParseState` (line 10) +- `LibParsePragma` (line 11) +- `LibAllStandardOps` (line 12) +- `LibBytes`, `Pointer` (line 13) +- `LibParseInterstitial` (line 14) +- `LITERAL_PARSER_FUNCTION_POINTERS`, `BYTECODE_HASH` (aliased `PARSER_BYTECODE_HASH`), `OPERAND_HANDLER_FUNCTION_POINTERS`, `PARSE_META`, `PARSE_META_BUILD_DEPTH` (lines 15-27) +- `IParserToolingV1` (line 28) + +**Using directives:** +- `LibParse for ParseState` (line 37) +- `LibParseState for ParseState` (line 38) +- `LibParsePragma for ParseState` (line 39) +- `LibParseInterstitial for ParseState` (line 40) +- `LibBytes for bytes` (line 41) + +**Modifier:** `checkParseMemoryOverflow` (line 46) + +**Functions:** + +| Function | Line | Visibility | Mutability | Keywords | +|----------|------|-----------|------------|----------| +| `unsafeParse` | 57 | external | view | (modifier: checkParseMemoryOverflow) | +| `supportsInterface` | 71 | public | view | virtual override | +| `parsePragma1` | 79 | external | view | virtual (modifier: checkParseMemoryOverflow) | +| `parseMeta` | 92 | internal | pure | virtual | +| `operandHandlerFunctionPointers` | 97 | internal | pure | virtual | +| `literalParserFunctionPointers` | 102 | internal | pure | virtual | +| `buildOperandHandlerFunctionPointers` | 107 | external | pure | override | +| `buildLiteralParserFunctionPointers` | 112 | external | pure | override | + +--- + +### 4. RainterpreterExpressionDeployer.sol (81 lines) + +**Contract:** `RainterpreterExpressionDeployer` (line 26), inherits `IDescribedByMetaV1`, `IParserV2`, `IParserPragmaV1`, `IIntegrityToolingV1`, `ERC165` + +**Imports:** +- `ERC165`, `IERC165` from openzeppelin (line 5) +- `Pointer` from rain.solmem (line 6) +- `IParserV2` (line 7) +- `IParserPragmaV1`, `PragmaV1` (line 8) +- `IDescribedByMetaV1` (line 10) +- `LibIntegrityCheck` (line 12) +- `LibInterpreterStateDataContract` (line 13) +- `LibAllStandardOps` (line 14) +- `INTEGRITY_FUNCTION_POINTERS`, `DESCRIBED_BY_META_HASH` (lines 15-18) +- `IIntegrityToolingV1` (line 19) +- `RainterpreterParser` (line 20) +- `LibInterpreterDeploy` (line 21) + +**Functions:** + +| Function | Line | Visibility | Mutability | Keywords | +|----------|------|-----------|------------|----------| +| `supportsInterface` | 34 | public | view | virtual override | +| `parse2` | 41 | external | view | virtual override | +| `parsePragma1` | 66 | external | view | virtual override | +| `buildIntegrityFunctionPointers` | 73 | external | view | virtual | +| `describedByMetaV1` | 78 | external | pure | override | + +--- + +### 5. RainterpreterDISPaiRegistry.sol (40 lines) + +**Contract:** `RainterpreterDISPaiRegistry` (line 15), inherits `IDISPaiRegistry`, `ERC165` + +**Imports:** +- `LibInterpreterDeploy` (line 5) +- `IDISPaiRegistry` (line 6) +- `ERC165` from openzeppelin (line 7) + +**Functions:** + +| Function | Line | Visibility | Mutability | Keywords | +|----------|------|-----------|------------|----------| +| `supportsInterface` | 17 | public | view | override | +| `expressionDeployerAddress` | 22 | external | pure | override | +| `interpreterAddress` | 27 | external | pure | override | +| `storeAddress` | 32 | external | pure | override | +| `parserAddress` | 37 | external | pure | override | + +--- + +### 6. IDISPaiRegistry.sol (25 lines) + +**Interface:** `IDISPaiRegistry` (line 9) + +**Functions:** +- `expressionDeployerAddress()` (line 12) -- external pure returns (address) +- `interpreterAddress()` (line 16) -- external pure returns (address) +- `storeAddress()` (line 20) -- external pure returns (address) +- `parserAddress()` (line 24) -- external pure returns (address) + +--- + +### 7. LibInterpreterDeploy.sol (66 lines) + +**Library:** `LibInterpreterDeploy` (line 11) + +**Constants:** + +| Constant | Line | Type | +|----------|------|------| +| `PARSER_DEPLOYED_ADDRESS` | 14 | address | +| `PARSER_DEPLOYED_CODEHASH` | 20-21 | bytes32 | +| `STORE_DEPLOYED_ADDRESS` | 25 | address | +| `STORE_DEPLOYED_CODEHASH` | 31-32 | bytes32 | +| `INTERPRETER_DEPLOYED_ADDRESS` | 36 | address | +| `INTERPRETER_DEPLOYED_CODEHASH` | 42-43 | bytes32 | +| `EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS` | 47 | address | +| `EXPRESSION_DEPLOYER_DEPLOYED_CODEHASH` | 53-54 | bytes32 | +| `DISPAIR_REGISTRY_DEPLOYED_ADDRESS` | 58 | address | +| `DISPAIR_REGISTRY_DEPLOYED_CODEHASH` | 64-65 | bytes32 | + +--- + +## Findings + +### P4-CC-01: Unused import `IERC165` in `RainterpreterExpressionDeployer` [LOW] + +**File:** `src/concrete/RainterpreterExpressionDeployer.sol`, line 5 + +**Evidence:** + +```solidity +import {ERC165, IERC165} from "openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; +``` + +`IERC165` is imported alongside `ERC165` but is never referenced anywhere in the file. `ERC165` is used as a base contract, but `IERC165` is not used in any type expression, cast, or `type()` call. All other concrete contracts import only `ERC165` from this module. + +**Impact:** Dead import. Adds noise to the dependency list. Could mislead readers into thinking the interface is needed directly. + +--- + +### P4-CC-02: `RainterpreterDISPaiRegistry` missing `virtual` on all functions, inconsistent with all other concrete contracts [LOW] + +**File:** `src/concrete/RainterpreterDISPaiRegistry.sol`, lines 17-37 + +**Evidence:** + +`RainterpreterDISPaiRegistry` declares zero `virtual` functions. Every other concrete contract in the reviewed scope uses `virtual` on all of its functions: + +| Contract | `virtual` on `supportsInterface` | `virtual` on other functions | +|----------|----------------------------------|------------------------------| +| `Rainterpreter` | Yes (line 73) | Yes -- `eval4`, `opcodeFunctionPointers`, `buildOpcodeFunctionPointers` | +| `RainterpreterStore` | Yes (line 43) | Yes -- `set`, `get` | +| `RainterpreterParser` | Yes (line 71) | Yes -- `parsePragma1`, `parseMeta`, `operandHandlerFunctionPointers`, `literalParserFunctionPointers` | +| `RainterpreterExpressionDeployer` | Yes (line 34) | Yes -- `parse2`, `parsePragma1`, `buildIntegrityFunctionPointers` | +| **`RainterpreterDISPaiRegistry`** | **No** (line 17) | **No** -- all four address functions | + +The registry contract is effectively sealed: no function can be overridden by a subclass. If this is intentional, it is undocumented. If not, it breaks the convention all other contracts follow. + +**Impact:** Prevents subclassing for testing or extension. Inconsistent style across the component suite. + +--- + +### P4-CC-03: `buildIntegrityFunctionPointers` missing `override` keyword in `RainterpreterExpressionDeployer` [LOW] + +**File:** `src/concrete/RainterpreterExpressionDeployer.sol`, line 73 + +**Evidence:** + +```solidity +function buildIntegrityFunctionPointers() external view virtual returns (bytes memory) { +``` + +This function implements `IIntegrityToolingV1.buildIntegrityFunctionPointers()`, but the `override` keyword is absent. Every other interface-implementing function in the same contract uses `override`: + +- `supportsInterface` -- `virtual override` (line 34) +- `parse2` -- `virtual override` (line 41) +- `parsePragma1` -- `virtual override` (line 66) +- `describedByMetaV1` -- `override` (line 78) + +The Solidity compiler allows omitting `override` for single-interface implementations as of 0.8.x in some circumstances, but the inconsistency within this single contract makes the omission look accidental. + +**Impact:** Style inconsistency. A reader checking that all interface functions are properly implemented cannot rely on the `override` keyword as a signal for this function. + +--- + +### P4-CC-04: `describedByMetaV1` missing `virtual` in `RainterpreterExpressionDeployer` [LOW] + +**File:** `src/concrete/RainterpreterExpressionDeployer.sol`, line 78 + +**Evidence:** + +```solidity +function describedByMetaV1() external pure override returns (bytes32) { +``` + +Four out of five functions in this contract use `virtual`. `describedByMetaV1` is the only one without it. The other four are: +- `supportsInterface` -- `virtual override` +- `parse2` -- `virtual override` +- `parsePragma1` -- `virtual override` +- `buildIntegrityFunctionPointers` -- `virtual` (no override) + +This means a subclass can override every function except `describedByMetaV1`. Since the meta hash is a generated constant that changes when bytecode changes, sealing this one function while leaving the rest virtual is an inconsistent design choice. + +**Impact:** Prevents subclass override of the meta hash accessor while allowing override of all other functions. Either all should be sealed or all should be virtual. + +--- + +### P4-CC-05: `RainterpreterParser.unsafeParse` missing `virtual` while sibling function `parsePragma1` is `virtual` [LOW] + +**File:** `src/concrete/RainterpreterParser.sol`, lines 57-68 + +**Evidence:** + +```solidity +function unsafeParse(bytes memory data) + external + view + checkParseMemoryOverflow + returns (bytes memory, bytes32[] memory) +``` + +`unsafeParse` is the primary parsing function but is not `virtual`. In the same contract, `parsePragma1` (line 79) IS `virtual`. The three internal helper functions (`parseMeta`, `operandHandlerFunctionPointers`, `literalParserFunctionPointers`) are also `virtual`. This means a subclass can override the pragma parser, the meta, and all the function pointer tables, but cannot override the main parse entry point. + +This may be intentional (the `unsafeParse` entry point should always use the same `LibParseState.newState(...).parse()` sequence), but it is not documented and breaks the pattern of the rest of the contract. + +**Impact:** Subclasses that customize parsing behavior via `virtual` internal functions cannot also customize the top-level `unsafeParse` coordination logic. + +--- + +### P4-CC-06: Inheritance order inconsistency -- `RainterpreterParser` puts `ERC165` first while all others put it last [INFO] + +**File:** `src/concrete/RainterpreterParser.sol`, line 36 + +**Evidence:** + +| Contract | Inheritance order | +|----------|-------------------| +| `Rainterpreter` (line 32) | `IInterpreterV4, IOpcodeToolingV1, ERC165` | +| `RainterpreterStore` (line 25) | `IInterpreterStoreV3, ERC165` | +| `RainterpreterParser` (line 36) | **`ERC165, IParserToolingV1`** | +| `RainterpreterExpressionDeployer` (lines 26-31) | `IDescribedByMetaV1, IParserV2, IParserPragmaV1, IIntegrityToolingV1, ERC165` | +| `RainterpreterDISPaiRegistry` (line 15) | `IDISPaiRegistry, ERC165` | + +Four out of five contracts list `ERC165` last. `RainterpreterParser` lists it first. No functional impact due to Solidity's C3 linearization, but the inconsistency is a readability concern. + +--- + +### P4-CC-07: `buildOpcodeFunctionPointers` is `public` in `Rainterpreter` while analogous tooling functions are `external` elsewhere [INFO] + +**File:** `src/concrete/Rainterpreter.sol`, line 78 + +**Evidence:** + +```solidity +function buildOpcodeFunctionPointers() public view virtual override returns (bytes memory) { +``` + +Comparison with analogous tooling functions: +- `RainterpreterParser.buildOperandHandlerFunctionPointers` -- `external pure override` (line 107) +- `RainterpreterParser.buildLiteralParserFunctionPointers` -- `external pure override` (line 112) +- `RainterpreterExpressionDeployer.buildIntegrityFunctionPointers` -- `external view virtual` (line 73) + +The interface `IOpcodeToolingV1` declares the function as `external view`. Solidity allows `public` to implement `external` interface functions, but the Rainterpreter is the only concrete contract that widens the visibility to `public`. There is no internal caller that would require `public` over `external`. + +--- + +### P4-CC-08: `opcodeFunctionPointers` is `view` but only reads a compile-time constant [INFO] + +**File:** `src/concrete/Rainterpreter.sol`, line 45 + +**Evidence:** + +```solidity +function opcodeFunctionPointers() internal view virtual returns (bytes memory) { + return OPCODE_FUNCTION_POINTERS; +} +``` + +The function returns `OPCODE_FUNCTION_POINTERS`, a `bytes constant` from the generated pointers file. This requires no state access -- `pure` would be sufficient. By contrast, the three analogous internal virtual functions in `RainterpreterParser` (`parseMeta`, `operandHandlerFunctionPointers`, `literalParserFunctionPointers`) are all declared `pure`. + +The `view` may be intentional to permit overrides that read state (e.g., a dynamic opcode table), but this design choice is undocumented and inconsistent with the parser. + +--- + +### P4-CC-09: No commented-out code found [N/A] + +A complete review of all seven files found zero commented-out code blocks. All comments are either NatSpec documentation, lint/slither suppressions, or explanatory inline comments. + +--- + +### P4-CC-10: No dead code or unused imports found (except P4-CC-01) [N/A] + +Apart from the unused `IERC165` import noted in P4-CC-01, all imports in all seven files are used. The `BYTECODE_HASH` re-exports (aliased as `INTERPRETER_BYTECODE_HASH`, `STORE_BYTECODE_HASH`, `PARSER_BYTECODE_HASH`) are marked with `//forge-lint: disable-next-line(unused-import)` comments explaining they are "exported for convenience." The `PARSE_META_BUILD_DEPTH` re-export in the parser is similarly marked. + +--- + +## Summary + +| ID | Severity | File | Summary | +|----|----------|------|---------| +| P4-CC-01 | LOW | RainterpreterExpressionDeployer.sol | Unused `IERC165` import | +| P4-CC-02 | LOW | RainterpreterDISPaiRegistry.sol | No `virtual` on any function; inconsistent with all other concrete contracts | +| P4-CC-03 | LOW | RainterpreterExpressionDeployer.sol | `buildIntegrityFunctionPointers` missing `override` keyword | +| P4-CC-04 | LOW | RainterpreterExpressionDeployer.sol | `describedByMetaV1` missing `virtual`; inconsistent within same contract | +| P4-CC-05 | LOW | RainterpreterParser.sol | `unsafeParse` missing `virtual` while `parsePragma1` is `virtual` | +| P4-CC-06 | INFO | RainterpreterParser.sol | Inheritance order has `ERC165` first; all others put it last | +| P4-CC-07 | INFO | Rainterpreter.sol | `buildOpcodeFunctionPointers` is `public`; analogous functions elsewhere are `external` | +| P4-CC-08 | INFO | Rainterpreter.sol | `opcodeFunctionPointers` is `view` but only reads a constant; parser equivalents are `pure` | + +Overall assessment: The core concrete contracts are clean and well-structured. There is no commented-out code, no dead code (except one unused import), and no magic numbers. The primary quality issue is inconsistent use of `virtual`/`override` keywords across the five concrete contracts, which creates uncertainty about the intended extensibility contract of each component. diff --git a/audit/2026-03-01-01/pass4/ErrRust.md b/audit/2026-03-01-01/pass4/ErrRust.md new file mode 100644 index 000000000..79872ef18 --- /dev/null +++ b/audit/2026-03-01-01/pass4/ErrRust.md @@ -0,0 +1,144 @@ +# Pass 4: Error Files + Rust Crates -- Maintainability, Consistency, Abstractions + +## Scope + +### Solidity Error Files (10 files) + +| File | Items | +|------|-------| +| `ErrBitwise.sol` | `UnsupportedBitwiseShiftAmount`, `TruncatedBitwiseEncoding`, `ZeroLengthBitwiseEncoding` | +| `ErrDeploy.sol` | `UnknownDeploymentSuite` | +| `ErrEval.sol` | `InputsLengthMismatch`, `ZeroFunctionPointers` | +| `ErrExtern.sol` | import of `NotAnExternContract`, `ExternOpcodeOutOfRange`, `ExternPointersMismatch`, `BadOutputsLength`, `ExternOpcodePointersEmpty` | +| `ErrIntegrity.sol` | `StackUnderflow`, `StackUnderflowHighwater`, `StackAllocationMismatch`, `StackOutputsMismatch`, `OutOfBoundsConstantRead`, `OutOfBoundsStackRead`, `CallOutputsExceedSource`, `OpcodeOutOfRange` | +| `ErrOpList.sol` | `BadDynamicLength` | +| `ErrParse.sol` | 37 errors: `UnexpectedOperand`, `UnexpectedOperandValue`, `ExpectedOperand`, `OperandValuesOverflow`, `UnclosedOperand`, `UnsupportedLiteralType`, `StringTooLong`, `UnclosedStringLiteral`, `HexLiteralOverflow`, `ZeroLengthHexLiteral`, `OddLengthHexLiteral`, `MalformedHexLiteral`, `MissingFinalSemi`, `UnexpectedLHSChar`, `UnexpectedRHSChar`, `ExpectedLeftParen`, `UnexpectedRightParen`, `UnclosedLeftParen`, `UnexpectedComment`, `UnclosedComment`, `MalformedCommentStart`, `DuplicateLHSItem`, `ExcessLHSItems`, `NotAcceptingInputs`, `ExcessRHSItems`, `WordSize`, `UnknownWord`, `MaxSources`, `DanglingSource`, `ParserOutOfBounds`, `ParseStackOverflow`, `ParseStackUnderflow`, `ParenOverflow`, `NoWhitespaceAfterUsingWordsFrom`, `InvalidSubParser`, `UnclosedSubParseableLiteral`, `SubParseableMissingDispatch`, `BadSubParserResult`, `OpcodeIOOverflow`, `OperandOverflow`, `ParseMemoryOverflow`, `SourceItemOpsOverflow`, `ParenInputOverflow`, `LineRHSItemsOverflow` | +| `ErrRainType.sol` | `NotAnAddress` | +| `ErrStore.sol` | `OddSetLength` | +| `ErrSubParse.sol` | `ExternDispatchConstantsHeightOverflow`, `ConstantOpcodeConstantsHeightOverflow`, `ContextGridOverflow`, `SubParserIndexOutOfBounds` | + +### Rust Crates (4 crates, 17 files) + +| Crate | File | Items | +|-------|------|-------| +| cli | `main.rs` | `Cli` struct, `main()` | +| cli | `lib.rs` | `Interpreter` enum (`Parse`, `Eval`), `execute()` | +| cli | `execute.rs` | `Execute` trait | +| cli | `output.rs` | `SupportedOutputEncoding` enum, `output()` fn | +| cli | `fork.rs` | `NewForkedEvmCliArgs` struct, `From` impl | +| cli | `commands/mod.rs` | re-exports `Eval`, `Parse` | +| cli | `commands/eval.rs` | `ForkEvalCliArgs`, `Eval` structs, `TryFrom` impl, `parse_int_or_hex()`, `Execute` impl, tests | +| cli | `commands/parse.rs` | `ForkParseArgsCli`, `Parse` structs, `From` impl, `Execute` impl | +| dispair | `lib.rs` | `DISPaiR` struct, `new()`, tests | +| eval | `lib.rs` | module declarations | +| eval | `error.rs` | `ForkCallError` enum (7 variants), `ReplayTransactionError` enum (5 variants), `From` impl | +| eval | `eval.rs` | `ForkEvalArgs`, `ForkParseArgs` structs, `From` impl, `fork_parse()`, `fork_eval()`, tests | +| eval | `fork.rs` | `Forker` struct, `ForkTypedReturn`, `NewForkedEvm`, `mk_journaled_state()`, `mk_env_mut()`, `new()`, `new_with_fork()`, `add_or_select()`, `alloy_call()`, `alloy_call_committing()`, `call()`, `call_committing()`, `roll_fork()`, `replay_transaction()`, tests | +| eval | `namespace.rs` | `qualify_namespace()`, tests | +| eval | `trace.rs` | `RAIN_TRACER_ADDRESS` const, `RainSourceTrace`, `RainEvalResult`, `RainEvalResultFromRawCallResultError`, `TraceSearchError`, `RainEvalResults`, `RainEvalResultsTable`, `flattened_trace_path_names()`, `search_trace_by_path()`, tests | +| parser | `lib.rs` | re-exports `error`, `v2` | +| parser | `error.rs` | `ParserError` enum (2 variants) | +| parser | `v2.rs` | `Parser2` trait (2x wasm/non-wasm), `ParserV2` struct, `From`, `From
`, `new()`, `Parser2` impl, tests | + +--- + +## Findings + +### P4-ERR-01: Inconsistent NatSpec on Solidity error declarations [LOW] + +**Files:** `src/error/ErrBitwise.sol`, `src/error/ErrEval.sol`, `src/error/ErrExtern.sol`, `src/error/ErrParse.sol` + +Many errors use `@notice` tags on their NatSpec doc blocks, but a significant number use bare `///` without any tag. Per project convention, when any tag is present in a doc block, all entries should be explicitly tagged. However, the inconsistency here is across the same file -- some errors have `@notice` and some do not. + +Errors missing `@notice`: +- `ErrBitwise.sol`: `ZeroLengthBitwiseEncoding` (line 22) +- `ErrEval.sol`: `ZeroFunctionPointers` (line 13) +- `ErrExtern.sol`: `ExternOpcodePointersEmpty` (line 28) +- `ErrParse.sol`: `UnexpectedOperand` (line 8), `UnexpectedOperandValue` (line 12), `ExpectedOperand` (line 16), `MaxSources` (line 121), `DanglingSource` (line 124), `ParserOutOfBounds` (line 127), `ParseStackOverflow` (line 130), `ParseStackUnderflow` (line 134), `ParenOverflow` (line 137), `OperandOverflow` (line 166), `SourceItemOpsOverflow` (line 174), `ParenInputOverflow` (line 178), `LineRHSItemsOverflow` (line 182) + +### P4-ERR-02: `ErrParse.sol` errors missing `@param` tags [LOW] + +**File:** `src/error/ErrParse.sol` + +Several parameterless errors have bare `///` doc blocks without `@notice`, which is noted in P4-ERR-01. But additionally, the three errors with parameters that use bare `///` -- `UnexpectedOperand`, `UnexpectedOperandValue`, `ExpectedOperand` -- have no params and are fine. However, this finding overlaps with P4-ERR-01 and is mainly about consistency. + +(Merged into P4-ERR-01 -- no separate fix needed.) + +### P4-RUST-01: Unused dependencies in `crates/eval/Cargo.toml` [LOW] + +**File:** `crates/eval/Cargo.toml` (lines 13-15) + +Three dependencies are declared but never used in any source file in the `crates/eval/src/` directory: +- `serde_json` +- `reqwest` +- `once_cell` + +Only `serde` (used in `trace.rs` for `Serialize`/`Deserialize`) and `eyre` (used in `error.rs` and `fork.rs`) are actually used from the optional-looking set. The unused dependencies add to compile time and dependency tree size. + +### P4-RUST-02: Wildcard imports in `dispair/src/lib.rs` and `parser/src/v2.rs` [LOW] + +**Files:** `crates/dispair/src/lib.rs` line 1, `crates/parser/src/v2.rs` line 2 + +Both files use `use alloy::primitives::*;` instead of importing specific items. In `dispair`, only `Address` is used. In `parser/v2.rs`, `Address` is the only type used from `primitives` (the `hex!` macro comes from `alloy::hex`). Wildcard imports make it harder to identify actual dependencies and can introduce name collisions. + +### P4-RUST-03: Duplicated error-handling logic in `alloy_call` and `alloy_call_committing` [LOW] + +**File:** `crates/eval/src/fork.rs` (lines 232-265 and 275-305) + +The two methods share nearly identical logic for: +1. Checking `decode_error && raw.exit_reason == InstructionResult::Revert` +2. Decoding the error via `AbiDecodedErrorType::selector_registry_abi_decode` +3. Checking `!raw.exit_reason.is_ok()` +4. Decoding the typed return via `T::abi_decode_returns` + +Additionally, the `TypedError` format string is inconsistent between the two: `alloy_call` includes `Raw:{:?}` in the format string but `alloy_call_committing` does not. This divergence suggests the duplication has already led to a maintenance inconsistency. + +### P4-RUST-04: Duplicated `EvmOpts`/`CreateFork` construction in `fork.rs` [INFO] + +**File:** `crates/eval/src/fork.rs` (lines 103-121 and 180-197) + +The `new_with_fork` and `add_or_select` methods both construct identical `EvmOpts` and `CreateFork` structs. This is a DRY violation that could be extracted into a helper. + +### P4-RUST-05: Typo in test variable name [INFO] + +**File:** `crates/eval/src/fork.rs` line 589 + +Variable `fully_quallified_namespace` should be `fully_qualified_namespace`. Test-only code, but reflects a maintenance smell. + +### P4-RUST-06: Doc comment typo "Rainalang" vs "Rainlang" [INFO] + +**File:** `crates/eval/src/eval.rs` line 11 + +The doc comment on `ForkEvalArgs.rainlang_string` says "The Rainalang string to evaluate" but the correct product name used everywhere else is "Rainlang". + +### P4-RUST-07: Duplicated `Parser2` trait for wasm/non-wasm [INFO] + +**File:** `crates/parser/src/v2.rs` (lines 9-52 and 54-98) + +The `Parser2` trait is defined twice with `#[cfg]` gates. The only difference is the `+ Send` bound on the associated return futures. This is 90 lines of duplication. A possible alternative is a macro or using `cfg_attr` to conditionally add the `Send` bound, though this may be impractical with current Rust ergonomics for async trait methods. + +### P4-RUST-08: Magic number 44 in `namespace.rs` [INFO] + +**File:** `crates/eval/src/namespace.rs` line 10 + +The slice `combined[44..]` uses a magic number. The value is `64 - 20` (buffer size minus address byte length), representing the left-padding of a 20-byte address in a 32-byte EVM slot. A named constant or inline comment explaining the derivation would improve readability. + +--- + +## Summary + +| ID | Severity | Category | Description | +|----|----------|----------|-------------| +| P4-ERR-01 | LOW | Style | Inconsistent `@notice` tags across Solidity error files | +| P4-RUST-01 | LOW | Dead deps | Unused `serde_json`, `reqwest`, `once_cell` in eval Cargo.toml | +| P4-RUST-02 | LOW | Style | Wildcard imports in dispair and parser crates | +| P4-RUST-03 | LOW | Duplication | Duplicated error-handling logic in `alloy_call`/`alloy_call_committing` with inconsistent format string | +| P4-RUST-04 | INFO | Duplication | Duplicated EvmOpts/CreateFork construction | +| P4-RUST-05 | INFO | Typo | `fully_quallified_namespace` in test | +| P4-RUST-06 | INFO | Typo | "Rainalang" should be "Rainlang" | +| P4-RUST-07 | INFO | Duplication | Parser2 trait defined twice for wasm/non-wasm | +| P4-RUST-08 | INFO | Readability | Magic number 44 in namespace.rs | + +**No CRITICAL or HIGH findings.** +**No commented-out code found.** +**No dead code found in Rust source files (only unused Cargo.toml dependencies).** diff --git a/audit/2026-03-01-01/pass4/ExternAbstract.md b/audit/2026-03-01-01/pass4/ExternAbstract.md new file mode 100644 index 000000000..ad8a3e473 --- /dev/null +++ b/audit/2026-03-01-01/pass4/ExternAbstract.md @@ -0,0 +1,259 @@ +# Pass 4 -- Maintainability, Consistency & Abstractions: Extern & Abstract Contracts + +Auditor: Claude Opus 4.6 +Date: 2026-03-01 +Scope: 10 files covering extern abstract bases, reference extern, LibExtern, and reference op/literal libraries. + +--- + +## File Inventory and Evidence of Thorough Reading + +### 1. `src/abstract/BaseRainterpreterExtern.sol` (131 lines) + +**Contract:** `BaseRainterpreterExtern` (abstract), inherits `IInterpreterExternV4`, `IIntegrityToolingV1`, `IOpcodeToolingV1`, `ERC165` + +**File-level constants:** +- `OPCODE_FUNCTION_POINTERS` (line 20) -- empty bytes placeholder +- `INTEGRITY_FUNCTION_POINTERS` (line 24) -- empty bytes placeholder + +**Functions:** +- `constructor()` (line 34) -- validates pointer table non-empty and matching lengths +- `extern(ExternDispatchV2, StackItem[])` (line 46) -- runtime dispatch with mod-based bounds safety +- `externIntegrity(ExternDispatchV2, uint256, uint256)` (line 83) -- integrity dispatch with explicit bounds check and revert +- `supportsInterface(bytes4)` (line 112) -- ERC165 for IInterpreterExternV4 + tooling interfaces +- `opcodeFunctionPointers()` (line 121) -- `internal view virtual`, returns empty placeholder +- `integrityFunctionPointers()` (line 128) -- `internal pure virtual`, returns empty placeholder + +### 2. `src/abstract/BaseRainterpreterSubParser.sol` (220 lines) + +**Contract:** `BaseRainterpreterSubParser` (abstract), inherits `ERC165`, `ISubParserV4`, `IDescribedByMetaV1`, `IParserToolingV1`, `ISubParserToolingV1` + +**Using directives:** `LibBytes`, `LibParse`, `LibParseMeta`, `LibParseOperand` + +**File-level constants:** +- `SUB_PARSER_WORD_PARSERS` (line 26) -- empty placeholder +- `SUB_PARSER_PARSE_META` (line 32) -- empty placeholder +- `SUB_PARSER_OPERAND_HANDLERS` (line 36) -- empty placeholder +- `SUB_PARSER_LITERAL_PARSERS` (line 40) -- empty placeholder + +**Functions:** +- `subParserParseMeta()` (line 93) -- `internal pure virtual` +- `subParserWordParsers()` (line 100) -- `internal pure virtual` +- `subParserOperandHandlers()` (line 107) -- `internal pure virtual` +- `subParserLiteralParsers()` (line 114) -- `internal pure virtual` +- `matchSubParseLiteralDispatch(uint256, uint256)` (line 139) -- `internal view virtual`, default returns false +- `subParseLiteral2(bytes)` (line 159) -- `external view virtual`, literal parsing entry point +- `subParseWord2(bytes)` (line 188) -- `external pure virtual`, word parsing entry point +- `supportsInterface(bytes4)` (line 215) -- ERC165 + +### 3. `src/concrete/extern/RainterpreterReferenceExtern.sol` (427 lines) + +**Library:** `LibRainterpreterReferenceExtern` (line 84) +- `authoringMetaV2()` (line 93) -- builds AuthoringMetaV2 array + +**Contract:** `RainterpreterReferenceExtern` (line 157), inherits `BaseRainterpreterSubParser`, `BaseRainterpreterExtern` + +**File-level constants:** +- `SUB_PARSER_WORD_PARSERS_LENGTH` = 5 (line 46) +- `SUB_PARSER_LITERAL_PARSERS_LENGTH` = 1 (line 49) +- `SUB_PARSER_LITERAL_REPEAT_KEYWORD` (line 53) +- `SUB_PARSER_LITERAL_REPEAT_KEYWORD_BYTES32` (line 58) +- `SUB_PARSER_LITERAL_REPEAT_KEYWORD_BYTES_LENGTH` = 18 (line 61) +- `SUB_PARSER_LITERAL_REPEAT_KEYWORD_MASK` (line 65) +- `SUB_PARSER_LITERAL_REPEAT_INDEX` = 0 (line 71) +- `OPCODE_FUNCTION_POINTERS_LENGTH` = 1 (line 77) + +**Errors:** +- `InvalidRepeatCount` (line 74) + +**Contract functions:** +- `describedByMetaV1()` (line 161) +- `subParserParseMeta()` (line 168) -- override, `pure` +- `subParserWordParsers()` (line 175) -- override, `pure` +- `subParserOperandHandlers()` (line 182) -- override, `pure` +- `subParserLiteralParsers()` (line 189) -- override, `pure` +- `opcodeFunctionPointers()` (line 196) -- override, `pure` +- `integrityFunctionPointers()` (line 203) -- override, `pure` +- `buildLiteralParserFunctionPointers()` (line 209) -- external +- `matchSubParseLiteralDispatch(uint256, uint256)` (line 232) -- override, `pure` +- `buildOperandHandlerFunctionPointers()` (line 275) -- external +- `buildSubParserWordParsers()` (line 318) -- external +- `buildOpcodeFunctionPointers()` (line 358) -- external +- `buildIntegrityFunctionPointers()` (line 390) -- external +- `supportsInterface(bytes4)` (line 418) -- resolves diamond + +### 4. `src/lib/extern/LibExtern.sol` (80 lines) + +**Library:** `LibExtern` +- `encodeExternDispatch(uint256, OperandV2)` (line 27) -- encodes opcode+operand into ExternDispatchV2 +- `decodeExternDispatch(ExternDispatchV2)` (line 35) -- inverse of encode +- `encodeExternCall(IInterpreterExternV4, ExternDispatchV2)` (line 56) -- encodes address+dispatch +- `decodeExternCall(EncodedExternDispatchV2)` (line 70) -- inverse of encodeExternCall + +### 5. `src/lib/extern/reference/literal/LibParseLiteralRepeat.sol` (73 lines) + +**Library:** `LibParseLiteralRepeat` + +**File-level constants:** +- `MAX_REPEAT_LITERAL_LENGTH` = 78 (line 34) + +**Errors:** +- `RepeatLiteralTooLong(uint256)` (line 39) +- `RepeatDispatchNotDigit(uint256)` (line 43) + +**Functions:** +- `parseRepeat(uint256, uint256, uint256)` (line 53) -- repeats a digit for every body byte + +### 6. `src/lib/extern/reference/op/LibExternOpContextCallingContract.sol` (23 lines) + +**Library:** `LibExternOpContextCallingContract` +**Functions:** +- `subParser(uint256, uint256, OperandV2)` (line 19) -- pure, delegates to `LibSubParse.subParserContext` + +### 7. `src/lib/extern/reference/op/LibExternOpContextRainlen.sol` (22 lines) + +**Library:** `LibExternOpContextRainlen` + +**File-level constants:** +- `CONTEXT_CALLER_CONTEXT_COLUMN` = 1 (line 8) +- `CONTEXT_CALLER_CONTEXT_ROW_RAINLEN` = 0 (line 9) + +**Functions:** +- `subParser(uint256, uint256, OperandV2)` (line 18) -- pure, delegates to `LibSubParse.subParserContext` + +### 8. `src/lib/extern/reference/op/LibExternOpContextSender.sol` (21 lines) + +**Library:** `LibExternOpContextSender` +**Functions:** +- `subParser(uint256, uint256, OperandV2)` (line 17) -- pure, delegates to `LibSubParse.subParserContext` + +### 9. `src/lib/extern/reference/op/LibExternOpIntInc.sol` (67 lines) + +**Library:** `LibExternOpIntInc` +**Using directives:** `LibDecimalFloat` + +**File-level constants:** +- `OP_INDEX_INCREMENT` = 0 (line 13) + +**Functions:** +- `run(OperandV2, StackItem[])` (line 27) -- pure, increments every input by 1 +- `integrity(OperandV2, uint256, uint256)` (line 44) -- pure, returns inputs == outputs +- `subParser(uint256, uint256, OperandV2)` (line 57) -- view (uses `address(this)`), delegates to `LibSubParse.subParserExtern` + +### 10. `src/lib/extern/reference/op/LibExternOpStackOperand.sol` (31 lines) + +**Library:** `LibExternOpStackOperand` +**Functions:** +- `subParser(uint256, uint256, OperandV2)` (line 23) -- pure, delegates to `LibSubParse.subParserConstant` + +--- + +## Findings + +### P4-EA-01 [LOW] -- Dispatch decoding duplicated inline instead of reusing `LibExtern` + +**Files:** +- `src/abstract/BaseRainterpreterExtern.sol`, lines 71-72 and 97+101 +- `src/lib/extern/LibExtern.sol`, lines 35-39 + +**Description:** `BaseRainterpreterExtern.extern()` and `externIntegrity()` both inline the dispatch decoding logic: +```solidity +uint256 opcode = uint256((ExternDispatchV2.unwrap(dispatch) >> 0x10) & bytes32(uint256(type(uint16).max))); +OperandV2 operand = OperandV2.wrap(ExternDispatchV2.unwrap(dispatch) & bytes32(uint256(type(uint16).max))); +``` +This logic is duplicated across two functions in the same contract, and is also a slightly different version of what `LibExtern.decodeExternDispatch()` does. The base contract applies a 16-bit mask (`type(uint16).max`) while `LibExtern.decodeExternDispatch()` does not mask the opcode at all. This creates three copies of "decode a dispatch" logic with two different semantics for the opcode extraction. If the encoding format changes, three locations need updating, and the semantic divergence (masked vs unmasked) makes it unclear which is the canonical behavior. + +**Fix:** Either `BaseRainterpreterExtern` should call `LibExtern.decodeExternDispatch` (with the mask added there), or a shared internal helper should exist. At minimum, the two inline copies in `extern()` and `externIntegrity()` could be extracted to a private function in `BaseRainterpreterExtern`. + +### P4-EA-02 [LOW] -- Inconsistent bitmask style: `type(uint16).max` vs `0xFFFF` + +**Files:** +- `src/abstract/BaseRainterpreterExtern.sol`, lines 71-72, 97, 101 -- uses `bytes32(uint256(type(uint16).max))` +- `src/lib/extern/LibExtern.sol`, line 38 -- uses `bytes32(uint256(0xFFFF))` + +**Description:** Within the scoped files, two different spellings of the same constant are used for the 16-bit operand/opcode mask. `BaseRainterpreterExtern` uses `type(uint16).max` while `LibExtern` uses the literal `0xFFFF`. Both produce the same value, but the inconsistency hurts readability when comparing the decoding logic across files. A reader comparing the two must verify they are equivalent rather than recognizing them at a glance. + +**Fix:** Pick one style and use it consistently. `type(uint16).max` is more self-documenting and avoids reliance on the reader knowing hex constants. + +### P4-EA-03 [LOW] -- Context position constants for rainlen defined locally instead of in a shared location + +**Files:** +- `src/lib/extern/reference/op/LibExternOpContextRainlen.sol`, lines 8-9 +- `src/lib/extern/reference/op/LibExternOpContextSender.sol`, line 7 (imports from `LibContext.sol`) +- `src/lib/extern/reference/op/LibExternOpContextCallingContract.sol`, lines 8-10 (imports from `LibContext.sol`) + +**Description:** `LibExternOpContextSender` and `LibExternOpContextCallingContract` import their context grid constants (`CONTEXT_BASE_COLUMN`, `CONTEXT_BASE_ROW_SENDER`, `CONTEXT_BASE_ROW_CALLING_CONTRACT`) from the canonical interface library `rain.interpreter.interface/lib/caller/LibContext.sol`. However, `LibExternOpContextRainlen` defines its constants locally: +```solidity +uint256 constant CONTEXT_CALLER_CONTEXT_COLUMN = 1; +uint256 constant CONTEXT_CALLER_CONTEXT_ROW_RAINLEN = 0; +``` +This breaks the pattern established by the other two context ops. The column 1 / row 0 position for rainlang byte length is a convention that any caller implementing the standard context grid would need to know. Having it defined only in a reference extern op file means other code that needs this position must either duplicate the constant or import from an unexpected location. If the interface library's `LibContext.sol` does not yet define constants for column 1, that is where they should be added. + +**Fix:** Add `CONTEXT_CALLER_CONTEXT_COLUMN` and `CONTEXT_CALLER_CONTEXT_ROW_RAINLEN` (or equivalent names) to `rain.interpreter.interface/lib/caller/LibContext.sol` alongside the existing base context constants, and import them in `LibExternOpContextRainlen.sol` instead of defining them locally. + +### P4-EA-04 [LOW] -- Inconsistent `subParser` function signatures across reference op libraries + +**Files:** +- `src/lib/extern/reference/op/LibExternOpContextCallingContract.sol`, line 19 +- `src/lib/extern/reference/op/LibExternOpContextRainlen.sol`, line 18 +- `src/lib/extern/reference/op/LibExternOpContextSender.sol`, line 17 +- `src/lib/extern/reference/op/LibExternOpIntInc.sol`, line 57 +- `src/lib/extern/reference/op/LibExternOpStackOperand.sol`, line 23 + +**Description:** The five reference op libraries all define `subParser` functions with the same parameter types `(uint256, uint256, OperandV2)`, but they differ in: + +1. **Parameter naming:** The three context ops leave all parameters unnamed, `LibExternOpStackOperand` names two of three (`constantsHeight`, `operand`), and `LibExternOpIntInc` names all three (`constantsHeight`, `ioByte`, `operand`). + +2. **Mutability:** `LibExternOpIntInc.subParser` is `view` (necessarily, because it uses `address(this)`), while all others are `pure`. This difference is legitimate but undocumented -- there is no comment on the context ops explaining that they are `pure` because they do not need the contract address. + +The unnamed parameters are a maintainability concern. A future developer reading `LibExternOpContextSender.subParser(uint256, uint256, OperandV2)` must look at the calling convention in `BaseRainterpreterSubParser.subParseWord2` or at `LibExternOpIntInc` to understand what each parameter means. + +**Fix:** Name all parameters consistently across all five libraries. The convention from `LibExternOpIntInc` is `(uint256 constantsHeight, uint256 ioByte, OperandV2 operand)` -- apply this to the context ops and `LibExternOpStackOperand`. + +### P4-EA-05 [LOW] -- Repeated build-function boilerplate in `RainterpreterReferenceExtern` + +**File:** `src/concrete/extern/RainterpreterReferenceExtern.sol`, lines 209-411 + +**Description:** The five `build*` functions (`buildLiteralParserFunctionPointers`, `buildOperandHandlerFunctionPointers`, `buildSubParserWordParsers`, `buildOpcodeFunctionPointers`, `buildIntegrityFunctionPointers`) all follow an identical structural pattern: +1. Declare a typed length pointer +2. Store the length via assembly +3. Build a fixed-size array with the length pointer in slot 0 +4. Reinterpret-cast to `uint256[]` +5. Sanity check `length == expected` +6. Call `LibConvert.unsafeTo16BitBytes` + +The only differences are the function pointer type, the length constant, and the array contents. This pattern is repeated five times with no abstraction. Each repetition is ~20 lines of near-identical code. Any change to the pattern (e.g., changing the sanity check error, switching from `unsafeTo16BitBytes` to a different encoding) requires updating all five functions. + +This is a known pattern in the codebase (it also appears in `LibAllStandardOps`), and extracting it into a generic helper is non-trivial due to the varying function pointer types. Flagging for awareness rather than an immediate fix. + +**Fix:** Consider a shared macro-style helper or code generation for this pattern, or document why duplication is accepted (e.g., a comment referencing the pattern at the top of the first build function). + +### P4-EA-06 [INFO] -- `parseRepeat` return type (`uint256`) differs from calling convention (`bytes32`) + +**Files:** +- `src/lib/extern/reference/literal/LibParseLiteralRepeat.sol`, line 53 -- returns `uint256` +- `src/abstract/BaseRainterpreterSubParser.sol`, line 165 -- function pointer typed as returning `bytes32` + +**Description:** `LibParseLiteralRepeat.parseRepeat` is declared as `function(uint256, uint256, uint256) internal pure returns (uint256)` but it is called through a function pointer typed as `function(bytes32, uint256, uint256) internal pure returns (bytes32)` in `BaseRainterpreterSubParser.subParseLiteral2`. The first parameter is also `bytes32` in the calling convention but `uint256` in the actual function. This works at the EVM level because both types occupy a single 256-bit stack slot, and the function pointer is loaded via assembly (bypassing Solidity type checking). This is an intentional pattern used throughout the codebase for dispatch tables, but it means the compiler cannot catch type mismatches between the dispatch table and the actual function signatures. + +### P4-EA-07 [INFO] -- `matchSubParseLiteralDispatch` base mutability is `view` but override is `pure` + +**Files:** +- `src/abstract/BaseRainterpreterSubParser.sol`, line 141 -- declared `view` +- `src/concrete/extern/RainterpreterReferenceExtern.sol`, line 234 -- overridden as `pure` + +**Description:** The base virtual function `matchSubParseLiteralDispatch` is declared `internal view virtual` but the default implementation does not read any state (it just returns false with unused parameters). The `RainterpreterReferenceExtern` override narrows this to `pure`. The base is `view` to allow future overrides that may need state access, which is a reasonable design choice. However, the base implementation's body `(cursor, end); success = false; index = 0; value = 0;` is a pattern that suppresses unused-parameter warnings by mentioning the parameters in a no-op expression statement. This is idiomatic Solidity for "default no-op" implementations but could be replaced with named return variables and no body for slightly cleaner code. + +--- + +## Summary + +| Severity | Count | +|----------|-------| +| CRITICAL | 0 | +| HIGH | 0 | +| MEDIUM | 0 | +| LOW | 5 | +| INFO | 2 | + +The extern system has generally good structure. The main maintainability concerns are: (1) duplicated dispatch-decoding logic between `BaseRainterpreterExtern` and `LibExtern` with subtly different semantics, (2) inconsistent bitmask spelling, (3) locally-defined context constants that break the import pattern used by sibling files, (4) inconsistent parameter naming across the five reference op `subParser` functions, and (5) heavily duplicated boilerplate in the build functions. No commented-out code, dead code, or unused imports were found. No magic numbers beyond standard EVM conventions (0x20 for memory word size, 0x10 for 16-bit shift width). diff --git a/audit/2026-03-01-01/pass4/LibEvalParse.md b/audit/2026-03-01-01/pass4/LibEvalParse.md new file mode 100644 index 000000000..17fecab29 --- /dev/null +++ b/audit/2026-03-01-01/pass4/LibEvalParse.md @@ -0,0 +1,263 @@ +# Pass 4: Maintainability, Consistency, and Abstractions + +**Scope:** LibEval.sol, LibIntegrityCheck.sol, LibInterpreterState.sol, +LibInterpreterStateDataContract.sol, LibParse.sol, LibParseState.sol, +LibParseError.sol, LibParseInterstitial.sol + +**Date:** 2026-03-01 + +--- + +## Evidence of Thorough Reading + +### LibEval.sol (`src/lib/eval/LibEval.sol`) + +- **Library:** `LibEval` +- **Imports:** `LibInterpreterState`, `InterpreterState`, `LibMemCpy`, `LibMemoryKV`, + `MemoryKV`, `LibBytecode`, `Pointer`, `OperandV2`, `StackItem`, `InputsLengthMismatch` +- **Functions:** `evalLoop`, `eval2` +- **Using:** `LibMemoryKV for MemoryKV` +- **Constants:** none (file-level) + +### LibIntegrityCheck.sol (`src/lib/integrity/LibIntegrityCheck.sol`) + +- **Library:** `LibIntegrityCheck` +- **Struct:** `IntegrityCheckState` (stackIndex, stackMaxIndex, readHighwater, + constants, opIndex, bytecode) +- **Imports:** `Pointer`, `OpcodeOutOfRange`, `StackAllocationMismatch`, + `StackOutputsMismatch`, `StackUnderflow`, `StackUnderflowHighwater`, + `BadOpInputsLength`, `BadOpOutputsLength`, `LibBytecode`, `OperandV2` +- **Functions:** `newState`, `integrityCheck2` +- **Using:** `LibIntegrityCheck for IntegrityCheckState` + +### LibInterpreterState.sol (`src/lib/state/LibInterpreterState.sol`) + +- **Library:** `LibInterpreterState` +- **Struct:** `InterpreterState` (stackBottoms, constants, sourceIndex, stateKV, + namespace, store, context, bytecode, fs) +- **Constant:** `STACK_TRACER` +- **Imports:** `Pointer`, `MemoryKV`, `FullyQualifiedNamespace`, + `IInterpreterStoreV3`, `StackItem` +- **Functions:** `stackBottoms`, `stackTrace` + +### LibInterpreterStateDataContract.sol (`src/lib/state/LibInterpreterStateDataContract.sol`) + +- **Library:** `LibInterpreterStateDataContract` +- **Imports:** `MemoryKV`, `Pointer`, `LibMemCpy`, `LibBytes`, + `FullyQualifiedNamespace`, `IInterpreterStoreV3`, `InterpreterState` +- **Functions:** `serializeSize`, `unsafeSerialize`, `unsafeDeserialize` +- **Using:** `LibBytes for bytes` + +### LibParse.sol (`src/lib/parse/LibParse.sol`) + +- **Library:** `LibParse` +- **Constant:** `SUB_PARSER_BYTECODE_HEADER_SIZE` (5) +- **Imports:** `LibPointer`, `Pointer`, `LibMemCpy`, CMASK_* (12 masks), + `LibParseChar`, `LibParseMeta`, `LibParseOperand`, `OperandV2`, `OPCODE_STACK`, + `OPCODE_UNKNOWN`, `LibParseStackName`, error types (8), `LibParseState`, + `ParseState`, FSM masks (4), `PARSE_STATE_PAREN_TRACKER0_OFFSET`, + `LibParsePragma`, `LibParseInterstitial`, `LibParseError`, `LibSubParse`, + `LibBytes`, `LibUint256Array`, `LibBytes32Array` +- **Functions:** `parseWord`, `parseLHS`, `parseRHS`, `parse` +- **Using:** 11 `using` declarations + +### LibParseState.sol (`src/lib/parse/LibParseState.sol`) + +- **Library:** `LibParseState` +- **Struct:** `ParseState` (19 fields: activeSourcePtr, topLevel0, topLevel1, + parenTracker0, parenTracker1, lineTracker, subParsers, sourcesBuilder, fsm, + stackNames, stackNameBloom, constantsBuilder, constantsBloom, literalParsers, + operandHandlers, operandValues, stackTracker, data, meta) +- **Constants:** `EMPTY_ACTIVE_SOURCE`, `FSM_YANG_MASK`, `FSM_WORD_END_MASK`, + `FSM_ACCEPTING_INPUTS_MASK`, `FSM_ACTIVE_SOURCE_MASK`, `FSM_DEFAULT`, + `OPERAND_VALUES_LENGTH`, `PARSE_STATE_TOP_LEVEL0_OFFSET`, + `PARSE_STATE_TOP_LEVEL0_DATA_OFFSET`, `PARSE_STATE_PAREN_TRACKER0_OFFSET`, + `PARSE_STATE_LINE_TRACKER_OFFSET` +- **Imports:** `OperandV2`, `OPCODE_CONSTANT`, `LibParseStackTracker`, + `ParseStackTracker`, `Pointer`, `LibMemCpy`, `LibUint256Array`, error types (14), + `LibParseLiteral`, `LibParseError` +- **Functions:** `newActiveSourcePointer`, `resetSource`, `newState`, + `pushSubParser`, `exportSubParsers`, `snapshotSourceHeadToLineTracker`, + `endLine`, `highwater`, `constantValueBloom`, `pushConstantValue`, + `pushLiteral`, `pushOpToSource`, `endSource`, `buildBytecode`, + `buildConstants`, `checkParseMemoryOverflow` +- **Using:** `LibParseState for ParseState`, `LibParseStackTracker for ParseStackTracker`, + `LibParseError for ParseState`, `LibParseLiteral for ParseState`, + `LibUint256Array for uint256[]` + +### LibParseError.sol (`src/lib/parse/LibParseError.sol`) + +- **Library:** `LibParseError` +- **Imports:** `ParseState` +- **Functions:** `parseErrorOffset`, `handleErrorSelector` + +### LibParseInterstitial.sol (`src/lib/parse/LibParseInterstitial.sol`) + +- **Library:** `LibParseInterstitial` +- **Imports:** `FSM_YANG_MASK`, `ParseState`, CMASK_COMMENT_HEAD, + CMASK_WHITESPACE, COMMENT_END_SEQUENCE, COMMENT_START_SEQUENCE, + CMASK_COMMENT_END_SEQUENCE_END, `MalformedCommentStart`, `UnclosedComment`, + `LibParseError`, `LibParseChar` +- **Functions:** `skipComment`, `skipWhitespace`, `parseInterstitial` +- **Using:** `LibParseError for ParseState`, `LibParseInterstitial for ParseState` + +--- + +## Findings + +### P4-EVLP-1: Unused `using` declaration in LibParse.sol [LOW] + +**File:** `src/lib/parse/LibParse.sol`, line 80 + +```solidity +using LibUint256Array for uint256[]; +``` + +`LibParse` declares `using LibUint256Array for uint256[]` but no `uint256[]` variable +in any of its four functions (`parseWord`, `parseLHS`, `parseRHS`, `parse`) calls +any method from `LibUint256Array`. The import of `LibUint256Array` on line 53 is +correspondingly dead code. Dead `using` declarations make it harder to understand +actual data flow and dependencies. + +### P4-EVLP-2: Unused return value suppressed with expression statement [LOW] + +**File:** `src/lib/parse/LibParse.sol`, lines 155-156 + +```solidity +(bool exists, uint256 index) = state.pushStackName(word); +(index); +``` + +The return value `index` from `pushStackName` is captured and then immediately +discarded via the expression statement `(index);`. This pattern suppresses the +compiler warning but obscures intent. The idiomatic Solidity approach is to use +a blank in the destructuring: + +```solidity +(bool exists,) = state.pushStackName(word); +``` + +This is a readability issue -- the `(index);` pattern looks like it might be doing +something to a reader unfamiliar with this codebase pattern. + +### P4-EVLP-3: Comment typo "ying" should be "yin" [INFO] + +**Files:** +- `src/lib/parse/LibParse.sol`, line 176 +- `src/lib/parse/LibParseInterstitial.sol`, line 98 + +Two comments say "Set ying" but the FSM state terminology throughout the codebase +is "yin/yang" (e.g., `FSM_YANG_MASK` doc on line 34 of `LibParseState.sol` says +"yin state"). The misspelling "ying" appears only in these two comments. All other +references consistently use "yin". + +### P4-EVLP-4: Inconsistent opcode bounds checking between eval and integrity [INFO] + +**Files:** +- `src/lib/eval/LibEval.sol`, line 100 et al. (mod-based wrapping) +- `src/lib/integrity/LibIntegrityCheck.sol`, line 152 (revert-based bounds check) + +The eval loop uses `mod(opcodeIndex, fsCount)` to silently wrap out-of-range opcodes +into the valid range, while the integrity check uses an explicit bounds check with +`revert OpcodeOutOfRange(...)`. This is intentional -- the eval loop NatSpec explains +the design -- but the two modules use fundamentally different strategies for the same +concern. The integrity check is the authoritative validation point, and the eval loop +deliberately avoids a branch for gas efficiency. This is documented and correct +behaviour. + +This finding is informational only. No change needed -- the design choice is +deliberate and documented. + +### P4-EVLP-5: Magic number 59 for paren depth limit [LOW] + +**File:** `src/lib/parse/LibParse.sol`, line 341 + +```solidity +if (newParenOffset > 59) { + revert ParenOverflow(); +} +``` + +The threshold `59` is derived from the paren tracker layout (62 bytes / 3 bytes per +group = 20 groups, minus 1 for the phantom counter write at `parenOffset + 4`, +giving a max offset of 57, but the check rejects at 60 since `newParenOffset` is +already incremented by 3). The inline comment (lines 335-340) explains the +derivation. A named constant would make this more maintainable and grep-friendly, +e.g.: + +```solidity +uint256 constant MAX_PAREN_OFFSET = 59; +``` + +### P4-EVLP-6: Magic number 0x3f for stack RHS overflow [LOW] + +**File:** `src/lib/parse/LibParseState.sol`, line 537 + +```solidity +if (newStackRHSOffset >= 0x3f) { + revert ParseStackOverflow(); +} +``` + +The inline comment (lines 534-536) explains why `0x3f` (63) is the limit, but this +magic number appears only once and could be a named constant for clarity. The +comment is already adequate, so this is low severity. + +### P4-EVLP-7: `LibParseState.sol` is 1053 lines with 16 functions [INFO] + +**File:** `src/lib/parse/LibParseState.sol` + +This file is the largest in the reviewed set at over 1000 lines. It contains 16 +functions that handle very different concerns: source allocation +(`newActiveSourcePointer`), FSM state management (`resetSource`, `highwater`), +constants management (`pushConstantValue`, `constantValueBloom`, `buildConstants`), +literal handling (`pushLiteral`), opcode emission (`pushOpToSource`), source +finalisation (`endSource`, `buildBytecode`), line tracking (`endLine`, +`snapshotSourceHeadToLineTracker`), sub parser management (`pushSubParser`, +`exportSubParsers`), and memory safety (`checkParseMemoryOverflow`). + +This is an observation, not a defect. The functions are cohesive around the +`ParseState` struct and its lifecycle. Splitting would fragment the struct's +invariant management. However, future contributors should be aware that this file +requires understanding the full `ParseState` memory layout to modify safely. + +### P4-EVLP-8: `ParseState` struct field ordering relies on assembly offset coupling [INFO] + +**File:** `src/lib/parse/LibParseState.sol`, lines 155-183 + +The `ParseState` struct has a comment block at lines 156-170 marking fields that +are "referenced directly in assembly by hardcoded offsets". The named constants +(`PARSE_STATE_TOP_LEVEL0_OFFSET`, `PARSE_STATE_PAREN_TRACKER0_OFFSET`, +`PARSE_STATE_LINE_TRACKER_OFFSET`) encode byte offsets into the struct's memory +layout. Reordering struct fields without updating these constants would silently +corrupt the parser. + +This coupling is necessary for the assembly-heavy implementation but makes the +code fragile to refactoring. The comment block acknowledging this is good practice. +No change needed -- flagging for awareness. + +--- + +## Cross-file Consistency Notes + +1. **Assembly `memory-safe` annotations:** All assembly blocks across all eight files + consistently use `assembly ("memory-safe")`. No missing annotations found. + +2. **Error handling:** All files use custom error types, no string reverts. Error + patterns are consistent: parse errors include byte offsets, integrity errors + include opcode indices, eval errors include expected vs actual values. + +3. **NatSpec coverage:** All public/internal functions have `@notice` and `@param` + tags. NatSpec is thorough and consistent across all eight files. + +4. **No commented-out code:** No commented-out code blocks found in any of the + reviewed files. + +5. **No dead code:** All functions are reachable. `handleErrorSelector` in + `LibParseError` is called from `LibParseLiteralDecimal`. + +6. **`unchecked` blocks:** Used consistently in arithmetic-heavy functions. + Functions that don't do arithmetic (e.g., `parseInterstitial`) correctly omit + `unchecked`. Minor inconsistency: `skipWhitespace` wraps a trivial + `state.fsm &= ~FSM_YANG_MASK` in `unchecked` but this has no practical impact + since that operation cannot overflow. diff --git a/audit/2026-03-01-01/pass4/LibOpAll.md b/audit/2026-03-01-01/pass4/LibOpAll.md new file mode 100644 index 000000000..4f1d45e2f --- /dev/null +++ b/audit/2026-03-01-01/pass4/LibOpAll.md @@ -0,0 +1,297 @@ +# Pass 4: Opcode Libraries Maintainability, Consistency & Abstractions + +Audit date: 2026-03-01 +Auditor: Claude Opus 4.6 +Scope: All files under `src/lib/op/` (72 opcodes across 68 files) + +## File Inventory + +### LibAllStandardOps.sol +- `authoringMetaV2()` (line 121) +- `literalParserFunctionPointers()` (line 330) +- `operandHandlerFunctionPointers()` (line 363) +- `integrityFunctionPointers()` (line 535) +- `opcodeFunctionPointers()` (line 639) + +### src/lib/op/00/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpConstant.sol | LibOpConstant | `integrity` (21), `run` (37), `referenceFn` (52) | +| LibOpContext.sol | LibOpContext | `integrity` (16), `run` (28), `referenceFn` (47) | +| LibOpExtern.sol | LibOpExtern | `integrity` (29), `run` (49), `referenceFn` (102) | +| LibOpStack.sol | LibOpStack | `integrity` (21), `run` (41), `referenceFn` (58) | + +### src/lib/op/bitwise/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpBitwiseAnd.sol | LibOpBitwiseAnd | `integrity` (16), `run` (24), `referenceFn` (36) | +| LibOpBitwiseOr.sol | LibOpBitwiseOr | `integrity` (16), `run` (24), `referenceFn` (36) | +| LibOpCtPop.sol | LibOpCtPop | `integrity` (22), `run` (30), `referenceFn` (47) | +| LibOpDecodeBits.sol | LibOpDecodeBits | `integrity` (20), `run` (33), `referenceFn` (65) | +| LibOpEncodeBits.sol | LibOpEncodeBits | `integrity` (19), `run` (36), `referenceFn` (76) | +| LibOpShiftBitsLeft.sol | LibOpShiftBitsLeft | `integrity` (19), `run` (38), `referenceFn` (49) | +| LibOpShiftBitsRight.sol | LibOpShiftBitsRight | `integrity` (19), `run` (38), `referenceFn` (49) | + +### src/lib/op/call/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpCall.sol | LibOpCall | `integrity` (85), `run` (122) | + +### src/lib/op/crypto/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpHash.sol | LibOpHash | `integrity` (17), `run` (28), `referenceFn` (41) | + +### src/lib/op/erc20/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpERC20Allowance.sol | LibOpERC20Allowance | `integrity` (21), `run` (30), `referenceFn` (83) | +| LibOpERC20BalanceOf.sol | LibOpERC20BalanceOf | `integrity` (21), `run` (30), `referenceFn` (67) | +| LibOpERC20TotalSupply.sol | LibOpERC20TotalSupply | `integrity` (21), `run` (30), `referenceFn` (61) | + +### src/lib/op/erc20/uint256/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpUint256ERC20Allowance.sol | LibOpUint256ERC20Allowance | `integrity` (16), `run` (25), `referenceFn` (60) | +| LibOpUint256ERC20BalanceOf.sol | LibOpUint256ERC20BalanceOf | `integrity` (16), `run` (25), `referenceFn` (54) | +| LibOpUint256ERC20TotalSupply.sol | LibOpUint256ERC20TotalSupply | `integrity` (16), `run` (25), `referenceFn` (48) | + +### src/lib/op/erc721/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpERC721BalanceOf.sol | LibOpERC721BalanceOf | `integrity` (19), `run` (28), `referenceFn` (60) | +| LibOpERC721OwnerOf.sol | LibOpERC721OwnerOf | `integrity` (18), `run` (27), `referenceFn` (53) | + +### src/lib/op/erc721/uint256/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpUint256ERC721BalanceOf.sol | LibOpUint256ERC721BalanceOf | `integrity` (16), `run` (25), `referenceFn` (52) | + +### src/lib/op/erc5313/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpERC5313Owner.sol | LibOpERC5313Owner | `integrity` (18), `run` (27), `referenceFn` (50) | + +### src/lib/op/evm/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpBlockNumber.sol | LibOpBlockNumber | `integrity` (19), `run` (26), `referenceFn` (39) | +| LibOpChainId.sol | LibOpChainId | `integrity` (19), `run` (26), `referenceFn` (39) | +| LibOpTimestamp.sol | LibOpTimestamp | `integrity` (19), `run` (26), `referenceFn` (39) | + +### src/lib/op/logic/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpAny.sol | LibOpAny | `integrity` (21), `run` (33), `referenceFn` (60) | +| LibOpBinaryEqualTo.sol | LibOpBinaryEqualTo | `integrity` (17), `run` (26), `referenceFn` (38) | +| LibOpConditions.sol | LibOpConditions | `integrity` (23), `run` (40), `referenceFn` (82) | +| LibOpEnsure.sol | LibOpEnsure | `integrity` (20), `run` (31), `referenceFn` (49) | +| LibOpEqualTo.sol | LibOpEqualTo | `integrity` (21), `run` (30), `referenceFn` (52) | +| LibOpEvery.sol | LibOpEvery | `integrity` (21), `run` (32), `referenceFn` (58) | +| LibOpGreaterThan.sol | LibOpGreaterThan | `integrity` (20), `run` (28), `referenceFn` (46) | +| LibOpGreaterThanOrEqualTo.sol | LibOpGreaterThanOrEqualTo | `integrity` (20), `run` (29), `referenceFn` (47) | +| LibOpIf.sol | LibOpIf | `integrity` (20), `run` (29), `referenceFn` (47) | +| LibOpIsZero.sol | LibOpIsZero | `integrity` (19), `run` (27), `referenceFn` (42) | +| LibOpLessThan.sol | LibOpLessThan | `integrity` (20), `run` (28), `referenceFn` (46) | +| LibOpLessThanOrEqualTo.sol | LibOpLessThanOrEqualTo | `integrity` (20), `run` (29), `referenceFn` (47) | + +### src/lib/op/math/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpAbs.sol | LibOpAbs | `integrity` (19), `run` (28), `referenceFn` (44) | +| LibOpAdd.sol | LibOpAdd | `integrity` (22), `run` (33), `referenceFn` (76) | +| LibOpAvg.sol | LibOpAvg | `integrity` (19), `run` (28), `referenceFn` (47) | +| LibOpCeil.sol | LibOpCeil | `integrity` (19), `run` (28), `referenceFn` (44) | +| LibOpDiv.sol | LibOpDiv | `integrity` (21), `run` (33), `referenceFn` (74) | +| LibOpE.sol | LibOpE | `integrity` (17), `run` (24), `referenceFn` (35) | +| LibOpExp.sol | LibOpExp | `integrity` (19), `run` (28), `referenceFn` (44) | +| LibOpExp2.sol | LibOpExp2 | `integrity` (19), `run` (28), `referenceFn` (45) | +| LibOpFloor.sol | LibOpFloor | `integrity` (19), `run` (28), `referenceFn` (44) | +| LibOpFrac.sol | LibOpFrac | `integrity` (19), `run` (28), `referenceFn` (44) | +| LibOpGm.sol | LibOpGm | `integrity` (21), `run` (31), `referenceFn` (55) | +| LibOpHeadroom.sol | LibOpHeadroom | `integrity` (20), `run` (30), `referenceFn` (49) | +| LibOpInv.sol | LibOpInv | `integrity` (19), `run` (28), `referenceFn` (44) | +| LibOpMax.sol | LibOpMax | `integrity` (20), `run` (32), `referenceFn` (67) | +| LibOpMaxNegativeValue.sol | LibOpMaxNegativeValue | `integrity` (19), `run` (26), `referenceFn` (37) | +| LibOpMaxPositiveValue.sol | LibOpMaxPositiveValue | `integrity` (19), `run` (26), `referenceFn` (37) | +| LibOpMin.sol | LibOpMin | `integrity` (20), `run` (32), `referenceFn` (68) | +| LibOpMinNegativeValue.sol | LibOpMinNegativeValue | `integrity` (19), `run` (26), `referenceFn` (37) | +| LibOpMinPositiveValue.sol | LibOpMinPositiveValue | `integrity` (19), `run` (26), `referenceFn` (37) | +| LibOpMul.sol | LibOpMul | `integrity` (21), `run` (32), `referenceFn` (74) | +| LibOpPow.sol | LibOpPow | `integrity` (19), `run` (28), `referenceFn` (47) | +| LibOpSqrt.sol | LibOpSqrt | `integrity` (19), `run` (28), `referenceFn` (44) | +| LibOpSub.sol | LibOpSub | `integrity` (21), `run` (33), `referenceFn` (75) | + +### src/lib/op/math/growth/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpExponentialGrowth.sol | LibOpExponentialGrowth | `integrity` (18), `run` (26), `referenceFn` (47) | +| LibOpLinearGrowth.sol | LibOpLinearGrowth | `integrity` (18), `run` (26), `referenceFn` (48) | + +### src/lib/op/math/uint256/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpMaxUint256.sol | LibOpMaxUint256 | `integrity` (14), `run` (21), `referenceFn` (31) | +| LibOpUint256Add.sol | LibOpUint256Add | `integrity` (17), `run` (30), `referenceFn` (64) | +| LibOpUint256Div.sol | LibOpUint256Div | `integrity` (18), `run` (30), `referenceFn` (65) | +| LibOpUint256Mul.sol | LibOpUint256Mul | `integrity` (17), `run` (30), `referenceFn` (64) | +| LibOpUint256Pow.sol | LibOpUint256Pow | `integrity` (17), `run` (30), `referenceFn` (64) | +| LibOpUint256Sub.sol | LibOpUint256Sub | `integrity` (17), `run` (30), `referenceFn` (64) | + +### src/lib/op/store/ +| File | Library | Functions (line) | +|------|---------|-----------------| +| LibOpGet.sol | LibOpGet | `integrity` (19), `run` (32), `referenceFn` (68) | +| LibOpSet.sol | LibOpSet | `integrity` (19), `run` (29), `referenceFn` (46) | + +--- + +## Findings + +### PASS4-LIBOP-1: Missing `@notice` tag on `integrity` NatSpec in several uint256/growth libraries [LOW] + +**Files affected:** +- `src/lib/op/erc20/uint256/LibOpUint256ERC20Allowance.sol` line 15 +- `src/lib/op/erc20/uint256/LibOpUint256ERC20BalanceOf.sol` line 15 +- `src/lib/op/erc20/uint256/LibOpUint256ERC20TotalSupply.sol` line 15 +- `src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol` line 15 +- `src/lib/op/math/growth/LibOpExponentialGrowth.sol` line 17 +- `src/lib/op/math/growth/LibOpLinearGrowth.sol` line 17 +- `src/lib/op/math/uint256/LibOpMaxUint256.sol` line 13, 30 + +**Evidence:** +The majority of opcode libraries tag the `integrity` function with `/// @notice`. For example, `LibOpERC20Allowance.integrity` (line 18) uses `/// @notice \`erc20-allowance\` integrity check.` However, the uint256 ERC variants, the growth opcodes, and the max-uint256 opcode use bare `///` (untagged NatSpec) for `integrity`. Since these doc blocks have no other explicit tags, the untagged line becomes implicit `@notice` and the compiler treats it the same. But the `CLAUDE.md` convention states: "when a doc block contains any explicit tag (e.g. `@title`), all entries must be explicitly tagged." While these particular doc blocks have no explicit tags so the implicit rule technically applies, this creates an inconsistency with the dominant pattern across the codebase. + +`LibOpMaxUint256.referenceFn` at line 30 also lacks `@notice` while every other `referenceFn` in the codebase has one. + +**Impact:** Readability/consistency only. No functional impact. + +--- + +### PASS4-LIBOP-2: Missing `referenceFn` in `LibOpCall` [LOW] + +**File:** `src/lib/op/call/LibOpCall.sol` + +**Evidence:** +Every other opcode library provides a `referenceFn` function for testing purposes. `LibOpCall` has only `integrity` (line 85) and `run` (line 122) -- no `referenceFn`. This is the only standard opcode library without one. The `call` opcode's cross-source semantics make a pure reference implementation more complex, but the deviation from the universal pattern is notable from a consistency standpoint. + +**Impact:** Reduces testability of the call opcode via the standard reference-comparison test harness. + +--- + +### PASS4-LIBOP-3: Magic number `0x0F` used without named constant for operand input-count mask [INFO] + +**Files affected:** Every opcode that reads input count from operand, including: +- `LibOpHash.sol` line 20: `uint256(OperandV2.unwrap(operand) >> 0x10) & 0x0F` +- `LibOpAdd.sol` line 24, 47 +- `LibOpDiv.sol` line 23, 47 +- `LibOpMul.sol` line 23, 46 +- `LibOpSub.sol` line 23, 47 +- `LibOpMax.sol` line 22, 43 +- `LibOpMin.sol` line 22, 43 +- `LibOpAny.sol` line 23, 35 +- `LibOpEvery.sol` line 23, 34 +- `LibOpConditions.sol` line 25 +- `LibOpCall.sol` line 125, 126 +- `LibOpExtern.sol` lines 38-39 +- All uint256 math opcodes + +**Evidence:** +The magic numbers `0x0F`, `0x10`, and `0x14` are repeated many dozens of times across the opcode libraries. They refer to the operand field layout (input count is 4 bits starting at bit 16, output count is 4 bits at bit 20). A named constant like `OPERAND_INPUTS_MASK` or similar would make the convention self-documenting. + +**Impact:** Not a bug. These values are consistent across all files. But a future change to the operand layout would require changes in dozens of files. A named constant would reduce that risk. + +--- + +### PASS4-LIBOP-4: Inconsistent `using ... for` declaration patterns [INFO] + +**Files affected:** Several opcode libraries import `LibDecimalFloat` but declare `using LibDecimalFloat for Float` while others do not use the `using` directive and call `LibDecimalFloat.functionName(...)` directly. + +**Examples using the directive:** +- `LibOpAbs.sol` line 14: `using LibDecimalFloat for Float;` +- `LibOpGreaterThan.sol` line 15: `using LibDecimalFloat for Float;` +- All logic ops with float comparisons + +**Examples without the directive (calling library functions directly):** +- `LibOpE.sol` line 25: `Float e = LibDecimalFloat.FLOAT_E;` (no `using` needed since it only accesses constants) +- `LibOpMaxNegativeValue.sol` line 14: `using LibDecimalFloat for Float;` but only accesses a constant in `run` -- the `using` is unnecessary since `run` only uses `LibDecimalFloat.FLOAT_MAX_NEGATIVE_VALUE`. + +**Impact:** Purely stylistic. The `using` declarations are harmless but unnecessary when only constants are used. Not a bug. + +--- + +### PASS4-LIBOP-5: `LibOpSub` operand handler is `handleOperandSingleFull` while all other multi-input opcodes use `handleOperandDisallowed` [INFO] + +**File:** `src/lib/op/LibAllStandardOps.sol` line 512 + +**Evidence:** +In `operandHandlerFunctionPointers()`, the `sub` opcode (line 512) uses `LibParseOperand.handleOperandSingleFull`, while all other N-input arithmetic opcodes (add, mul, div, max, min, uint256-add, uint256-sub, etc.) use `LibParseOperand.handleOperandDisallowed`. This is a deliberate difference -- the `sub` operand presumably enables explicit negation semantics via operand. But it is the only multi-input float math op with an explicit operand, which is worth documenting for maintainers. + +**Impact:** This appears intentional based on the code structure, but the asymmetry could confuse maintainers. Consider a comment in `LibAllStandardOps.sol` explaining why `sub` has operand handling while other N-input math ops do not. + +--- + +### PASS4-LIBOP-6: Inconsistent `referenceFn` return patterns -- some mutate inputs array, others allocate new outputs [INFO] + +**Files affected:** +- `LibOpCtPop.sol` line 52: mutates `inputs[0]` and returns `inputs` +- `LibOpDecodeBits.sol` line 80: mutates `inputs[0]` and returns `inputs` +- `LibOpShiftBitsLeft.sol` line 55: mutates `inputs[0]` and returns `inputs` +- `LibOpShiftBitsRight.sol` line 55: mutates `inputs[0]` and returns `inputs` + +vs. + +- `LibOpAbs.sol` line 49: `outputs = new StackItem[](1);` allocates new array +- `LibOpIsZero.sol` line 47: `outputs = new StackItem[](1);` allocates new array +- All comparison ops allocate new arrays + +**Evidence:** +For 1-input/1-output opcodes, there are two patterns: +1. Allocate a new `outputs` array and return it. +2. Mutate `inputs[0]` in-place and `return inputs`. + +Both produce correct results for the test harness, but they are inconsistent. The pattern (2) is slightly more gas efficient for tests but could be surprising when reading the code, as it violates the implied contract that `inputs` remains unchanged. + +**Impact:** Test-only code. No production impact. Minor readability issue. + +--- + +### PASS4-LIBOP-7: `LibOpEnsure.integrity` missing `@notice` tag on the function NatSpec [INFO] + +**File:** `src/lib/op/logic/LibOpEnsure.sol` line 18-20 + +**Evidence:** +```solidity +/// @return The number of inputs. +/// @return The number of outputs. +function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { +``` + +The doc block jumps directly to `@return` tags without a `@notice` line. Since `@return` is an explicit tag, any untagged description would need `@notice` per the project convention. In this case there is no description at all -- only `@return` tags. Most other `integrity` functions include a `@notice` line like `/// @notice \`ensure\` integrity check. ...`. + +**Impact:** Consistency issue. No functional impact. + +--- + +### PASS4-LIBOP-8: No commented-out code or dead code found [N/A] + +A complete scan of all 68 opcode library files found: +- Zero commented-out code blocks +- Zero unused imports (all imports are used by the functions in each file) +- Zero dead functions + +--- + +## Summary + +| ID | Severity | Description | +|----|----------|-------------| +| PASS4-LIBOP-1 | LOW | Missing `@notice` tag on integrity/referenceFn NatSpec in 8 files | +| PASS4-LIBOP-2 | LOW | Missing `referenceFn` in `LibOpCall` -- only standard opcode without one | +| PASS4-LIBOP-3 | INFO | Magic number `0x0F` / `0x10` / `0x14` repeated dozens of times for operand masks | +| PASS4-LIBOP-4 | INFO | Inconsistent `using ... for` declarations (some unnecessary) | +| PASS4-LIBOP-5 | INFO | `sub` is the only multi-input float math op with `handleOperandSingleFull` | +| PASS4-LIBOP-6 | INFO | Inconsistent `referenceFn` return pattern (mutate inputs vs. allocate outputs) | +| PASS4-LIBOP-7 | INFO | `LibOpEnsure.integrity` missing `@notice` while having `@return` tags | + +Overall assessment: The opcode libraries are highly consistent. All 72 opcodes follow the `integrity`/`run`/`referenceFn` pattern (with the single exception of `LibOpCall`). Assembly patterns are consistent within each category (fixed-input ops, N-input ops, zero-input constant ops). There is no commented-out code, no dead code, and no unused imports. The findings above are all LOW or INFO severity, reflecting a well-maintained codebase. diff --git a/audit/2026-03-01-01/pass4/LibParseUtilities.md b/audit/2026-03-01-01/pass4/LibParseUtilities.md new file mode 100644 index 000000000..cc9b8a02d --- /dev/null +++ b/audit/2026-03-01-01/pass4/LibParseUtilities.md @@ -0,0 +1,254 @@ +# Pass 4 — Maintainability, Consistency, and Abstractions + +## Scope + +Ten source files in `src/lib/parse/` and `src/lib/parse/literal/`: + +1. `LibParseOperand.sol` +2. `LibParsePragma.sol` +3. `LibParseStackName.sol` +4. `LibParseStackTracker.sol` +5. `LibSubParse.sol` +6. `LibParseLiteral.sol` +7. `LibParseLiteralDecimal.sol` +8. `LibParseLiteralHex.sol` +9. `LibParseLiteralString.sol` +10. `LibParseLiteralSubParseable.sol` + +## Evidence of Thorough Reading + +### LibParseOperand.sol +- **Library**: `LibParseOperand` +- **Imports**: `ExpectedOperand`, `UnclosedOperand`, `OperandValuesOverflow`, `UnexpectedOperand`, `UnexpectedOperandValue`, `OperandOverflow` (from ErrParse); `OperandV2`; `LibParseLiteral`; `CMASK_OPERAND_END`, `CMASK_WHITESPACE`, `CMASK_OPERAND_START`; `ParseState`, `OPERAND_VALUES_LENGTH`, `FSM_YANG_MASK`; `LibParseError`; `LibParseInterstitial`; `LibDecimalFloat`, `Float` +- **Using**: `LibParseError for ParseState`, `LibParseLiteral for ParseState`, `LibParseOperand for ParseState`, `LibParseInterstitial for ParseState`, `LibDecimalFloat for Float` +- **Functions**: `parseOperand`, `handleOperand`, `handleOperandDisallowed`, `handleOperandDisallowedAlwaysOne`, `handleOperandSingleFull`, `handleOperandSingleFullNoDefault`, `handleOperandDoublePerByteNoDefault`, `handleOperand8M1M1`, `handleOperandM1M1` + +### LibParsePragma.sol +- **Library**: `LibParsePragma` +- **Imports**: `LibParseState`, `ParseState`; `CMASK_WHITESPACE`; `NoWhitespaceAfterUsingWordsFrom`; `LibParseError`; `LibParseInterstitial`; `LibParseLiteral` +- **Constants**: `PRAGMA_KEYWORD_BYTES`, `PRAGMA_KEYWORD_BYTES32`, `PRAGMA_KEYWORD_BYTES_LENGTH`, `PRAGMA_KEYWORD_MASK` +- **Using**: `LibParseError for ParseState`, `LibParseInterstitial for ParseState`, `LibParseLiteral for ParseState`, `LibParseState for ParseState` +- **Functions**: `parsePragma` + +### LibParseStackName.sol +- **Library**: `LibParseStackName` +- **Imports**: `ParseState` +- **Functions**: `pushStackName`, `stackNameIndex` +- **NatSpec**: `@title LibParseStackName` with detailed struct packing and bloom filter documentation + +### LibParseStackTracker.sol +- **Library**: `LibParseStackTracker` +- **Type**: `ParseStackTracker` (user-defined value type wrapping `uint256`) +- **Imports**: `ParseStackUnderflow`, `ParseStackOverflow` +- **Using**: `LibParseStackTracker for ParseStackTracker` +- **Functions**: `pushInputs`, `push`, `pop` + +### LibSubParse.sol +- **Library**: `LibSubParse` +- **Imports**: `LibParseState`, `ParseState`; `OPCODE_UNKNOWN`, `OPCODE_EXTERN`, `OPCODE_CONSTANT`, `OPCODE_CONTEXT`, `OperandV2`; `LibBytecode`, `Pointer`; `ISubParserV4`; `BadSubParserResult`, `UnknownWord`, `UnsupportedLiteralType`; `IInterpreterExternV4`, `LibExtern`, `EncodedExternDispatchV2`; `ExternDispatchConstantsHeightOverflow`, `ConstantOpcodeConstantsHeightOverflow`, `ContextGridOverflow`; `LibMemCpy`; `LibParseError` +- **Using**: `LibParseState for ParseState`, `LibParseError for ParseState` +- **NatSpec**: `@title LibSubParse` with trust model documentation +- **Functions**: `subParserContext`, `subParserConstant`, `subParserExtern`, `subParseWordSlice`, `subParseWords`, `subParseLiteral`, `consumeSubParseWordInputData`, `consumeSubParseLiteralInputData` + +### LibParseLiteral.sol +- **Library**: `LibParseLiteral` +- **Imports**: `CMASK_STRING_LITERAL_HEAD`, `CMASK_LITERAL_HEX_DISPATCH`, `CMASK_NUMERIC_LITERAL_HEAD`, `CMASK_SUB_PARSEABLE_LITERAL_HEAD`; `UnsupportedLiteralType`; `ParseState`; `LibParseError` +- **Constants**: `LITERAL_PARSERS_LENGTH`, `LITERAL_PARSER_INDEX_HEX`, `LITERAL_PARSER_INDEX_DECIMAL`, `LITERAL_PARSER_INDEX_STRING`, `LITERAL_PARSER_INDEX_SUB_PARSE` +- **Using**: `LibParseLiteral for ParseState`, `LibParseError for ParseState` +- **Functions**: `selectLiteralParserByIndex`, `parseLiteral`, `tryParseLiteral` + +### LibParseLiteralDecimal.sol +- **Library**: `LibParseLiteralDecimal` +- **Imports**: `ParseState`; `LibParseError`; `LibParseDecimalFloat`, `Float`; `LibDecimalFloat` +- **Using**: `LibParseError for ParseState` +- **Functions**: `parseDecimalFloatPacked` + +### LibParseLiteralHex.sol +- **Library**: `LibParseLiteralHex` +- **Imports**: `ParseState`; `MalformedHexLiteral`, `OddLengthHexLiteral`, `ZeroLengthHexLiteral`, `HexLiteralOverflow`; `CMASK_UPPER_ALPHA_A_F`, `CMASK_LOWER_ALPHA_A_F`, `CMASK_NUMERIC_0_9`, `CMASK_HEX`; `LibParseError` +- **Using**: `LibParseLiteralHex for ParseState`, `LibParseError for ParseState` +- **Functions**: `boundHex`, `parseHex` + +### LibParseLiteralString.sol +- **Library**: `LibParseLiteralString` +- **Imports**: `ParseState`; `IntOrAString`, `LibIntOrAString`; `UnclosedStringLiteral`, `StringTooLong`; `CMASK_STRING_LITERAL_END`, `CMASK_STRING_LITERAL_TAIL`; `LibParseError` +- **NatSpec**: `@title LibParseLiteralString` +- **Using**: `LibParseError for ParseState`, `LibParseLiteralString for ParseState` +- **Functions**: `boundString`, `parseString` + +### LibParseLiteralSubParseable.sol +- **Library**: `LibParseLiteralSubParseable` +- **Imports**: `ParseState`; `LibParse`; `UnclosedSubParseableLiteral`, `SubParseableMissingDispatch`; `CMASK_WHITESPACE`, `CMASK_SUB_PARSEABLE_LITERAL_END`; `LibParseInterstitial`; `LibParseError`; `LibSubParse`; `LibParseChar` +- **Using**: `LibParse for ParseState`, `LibParseInterstitial for ParseState`, `LibParseError for ParseState`, `LibSubParse for ParseState` +- **Functions**: `parseSubParseable` + +--- + +## Findings + +### P4-PARSE-1: Repeated Float-to-uint conversion pattern (INFO) + +**File**: `src/lib/parse/LibParseOperand.sol` +**Lines**: 183-184, 207-208, 235-238, 286-291, 334-337 + +The two-step `unpack` + `toFixedDecimalLossless(..., 0)` pattern is repeated 9 times across 5 operand handlers, each time converting a `Float` to a `uint256` integer. The pattern is: + +```solidity +(signedCoefficient, exponent) = LibDecimalFloat.unpack(x); +uint256 xUint = LibDecimalFloat.toFixedDecimalLossless(signedCoefficient, exponent, 0); +``` + +A small helper (e.g. `floatToUint(Float) returns (uint256)`) would reduce repetition and ensure consistent conversion behavior in one place. + +This also masks a secondary style inconsistency: `handleOperandSingleFull` and `handleOperandSingleFullNoDefault` use the method syntax `Float.wrap(...).unpack()` (leveraging the `using LibDecimalFloat for Float` directive), while all other handlers call `LibDecimalFloat.unpack(a)` directly. Both work identically but the two calling conventions within the same library are inconsistent. + +**Severity**: INFO + +--- + +### P4-PARSE-2: Inconsistent `type(uint16).max` cast style (LOW) + +**File**: `src/lib/parse/LibParseOperand.sol` +**Lines**: 185, 209 + +`handleOperandSingleFull` at line 185: +```solidity +if (operandUint > type(uint16).max) { +``` + +`handleOperandSingleFullNoDefault` at line 209: +```solidity +if (operandUint > uint256(type(uint16).max)) { +``` + +These two functions are near-identical in structure and purpose. Using `uint256(...)` wrapping in one but not the other is inconsistent. Both compile identically, but the inconsistency can cause confusion during review about whether the cast is required. + +**Severity**: LOW + +--- + +### P4-PARSE-3: Double-parenthesized cast in sub-parser linked list walk (LOW) + +**File**: `src/lib/parse/LibSubParse.sol` +**Lines**: 225, 381 + +The `subParseWordSlice` function at line 225: +```solidity +ISubParserV4 subParser = ISubParserV4(address(uint160(uint256((deref))))); +``` + +The `subParseLiteral` function at line 381: +```solidity +ISubParserV4 subParser = ISubParserV4(address(uint160(uint256(deref)))); +``` + +Line 225 has a redundant extra set of parentheses around `deref`: `uint256((deref))`. Line 381 has the normal form. The code is functionally identical but the styles differ within the same library for the same operation. + +**Severity**: LOW + +--- + +### P4-PARSE-4: Inconsistent bounds-check constant style across files (LOW) + +**Files**: `src/lib/parse/LibParseOperand.sol` (line 240), `src/lib/parse/LibSubParse.sol` (line 53) + +`LibParseOperand` uses Solidity type constants for bounds checks: +```solidity +if (aUint > type(uint8).max || bUint > type(uint8).max) { +``` + +`LibSubParse.subParserContext` uses raw hex literals for the same logical check: +```solidity +if (column > 0xFF || row > 0xFF) { +``` + +Both check that a value fits in a `uint8`. Using `type(uint8).max` is the idiomatic Solidity convention and communicates intent better than `0xFF`. Similarly, line 101 uses `0xFFFF` instead of `type(uint16).max` for the constants height check. + +**Severity**: LOW + +--- + +### P4-PARSE-5: Inconsistent `@title` NatSpec on libraries (LOW) + +**Files**: All 10 files in scope + +Only 3 of the 10 libraries in scope have a `@title` NatSpec tag: +- `LibParseStackName` (with detailed struct docs) +- `LibSubParse` (with trust model docs) +- `LibParseLiteralString` (minimal title) + +The other 7 have no library-level documentation at all: +- `LibParseOperand` +- `LibParsePragma` +- `LibParseStackTracker` +- `LibParseLiteral` +- `LibParseLiteralDecimal` +- `LibParseLiteralHex` +- `LibParseLiteralSubParseable` + +For a parser codebase with significant complexity, library-level documentation describing each library's responsibility within the parsing pipeline would aid maintainability. + +**Severity**: LOW + +--- + +### P4-PARSE-6: Hex literal max length magic number (INFO) + +**File**: `src/lib/parse/literal/LibParseLiteralHex.sol` +**Line**: 71 + +```solidity +if (hexLength > 0x40) { + revert HexLiteralOverflow(state.parseErrorOffset(hexStart)); +} +``` + +The value `0x40` (64) represents the maximum number of hex nybbles in a 256-bit value. While arguably obvious in context, a named constant such as `MAX_HEX_LITERAL_NYBBLES` would improve readability and make the relationship to `bytes32` explicit. + +**Severity**: INFO + +--- + +### P4-PARSE-7: Bitmask construction style inconsistency in hex parser (INFO) + +**File**: `src/lib/parse/literal/LibParseLiteralHex.sol` +**Line**: 89 + +```solidity +uint256 hexChar = 1 << hexCharByte; +``` + +Every other file in the parse system constructs character bitmasks in assembly via `shl(byte(0, mload(cursor)), 1)`. The hex parser constructs it in Solidity as `1 << hexCharByte`. Both are correct, but the hex parser is the only place in the 10-file scope that uses the Solidity shift syntax for this purpose. The context differs slightly (byte already extracted vs inline), so this is informational. + +**Severity**: INFO + +--- + +### P4-PARSE-8: No dead code or commented-out code found (OK) + +All 10 files were scanned for commented-out code patterns (commented `if`, `for`, `return`, variable declarations, etc.). No commented-out code was found. All comments are explanatory. + +--- + +### P4-PARSE-9: No unused imports found (OK) + +All imports across the 10 files were traced to usage: +- `LibParsePragma.sol`: `LibParseState` is needed for the `using` directive (provides `pushSubParser`). `LibParseLiteral` is needed for `tryParseLiteral`. +- `LibParseOperand.sol`: `LibDecimalFloat` + `Float` are needed for `using` and direct calls. All error types are used. +- All other imports are directly consumed. + +--- + +## Summary + +| ID | Severity | File(s) | Description | +|----|----------|---------|-------------| +| P4-PARSE-1 | INFO | LibParseOperand.sol | Repeated float-to-uint conversion pattern (9 occurrences) with mixed calling convention | +| P4-PARSE-2 | LOW | LibParseOperand.sol | Inconsistent `uint256()` cast on `type(uint16).max` between sibling functions | +| P4-PARSE-3 | LOW | LibSubParse.sol | Redundant double-parentheses in one of two identical cast patterns | +| P4-PARSE-4 | LOW | LibParseOperand.sol, LibSubParse.sol | `type(uint8).max` vs `0xFF` for identical bounds checks across files | +| P4-PARSE-5 | LOW | All 10 files | 7 of 10 libraries missing `@title` NatSpec | +| P4-PARSE-6 | INFO | LibParseLiteralHex.sol | Hex literal max length `0x40` is a magic number | +| P4-PARSE-7 | INFO | LibParseLiteralHex.sol | Solidity-syntax bitmask (`1 << x`) vs assembly-syntax (`shl(x, 1)`) used everywhere else | +| P4-PARSE-8 | OK | All 10 files | No commented-out code | +| P4-PARSE-9 | OK | All 10 files | No unused imports | diff --git a/audit/2026-03-01-01/triage.md b/audit/2026-03-01-01/triage.md new file mode 100644 index 000000000..c3d423e98 --- /dev/null +++ b/audit/2026-03-01-01/triage.md @@ -0,0 +1,123 @@ +# Audit 2026-03-01-01 Triage + +## Pass 0: Process + +- [FIXED] P0-1: (LOW) Missing Bash tool in pass2 skill +- [FIXED] P0-2: (LOW) Missing command syntax in CLAUDE.md +- [FIXED] P0-3: (LOW) foundry.toml not referenced in CLAUDE.md +- [FIXED] P0-4: (LOW) Test base contracts missing file paths in TESTING.md +- [FIXED] P0-5: (LOW) Duplicated general rules across skill files +- [FIXED] P0-6: (LOW) known-false-positives.md not referenced in skill +- [FIXED] P0-7: (LOW) Missing DISPaiRegistry from architecture docs +- [DISMISSED] P0-8: (LOW) .fixes/ convention ambiguous for Pass 0 — .fixes/ can contain any fix including process doc edits + +## Pass 1: Security + +- [FIXED] A43-1: (MEDIUM) endSource ops-count byte overflow when total ops > 255 +- [DISMISSED] A05-1: (LOW) sourceIndex unchecked in eval2 — documented trust assumption; callers validate (carried from prior triage) +- [DISMISSED] A05-2: (LOW) Empty fs div-by-zero in evalLoop — constructor guard prevents deployment with empty fs (carried from prior triage) +- [FIXED] A12-2: (LOW) readHighwater NatSpec inaccuracy in LibIntegrityCheck +- [DOCUMENTED] A45-9: (LOW) Virtual opcodeFunctionPointers bypass in Rainterpreter — added NatSpec invariant +- [DISMISSED] A47-1: (LOW) serializeSize unchecked overflow in RainterpreterExpressionDeployer — practically unreachable; parser memory limits (carried from prior triage) +- [DISMISSED] A15-3: (LOW) unsafeDeserialize zero-source revert — unsafe prefix documents caller responsibility (carried from prior triage) +- [DISMISSED] A01-1: (LOW) Odd-length function pointer tables in BaseRainterpreterExtern — not user-controlled; generated by build system (carried from prior triage A12-7) +- [FIXED] A49-6: (LOW) Dispatch cursor in RainterpreterReferenceExtern — added cursor != end check and UnconsumedRepeatDispatchBytes error +- [DOCUMENTED] A49-8: (LOW) Local constants in RainterpreterReferenceExtern — added NatSpec explaining why defined locally +- [FIXED] A20-6: (LOW) Call order discrepancy in LibOpERC20 — swapped referenceFn to match run order +- [FIXED] A04-LOW-01: (LOW) Sub operand handler silently ignores values in LibDeployRegistry — changed to handleOperandDisallowed +- [DISMISSED] A35-1: (LOW) Unreachable MalformedHexLiteral dead code in LibParseLiteral — defensive invariant; boundHex constrains range to valid hex chars (carried from prior triage) +- [FIXED] A44-1: (LOW) Unaligned free memory pointer in LibParseUtilities — added 32-byte alignment rounding in subParseLiteral +- [FIXED] R02-RUST-01: (LOW) Genesis block underflow in Rust crates + +### External Audit (Preliminary Report, Feb 2026) + +- [FIXED] EXT-M01: (MEDIUM) Out-of-bounds second-byte read causes valid decimals to revert (LibParseLiteral.sol) +- [FIXED] EXT-M02: (MEDIUM) Out-of-bounds memory read and garbage literal parsing in pragma (LibParsePragma.sol) +- [FIXED] EXT-M03: (MEDIUM) Silent truncation of sub-parser dispatch length >0xFFFF (LibSubParse.sol) +- [FIXED] EXT-M04: (MEDIUM) LHS item count overflow causes bitwise carry-over and parser state corruption (LibParse.sol) +- [FIXED] EXT-L01: (LOW) Uppercase hexadecimal prefix bypasses hex parser (LibParseLiteral.sol) +- [DOCUMENTED] EXT-I02: (INFO) Unused ParseState parameter in boundHex (LibParseLiteralHex.sol) +- [DOCUMENTED] EXT-I03: (INFO) Misleading documentation comment regarding non-ASCII characters (LibParseLiteralSubParseable.sol) + +## Pass 2: Test Coverage + +- [FIXED] A30-P2-1: (MEDIUM) No test for total source ops > 255 across multiple top-level items +- [PENDING] P2-EI-1: (LOW) eval2 InputsLengthMismatch not tested at library level +- [PENDING] P2-EI-2: (LOW) integrityCheck2 BadOpInputsLength/BadOpOutputsLength not directly tested +- [PENDING] P2-EI-3: (LOW) evalLoop remainder-only path (1-7 opcodes) not tested +- [PENDING] P2-CC-01: (LOW) Rainterpreter.supportsInterface omits IOpcodeToolingV1 +- [PENDING] P2-CC-02: (LOW) RainterpreterExpressionDeployer missing dedicated pointer consistency test +- [PENDING] P2-CC-03: (LOW) Missing direct test for StateNamespace isolation (same sender) +- [PENDING] A30-P2-2: (LOW) No test for ParserOutOfBounds error in parse() +- [PENDING] A30-P2-3: (LOW) testEndSourceByteLengthFuzz upper bound too low +- [PENDING] A32-1: (LOW) skipComment no test for UnclosedComment when well-formed but never closed +- [PENDING] A32-2: (LOW) skipComment no fuzz test for well-formed comments +- [PENDING] A42-1: (LOW) pushInputs no test for push-overflow-inside-pushInputs +- [PENDING] A44-1-P2: (LOW) subParseWordSlice no test for no-sub-parsers-registered path +- [PENDING] P2-EAD-01: (LOW) BaseRainterpreterSubParser.subParseWord2 missing happy-path and no-match tests +- [PENDING] P2-EAD-02: (LOW) authoringMetaV2 word names not verified beyond index 3 +- [PENDING] P2-01: (LOW) Missing operand-disallowed tests for 10 logic opcodes +- [PENDING] P2-02: (LOW) Missing operand-disallowed tests for 5 math opcodes +- [PENDING] P2-03: (LOW) Missing operand-disallowed test for LibOpHash +- [PENDING] R02-PASS2-01: (LOW) No tests for error paths in Forker methods +- [PENDING] R02-PASS2-02: (LOW) Forker::new() has no test +- [PENDING] R02-PASS2-04: (LOW) RainSourceTrace::from_data() edge cases untested +- [PENDING] R02-PASS2-07: (LOW) CLI Parse command entirely untested + +## Pass 3: Documentation + +- [PENDING] P3-ERR-1: (LOW) 16 errors use plain /// without @notice while siblings use @notice +- [PENDING] P3-EA-01: (LOW) "word dispatches" should be "opcode dispatches" in BaseRainterpreterExtern NatSpec +- [PENDING] P3-EA-02: (LOW) Missing @return tags on buildOpcodeFunctionPointers/buildIntegrityFunctionPointers +- [PENDING] P3-EA-03: (LOW) Three context op subParser functions have unnamed parameters and no @param/@return +- [PENDING] P3-EA-04: (LOW) Undocumented CONTEXT_CALLER_CONTEXT constants in LibExternOpContextRainlen +- [PENDING] P3-EA-05: (LOW) Typo "determin" in SUB_PARSER_LITERAL_REPEAT_KEYWORD_MASK NatSpec +- [PENDING] P3-LPS-01: (LOW) LibParseState library missing library-level NatSpec +- [PENDING] P3-LPS-02: (LOW) checkParseMemoryOverflow missing explicit @notice tag +- [PENDING] P3-LPP-02: (LOW) 4 pragma keyword constants missing NatSpec +- [PENDING] P3-LPST-02: (LOW) ParseStackTracker user-defined type missing NatSpec +- [PENDING] P3-LPL-02: (LOW) 5 literal parser index constants missing NatSpec +- [PENDING] P3-ERR-01: (LOW) 13 errors in ErrParse.sol have doc comments but no explicit @notice tag +- [PENDING] P3-CC-01: (LOW) opcodeFunctionPointers has untagged lines before @return tag +- [PENDING] P3-CC-02: (LOW) buildOperandHandlerFunctionPointers/buildLiteralParserFunctionPointers use bare /// instead of @inheritdoc +- [PENDING] P3-CC-03: (LOW) Three internal virtual functions missing @return tags in RainterpreterParser +- [PENDING] P3-CC-04: (LOW) IDISPaiRegistry functions have untagged description + @return tag +- [PENDING] P3-OPALL-01: (LOW) Missing @notice tag on integrity() in 7 files +- [PENDING] P3-OPALL-02: (LOW) Missing @return tags on integrity() in 7 files +- [PENDING] P3-OPALL-03: (LOW) Missing @notice and @return on referenceFn() in LibOpMaxUint256 +- [PENDING] P3-OPALL-04: (LOW) Missing @notice on integrity() in LibOpEnsure +- [PENDING] P3-RC-01: (LOW) No crate-level //! documentation on any Rust crate +- [PENDING] P3-RC-02: (LOW) CLI crate all 13 public items completely undocumented +- [PENDING] P3-RC-03: (LOW) eval crate 10 undocumented public items +- [PENDING] P3-RC-04: (LOW) parser crate 3 undocumented public items +- [PENDING] P3-RC-05: (LOW) dispair crate DISPaiR::new constructor lacks doc comment +- [PENDING] P3-RC-06: (LOW) "Rainalang" typo in ForkEvalArgs.rainlang_string field doc +- [PENDING] P3-RC-07: (LOW) Forker::new_with_fork doc lists wrong params and missing await +- [PENDING] P3-RC-08: (LOW) alloy_call/alloy_call_committing docs omit decode_error param + +## Pass 4: Code Quality + +- [PENDING] P4-CC-01: (LOW) Unused IERC165 import in RainterpreterExpressionDeployer +- [PENDING] P4-CC-02: (LOW) No virtual on any function in RainterpreterDISPaiRegistry +- [PENDING] P4-CC-03: (LOW) buildIntegrityFunctionPointers missing override in RainterpreterExpressionDeployer +- [PENDING] P4-CC-04: (LOW) describedByMetaV1 missing virtual in RainterpreterExpressionDeployer +- [PENDING] P4-CC-05: (LOW) unsafeParse missing virtual in RainterpreterParser +- [PENDING] P4-EVLP-1: (LOW) Unused using LibUint256Array and import in LibParse.sol +- [PENDING] P4-EVLP-2: (LOW) Unused return value suppressed with (index); instead of blank destructuring +- [PENDING] P4-EVLP-5: (LOW) Magic number 59 for paren depth limit +- [PENDING] P4-EVLP-6: (LOW) Magic number 0x3f for stack RHS overflow +- [PENDING] P4-PARSE-2: (LOW) handleOperandSingleFull type(uint16).max vs uint256() inconsistency +- [PENDING] P4-PARSE-3: (LOW) Redundant double-parentheses in LibSubParse.sol +- [PENDING] P4-PARSE-4: (LOW) type(uint8).max vs raw 0xFF bounds check inconsistency +- [PENDING] P4-PARSE-5: (LOW) 7 of 10 parse libraries missing @title NatSpec +- [PENDING] P4-EA-01: (LOW) Dispatch decoding duplicated inline instead of reusing LibExtern +- [PENDING] P4-EA-02: (LOW) Inconsistent bitmask style type(uint16).max vs 0xFFFF +- [PENDING] P4-EA-03: (LOW) Context position constants defined locally instead of shared +- [PENDING] P4-EA-04: (LOW) Inconsistent subParser parameter naming across context ops +- [PENDING] P4-EA-05: (LOW) Five build* functions in RainterpreterReferenceExtern repeat boilerplate +- [PENDING] PASS4-LIBOP-1: (LOW) Missing @notice tag on integrity/referenceFn in 8 files +- [PENDING] PASS4-LIBOP-2: (LOW) LibOpCall only standard opcode without referenceFn +- [PENDING] P4-ERR-01: (LOW) Inconsistent @notice tags across error files +- [PENDING] P4-RUST-01: (LOW) Unused dependencies serde_json, reqwest, once_cell in eval Cargo.toml +- [PENDING] P4-RUST-02: (LOW) Wildcard use alloy::primitives::* in dispair and parser +- [PENDING] P4-RUST-03: (LOW) Duplicated error-handling logic in alloy_call/alloy_call_committing with inconsistent format diff --git a/crates/eval/src/error.rs b/crates/eval/src/error.rs index 847b245a3..136e72122 100644 --- a/crates/eval/src/error.rs +++ b/crates/eval/src/error.rs @@ -40,6 +40,8 @@ pub enum ReplayTransactionError { NoBlockNumberFound(String, String), #[error("No from address found in transaction for hash {0} and fork url {1}")] NoFromAddressFound(String, String), + #[error("Cannot replay genesis block (block 0) transaction for hash {0} and fork url {1}")] + GenesisBlockReplay(String, String), } #[cfg(not(target_family = "wasm"))] diff --git a/crates/eval/src/fork.rs b/crates/eval/src/fork.rs index 267099565..c1d77a7fc 100644 --- a/crates/eval/src/fork.rs +++ b/crates/eval/src/fork.rs @@ -448,7 +448,14 @@ impl Forker { self.add_or_select( NewForkedEvm { fork_url: fork_url.clone(), - fork_block_number: Some(block_number - 1), + fork_block_number: Some( + block_number + .checked_sub(1) + .ok_or(ReplayTransactionError::GenesisBlockReplay( + tx_hash.to_string(), + fork_url.clone(), + ))?, + ), }, None, ) diff --git a/src/concrete/Rainterpreter.sol b/src/concrete/Rainterpreter.sol index 9cf191cea..71a9d9cce 100644 --- a/src/concrete/Rainterpreter.sol +++ b/src/concrete/Rainterpreter.sol @@ -41,6 +41,10 @@ contract Rainterpreter is IInterpreterV4, IOpcodeToolingV1, ERC165 { /// Returns the packed 2-byte function pointer table used by the eval loop /// to dispatch each opcode. Virtual so subclasses can override the table. + /// @notice Overrides MUST return the same non-empty value at construction + /// time and at runtime. Returning empty bytes at runtime would cause + /// division-by-zero in the eval loop's modulo-based dispatch, leading to + /// reads from arbitrary memory interpreted as function pointers. /// @return The opcode function pointers for the interpreter. function opcodeFunctionPointers() internal view virtual returns (bytes memory) { return OPCODE_FUNCTION_POINTERS; diff --git a/src/concrete/extern/RainterpreterReferenceExtern.sol b/src/concrete/extern/RainterpreterReferenceExtern.sol index 9f68beffc..ba55a35b4 100644 --- a/src/concrete/extern/RainterpreterReferenceExtern.sol +++ b/src/concrete/extern/RainterpreterReferenceExtern.sol @@ -73,6 +73,10 @@ uint256 constant SUB_PARSER_LITERAL_REPEAT_INDEX = 0; /// @dev Thrown when the repeat literal parser is not a single digit. error InvalidRepeatCount(); +/// @dev Thrown when the repeat literal dispatch has trailing bytes after +/// the decimal digit. +error UnconsumedRepeatDispatchBytes(); + /// @dev Number of opcode function pointers available to run at eval time. uint256 constant OPCODE_FUNCTION_POINTERS_LENGTH = 1; @@ -253,6 +257,9 @@ contract RainterpreterReferenceExtern is BaseRainterpreterSubParser, BaseRainter (cursor, floatBytes) = LibParseLiteralDecimal.parseDecimalFloatPacked( state, cursor + SUB_PARSER_LITERAL_REPEAT_KEYWORD_BYTES_LENGTH, end ); + if (cursor != end) { + revert UnconsumedRepeatDispatchBytes(); + } Float repeatCount = Float.wrap(floatBytes); // We can only repeat a single digit integer 0-9. if ( diff --git a/src/error/ErrParse.sol b/src/error/ErrParse.sol index 32d330e70..93afc1ad8 100644 --- a/src/error/ErrParse.sol +++ b/src/error/ErrParse.sol @@ -174,6 +174,10 @@ error ParseMemoryOverflow(uint256 freeMemoryPointer); /// would silently wrap, corrupting source bytecode. error SourceItemOpsOverflow(); +/// The total number of opcodes across all top-level items in a single source +/// exceeded 255. The source prefix byte can only represent 0-255. +error SourceTotalOpsOverflow(); + /// A paren group exceeded 255 inputs. The per-paren byte counter would /// silently wrap, corrupting operand data. error ParenInputOverflow(); @@ -181,3 +185,15 @@ error ParenInputOverflow(); /// A single line exceeded the maximum number of RHS top-level items that /// can be tracked in the 256-bit lineTracker (14 items). error LineRHSItemsOverflow(); + +/// @notice Thrown when a numeric literal starts with `0X` (uppercase). Only +/// lowercase `0x` is a valid hexadecimal prefix. Uppercase `0X` would +/// otherwise silently parse as decimal zero. +/// @param offset The byte offset in the source where the error occurred. +error UppercaseHexPrefix(uint256 offset); + +/// The number of LHS items overflowed the single-byte counter in +/// `lineTracker` (per line) or `topLevel1` (per source). Maximum 255 LHS +/// items per line and per source. +/// @param offset The byte offset in the source where the error occurred. +error LHSItemCountOverflow(uint256 offset); diff --git a/src/error/ErrSubParse.sol b/src/error/ErrSubParse.sol index 8ee0995fb..f82d2a6c2 100644 --- a/src/error/ErrSubParse.sol +++ b/src/error/ErrSubParse.sol @@ -25,3 +25,9 @@ error ContextGridOverflow(uint256 column, uint256 row); /// @param index The out-of-bounds index. /// @param length The number of function pointers available. error SubParserIndexOutOfBounds(uint256 index, uint256 length); + +/// @notice Thrown when the dispatch region passed to `subParseLiteral` exceeds +/// the 16-bit encoding limit (0xFFFF bytes). The dispatch length is packed into +/// a 2-byte field; values above 0xFFFF would be silently truncated. +/// @param dispatchLength The dispatch length that overflowed. +error SubParseLiteralDispatchLengthOverflow(uint256 dispatchLength); diff --git a/src/generated/RainterpreterExpressionDeployer.pointers.sol b/src/generated/RainterpreterExpressionDeployer.pointers.sol index 2b356e8c9..0d5040c72 100644 --- a/src/generated/RainterpreterExpressionDeployer.pointers.sol +++ b/src/generated/RainterpreterExpressionDeployer.pointers.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0xf9e6af09d0671611f8be055870c97c3a7c4c598329e2d617158e194b0484893e); +bytes32 constant BYTECODE_HASH = bytes32(0x58937b2f2590a07a3c3f93b766fd9dc1890de7a85239282e9a0928068d329b6c); /// @dev The hash of the meta that describes the contract. bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0xb2500441a27ea683f814327be6e43c90f516b8f033203ad3e0ba2cde847fb0ba); diff --git a/src/generated/RainterpreterParser.pointers.sol b/src/generated/RainterpreterParser.pointers.sol index fce78f8a9..01f3d964b 100644 --- a/src/generated/RainterpreterParser.pointers.sol +++ b/src/generated/RainterpreterParser.pointers.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0x0a82033aa519f6cfc574ff2fd37340285f1bf5e432eec61b047dab78f453d615); +bytes32 constant BYTECODE_HASH = bytes32(0xcca04b4215c721df539f9a2525ea402fd5fc6905f4a0fee036a6e979b905ca18); /// @dev The parse meta that is used to lookup word definitions. /// The structure of the parse meta is: @@ -39,11 +39,11 @@ uint8 constant PARSE_META_BUILD_DEPTH = 2; /// These positional indexes all map to the same indexes looked up in the parse /// meta. bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = - hex"197f197f197f1a1d1aee1aee1aee1a1d1a1d197f197f197f1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee1aee197f1aee1aee"; + hex"1a5c1a5c1a5c1afa1bcb1bcb1bcb1afa1afa1a5c1a5c1a5c1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb"; /// @dev Every two bytes is a function pointer for a literal parser. /// Literal dispatches are determined by the first byte(s) of the literal /// rather than a full word lookup, and are done with simple conditional /// jumps as the possibilities are limited compared to the number of words we /// have. -bytes constant LITERAL_PARSER_FUNCTION_POINTERS = hex"15741747178717c6"; +bytes constant LITERAL_PARSER_FUNCTION_POINTERS = hex"16511824186418a3"; diff --git a/src/generated/RainterpreterReferenceExtern.pointers.sol b/src/generated/RainterpreterReferenceExtern.pointers.sol index 34de2408d..180fac0cd 100644 --- a/src/generated/RainterpreterReferenceExtern.pointers.sol +++ b/src/generated/RainterpreterReferenceExtern.pointers.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0x13127daf097982018a78140bc5b64e436e2be46e54441d680f53b73d2e03d7fe); +bytes32 constant BYTECODE_HASH = bytes32(0x34953b70351aa66f7b71ff5bd07b3df43fc8dd5fdb4b6a4a8392cc9ce0712e98); /// @dev The hash of the meta that describes the contract. bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0xadf71693c6ecf3fd560904bc46973d1b6e651440d15366673f9b3984749e7c16); @@ -48,17 +48,17 @@ bytes constant SUB_PARSER_WORD_PARSERS = hex"079f07bf07cd07db07ea"; /// @dev Every two bytes is a function pointer for an operand handler. /// These positional indexes all map to the same indexes looked up in the parse /// meta. -bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = hex"0de70e290de70de70de7"; +bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = hex"0e3e0e800e3e0e3e0e3e"; /// @dev Every two bytes is a function pointer for a literal parser. /// Literal dispatches are determined by the first byte(s) of the literal /// rather than a full word lookup, and are done with simple conditional /// jumps as the possibilities are limited compared to the number of words we /// have. -bytes constant LITERAL_PARSER_FUNCTION_POINTERS = hex"0d3e"; +bytes constant LITERAL_PARSER_FUNCTION_POINTERS = hex"0d95"; /// @dev The function pointers for the integrity check fns. -bytes constant INTEGRITY_FUNCTION_POINTERS = hex"0bc5"; +bytes constant INTEGRITY_FUNCTION_POINTERS = hex"0be3"; /// @dev The function pointers known to the interpreter for dynamic dispatch. /// By setting these as a constant they can be inlined into the interpreter diff --git a/src/lib/deploy/LibInterpreterDeploy.sol b/src/lib/deploy/LibInterpreterDeploy.sol index b7430704b..dc69b6b9e 100644 --- a/src/lib/deploy/LibInterpreterDeploy.sol +++ b/src/lib/deploy/LibInterpreterDeploy.sol @@ -11,14 +11,14 @@ pragma solidity ^0.8.25; library LibInterpreterDeploy { /// The address of the `RainterpreterParser` contract when deployed with the /// rain standard zoltu deployer. - address constant PARSER_DEPLOYED_ADDRESS = address(0xE771CB7A2C0cC34ab68Eb7f5f7973ccBDf4f18E7); + address constant PARSER_DEPLOYED_ADDRESS = address(0x744d1fFF170FC824EcEDb4E220819682095dFE83); /// The code hash of the `RainterpreterParser` contract when deployed with /// the rain standard zoltu deployer. This can be used to verify that the /// deployed contract has the expected bytecode, which provides stronger /// guarantees than just checking the address. bytes32 constant PARSER_DEPLOYED_CODEHASH = - bytes32(0x0a82033aa519f6cfc574ff2fd37340285f1bf5e432eec61b047dab78f453d615); + bytes32(0xcca04b4215c721df539f9a2525ea402fd5fc6905f4a0fee036a6e979b905ca18); /// The address of the `RainterpreterStore` contract when deployed with the /// rain standard zoltu deployer. @@ -44,23 +44,23 @@ library LibInterpreterDeploy { /// The address of the `RainterpreterExpressionDeployer` contract when /// deployed with the rain standard zoltu deployer. - address constant EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS = address(0xa7067aa8834DE74eDa4061999929Ae02adcDDe64); + address constant EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS = address(0x3Dcbe436dC61dd38635bB32aFc8C9487F6EFa5b6); /// The code hash of the `RainterpreterExpressionDeployer` contract when /// deployed with the rain standard zoltu deployer. This can be used to /// verify that the deployed contract has the expected bytecode, which /// provides stronger guarantees than just checking the address. bytes32 constant EXPRESSION_DEPLOYER_DEPLOYED_CODEHASH = - bytes32(0xf9e6af09d0671611f8be055870c97c3a7c4c598329e2d617158e194b0484893e); + bytes32(0x58937b2f2590a07a3c3f93b766fd9dc1890de7a85239282e9a0928068d329b6c); /// The address of the `RainterpreterDISPaiRegistry` contract when deployed /// with the rain standard zoltu deployer. - address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0xb0C5ec55F355e8FaB78D4c82437D0dF2f4688a67); + address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0x877e3e8D0860235f4F4F771a3E71B076f47b23Ac); /// The code hash of the `RainterpreterDISPaiRegistry` contract when /// deployed with the rain standard zoltu deployer. This can be used to /// verify that the deployed contract has the expected bytecode, which /// provides stronger guarantees than just checking the address. bytes32 constant DISPAIR_REGISTRY_DEPLOYED_CODEHASH = - bytes32(0x79edc50d772f7f1c1105742f21a20089bea85ad001fe7f156ef3ac0b3e7a1191); + bytes32(0xaa0a08b32797ce90d646ca05dcc64b53b0bb34ba7b15834619a51b567294470f); } diff --git a/src/lib/extern/reference/op/LibExternOpContextRainlen.sol b/src/lib/extern/reference/op/LibExternOpContextRainlen.sol index ba2f5295b..a414afb22 100644 --- a/src/lib/extern/reference/op/LibExternOpContextRainlen.sol +++ b/src/lib/extern/reference/op/LibExternOpContextRainlen.sol @@ -5,7 +5,16 @@ pragma solidity ^0.8.25; import {OperandV2} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {LibSubParse} from "../../../parse/LibSubParse.sol"; +/// @dev Column index for caller-provided context. Column 0 is the base +/// context (sender, calling contract) built by the interpreter itself. +/// Column 1 is always the caller context passed in by the calling contract. +/// Defined locally rather than in `LibContext.sol` because `LibContext` only +/// covers the base context grid; caller context layout is caller-specific. uint256 constant CONTEXT_CALLER_CONTEXT_COLUMN = 1; + +/// @dev Row index for the Rainlang byte length within the caller context +/// column. This position is specific to the reference extern implementation +/// and is not a universal convention, so it is defined locally. uint256 constant CONTEXT_CALLER_CONTEXT_ROW_RAINLEN = 0; /// @title LibExternOpContextRainlen diff --git a/src/lib/integrity/LibIntegrityCheck.sol b/src/lib/integrity/LibIntegrityCheck.sol index 31fdce285..895b09684 100644 --- a/src/lib/integrity/LibIntegrityCheck.sol +++ b/src/lib/integrity/LibIntegrityCheck.sol @@ -21,7 +21,11 @@ import {OperandV2} from "rain.interpreter.interface/interface/IInterpreterV4.sol /// @param stackMaxIndex Peak stack depth seen so far, used to verify the /// bytecode-declared stack allocation. /// @param readHighwater Lowest stack index that opcodes are allowed to read -/// from. Advances past multi-output regions to prevent aliasing reads. +/// from via consumption. After an opcode produces multiple outputs, the +/// highwater advances to prevent subsequent opcodes from consuming below +/// the multi-output region, which would leave orphaned values. Read-only +/// access (e.g. the `stack` opcode) is permitted below the highwater +/// because copying does not create aliasing. /// @param constants The constants array for the expression, passed through /// to opcode integrity functions that need it. /// @param opIndex Sequential counter of opcodes processed, used for error diff --git a/src/lib/op/LibAllStandardOps.sol b/src/lib/op/LibAllStandardOps.sol index a62678afd..a4e335d61 100644 --- a/src/lib/op/LibAllStandardOps.sol +++ b/src/lib/op/LibAllStandardOps.sol @@ -509,7 +509,7 @@ library LibAllStandardOps { // sqrt LibParseOperand.handleOperandDisallowed, // sub - LibParseOperand.handleOperandSingleFull, + LibParseOperand.handleOperandDisallowed, // get LibParseOperand.handleOperandDisallowed, // set diff --git a/src/lib/op/erc20/LibOpERC20Allowance.sol b/src/lib/op/erc20/LibOpERC20Allowance.sol index 464f3587c..4ccc67a62 100644 --- a/src/lib/op/erc20/LibOpERC20Allowance.sol +++ b/src/lib/op/erc20/LibOpERC20Allowance.sol @@ -110,8 +110,8 @@ library LibOpERC20Allowance { //forge-lint: disable-next-line(unsafe-typecast) address spender = address(uint160(spenderValue)); - uint8 tokenDecimals = LibTOFUTokenDecimals.safeDecimalsForTokenReadOnly(token); uint256 tokenAllowance = IERC20(token).allowance(owner, spender); + uint8 tokenDecimals = LibTOFUTokenDecimals.safeDecimalsForTokenReadOnly(token); // Same as in the run implementation. //slither-disable-next-line unused-return (Float tokenAllowanceFloat,) = LibDecimalFloat.fromFixedDecimalLossyPacked(tokenAllowance, tokenDecimals); diff --git a/src/lib/parse/LibParse.sol b/src/lib/parse/LibParse.sol index 52af8188b..0e0575ca8 100644 --- a/src/lib/parse/LibParse.sol +++ b/src/lib/parse/LibParse.sol @@ -34,7 +34,8 @@ import { UnexpectedLHSChar, MissingFinalSemi, UnexpectedComment, - ParenOverflow + ParenOverflow, + LHSItemCountOverflow } from "../../error/ErrParse.sol"; import { LibParseState, @@ -165,7 +166,17 @@ library LibParse { cursor = LibParseChar.skipMask(cursor + 1, end, CMASK_LHS_STACK_TAIL); } // Bump the index regardless of whether the stack - // item is named or not. + // item is named or not. Both counters are packed into + // the low byte of their respective uint256 fields. + // Exceeding 0xFF would carry into the adjacent byte, + // silently corrupting parser state. + // The low byte of each field is only ever mutated by + // single increments (here) and resets to 0 (newSource + // / endLine), so reading via `& 0xFF` reliably gives + // the true count at this point. + if ((state.topLevel1 & 0xFF) == 0xFF || (state.lineTracker & 0xFF) == 0xFF) { + revert LHSItemCountOverflow(state.parseErrorOffset(cursor)); + } state.topLevel1++; state.lineTracker++; diff --git a/src/lib/parse/LibParsePragma.sol b/src/lib/parse/LibParsePragma.sol index e1c149d48..1797a3053 100644 --- a/src/lib/parse/LibParsePragma.sol +++ b/src/lib/parse/LibParsePragma.sol @@ -74,6 +74,13 @@ library LibParsePragma { // the last address as we don't break til just below. cursor = state.parseInterstitial(cursor, end); + // parseInterstitial may have consumed trailing whitespace + // up to end. Must re-check before tryParseLiteral, which + // does mload(cursor) and would read past bounds. + if (cursor >= end) { + break; + } + // Try to parse a literal and treat it as an address. bool success; bytes32 value; diff --git a/src/lib/parse/LibParseState.sol b/src/lib/parse/LibParseState.sol index affbe74dd..092627be6 100644 --- a/src/lib/parse/LibParseState.sol +++ b/src/lib/parse/LibParseState.sol @@ -20,6 +20,7 @@ import { InvalidSubParser, OpcodeIOOverflow, SourceItemOpsOverflow, + SourceTotalOpsOverflow, ParenInputOverflow, LineRHSItemsOverflow } from "../../error/ErrParse.sol"; @@ -787,6 +788,7 @@ library LibParseState { // evaluated correctly similar to reverse polish notation. else { uint256 source; + bool totalOpsOverflow; ParseStackTracker stackTracker = state.stackTracker; uint256 cursor = state.activeSourcePtr; uint256 topLevel0DataOffset = PARSE_STATE_TOP_LEVEL0_DATA_OFFSET; @@ -864,6 +866,11 @@ library LibParseState { } } } + // The total ops count across all top-level items must + // fit in a single byte. `length` includes the 4-byte + // prefix, so `div(length, 4) - 1` is the opcode count. + totalOpsOverflow := gt(sub(div(length, 4), 1), 0xFF) + // Store the bytes length in the source. mstore(source, length) // Store the opcodes length and stack tracker in the source @@ -880,6 +887,9 @@ library LibParseState { // Round up to the nearest 32 bytes to realign memory. mstore(0x40, and(add(writeCursor, 0x1f), not(0x1f))) } + if (totalOpsOverflow) { + revert SourceTotalOpsOverflow(); + } //slither-disable-next-line incorrect-shift state.sourcesBuilder = diff --git a/src/lib/parse/LibSubParse.sol b/src/lib/parse/LibSubParse.sol index e343580c3..2422d4573 100644 --- a/src/lib/parse/LibSubParse.sol +++ b/src/lib/parse/LibSubParse.sol @@ -17,7 +17,8 @@ import {IInterpreterExternV4, LibExtern, EncodedExternDispatchV2} from "../exter import { ExternDispatchConstantsHeightOverflow, ConstantOpcodeConstantsHeightOverflow, - ContextGridOverflow + ContextGridOverflow, + SubParseLiteralDispatchLengthOverflow } from "../../error/ErrSubParse.sol"; import {LibMemCpy} from "rain.solmem/lib/LibMemCpy.sol"; import {LibParseError} from "./LibParseError.sol"; @@ -359,12 +360,17 @@ library LibSubParse { { uint256 copyPointer; uint256 dispatchLength = dispatchEnd - dispatchStart; + if (dispatchLength > 0xFFFF) { + revert SubParseLiteralDispatchLengthOverflow(dispatchLength); + } uint256 bodyLength = bodyEnd - bodyStart; { uint256 dataLength = 2 + dispatchLength + bodyLength; assembly ("memory-safe") { data := mload(0x40) - mstore(0x40, add(data, add(dataLength, 0x20))) + // Round up to 32-byte alignment to maintain + // Solidity's free memory pointer invariant. + mstore(0x40, and(add(add(data, add(dataLength, 0x20)), 0x1f), not(0x1f))) mstore(add(data, 2), dispatchLength) mstore(data, dataLength) copyPointer := add(data, 0x22) diff --git a/src/lib/parse/literal/LibParseLiteral.sol b/src/lib/parse/literal/LibParseLiteral.sol index b90384029..77cd404a5 100644 --- a/src/lib/parse/literal/LibParseLiteral.sol +++ b/src/lib/parse/literal/LibParseLiteral.sol @@ -6,10 +6,12 @@ import { CMASK_STRING_LITERAL_HEAD, CMASK_LITERAL_HEX_DISPATCH, CMASK_NUMERIC_LITERAL_HEAD, - CMASK_SUB_PARSEABLE_LITERAL_HEAD + CMASK_SUB_PARSEABLE_LITERAL_HEAD, + CMASK_ZERO, + CMASK_UPPER_X } from "rain.string/lib/parse/LibParseCMask.sol"; -import {UnsupportedLiteralType} from "../../../error/ErrParse.sol"; +import {UnsupportedLiteralType, UppercaseHexPrefix} from "../../../error/ErrParse.sol"; import {ParseState} from "../LibParseState.sol"; import {LibParseError} from "../LibParseError.sol"; @@ -92,15 +94,29 @@ library LibParseLiteral { // Figure out the literal type and dispatch to the correct parser. // Probably a numeric, most things are. if ((head & CMASK_NUMERIC_LITERAL_HEAD) != 0) { - uint256 disambiguate; - assembly ("memory-safe") { - //slither-disable-next-line incorrect-shift - disambiguate := shl(byte(1, word), 1) - } - // Hexadecimal literal dispatch is 0x. We can't accidentally - // match x0 because we already checked that the head is 0-9. - if ((head | disambiguate) == CMASK_LITERAL_HEX_DISPATCH) { - index = LITERAL_PARSER_INDEX_HEX; + // Only read the second byte for hex disambiguation if it + // exists within the source bounds. If the numeric literal is + // the last byte of the source, default to decimal — reading + // past end would pick up adjacent memory that could + // coincidentally be 'x' (0x78). + if (cursor + 1 < end) { + uint256 disambiguate; + assembly ("memory-safe") { + //slither-disable-next-line incorrect-shift + disambiguate := shl(byte(1, word), 1) + } + // Hexadecimal literal dispatch is 0x. We can't accidentally + // match x0 because we already checked that the head is 0-9. + if ((head | disambiguate) == CMASK_LITERAL_HEX_DISPATCH) { + index = LITERAL_PARSER_INDEX_HEX; + } + // Uppercase 0X is not valid — revert explicitly rather + // than silently parsing as decimal zero. + else if ((head | disambiguate) == (CMASK_ZERO | CMASK_UPPER_X)) { + revert UppercaseHexPrefix(state.parseErrorOffset(cursor)); + } else { + index = LITERAL_PARSER_INDEX_DECIMAL; + } } else { index = LITERAL_PARSER_INDEX_DECIMAL; } diff --git a/src/lib/parse/literal/LibParseLiteralHex.sol b/src/lib/parse/literal/LibParseLiteralHex.sol index b2732fb0c..3bd1bf6d3 100644 --- a/src/lib/parse/literal/LibParseLiteralHex.sol +++ b/src/lib/parse/literal/LibParseLiteralHex.sol @@ -22,7 +22,9 @@ library LibParseLiteralHex { using LibParseError for ParseState; /// @notice Finds the bounds of a hex literal by scanning forward from past the - /// "0x" prefix until a non-hex character is encountered. + /// "0x" prefix until a non-hex character is encountered. The `ParseState` + /// parameter is unused here but kept for a consistent `bound*` signature + /// across literal types (e.g. `boundString` uses it for error reporting). /// @param cursor The cursor position at the start of the hex literal. /// @param end The end of the source string. /// @return The start of the hex digits (past "0x"). diff --git a/src/lib/parse/literal/LibParseLiteralSubParseable.sol b/src/lib/parse/literal/LibParseLiteralSubParseable.sol index 5daf7d9ff..104c7f865 100644 --- a/src/lib/parse/literal/LibParseLiteralSubParseable.sol +++ b/src/lib/parse/literal/LibParseLiteralSubParseable.sol @@ -58,10 +58,11 @@ library LibParseLiteralSubParseable { uint256 bodyStart = cursor; - // Skip all chars til the close. - // Note that as multibyte is not supported, and the mask is 128 bits, - // non-ascii chars MAY either fail to be skipped or will be treated - // as a closing bracket. + // Skip all chars til the close. Each byte is checked independently + // against the mask — multibyte encodings are not understood. A byte + // in a multibyte sequence that equals `]` (0x5D) would stop the + // scan prematurely. This cannot happen in valid UTF-8 (continuation + // bytes are 0x80-0xBF), but is possible in other encodings. cursor = LibParseChar.skipMask(cursor, end, ~CMASK_SUB_PARSEABLE_LITERAL_END); uint256 bodyEnd = cursor; diff --git a/test/src/concrete/RainterpreterParser.parsePragmaEmpty.t.sol b/test/src/concrete/RainterpreterParser.parsePragmaEmpty.t.sol index f0349a18a..a9e6272b8 100644 --- a/test/src/concrete/RainterpreterParser.parsePragmaEmpty.t.sol +++ b/test/src/concrete/RainterpreterParser.parsePragmaEmpty.t.sol @@ -7,7 +7,7 @@ import {RainterpreterParser} from "src/concrete/RainterpreterParser.sol"; import {PragmaV1} from "rain.interpreter.interface/interface/IParserPragmaV1.sol"; /// @title RainterpreterParserParsePragmaEmptyInputTest -/// @notice A48-4: Test that `parsePragma1` handles empty input correctly. +/// @notice Test that `parsePragma1` handles empty input correctly. contract RainterpreterParserParsePragmaEmptyInputTest is Test { /// @notice parsePragma1 with empty bytes should return a PragmaV1 with an /// empty usingWordsFrom array. diff --git a/test/src/concrete/RainterpreterReferenceExtern.repeat.t.sol b/test/src/concrete/RainterpreterReferenceExtern.repeat.t.sol index 2710cf02e..5604dc9a6 100644 --- a/test/src/concrete/RainterpreterReferenceExtern.repeat.t.sol +++ b/test/src/concrete/RainterpreterReferenceExtern.repeat.t.sol @@ -6,7 +6,8 @@ import {OpTest} from "test/abstract/OpTest.sol"; import { RainterpreterReferenceExtern, StackItem, - InvalidRepeatCount + InvalidRepeatCount, + UnconsumedRepeatDispatchBytes } from "src/concrete/extern/RainterpreterReferenceExtern.sol"; import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; @@ -57,4 +58,14 @@ contract RainterpreterReferenceExternRepeatTest is OpTest { bytes memory bytecode = I_DEPLOYER.parse2(bytes(string.concat(baseStr, "_: [ref-extern-repeat-10 abc];"))); (bytecode); } + + /// Trailing bytes after the decimal digit must revert. + function testRainterpreterReferenceExternRepeatTrailingBytes() external { + RainterpreterReferenceExtern extern = new RainterpreterReferenceExtern(); + string memory baseStr = string.concat("using-words-from ", address(extern).toHexString(), " "); + + vm.expectRevert(abi.encodeWithSelector(UnconsumedRepeatDispatchBytes.selector)); + bytes memory bytecode = I_DEPLOYER.parse2(bytes(string.concat(baseStr, "_: [ref-extern-repeat-5x abc];"))); + (bytecode); + } } diff --git a/test/src/concrete/RainterpreterReferenceExtern.subParserIndexOutOfBounds.t.sol b/test/src/concrete/RainterpreterReferenceExtern.subParserIndexOutOfBounds.t.sol index 18eaa2ad9..f993bae62 100644 --- a/test/src/concrete/RainterpreterReferenceExtern.subParserIndexOutOfBounds.t.sol +++ b/test/src/concrete/RainterpreterReferenceExtern.subParserIndexOutOfBounds.t.sol @@ -16,7 +16,7 @@ contract MockExternBadLiteralIndex is RainterpreterReferenceExtern { } /// @title RainterpreterReferenceExternSubParserIndexOutOfBoundsTest -/// @notice A49-3: Test that `SubParserIndexOutOfBounds` reverts when an +/// @notice Test that `SubParserIndexOutOfBounds` reverts when an /// out-of-bounds index is returned from the literal dispatch lookup. contract RainterpreterReferenceExternSubParserIndexOutOfBoundsTest is Test { /// @notice Calling subParseLiteral2 on a mock that returns an out-of-bounds diff --git a/test/src/concrete/RainterpreterStore.getUninitialized.t.sol b/test/src/concrete/RainterpreterStore.getUninitialized.t.sol index c8f01456d..88320e868 100644 --- a/test/src/concrete/RainterpreterStore.getUninitialized.t.sol +++ b/test/src/concrete/RainterpreterStore.getUninitialized.t.sol @@ -11,7 +11,7 @@ import { } from "rain.interpreter.interface/lib/ns/LibNamespace.sol"; /// @title RainterpreterStoreGetUninitializedTest -/// @notice A50-4: Test that `get()` returns `bytes32(0)` for a key that has +/// @notice Test that `get()` returns `bytes32(0)` for a key that has /// never been set. contract RainterpreterStoreGetUninitializedTest is Test { using LibNamespace for StateNamespace; diff --git a/test/src/concrete/RainterpreterStore.overwriteKey.t.sol b/test/src/concrete/RainterpreterStore.overwriteKey.t.sol index 17b4c0f7d..9d4283dab 100644 --- a/test/src/concrete/RainterpreterStore.overwriteKey.t.sol +++ b/test/src/concrete/RainterpreterStore.overwriteKey.t.sol @@ -11,7 +11,7 @@ import { } from "rain.interpreter.interface/lib/ns/LibNamespace.sol"; /// @title RainterpreterStoreOverwriteKeyTest -/// @notice A50-5: Test that a key appearing twice in a single `kvs` array +/// @notice Test that a key appearing twice in a single `kvs` array /// results in the last value winning. contract RainterpreterStoreOverwriteKeyTest is Test { using LibNamespace for StateNamespace; diff --git a/test/src/concrete/RainterpreterStore.setEmpty.t.sol b/test/src/concrete/RainterpreterStore.setEmpty.t.sol index e176f4abf..01471cb7d 100644 --- a/test/src/concrete/RainterpreterStore.setEmpty.t.sol +++ b/test/src/concrete/RainterpreterStore.setEmpty.t.sol @@ -7,7 +7,7 @@ import {RainterpreterStore} from "src/concrete/RainterpreterStore.sol"; import {StateNamespace} from "rain.interpreter.interface/lib/ns/LibNamespace.sol"; /// @title RainterpreterStoreSetEmptyArrayTest -/// @notice A50-3: Test that `set()` with an empty (zero-length) `kvs` array +/// @notice Test that `set()` with an empty (zero-length) `kvs` array /// succeeds without reverting. contract RainterpreterStoreSetEmptyArrayTest is Test { /// @notice set() with a zero-length kvs array must not revert. diff --git a/test/src/concrete/RainterpreterStore.setEvent.t.sol b/test/src/concrete/RainterpreterStore.setEvent.t.sol index ec1e88a27..fb127dc85 100644 --- a/test/src/concrete/RainterpreterStore.setEvent.t.sol +++ b/test/src/concrete/RainterpreterStore.setEvent.t.sol @@ -12,7 +12,7 @@ import { import {IInterpreterStoreV3} from "rain.interpreter.interface/interface/IInterpreterStoreV3.sol"; /// @title RainterpreterStoreSetEventTest -/// @notice A50-2: Test that the `Set` event is emitted correctly for every +/// @notice Test that the `Set` event is emitted correctly for every /// key-value pair stored via `set()`. contract RainterpreterStoreSetEventTest is Test { using LibNamespace for StateNamespace; diff --git a/test/src/lib/op/math/LibOpSub.t.sol b/test/src/lib/op/math/LibOpSub.t.sol index 0141f99d0..ddc82c8ed 100644 --- a/test/src/lib/op/math/LibOpSub.t.sol +++ b/test/src/lib/op/math/LibOpSub.t.sol @@ -3,7 +3,7 @@ pragma solidity =0.8.25; import {OpTest, IntegrityCheckState, OperandV2} from "test/abstract/OpTest.sol"; -import {UnexpectedOperandValue} from "src/error/ErrParse.sol"; +import {UnexpectedOperandValue, UnexpectedOperand} from "src/error/ErrParse.sol"; import {LibOpSub} from "src/lib/op/math/LibOpSub.sol"; import {LibOperand} from "test/lib/operand/LibOperand.sol"; import {StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -115,9 +115,12 @@ contract LibOpSubTest is OpTest { checkBadOutputs("_ _: sub(1 1);", 2, 1, 2); } - function testOpSubEvalTwoOperandsDisallowed() external { - checkUnhappyParse("_: sub<0 0>(1 1);", abi.encodeWithSelector(UnexpectedOperandValue.selector)); - checkUnhappyParse("_: sub<0 1>(1 1);", abi.encodeWithSelector(UnexpectedOperandValue.selector)); - checkUnhappyParse("_: sub<1 0>(1 1);", abi.encodeWithSelector(UnexpectedOperandValue.selector)); + function testOpSubEvalOperandDisallowed() external { + checkDisallowedOperand("_: sub<0>(1 1);"); + checkDisallowedOperand("_: sub<1>(1 1);"); + checkDisallowedOperand("_: sub<2>(1 1);"); + checkDisallowedOperand("_: sub<0 0>(1 1);"); + checkDisallowedOperand("_: sub<0 1>(1 1);"); + checkDisallowedOperand("_: sub<1 0>(1 1);"); } } diff --git a/test/src/lib/parse/LibParse.lhsOverflow.t.sol b/test/src/lib/parse/LibParse.lhsOverflow.t.sol new file mode 100644 index 000000000..acdc515ac --- /dev/null +++ b/test/src/lib/parse/LibParse.lhsOverflow.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {RainterpreterParser} from "src/concrete/RainterpreterParser.sol"; +import {LHSItemCountOverflow} from "src/error/ErrParse.sol"; + +/// @title LibParseLHSOverflowTest +/// @notice A single-byte counter in lineTracker and topLevel1 tracks LHS item +/// counts. If more than 255 items are parsed, the increment carries into the +/// adjacent byte, silently corrupting parser state. This test verifies that +/// the parser reverts before the carry-over can occur. +contract LibParseLHSOverflowTest is Test { + RainterpreterParser internal parser; + + function setUp() external { + parser = new RainterpreterParser(); + } + + /// External wrapper so vm.expectRevert works. + function externalUnsafeParse(bytes memory data) external view returns (bytes memory, bytes32[] memory) { + return parser.unsafeParse(data); + } + + /// 256 anonymous LHS items on a single line overflows both lineTracker + /// (per-line counter) and topLevel1 (per-source counter). The parser + /// must revert with LHSItemCountOverflow, not silently corrupt state. + function testLHSItemCountOverflow256() external { + // Build "_ _ _ ... (256 times) _ :;" + bytes memory data = new bytes(256 * 2 + 2); + for (uint256 i = 0; i < 256; i++) { + data[i * 2] = "_"; + data[i * 2 + 1] = " "; + } + data[512] = ":"; + data[513] = ";"; + + // The call must revert with LHSItemCountOverflow specifically. + // Before the fix, it reverts with ExcessRHSItems (wrong error from + // corruption), causing this assertion to fail. + try this.externalUnsafeParse(data) { + revert("expected revert, got success"); + } catch (bytes memory reason) { + bytes4 selector; + assembly ("memory-safe") { + selector := mload(add(reason, 0x20)) + } + assertEq(selector, LHSItemCountOverflow.selector, "must revert with LHSItemCountOverflow"); + } + } + + /// 255 anonymous LHS items is the maximum valid count — must not revert + /// with the overflow error. + function testLHSItemCount255() external view { + // Build "_ _ _ ... (255 times) _ :;" + bytes memory data = new bytes(255 * 2 + 2); + for (uint256 i = 0; i < 255; i++) { + data[i * 2] = "_"; + data[i * 2 + 1] = " "; + } + data[510] = ":"; + data[511] = ";"; + + // This should not revert with LHSItemCountOverflow. + // It may revert with a different error (e.g. too many top-level items + // for the 62-slot limit), but NOT the byte overflow error. + try this.externalUnsafeParse(data) {} + catch (bytes memory reason) { + bytes4 selector; + assembly ("memory-safe") { + selector := mload(add(reason, 0x20)) + } + assertTrue(selector != LHSItemCountOverflow.selector, "255 items must not trigger overflow"); + } + } +} diff --git a/test/src/lib/parse/LibParsePragma.keyword.t.sol b/test/src/lib/parse/LibParsePragma.keyword.t.sol index 719c74cef..7fe93a5b5 100644 --- a/test/src/lib/parse/LibParsePragma.keyword.t.sol +++ b/test/src/lib/parse/LibParsePragma.keyword.t.sol @@ -279,6 +279,44 @@ contract LibParsePragmaKeywordTest is Test { checkPragmaParsing(str, 118, values, "comment between addresses"); } + /// After parseInterstitial advances cursor to end, tryParseLiteral + /// reads past bounds via mload(cursor). If adjacent memory contains bytes + /// that look like a valid literal head (e.g., a digit), garbage is parsed + /// and pushed as a sub-parser address. + /// + /// To reproduce: include poison bytes inside the buffer but set end before + /// them. parseInterstitial consumes the trailing space to reach end, then + /// tryParseLiteral reads the poison digit '1' at end and dispatches to the + /// decimal parser, which pushes a garbage sub-parser. + function testParsePragmaOOBAfterInterstitial() external view { + // Data includes the real pragma + poison bytes ("1 ") past where end + // will point. The pragma is 60 bytes: "using-words-from " (17) + + // 42-char hex address + " " (1 trailing space). + bytes memory data = bytes("using-words-from 0x1234567890123456789012345678901234567890 1 "); + + ParseState memory state = + LibParseState.newState(data, "", "", LibAllStandardOps.literalParserFunctionPointers()); + state.literalParsers = LibAllStandardOps.literalParserFunctionPointers(); + + uint256 cursor = Pointer.unwrap(data.dataPointer()); + // Set end to 60: just past the trailing space after the address. + // The "1 " at positions 60-61 is our poison — in memory but past end. + uint256 end = cursor + 60; + + uint256 cursorAfter = state.parsePragma(cursor, end); + (cursorAfter); + + // Verify exactly one sub-parser was pushed (the real address). + bytes32 deref = state.subParsers; + assertEq(uint160(uint256(deref)), uint160(0x1234567890123456789012345678901234567890), "real address"); + // The linked list must terminate: no garbage sub-parser was pushed. + uint256 pointer = uint256(deref) >> 0xF0; + assembly ("memory-safe") { + deref := mload(pointer) + } + assertEq(uint256(deref), 0, "should have only one sub-parser, not garbage from OOB read"); + } + /// Test a specific string. function testPragmaKeywordParseSubParserSpecificStrings() external view { string memory str = diff --git a/test/src/lib/parse/LibParseState.endSourceTotalOpsOverflow.t.sol b/test/src/lib/parse/LibParseState.endSourceTotalOpsOverflow.t.sol new file mode 100644 index 000000000..3016740eb --- /dev/null +++ b/test/src/lib/parse/LibParseState.endSourceTotalOpsOverflow.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ParseTest} from "test/abstract/ParseTest.sol"; +import {LibMetaFixture} from "test/lib/parse/LibMetaFixture.sol"; +import {LibParse} from "src/lib/parse/LibParse.sol"; +import {ParseState} from "src/lib/parse/LibParseState.sol"; +import {SourceTotalOpsOverflow} from "src/error/ErrParse.sol"; + +/// @title LibParseStateEndSourceTotalOpsOverflowTest +/// @notice Tests that endSource reverts when the total ops count across all +/// top-level items in a single source exceeds the 255 limit of the prefix byte. +contract LibParseStateEndSourceTotalOpsOverflowTest is ParseTest { + using LibParse for ParseState; + + /// Builds a balanced binary tree expression with 2^(depth+1)-1 total ops. + /// Uses the zero-input word `a()` at the leaves. + function buildTree(uint256 depth) internal pure returns (bytes memory) { + bytes memory s = "a()"; + for (uint256 i = 0; i < depth; i++) { + s = bytes.concat("a(", s, " ", s, ")"); + } + return s; + } + + /// 254 total ops across two items must NOT overflow. + /// Two items: 127 ops + 127 ops = 254 total. + function testTotalOps254NoOverflow() external view { + bytes memory tree127 = buildTree(6); + string memory s = string(bytes.concat("_: ", tree127, ",\n_: ", tree127, ";")); + LibMetaFixture.newState(s).parse(); + } + + /// 256 total ops across two items MUST overflow. + /// Two items: 128 ops + 128 ops = 256 total. + function testTotalOpsOverflow256() external { + bytes memory tree128 = bytes.concat("a(", buildTree(6), " a())"); + string memory s = string(bytes.concat("_: ", tree128, ",\n_: ", tree128, ";")); + vm.expectRevert(abi.encodeWithSelector(SourceTotalOpsOverflow.selector)); + this.parseExternal(s); + } + + /// 510 total ops across two items MUST overflow. + /// Two items: 255 ops + 255 ops = 510 total. + function testTotalOpsOverflow510() external { + bytes memory tree = buildTree(7); + string memory s = string(bytes.concat("_: ", tree, ",\n_: ", tree, ";")); + vm.expectRevert(abi.encodeWithSelector(SourceTotalOpsOverflow.selector)); + this.parseExternal(s); + } +} diff --git a/test/src/lib/parse/LibSubParse.constantAccumulation.t.sol b/test/src/lib/parse/LibSubParse.constantAccumulation.t.sol index 4dde006d9..f6022de19 100644 --- a/test/src/lib/parse/LibSubParse.constantAccumulation.t.sol +++ b/test/src/lib/parse/LibSubParse.constantAccumulation.t.sol @@ -92,9 +92,8 @@ contract MultiConstantSubParser is ISubParserV4, IERC165 { /// @title LibSubParseConstantAccumulationTest /// @notice Tests that constants returned by sub parsers during word resolution /// are correctly accumulated into the final constants array at the right -/// indices. This addresses finding A44-8: the existing `badSubParserResult` -/// test returns empty constants arrays, so constant accumulation from sub -/// parsers was never verified. +/// indices. The existing `badSubParserResult` test returns empty constants +/// arrays, so constant accumulation from sub parsers was never verified. contract LibSubParseConstantAccumulationTest is Test { using LibParseState for ParseState; using LibParse for ParseState; diff --git a/test/src/lib/parse/LibSubParse.subParseLiteral.t.sol b/test/src/lib/parse/LibSubParse.subParseLiteral.t.sol index 2ecc07a66..369a623f1 100644 --- a/test/src/lib/parse/LibSubParse.subParseLiteral.t.sol +++ b/test/src/lib/parse/LibSubParse.subParseLiteral.t.sol @@ -9,6 +9,7 @@ import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {LibBytes} from "rain.solmem/lib/LibBytes.sol"; import {ISubParserV4} from "rain.interpreter.interface/interface/ISubParserV4.sol"; import {UnsupportedLiteralType} from "src/error/ErrParse.sol"; +import {SubParseLiteralDispatchLengthOverflow} from "src/error/ErrSubParse.sol"; /// @title LibSubParseSubParseLiteralTest /// @notice Direct unit tests for `LibSubParse.subParseLiteral`. @@ -163,6 +164,28 @@ contract LibSubParseSubParseLiteralTest is Test { assertEq(result, expectedValue); } + /// Dispatch region exceeding 0xFFFF bytes causes silent truncation of + /// the 2-byte encoded length. The sub-parser receives corrupted data + /// where the dispatch/body boundary is wrong. + function testSubParseLiteralDispatchLengthOverflow() external { + address subParser = makeAddr("subParser"); + // Create a dispatch of 0x10001 bytes — one byte past the 16-bit max. + bytes memory dispatch = new bytes(0x10001); + bytes memory body = bytes(""); + + vm.mockCall( + subParser, + abi.encodeWithSelector(ISubParserV4.subParseLiteral2.selector), + abi.encode(true, bytes32(uint256(1))) + ); + + address[] memory subs = new address[](1); + subs[0] = subParser; + + vm.expectRevert(abi.encodeWithSelector(SubParseLiteralDispatchLengthOverflow.selector, 0x10001)); + this.externalSubParseLiteral(dispatch, body, subs); + } + /// @notice First sub parser accepts, second is never called. function testSubParseLiteralFirstAcceptsSecondNotCalled() external { address first = makeAddr("first"); diff --git a/test/src/lib/parse/literal/LibParseLiteral.dispatch.t.sol b/test/src/lib/parse/literal/LibParseLiteral.dispatch.t.sol index 6d1f279f9..ad555188f 100644 --- a/test/src/lib/parse/literal/LibParseLiteral.dispatch.t.sol +++ b/test/src/lib/parse/literal/LibParseLiteral.dispatch.t.sol @@ -6,6 +6,7 @@ import {Test} from "forge-std/Test.sol"; import {LibBytes, Pointer} from "rain.solmem/lib/LibBytes.sol"; import {LibParseState, ParseState} from "src/lib/parse/LibParseState.sol"; import {LibParseLiteral, UnsupportedLiteralType} from "src/lib/parse/literal/LibParseLiteral.sol"; +import {UppercaseHexPrefix} from "src/error/ErrParse.sol"; import {LibAllStandardOps} from "src/lib/op/LibAllStandardOps.sol"; import {LibDecimalFloat, Float} from "rain.math.float/lib/LibDecimalFloat.sol"; import {ISubParserV4} from "rain.interpreter.interface/interface/ISubParserV4.sol"; @@ -108,12 +109,11 @@ contract LibParseLiteralDispatchTest is Test { assertEq(value, bytes32(0), "zero value"); } - /// Uppercase '0X' does NOT route to hex — only lowercase '0x' does. - /// '0X' routes to decimal and parses '0', leaving 'X' for the next token. - function testTryParseLiteralUppercaseXNotHex() external view { - (bool success,, bytes32 value) = this.externalTryParseLiteral(bytes("0X ")); - assertTrue(success, "0X dispatches as decimal"); - assertEq(value, bytes32(0), "0X value is 0"); + /// Uppercase '0X' must revert with UppercaseHexPrefix rather than + /// silently parsing as decimal 0. + function testTryParseLiteralUppercaseXReverts() external { + vm.expectRevert(abi.encodeWithSelector(UppercaseHexPrefix.selector, 0)); + this.externalParseLiteral(bytes("0X ")); } /// Decimal literal returns correct float-encoded value. @@ -243,4 +243,30 @@ contract LibParseLiteralDispatchTest is Test { vm.expectRevert(abi.encodeWithSelector(UnsupportedLiteralType.selector, 0)); this.externalParseLiteral(bytes("@ ")); } + + /// A single "0" at the end of the source with 0x78 ('x') as the + /// next byte in memory must route to decimal, not hex. Without a bounds + /// check the parser reads past end and sees "0x", incorrectly dispatching + /// to the hex parser which reverts with ZeroLengthHexLiteral. + function testTryParseLiteralOOBSecondBytePoison() external { + // Allocate data "0" then immediately write 0x78 ('x') right after + // the data in memory. end = cursor+1 (just the "0" byte), but + // mload(cursor) reads 32 bytes so byte(1, word) picks up the 'x'. + bytes memory data = bytes("0"); + uint256 cursor; + uint256 end; + assembly ("memory-safe") { + cursor := add(data, 0x20) + end := add(cursor, mload(data)) + // Write 'x' immediately after the data content. + mstore8(end, 0x78) + } + ParseState memory state = + LibParseState.newState(data, "", "", LibAllStandardOps.literalParserFunctionPointers()); + state.literalParsers = LibAllStandardOps.literalParserFunctionPointers(); + // If the bug exists, tryParseLiteral dispatches to hex and reverts. + (bool success,, bytes32 value) = state.tryParseLiteral(cursor, end); + assertTrue(success, "single 0 with poison x must parse as decimal"); + assertEq(value, bytes32(0), "single 0 value"); + } } From 1047e48faa4eecfdf236bd34e2adb3eac000c0f6 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Tue, 3 Mar 2026 22:36:19 +0400 Subject: [PATCH 03/13] Audit triage: fix Pass 2 findings and add Pass 5 Pass 2 fixes: - P2-EI-1: Add eval4 InputsLengthMismatch library-level test, rename eval2->eval4 - P2-EI-2: Add integrityCheck2 BadOpInputsLength/BadOpOutputsLength tests - P2-EI-3: Add evalLoop remainder-only path test (7 fuzzed constants) - P2-CC-01: Fix Rainterpreter.supportsInterface to include IOpcodeToolingV1 - P2-CC-02: Add RainterpreterExpressionDeployer pointer consistency test - P2-CC-03: Add same-sender StateNamespace isolation test - A30-P2-2: Document ParserOutOfBounds as unreachable defensive guard - A30-P2-3: Raise testEndSourceByteLengthFuzz upper bound from 50 to 255 - A32-2: Add fuzz test for skipComment with arbitrary body content - A42-1: Add pushInputs push-overflow test - A44-1-P2: Add subParseWords no-sub-parsers-registered test - P2-EAD-02: Verify all 72 authoringMetaV2 word names Pass 5 (new): - A01: Math opcodes correctness review (31 opcodes verified, 3 INFO) Co-Authored-By: Claude Opus 4.6 --- audit/2026-03-01-01/pass5/LibOpMath.md | 76 ++++++++++ audit/2026-03-01-01/triage.md | 28 ++-- src/concrete/Rainterpreter.sol | 5 +- src/error/ErrParse.sol | 6 +- src/lib/eval/LibEval.sol | 4 +- src/lib/state/LibInterpreterState.sol | 2 +- test/src/concrete/Rainterpreter.ierc165.t.sol | 3 + ...terpreterExpressionDeployer.pointers.t.sol | 18 +++ ...ainterpreterStore.namespaceIsolation.t.sol | 35 +++++ test/src/lib/eval/LibEval.fBounds.t.sol | 4 +- .../eval/LibEval.inputsLengthMismatch.t.sol | 94 ++++++++++++ test/src/lib/eval/LibEval.maxOutputs.t.sol | 4 +- test/src/lib/eval/LibEval.remainderOnly.t.sol | 77 ++++++++++ .../integrity/LibIntegrityCheck.badOpIO.t.sol | 72 +++++++++ .../src/lib/integrity/LibIntegrityCheck.t.sol | 1 + test/src/lib/op/LibAllStandardOps.t.sol | 141 +++++++++++++++++- test/src/lib/parse/LibParseInterstitial.t.sol | 28 ++++ test/src/lib/parse/LibParseStackTracker.t.sol | 10 ++ .../lib/parse/LibParseState.endSource.t.sol | 2 +- .../lib/parse/LibSubParse.subParseWords.t.sol | 21 +++ 20 files changed, 602 insertions(+), 29 deletions(-) create mode 100644 audit/2026-03-01-01/pass5/LibOpMath.md create mode 100644 test/src/concrete/RainterpreterExpressionDeployer.pointers.t.sol create mode 100644 test/src/lib/eval/LibEval.inputsLengthMismatch.t.sol create mode 100644 test/src/lib/eval/LibEval.remainderOnly.t.sol create mode 100644 test/src/lib/integrity/LibIntegrityCheck.badOpIO.t.sol diff --git a/audit/2026-03-01-01/pass5/LibOpMath.md b/audit/2026-03-01-01/pass5/LibOpMath.md new file mode 100644 index 000000000..ffe009cdf --- /dev/null +++ b/audit/2026-03-01-01/pass5/LibOpMath.md @@ -0,0 +1,76 @@ +# Pass 5: Correctness / Intent Verification -- Math Opcodes + +**Audit agent**: A01 +**Date**: 2026-03-03 + +Reviewed all 31 math opcode source files in `src/lib/op/math/` and their corresponding test files in `test/src/lib/op/math/`. + +## Methodology + +For each opcode library, verified: +1. The `run` function implements the mathematical operation its name claims. +2. The `referenceFn` matches the `run` behavior (same math library calls, same logic). +3. The `integrity` function correctly declares (inputs, outputs) matching what `run` actually consumes/produces. +4. Constants and formulas used are mathematically correct. +5. NatSpec matches actual behavior. +6. Test assertions verify the correct mathematical relationship. + +All float opcodes use `rain.math.float/lib/LibDecimalFloat.sol` and `LibDecimalFloatImplementation` for decimal floating-point arithmetic. The uint256 opcodes use native Solidity arithmetic with overflow checks. + +## Summary of Findings + +| ID | Severity | File(s) | Description | +|----|----------|---------|-------------| +| P5-HEADROOM-01 | Informational | `src/lib/op/math/LibOpHeadroom.sol` | Headroom semantics for negative non-integers may be surprising to users (returns distance-to-ceiling, not distance-to-nearest-integer-toward-zero). Implementation is correct per NatSpec. | +| P5-EXPGROWTH-01 | Informational | `src/lib/op/math/growth/LibOpExponentialGrowth.sol`, `src/lib/op/math/growth/LibOpLinearGrowth.sol`, `src/lib/op/math/uint256/LibOpMaxUint256.sol` | `integrity` function NatSpec uses inline code style without explicit `@notice` tag. Per project convention, when other tags are present in the same library block, all entries should be explicitly tagged. | +| P5-UINT256POW-01 | Informational | `src/lib/op/math/uint256/LibOpUint256Pow.sol` | N-ary `uint256-power` applies left-to-right `((a**b)**c)` which is correct and tested but the NatSpec "raise x successively to N integers" could be clearer about associativity. | + +## Per-Opcode Review + +**All 31 opcodes verified correct.** For each: + +- **LibOpAbs**: `run` calls `a.abs()`. Integrity (1,1). Correct. +- **LibOpAdd**: `run` calls `LibDecimalFloatImplementation.add` in loop. Integrity (max(operand,2),1). Correct. +- **LibOpAvg**: `run` computes `a.add(b).div(FLOAT_TWO)`. Integrity (2,1). Correct. +- **LibOpCeil**: `run` calls `a.ceil()`. Integrity (1,1). Correct. +- **LibOpDiv**: `run` calls `LibDecimalFloatImplementation.div` in loop. Integrity (max(operand,2),1). Correct. +- **LibOpE**: `run` pushes `FLOAT_E` (Euler's number). Integrity (0,1). Correct. +- **LibOpExp**: `run` computes `FLOAT_E.pow(a, LOG_TABLES_ADDRESS)` = e^a. Integrity (1,1). Correct. +- **LibOpExp2**: `run` computes `FLOAT_TWO.pow(a, LOG_TABLES_ADDRESS)` = 2^a. Integrity (1,1). Correct. +- **LibOpFloor**: `run` calls `a.floor()`. Integrity (1,1). Correct. +- **LibOpFrac**: `run` calls `a.frac()`. Integrity (1,1). Correct. +- **LibOpGm**: `run` computes `sign * sqrt(|a| * |b|)` via `a.abs().mul(b.abs()).pow(FLOAT_HALF, ...)`. Integrity (2,1). Correct. +- **LibOpHeadroom**: `run` computes `ceil(x) - x`, returns 1 if zero. Integrity (1,1). Correct per NatSpec. See P5-HEADROOM-01. +- **LibOpInv**: `run` calls `a.inv()` = 1/x. Integrity (1,1). Correct. +- **LibOpMax**: `run` calls `a.max(b)` in loop. Integrity (max(operand,2),1). Correct. +- **LibOpMin**: `run` calls `a.min(b)` in loop. Integrity (max(operand,2),1). Correct. +- **LibOpMul**: `run` calls `LibDecimalFloatImplementation.mul` in loop. Integrity (max(operand,2),1). Correct. +- **LibOpPow**: `run` computes `a.pow(b, LOG_TABLES_ADDRESS)`. Integrity (2,1). Correct. +- **LibOpSqrt**: `run` calls `a.sqrt(LOG_TABLES_ADDRESS)` = `a^0.5`. Integrity (1,1). Correct. +- **LibOpSub**: `run` calls `LibDecimalFloatImplementation.sub` in loop. Integrity (max(operand,2),1). Correct. +- **LibOpMaxNegativeValue**: `run` pushes `FLOAT_MAX_NEGATIVE_VALUE` = (-1, int32.min). Integrity (0,1). Correct. +- **LibOpMaxPositiveValue**: `run` pushes `FLOAT_MAX_POSITIVE_VALUE` = (int224.max, int32.max). Integrity (0,1). Correct. +- **LibOpMinNegativeValue**: `run` pushes `FLOAT_MIN_NEGATIVE_VALUE` = (int224.min, int32.max). Integrity (0,1). Correct. +- **LibOpMinPositiveValue**: `run` pushes `FLOAT_MIN_POSITIVE_VALUE` = (1, int32.min). Integrity (0,1). Correct. +- **LibOpExponentialGrowth**: `run` computes `base * (1 + rate)^t`. Integrity (3,1). Correct. See P5-EXPGROWTH-01. +- **LibOpLinearGrowth**: `run` computes `base + rate * t`. Integrity (3,1). Correct. See P5-EXPGROWTH-01. +- **LibOpMaxUint256**: `run` pushes `type(uint256).max`. Integrity (0,1). Correct. See P5-EXPGROWTH-01. +- **LibOpUint256Add**: `run` uses checked `+=`. Integrity (max(operand,2),1). Correct. +- **LibOpUint256Div**: `run` uses checked `/=`. Integrity (max(operand,2),1). Correct. +- **LibOpUint256Mul**: `run` uses checked `*=`. Integrity (max(operand,2),1). Correct. +- **LibOpUint256Pow**: `run` uses checked `**`. Integrity (max(operand,2),1). Left-to-right associativity. Correct. See P5-UINT256POW-01. +- **LibOpUint256Sub**: `run` uses checked `-=`. Integrity (max(operand,2),1). Correct. + +## Cross-Cutting Observations + +1. **All `run` / `referenceFn` pairs are consistent.** In every opcode, the `referenceFn` uses the same mathematical operations as `run`. + +2. **All `integrity` functions correctly declare inputs/outputs.** The N-ary opcodes read the input count from operand bits [16:20] and enforce a minimum of 2. + +3. **Float opcodes use `LibDecimalFloat` / `LibDecimalFloatImplementation` correctly.** The N-ary arithmetic opcodes unpack to coefficient/exponent, perform operations at extended precision, and repack with `packLossy`. None of the float opcodes use raw Solidity arithmetic on the Float type. + +4. **Uint256 opcodes use native Solidity checked arithmetic.** The `run` functions use checked operators while `referenceFn` functions use `unchecked` blocks, correctly enabling the test harness to detect overflow/underflow errors. + +5. **No PRBMath usage found.** The codebase uses `rain.math.float` for floating-point arithmetic, not PRBMath. The decimal float library uses a signed coefficient + exponent representation, not SD59x18/UD60x18 fixed-point. + +6. **Test coverage is thorough.** Every opcode has: integrity fuzz test, runtime fuzz test via `opReferenceCheck`, concrete eval examples, bad-input tests, bad-output tests, and operand-disallowed tests. diff --git a/audit/2026-03-01-01/triage.md b/audit/2026-03-01-01/triage.md index c3d423e98..d92e5c5eb 100644 --- a/audit/2026-03-01-01/triage.md +++ b/audit/2026-03-01-01/triage.md @@ -42,20 +42,20 @@ ## Pass 2: Test Coverage - [FIXED] A30-P2-1: (MEDIUM) No test for total source ops > 255 across multiple top-level items -- [PENDING] P2-EI-1: (LOW) eval2 InputsLengthMismatch not tested at library level -- [PENDING] P2-EI-2: (LOW) integrityCheck2 BadOpInputsLength/BadOpOutputsLength not directly tested -- [PENDING] P2-EI-3: (LOW) evalLoop remainder-only path (1-7 opcodes) not tested -- [PENDING] P2-CC-01: (LOW) Rainterpreter.supportsInterface omits IOpcodeToolingV1 -- [PENDING] P2-CC-02: (LOW) RainterpreterExpressionDeployer missing dedicated pointer consistency test -- [PENDING] P2-CC-03: (LOW) Missing direct test for StateNamespace isolation (same sender) -- [PENDING] A30-P2-2: (LOW) No test for ParserOutOfBounds error in parse() -- [PENDING] A30-P2-3: (LOW) testEndSourceByteLengthFuzz upper bound too low -- [PENDING] A32-1: (LOW) skipComment no test for UnclosedComment when well-formed but never closed -- [PENDING] A32-2: (LOW) skipComment no fuzz test for well-formed comments -- [PENDING] A42-1: (LOW) pushInputs no test for push-overflow-inside-pushInputs -- [PENDING] A44-1-P2: (LOW) subParseWordSlice no test for no-sub-parsers-registered path -- [PENDING] P2-EAD-01: (LOW) BaseRainterpreterSubParser.subParseWord2 missing happy-path and no-match tests -- [PENDING] P2-EAD-02: (LOW) authoringMetaV2 word names not verified beyond index 3 +- [FIXED] P2-EI-1: (LOW) eval4 InputsLengthMismatch not tested at library level (also renamed eval2 -> eval4 to match interface) +- [FIXED] P2-EI-2: (LOW) integrityCheck2 BadOpInputsLength/BadOpOutputsLength not directly tested +- [FIXED] P2-EI-3: (LOW) evalLoop remainder-only path (1-7 opcodes) not tested +- [FIXED] P2-CC-01: (LOW) Rainterpreter.supportsInterface omits IOpcodeToolingV1 +- [FIXED] P2-CC-02: (LOW) RainterpreterExpressionDeployer missing dedicated pointer consistency test +- [FIXED] P2-CC-03: (LOW) Missing direct test for StateNamespace isolation (same sender) +- [DOCUMENTED] A30-P2-2: (LOW) No test for ParserOutOfBounds error in parse() — unreachable defensive guard; documented in ErrParse.sol +- [FIXED] A30-P2-3: (LOW) testEndSourceByteLengthFuzz upper bound too low +- [DISMISSED] A32-1: (LOW) skipComment no test for UnclosedComment when well-formed but never closed — already tested by testParseCommentUnclosed in LibParse.comments.t.sol (line 445-449) +- [FIXED] A32-2: (LOW) skipComment no fuzz test for well-formed comments +- [FIXED] A42-1: (LOW) pushInputs no test for push-overflow-inside-pushInputs +- [FIXED] A44-1-P2: (LOW) subParseWordSlice no test for no-sub-parsers-registered path +- [DISMISSED] P2-EAD-01: (LOW) BaseRainterpreterSubParser.subParseWord2 missing happy-path and no-match tests — both paths tested in RainterpreterReferenceExtern.intInc.t.sol (lines 78-81 happy, 122-127 no-match) +- [FIXED] P2-EAD-02: (LOW) authoringMetaV2 word names not verified beyond index 3 - [PENDING] P2-01: (LOW) Missing operand-disallowed tests for 10 logic opcodes - [PENDING] P2-02: (LOW) Missing operand-disallowed tests for 5 math opcodes - [PENDING] P2-03: (LOW) Missing operand-disallowed test for LibOpHash diff --git a/src/concrete/Rainterpreter.sol b/src/concrete/Rainterpreter.sol index 71a9d9cce..dfae15d23 100644 --- a/src/concrete/Rainterpreter.sol +++ b/src/concrete/Rainterpreter.sol @@ -70,12 +70,13 @@ contract Rainterpreter is IInterpreterV4, IOpcodeToolingV1, ERC165 { } // We use the return by returning it. Slither false positive. //slither-disable-next-line unused-return - return state.eval2(eval.inputs, type(uint256).max); + return state.eval4(eval.inputs, type(uint256).max); } /// @inheritdoc ERC165 function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IInterpreterV4).interfaceId || super.supportsInterface(interfaceId); + return interfaceId == type(IInterpreterV4).interfaceId || interfaceId == type(IOpcodeToolingV1).interfaceId + || super.supportsInterface(interfaceId); } /// @inheritdoc IOpcodeToolingV1 diff --git a/src/error/ErrParse.sol b/src/error/ErrParse.sol index 93afc1ad8..fd5e62f9b 100644 --- a/src/error/ErrParse.sol +++ b/src/error/ErrParse.sol @@ -123,7 +123,11 @@ error MaxSources(); /// The parser encountered a dangling source. This is a bug in the parser. error DanglingSource(); -/// The parser moved past the end of the data. +/// The parser moved past the end of the data. Defensive guard only — all +/// sub-parsers (parseInterstitial, parseLHS, parseRHS) receive `end` and +/// respect it, so this condition is unreachable under normal operation. It +/// exists to catch internal sub-parser bugs that advance the cursor past +/// `end`. Cannot be tested without mocking a sub-parser to force overshoot. error ParserOutOfBounds(); /// The parser encountered a stack deeper than it can process in the memory diff --git a/src/lib/eval/LibEval.sol b/src/lib/eval/LibEval.sol index 15ea0de6b..55d3aa543 100644 --- a/src/lib/eval/LibEval.sol +++ b/src/lib/eval/LibEval.sol @@ -23,7 +23,7 @@ library LibEval { /// of 8. Emits a stack trace via `STACK_TRACER` after execution. /// /// TRUST: `state.sourceIndex` is NOT bounds-checked against the bytecode's - /// source count. All callers MUST validate it before calling. `eval2` does + /// source count. All callers MUST validate it before calling. `eval4` does /// this via `LibBytecode.sourceInputsOutputsLength` (which reverts with /// `SourceIndexOutOfBounds`). `LibOpCall.run` relies on integrity checks /// at deploy time to reject invalid source indices in operands. The @@ -188,7 +188,7 @@ library LibEval { /// @return The output stack items, truncated to `maxOutputs`. /// @return The state KV writes as a flat array of interleaved keys and /// values from the in-memory KV store. - function eval2(InterpreterState memory state, StackItem[] memory inputs, uint256 maxOutputs) + function eval4(InterpreterState memory state, StackItem[] memory inputs, uint256 maxOutputs) internal view returns (StackItem[] memory, bytes32[] memory) diff --git a/src/lib/state/LibInterpreterState.sol b/src/lib/state/LibInterpreterState.sol index 01acd4f5f..415d0b1cd 100644 --- a/src/lib/state/LibInterpreterState.sol +++ b/src/lib/state/LibInterpreterState.sol @@ -17,7 +17,7 @@ import {StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol address constant STACK_TRACER = address(uint160(uint256(keccak256("rain.interpreter.stack-tracer.0")))); /// @notice Runtime state threaded through the eval loop and all opcode -/// implementations. Built once per `eval2` call from deserialized +/// implementations. Built once per `eval4` call from deserialized /// bytecode, caller-provided context, and store configuration. /// @param stackBottoms Bottom pointer for each source's stack. The eval /// loop starts the stack top here and grows downward as values are pushed. diff --git a/test/src/concrete/Rainterpreter.ierc165.t.sol b/test/src/concrete/Rainterpreter.ierc165.t.sol index 679ac66b5..bf760ead5 100644 --- a/test/src/concrete/Rainterpreter.ierc165.t.sol +++ b/test/src/concrete/Rainterpreter.ierc165.t.sol @@ -7,16 +7,19 @@ import {Test} from "forge-std/Test.sol"; import {IERC165} from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; import {Rainterpreter} from "src/concrete/Rainterpreter.sol"; import {IInterpreterV4} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; +import {IOpcodeToolingV1} from "rain.sol.codegen/interface/IOpcodeToolingV1.sol"; contract RainterpreterIERC165Test is Test { /// Test that ERC165 is implemented for all interfaces. function testRainterpreterIERC165(bytes4 badInterfaceId) external { vm.assume(badInterfaceId != type(IERC165).interfaceId); vm.assume(badInterfaceId != type(IInterpreterV4).interfaceId); + vm.assume(badInterfaceId != type(IOpcodeToolingV1).interfaceId); Rainterpreter interpreter = new Rainterpreter(); assertTrue(interpreter.supportsInterface(type(IERC165).interfaceId)); assertTrue(interpreter.supportsInterface(type(IInterpreterV4).interfaceId)); + assertTrue(interpreter.supportsInterface(type(IOpcodeToolingV1).interfaceId)); assertFalse(interpreter.supportsInterface(badInterfaceId)); } diff --git a/test/src/concrete/RainterpreterExpressionDeployer.pointers.t.sol b/test/src/concrete/RainterpreterExpressionDeployer.pointers.t.sol new file mode 100644 index 000000000..0e5a7dd89 --- /dev/null +++ b/test/src/concrete/RainterpreterExpressionDeployer.pointers.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import { + RainterpreterExpressionDeployer, + INTEGRITY_FUNCTION_POINTERS +} from "src/concrete/RainterpreterExpressionDeployer.sol"; + +contract RainterpreterExpressionDeployerPointersTest is Test { + function testIntegrityFunctionPointers() external { + RainterpreterExpressionDeployer deployer = new RainterpreterExpressionDeployer(); + bytes memory expected = deployer.buildIntegrityFunctionPointers(); + bytes memory actual = INTEGRITY_FUNCTION_POINTERS; + assertEq(actual, expected); + } +} diff --git a/test/src/concrete/RainterpreterStore.namespaceIsolation.t.sol b/test/src/concrete/RainterpreterStore.namespaceIsolation.t.sol index 3a58efc17..8e0097ab6 100644 --- a/test/src/concrete/RainterpreterStore.namespaceIsolation.t.sol +++ b/test/src/concrete/RainterpreterStore.namespaceIsolation.t.sol @@ -46,6 +46,41 @@ contract RainterpreterStoreNamespaceIsolationTest is Test { assertEq(store.get(fqnB, key), bytes32(0)); } + /// Same sender, different StateNamespace values — data must be isolated + /// per namespace. + function testNamespaceIsolationSameSender( + address sender, + uint256 nsSeedA, + uint256 nsSeedB, + bytes32 key, + bytes32 valueA, + bytes32 valueB + ) external { + vm.assume(nsSeedA != nsSeedB); + vm.assume(valueA != valueB); + + StateNamespace nsA = StateNamespace.wrap(nsSeedA); + StateNamespace nsB = StateNamespace.wrap(nsSeedB); + RainterpreterStore store = new RainterpreterStore(); + + bytes32[] memory kvs = new bytes32[](2); + kvs[0] = key; + + kvs[1] = valueA; + vm.prank(sender); + store.set(nsA, kvs); + + kvs[1] = valueB; + vm.prank(sender); + store.set(nsB, kvs); + + FullyQualifiedNamespace fqnA = nsA.qualifyNamespace(sender); + FullyQualifiedNamespace fqnB = nsB.qualifyNamespace(sender); + + assertEq(store.get(fqnA, key), valueA); + assertEq(store.get(fqnB, key), valueB); + } + /// Both A and B write different values to the same key. Each must see /// only their own value. function testNamespaceIsolationBidirectional( diff --git a/test/src/lib/eval/LibEval.fBounds.t.sol b/test/src/lib/eval/LibEval.fBounds.t.sol index c5e036396..248ec87f7 100644 --- a/test/src/lib/eval/LibEval.fBounds.t.sol +++ b/test/src/lib/eval/LibEval.fBounds.t.sol @@ -128,7 +128,7 @@ contract LibEvalFBoundsTest is Test { fs ); - (StackItem[] memory outputs, bytes32[] memory kvs) = LibEval.eval2(state, new StackItem[](0), type(uint256).max); + (StackItem[] memory outputs, bytes32[] memory kvs) = LibEval.eval4(state, new StackItem[](0), type(uint256).max); assertEq(outputs.length, expectedLength); for (uint256 i = 0; i < outputs.length; i++) { assertEq(StackItem.unwrap(outputs[i]), c); @@ -140,7 +140,7 @@ contract LibEvalFBoundsTest is Test { bytecode[i] = bytes1(uint8(uint8(fs.length / 2) + 1)); } - (outputs, kvs) = LibEval.eval2(state, new StackItem[](0), type(uint256).max); + (outputs, kvs) = LibEval.eval4(state, new StackItem[](0), type(uint256).max); assertEq(outputs.length, expectedLength); for (uint256 i = 0; i < outputs.length; i++) { assertEq(StackItem.unwrap(outputs[i]), c); diff --git a/test/src/lib/eval/LibEval.inputsLengthMismatch.t.sol b/test/src/lib/eval/LibEval.inputsLengthMismatch.t.sol new file mode 100644 index 000000000..7343bfa0d --- /dev/null +++ b/test/src/lib/eval/LibEval.inputsLengthMismatch.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {LibInterpreterState, InterpreterState} from "src/lib/state/LibInterpreterState.sol"; +import {LibAllStandardOps} from "src/lib/op/LibAllStandardOps.sol"; +import {LibEval} from "src/lib/eval/LibEval.sol"; +import {MemoryKV} from "rain.lib.memkv/lib/LibMemoryKV.sol"; +import { + IInterpreterStoreV3, + FullyQualifiedNamespace +} from "rain.interpreter.interface/interface/IInterpreterStoreV3.sol"; +import {StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; +import {InputsLengthMismatch} from "src/error/ErrEval.sol"; + +/// @title LibEvalInputsLengthMismatchTest +/// @notice Direct library-level tests for the InputsLengthMismatch revert in +/// LibEval.eval4, using hand-built bytecode and InterpreterState. +contract LibEvalInputsLengthMismatchTest is Test { + /// External wrapper so vm.expectRevert works with the library call. + function externalEval4( + InterpreterState memory state, + StackItem[] memory inputs, + uint256 maxOutputs + ) external view returns (StackItem[] memory, bytes32[] memory) { + return LibEval.eval4(state, inputs, maxOutputs); + } + + /// Build an InterpreterState with a single source that expects + /// `sourceInputs` inputs and has 0 ops / 0 outputs. + function buildState(uint8 sourceInputs) internal view returns (InterpreterState memory) { + bytes memory fs = LibAllStandardOps.opcodeFunctionPointers(); + + // Bytecode: 1 source, 0 offset, 0 ops, sourceInputs stack allocation, + // sourceInputs inputs, 0 outputs. + bytes memory bytecode = abi.encodePacked( + uint8(1), uint16(0), uint8(0), sourceInputs, sourceInputs, uint8(0) + ); + + StackItem[][] memory stacks = new StackItem[][](1); + stacks[0] = new StackItem[](sourceInputs); + + return InterpreterState( + LibInterpreterState.stackBottoms(stacks), + new bytes32[](0), + 0, + MemoryKV.wrap(0), + FullyQualifiedNamespace.wrap(0), + IInterpreterStoreV3(address(0)), + new bytes32[][](0), + bytecode, + fs + ); + } + + /// Passing fewer inputs than the source expects must revert. + function testEval4InputsTooFew() external { + InterpreterState memory state = buildState(2); + StackItem[] memory inputs = new StackItem[](1); + + vm.expectRevert(abi.encodeWithSelector(InputsLengthMismatch.selector, 2, 1)); + this.externalEval4(state, inputs, type(uint256).max); + } + + /// Passing more inputs than the source expects must revert. + function testEval4InputsTooMany() external { + InterpreterState memory state = buildState(1); + StackItem[] memory inputs = new StackItem[](2); + + vm.expectRevert(abi.encodeWithSelector(InputsLengthMismatch.selector, 1, 2)); + this.externalEval4(state, inputs, type(uint256).max); + } + + /// Passing zero inputs when the source expects some must revert. + function testEval4InputsZeroWhenExpected() external { + InterpreterState memory state = buildState(3); + StackItem[] memory inputs = new StackItem[](0); + + vm.expectRevert(abi.encodeWithSelector(InputsLengthMismatch.selector, 3, 0)); + this.externalEval4(state, inputs, type(uint256).max); + } + + /// Passing the correct number of inputs must not revert. + function testEval4InputsMatch() external view { + InterpreterState memory state = buildState(2); + StackItem[] memory inputs = new StackItem[](2); + inputs[0] = StackItem.wrap(bytes32(uint256(1))); + inputs[1] = StackItem.wrap(bytes32(uint256(2))); + + this.externalEval4(state, inputs, type(uint256).max); + } +} diff --git a/test/src/lib/eval/LibEval.maxOutputs.t.sol b/test/src/lib/eval/LibEval.maxOutputs.t.sol index 07da01912..8b595c7bc 100644 --- a/test/src/lib/eval/LibEval.maxOutputs.t.sol +++ b/test/src/lib/eval/LibEval.maxOutputs.t.sol @@ -16,7 +16,7 @@ import { import {StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; /// @title LibEvalMaxOutputsTest -/// @notice Tests that eval2 truncates outputs when maxOutputs < sourceOutputs. +/// @notice Tests that eval4 truncates outputs when maxOutputs < sourceOutputs. contract LibEvalMaxOutputsTest is RainterpreterExpressionDeployerDeploymentTest { /// When maxOutputs < sourceOutputs, the returned array length must /// equal maxOutputs and contain the topmost stack items. @@ -48,7 +48,7 @@ contract LibEvalMaxOutputsTest is RainterpreterExpressionDeployerDeploymentTest ); (StackItem[] memory outputs, bytes32[] memory kvs) = - LibEval.eval2(state, new StackItem[](0), uint256(maxOutputs)); + LibEval.eval4(state, new StackItem[](0), uint256(maxOutputs)); assertEq(outputs.length, uint256(maxOutputs)); if (maxOutputs >= 1) assertEq(StackItem.unwrap(outputs[0]), c2); diff --git a/test/src/lib/eval/LibEval.remainderOnly.t.sol b/test/src/lib/eval/LibEval.remainderOnly.t.sol new file mode 100644 index 000000000..824663894 --- /dev/null +++ b/test/src/lib/eval/LibEval.remainderOnly.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import { + RainterpreterExpressionDeployerDeploymentTest +} from "test/abstract/RainterpreterExpressionDeployerDeploymentTest.sol"; +import {LibInterpreterState, InterpreterState} from "src/lib/state/LibInterpreterState.sol"; +import {LibAllStandardOps} from "src/lib/op/LibAllStandardOps.sol"; +import {LibEval} from "src/lib/eval/LibEval.sol"; +import {MemoryKV} from "rain.lib.memkv/lib/LibMemoryKV.sol"; +import { + IInterpreterStoreV3, + FullyQualifiedNamespace +} from "rain.interpreter.interface/interface/IInterpreterStoreV3.sol"; +import {StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; + +/// @title LibEvalRemainderOnlyTest +/// @notice Tests that the evalLoop remainder path correctly dispatches +/// opcodes when the total opcode count is less than 8, so the main +/// 8-at-a-time loop never executes. +contract LibEvalRemainderOnlyTest is RainterpreterExpressionDeployerDeploymentTest { + /// 7 constant opcodes: all handled by the remainder loop (7 < 8, so the + /// main loop body is never entered). Fuzz the constant values to verify + /// they flow through correctly. + function testEvalLoopRemainderOnlySeven( + bytes32 c0, + bytes32 c1, + bytes32 c2, + bytes32 c3, + bytes32 c4, + bytes32 c5, + bytes32 c6 + ) external view { + // 7 distinct hex literals produce 7 constant opcodes. + (bytes memory bytecode,) = + I_PARSER.unsafeParse(bytes("_ _ _ _ _ _ _: 0xaa 0xbb 0xcc 0xdd 0xee 0xff 0x11;")); + + bytes32[] memory constants = new bytes32[](7); + constants[0] = c0; + constants[1] = c1; + constants[2] = c2; + constants[3] = c3; + constants[4] = c4; + constants[5] = c5; + constants[6] = c6; + + StackItem[][] memory stacks = new StackItem[][](1); + stacks[0] = new StackItem[](7); + + InterpreterState memory state = InterpreterState( + LibInterpreterState.stackBottoms(stacks), + constants, + 0, + MemoryKV.wrap(0), + FullyQualifiedNamespace.wrap(0), + IInterpreterStoreV3(address(0)), + new bytes32[][](0), + bytecode, + LibAllStandardOps.opcodeFunctionPointers() + ); + + (StackItem[] memory outputs, bytes32[] memory kvs) = + LibEval.eval4(state, new StackItem[](0), type(uint256).max); + + assertEq(outputs.length, 7); + // Stack outputs are top-first: last pushed constant is first output. + assertEq(StackItem.unwrap(outputs[0]), c6); + assertEq(StackItem.unwrap(outputs[1]), c5); + assertEq(StackItem.unwrap(outputs[2]), c4); + assertEq(StackItem.unwrap(outputs[3]), c3); + assertEq(StackItem.unwrap(outputs[4]), c2); + assertEq(StackItem.unwrap(outputs[5]), c1); + assertEq(StackItem.unwrap(outputs[6]), c0); + assertEq(kvs.length, 0); + } +} diff --git a/test/src/lib/integrity/LibIntegrityCheck.badOpIO.t.sol b/test/src/lib/integrity/LibIntegrityCheck.badOpIO.t.sol new file mode 100644 index 000000000..271ab2d5d --- /dev/null +++ b/test/src/lib/integrity/LibIntegrityCheck.badOpIO.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import { + RainterpreterExpressionDeployerDeploymentTest +} from "test/abstract/RainterpreterExpressionDeployerDeploymentTest.sol"; +import {LibIntegrityCheck} from "src/lib/integrity/LibIntegrityCheck.sol"; +import {LibAllStandardOps} from "src/lib/op/LibAllStandardOps.sol"; +import {BadOpInputsLength, BadOpOutputsLength} from "rain.interpreter.interface/error/ErrIntegrity.sol"; +import {LibBytecode} from "rain.interpreter.interface/lib/bytecode/LibBytecode.sol"; +import {Pointer} from "rain.solmem/lib/LibPointer.sol"; + +/// @title LibIntegrityCheckBadOpIOTest +/// @notice Verifies that integrityCheck2 reverts with BadOpInputsLength and +/// BadOpOutputsLength when a surgically corrupted IO byte in the bytecode +/// does not match the integrity function's computed inputs/outputs. +contract LibIntegrityCheckBadOpIOTest is RainterpreterExpressionDeployerDeploymentTest { + /// External wrapper so vm.expectRevert works with the library call. + /// Computes integrity function pointers at runtime so they are valid + /// for this contract's inlined library code. + function externalIntegrityCheck(bytes memory bytecode, bytes32[] memory constants) + external + view + returns (bytes memory) + { + return LibIntegrityCheck.integrityCheck2(LibAllStandardOps.integrityFunctionPointers(), bytecode, constants); + } + + /// Parse valid rainlang, surgically corrupt the IO byte to declare wrong + /// inputs, verify the integrity check reverts with BadOpInputsLength. + function testBadOpInputsLength() external { + // Parse a minimal expression: one constant opcode. + // Constant integrity returns (0 inputs, 1 output), so the parser + // sets the IO byte to 0x10. + (bytes memory bytecode, bytes32[] memory constants) = I_PARSER.unsafeParse(bytes("_: 0xdeadbeef;")); + + // Locate the IO byte of the first opcode via LibBytecode. + // Source header is 4 bytes (opsCount, stackAlloc, inputs, outputs), + // then each opcode is 4 bytes: opcodeIndex(1) + ioByte(1) + operand(2). + Pointer sourcePtr = LibBytecode.sourcePointer(bytecode, 0); + uint256 ioByteAddr = Pointer.unwrap(sourcePtr) + 5; + + // Corrupt: change inputs from 0 to 1 (0x10 -> 0x11). + assembly ("memory-safe") { + mstore8(ioByteAddr, 0x11) + } + + // Integrity function says 0 inputs, bytecode now says 1. + vm.expectRevert(abi.encodeWithSelector(BadOpInputsLength.selector, 0, 0, 1)); + this.externalIntegrityCheck(bytecode, constants); + } + + /// Parse valid rainlang, surgically corrupt the IO byte to declare wrong + /// outputs, verify the integrity check reverts with BadOpOutputsLength. + function testBadOpOutputsLength() external { + (bytes memory bytecode, bytes32[] memory constants) = I_PARSER.unsafeParse(bytes("_: 0xdeadbeef;")); + + Pointer sourcePtr = LibBytecode.sourcePointer(bytecode, 0); + uint256 ioByteAddr = Pointer.unwrap(sourcePtr) + 5; + + // Corrupt: change outputs from 1 to 0, keep inputs at 0 (0x10 -> 0x00). + // Inputs still match so BadOpInputsLength is not triggered. + assembly ("memory-safe") { + mstore8(ioByteAddr, 0x00) + } + + // Integrity function says 1 output, bytecode now says 0. + vm.expectRevert(abi.encodeWithSelector(BadOpOutputsLength.selector, 0, 1, 0)); + this.externalIntegrityCheck(bytecode, constants); + } +} diff --git a/test/src/lib/integrity/LibIntegrityCheck.t.sol b/test/src/lib/integrity/LibIntegrityCheck.t.sol index 2c25f9644..846add6cb 100644 --- a/test/src/lib/integrity/LibIntegrityCheck.t.sol +++ b/test/src/lib/integrity/LibIntegrityCheck.t.sol @@ -11,6 +11,7 @@ import { StackAllocationMismatch, StackOutputsMismatch } from "src/error/ErrIntegrity.sol"; +import {BadOpInputsLength, BadOpOutputsLength} from "rain.interpreter.interface/error/ErrIntegrity.sol"; import {INTEGRITY_FUNCTION_POINTERS} from "src/generated/RainterpreterExpressionDeployer.pointers.sol"; import {ALL_STANDARD_OPS_LENGTH} from "src/lib/op/LibAllStandardOps.sol"; import {LibConvert} from "rain.lib.typecast/LibConvert.sol"; diff --git a/test/src/lib/op/LibAllStandardOps.t.sol b/test/src/lib/op/LibAllStandardOps.t.sol index 39383bb45..b548cc98d 100644 --- a/test/src/lib/op/LibAllStandardOps.t.sol +++ b/test/src/lib/op/LibAllStandardOps.t.sol @@ -78,9 +78,142 @@ contract LibAllStandardOpsTest is Test { //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[3].word, bytes32("context")); - // Every word must be non-empty. - for (uint256 i = 0; i < words.length; i++) { - assertTrue(words[i].word != bytes32(0)); - } + // Verify every word name and ordering. + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[4].word, bytes32("bitwise-and")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[5].word, bytes32("bitwise-or")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[6].word, bytes32("bitwise-count-ones")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[7].word, bytes32("bitwise-decode")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[8].word, bytes32("bitwise-encode")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[9].word, bytes32("bitwise-shift-left")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[10].word, bytes32("bitwise-shift-right")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[11].word, bytes32("call")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[12].word, bytes32("hash")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[13].word, bytes32("uint256-erc20-allowance")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[14].word, bytes32("uint256-erc20-balance-of")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[15].word, bytes32("uint256-erc20-total-supply")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[16].word, bytes32("erc20-allowance")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[17].word, bytes32("erc20-balance-of")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[18].word, bytes32("erc20-total-supply")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[19].word, bytes32("uint256-erc721-balance-of")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[20].word, bytes32("erc721-balance-of")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[21].word, bytes32("erc721-owner-of")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[22].word, bytes32("erc5313-owner")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[23].word, bytes32("block-number")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[24].word, bytes32("chain-id")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[25].word, bytes32("block-timestamp")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[26].word, bytes32("now")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[27].word, bytes32("any")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[28].word, bytes32("conditions")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[29].word, bytes32("ensure")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[30].word, bytes32("equal-to")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[31].word, bytes32("binary-equal-to")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[32].word, bytes32("every")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[33].word, bytes32("greater-than")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[34].word, bytes32("greater-than-or-equal-to")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[35].word, bytes32("if")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[36].word, bytes32("is-zero")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[37].word, bytes32("less-than")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[38].word, bytes32("less-than-or-equal-to")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[39].word, bytes32("exponential-growth")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[40].word, bytes32("linear-growth")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[41].word, bytes32("uint256-max-value")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[42].word, bytes32("uint256-add")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[43].word, bytes32("uint256-div")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[44].word, bytes32("uint256-mul")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[45].word, bytes32("uint256-power")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[46].word, bytes32("uint256-sub")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[47].word, bytes32("abs")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[48].word, bytes32("add")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[49].word, bytes32("avg")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[50].word, bytes32("ceil")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[51].word, bytes32("div")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[52].word, bytes32("e")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[53].word, bytes32("exp")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[54].word, bytes32("exp2")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[55].word, bytes32("floor")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[56].word, bytes32("frac")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[57].word, bytes32("gm")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[58].word, bytes32("headroom")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[59].word, bytes32("inv")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[60].word, bytes32("max")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[61].word, bytes32("max-negative-value")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[62].word, bytes32("max-positive-value")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[63].word, bytes32("min")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[64].word, bytes32("min-negative-value")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[65].word, bytes32("min-positive-value")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[66].word, bytes32("mul")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[67].word, bytes32("power")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[68].word, bytes32("sqrt")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[69].word, bytes32("sub")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[70].word, bytes32("get")); + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(words[71].word, bytes32("set")); } } diff --git a/test/src/lib/parse/LibParseInterstitial.t.sol b/test/src/lib/parse/LibParseInterstitial.t.sol index 2eccf4294..ffa6dbe61 100644 --- a/test/src/lib/parse/LibParseInterstitial.t.sol +++ b/test/src/lib/parse/LibParseInterstitial.t.sol @@ -151,6 +151,34 @@ contract LibParseInterstitialTest is Test { assertEq(charAtCursor, uint256(uint8(bytes1("x"))), "cursor at x after content comment"); } + /// Fuzz: skipComment with arbitrary body content always lands cursor + /// immediately after the closing `*/`. + function testSkipCommentFuzzBody(bytes memory body) external pure { + // Replace all `*` in body with `~` so no `*/` can appear. + for (uint256 i = 0; i < body.length; i++) { + if (body[i] == bytes1("*")) { + body[i] = bytes1("~"); + } + } + + // Build: /* */ x + bytes memory data = abi.encodePacked("/*", body, "*/x"); + + ParseState memory state = LibParseState.newState(data, "", "", ""); + uint256 cursor = Pointer.unwrap(state.data.dataPointer()); + uint256 end = Pointer.unwrap(data.endDataPointer()); + cursor = state.skipComment(cursor, end); + + // Cursor should be at 'x', which is one byte before end. + assertEq(cursor, end - 1, "cursor at x after fuzzed comment"); + uint256 charAtCursor; + assembly ("memory-safe") { + charAtCursor := byte(0, mload(cursor)) + } + //forge-lint: disable-next-line(unsafe-typecast) + assertEq(charAtCursor, uint256(uint8(bytes1("x"))), "char is x"); + } + /// parseInterstitial returns immediately when first character is not /// whitespace or comment head. function testParseInterstitialNonInterstitialFirst() external pure { diff --git a/test/src/lib/parse/LibParseStackTracker.t.sol b/test/src/lib/parse/LibParseStackTracker.t.sol index f0904589a..204048743 100644 --- a/test/src/lib/parse/LibParseStackTracker.t.sol +++ b/test/src/lib/parse/LibParseStackTracker.t.sol @@ -68,6 +68,16 @@ contract LibParseStackTrackerTest is Test { assertEq((result >> 8) & 0xFF, uint256(existingInputs) + uint256(n)); } + /// pushInputs reverts with ParseStackOverflow when the internal push + /// overflows (current + n > 0xFF), before the inputs check is reached. + function testPushInputsPushOverflow(uint8 current, uint8 n) external { + vm.assume(uint256(current) + uint256(n) > 0xFF); + // Inputs byte is 0 so the inputs check would pass — overflow must + // come from push(n). + vm.expectRevert(abi.encodeWithSelector(ParseStackOverflow.selector)); + this.externalPushInputs(uint256(current), uint256(n)); + } + /// push updates high watermark when current + n exceeds previous max. function testPushUpdatesHighWatermark(uint8 n) external pure { vm.assume(n > 0); diff --git a/test/src/lib/parse/LibParseState.endSource.t.sol b/test/src/lib/parse/LibParseState.endSource.t.sol index 4ecd0693d..783562def 100644 --- a/test/src/lib/parse/LibParseState.endSource.t.sol +++ b/test/src/lib/parse/LibParseState.endSource.t.sol @@ -101,7 +101,7 @@ contract LibParseStateEndSourceTest is Test { /// Fuzz the op count: source byte length must be 4 * opCount + 4. function testEndSourceByteLengthFuzz(uint256 opCount) external pure { - opCount = bound(opCount, 1, 50); + opCount = bound(opCount, 1, 255); ParseState memory state = LibParseState.newState("", "", "", ""); for (uint256 i = 0; i < opCount; i++) { diff --git a/test/src/lib/parse/LibSubParse.subParseWords.t.sol b/test/src/lib/parse/LibSubParse.subParseWords.t.sol index 715b00b71..05ddc68c6 100644 --- a/test/src/lib/parse/LibSubParse.subParseWords.t.sol +++ b/test/src/lib/parse/LibSubParse.subParseWords.t.sol @@ -141,6 +141,16 @@ contract LibSubParseSubParseWordsTest is Test { return state.parse(); } + /// @notice External wrapper for subParseWords so reverts can be caught. + function externalSubParseWords(bytes memory bytecode) + external + view + returns (bytes memory, bytes32[] memory) + { + ParseState memory state = LibParseState.newState("", "", "", ""); + return state.subParseWords(bytecode); + } + /// @notice When the sub parser rejects a word, parsing reverts with /// UnknownWord. function testSubParseWordsUnknownWordReverts() external { @@ -159,6 +169,17 @@ contract LibSubParseSubParseWordsTest is Test { this.externalParse(src); } + /// @notice When no sub parsers are registered and bytecode contains an + /// unknown opcode, subParseWords reverts with UnknownWord. + function testSubParseWordsNoSubParsersUnknownReverts() external { + // Build bytecode with OPCODE_UNKNOWN directly. + //forge-lint: disable-next-line(unsafe-typecast) + bytes memory bytecode = buildSingleOpBytecode(uint8(OPCODE_UNKNOWN), 0x10, 0x0000); + + vm.expectRevert(); + this.externalSubParseWords(bytecode); + } + /// @notice Multiple known opcodes in a single source are not modified. function testSubParseWordsMultipleKnownOpcodes() external view { ParseState memory state = LibParseState.newState("", "", "", ""); From 7e8d3a0ed43d725a865b583c236357ae5d5f4d59 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Tue, 3 Mar 2026 23:39:20 +0400 Subject: [PATCH 04/13] Rename 9 LibOp files to match authoring meta word names and reorder arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename source and test files so CamelCase filenames convert to the same kebab-case as their authoring meta word names. Reorder all four parallel arrays in LibAllStandardOps (authoring meta, operand handlers, integrity pointers, opcode pointers) to match filesystem sort order. Renames: LibOpCtPop→BitwiseCountOnes, LibOpDecodeBits→BitwiseDecode, LibOpEncodeBits→BitwiseEncode, LibOpShiftBitsLeft→BitwiseShiftLeft, LibOpShiftBitsRight→BitwiseShiftRight, LibOpTimestamp→BlockTimestamp, LibOpPow→Power, LibOpMaxUint256→Uint256MaxValue, LibOpUint256Pow→Uint256Power. Add filesystem ordering test (LibCamelToKebab + ffi-based find|sort) to prevent future drift. Update deploy constants and regenerate pointers. Co-Authored-By: Claude Opus 4.6 --- src/generated/Rainterpreter.pointers.sol | 4 +- ...interpreterExpressionDeployer.pointers.sol | 6 +- .../RainterpreterParser.pointers.sol | 6 +- src/lib/deploy/LibInterpreterDeploy.sol | 16 +- src/lib/op/LibAllStandardOps.sol | 318 +++++++++--------- ...bOpCtPop.sol => LibOpBitwiseCountOnes.sol} | 11 +- ...pDecodeBits.sol => LibOpBitwiseDecode.sol} | 10 +- ...pEncodeBits.sol => LibOpBitwiseEncode.sol} | 4 +- ...BitsLeft.sol => LibOpBitwiseShiftLeft.sol} | 4 +- ...tsRight.sol => LibOpBitwiseShiftRight.sol} | 4 +- ...pTimestamp.sol => LibOpBlockTimestamp.sol} | 4 +- .../op/math/{LibOpPow.sol => LibOpPower.sol} | 6 +- ...axUint256.sol => LibOpUint256MaxValue.sol} | 4 +- ...OpUint256Pow.sol => LibOpUint256Power.sol} | 4 +- test/lib/string/LibCamelToKebab.sol | 61 ++++ test/lib/string/LibCamelToKebab.t.sol | 56 +++ ...LibAllStandardOps.filesystemOrdering.t.sol | 98 ++++++ test/src/lib/op/LibAllStandardOps.t.sol | 114 ++++--- ...tPop.t.sol => LibOpBitwiseCountOnes.t.sol} | 19 +- ...odeBits.t.sol => LibOpBitwiseDecode.t.sol} | 18 +- ...odeBits.t.sol => LibOpBitwiseEncode.t.sol} | 18 +- ...Left.t.sol => LibOpBitwiseShiftLeft.t.sol} | 23 +- ...ght.t.sol => LibOpBitwiseShiftRight.t.sol} | 22 +- ...estamp.t.sol => LibOpBlockTimestamp.t.sol} | 21 +- .../math/{LibOpPow.t.sol => LibOpPower.t.sol} | 12 +- ...nt256.t.sol => LibOpUint256MaxValue.t.sol} | 23 +- ...nt256Pow.t.sol => LibOpUint256Power.t.sol} | 20 +- .../parse/LibParseOperand.handleOperand.t.sol | 20 +- 28 files changed, 594 insertions(+), 332 deletions(-) rename src/lib/op/bitwise/{LibOpCtPop.sol => LibOpBitwiseCountOnes.sol} (86%) rename src/lib/op/bitwise/{LibOpDecodeBits.sol => LibOpBitwiseDecode.sol} (94%) rename src/lib/op/bitwise/{LibOpEncodeBits.sol => LibOpBitwiseEncode.sol} (98%) rename src/lib/op/bitwise/{LibOpShiftBitsLeft.sol => LibOpBitwiseShiftLeft.sol} (97%) rename src/lib/op/bitwise/{LibOpShiftBitsRight.sol => LibOpBitwiseShiftRight.sol} (97%) rename src/lib/op/evm/{LibOpTimestamp.sol => LibOpBlockTimestamp.sol} (97%) rename src/lib/op/math/{LibOpPow.sol => LibOpPower.sol} (94%) rename src/lib/op/math/uint256/{LibOpMaxUint256.sol => LibOpUint256MaxValue.sol} (96%) rename src/lib/op/math/uint256/{LibOpUint256Pow.sol => LibOpUint256Power.sol} (98%) create mode 100644 test/lib/string/LibCamelToKebab.sol create mode 100644 test/lib/string/LibCamelToKebab.t.sol create mode 100644 test/src/lib/op/LibAllStandardOps.filesystemOrdering.t.sol rename test/src/lib/op/bitwise/{LibOpCtPop.t.sol => LibOpBitwiseCountOnes.t.sol} (79%) rename test/src/lib/op/bitwise/{LibOpDecodeBits.t.sol => LibOpBitwiseDecode.t.sol} (89%) rename test/src/lib/op/bitwise/{LibOpEncodeBits.t.sol => LibOpBitwiseEncode.t.sol} (89%) rename test/src/lib/op/bitwise/{LibOpShiftBitsLeft.t.sol => LibOpBitwiseShiftLeft.t.sol} (89%) rename test/src/lib/op/bitwise/{LibOpShiftBitsRight.t.sol => LibOpBitwiseShiftRight.t.sol} (89%) rename test/src/lib/op/evm/{LibOpTimestamp.t.sol => LibOpBlockTimestamp.t.sol} (87%) rename test/src/lib/op/math/{LibOpPow.t.sol => LibOpPower.t.sol} (90%) rename test/src/lib/op/math/uint256/{LibOpMaxUint256.t.sol => LibOpUint256MaxValue.t.sol} (75%) rename test/src/lib/op/math/uint256/{LibOpUint256Pow.t.sol => LibOpUint256Power.t.sol} (94%) diff --git a/src/generated/Rainterpreter.pointers.sol b/src/generated/Rainterpreter.pointers.sol index a19ddf7f3..a6577203d 100644 --- a/src/generated/Rainterpreter.pointers.sol +++ b/src/generated/Rainterpreter.pointers.sol @@ -10,11 +10,11 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0x66a2e1c7c62ac40c6dc3c281364370e8fd18e2cce1a2819dc38b01880cbce0fa); +bytes32 constant BYTECODE_HASH = bytes32(0x6ed341c240b58c7e314deee17aec3f1fc7ef5165c722350ac479aed9c1003db8); /// @dev The function pointers known to the interpreter for dynamic dispatch. /// By setting these as a constant they can be inlined into the interpreter /// and loaded at eval time for very low gas (~100) due to the compiler /// optimising it to a single `codecopy` to build the in memory bytes array. bytes constant OPCODE_FUNCTION_POINTERS = - hex"08f70929094d0ac50b270b390b4b0b630b860bbc0bce0be00c810ca00db50e900f2c105d11530db511fc12d713911421143214431443145414a9158115d015e815fc1644165c1674169816ad16c516dd1725174c175e17bf180c185918a618f3190019b319d519e21a701aa11ae41b081b151b221bb41be81bf51c421c731ca41cf11d221d3a1dc81df41e161ea41f88"; + hex"08fb092d09510ac90b2b0b3d0b550b780bae0bc00bd20be40c850ca40dd50ecb0f821089116412001290136b10891425143614361447145814ad14c1159915e81600164816601678169c16b116c916e116ee17a117c317d0185e188f18d218f61903191019a919dd19ea1a4b1a7c1aad1afa1b2b1b431bd11bfd1c1f1cad1cee1d151d621daf1dc11e0e1e5b1ea81f8c"; diff --git a/src/generated/RainterpreterExpressionDeployer.pointers.sol b/src/generated/RainterpreterExpressionDeployer.pointers.sol index 0d5040c72..9dbee4697 100644 --- a/src/generated/RainterpreterExpressionDeployer.pointers.sol +++ b/src/generated/RainterpreterExpressionDeployer.pointers.sol @@ -10,11 +10,11 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0x58937b2f2590a07a3c3f93b766fd9dc1890de7a85239282e9a0928068d329b6c); +bytes32 constant BYTECODE_HASH = bytes32(0x445c1b70d019ad0fda5b60cd2ac7e04c5fcd1f59afff4adea3db4e37d829a38d); /// @dev The hash of the meta that describes the contract. -bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0xb2500441a27ea683f814327be6e43c90f516b8f033203ad3e0ba2cde847fb0ba); +bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0x0ae1ecb6c0f6314beaf4d4cd803ba14c900b0eecb1ecd39a52739cff9ae2c34a); /// @dev The function pointers for the integrity check fns. bytes constant INTEGRITY_FUNCTION_POINTERS = - hex"0d560dd20e350fab0fb40fb40fbe0fc70fe11086108610e1115811650fb40fbe11650fb40fbe0fb40fb40fb40fbe0fab0fab0fab0fab116f119311ac0fb40fb4116f0fb40fb411650fbe0fb40fb4116511650fab11b511b511b511b511b50fbe11b50fb40fbe11b50fab0fbe0fbe0fbe0fbe0fb40fbe0fbe11b50fab0fab11b50fab0fab11b50fb40fbe11b50fbe11ac"; + hex"0d560dd20e350fab0fb40fbe0fc70fe10fb41086108610e1115811650fb40fbe11650fb40fbe0fbe0fb40fb40fb40fab0fab0fab0fab116f0fb4119311ac0fb4116f0fb40fb411650fbe0fb40fb40fbe11b50fb40fbe11b50fab0fbe0fbe0fbe0fbe0fb40fbe0fbe11b50fab0fab11b50fab0fab11b50fb40fbe11b51165116511b511b50fab11b511b511b50fbe11ac"; diff --git a/src/generated/RainterpreterParser.pointers.sol b/src/generated/RainterpreterParser.pointers.sol index 01f3d964b..96c353f2b 100644 --- a/src/generated/RainterpreterParser.pointers.sol +++ b/src/generated/RainterpreterParser.pointers.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0xcca04b4215c721df539f9a2525ea402fd5fc6905f4a0fee036a6e979b905ca18); +bytes32 constant BYTECODE_HASH = bytes32(0xde67804761c572b0c215bb2f8baf297d6ae7ff9b2690c076896856ee9c638cdb); /// @dev The parse meta that is used to lookup word definitions. /// The structure of the parse meta is: @@ -29,7 +29,7 @@ bytes32 constant BYTECODE_HASH = bytes32(0xcca04b4215c721df539f9a2525ea402fd5fc6 /// bit count of the previous bloom filter. If we reach the end of the bloom /// filters then we have a miss. bytes constant PARSE_META = - hex"0288400100420b0280046b0641220186adb8a044003012020f2a880521281ac8811a000000400000000000000000000000000000000001000002000000000000000000310ea98a4210f9c54127bda61f6395ad119fb4a8085dbeaf35313e9e09b06d681cfe490634e21ac12783a6cc32d2d2123c22c3e0062f369e1e27267220a0a68d37357696465d217b161120881224f4a8242f1af52841f7290afa3e8640acf3d13b97e9fd00443a4543406bb547b9059d25a3cd1e0ed0c3260b15eb4d13fc94272d27bc4133af2cc23a1b8f6310ca9efe2a3ce35f140068482ec291e3295d68b1455f9a151db98c15239cd9c10c1f69823dc8d3844408d5792f3459d4182c9ff617a5b2cd15c003180df621af0f7ba34c26bb6c412cb066fb1b73563d0210215f3eaa563921098e690457fb180376e7520529b7c530f11de101fcc60522de7ddd193d3d6d2b96c37039cba287367b3af5384eee721aa809353f10da5007c52518"; + hex"0288400100420b0280046b0641220186adb8a044003012020f2a880521281ac8811a000000000000000000080000000000100000000000000002000000000000000000290ea98a3a10f9c53927bda61c6395ad0e9fb4a8075dbeaf2d313e9e09b06d681dfe49062ce21ac13e83a6cc2ad2d2123422c3e0052f369e1f27267220a0a68d2f357696465d217b131120880f24f4a8242f1af53f41f7290afa3e8638acf3d13397e9fd00443a453b406bb547b9059d25a3cd1e11d0c3260b15eb4d16fc94274427bc412baf2cc2321b8f630dca9efe403ce35f1400684845c291e3425d68b13d5f9a151eb98c15239cd9c10c1f698235c8d3843c08d579273459d41a2c9ff617a5b2cd15c0031810f621af127ba34c26bb6c4137b2ad3d1b73563d0210215f36aa563921098e690457fb180376e752066a470128f11de101fcc60522de7ddd183d3d6d4196c37031cba2872e7b3af5304eee7219a8093543082b5f0801f5a5"; /// @dev The build depth of the parser meta. @@ -39,7 +39,7 @@ uint8 constant PARSE_META_BUILD_DEPTH = 2; /// These positional indexes all map to the same indexes looked up in the parse /// meta. bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = - hex"1a5c1a5c1a5c1afa1bcb1bcb1bcb1afa1afa1a5c1a5c1a5c1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb"; + hex"1a5c1a5c1a5c1afa1bcb1bcb1afa1afa1bcb1a5c1a5c1a5c1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb"; /// @dev Every two bytes is a function pointer for a literal parser. /// Literal dispatches are determined by the first byte(s) of the literal diff --git a/src/lib/deploy/LibInterpreterDeploy.sol b/src/lib/deploy/LibInterpreterDeploy.sol index dc69b6b9e..8316b27e5 100644 --- a/src/lib/deploy/LibInterpreterDeploy.sol +++ b/src/lib/deploy/LibInterpreterDeploy.sol @@ -11,14 +11,14 @@ pragma solidity ^0.8.25; library LibInterpreterDeploy { /// The address of the `RainterpreterParser` contract when deployed with the /// rain standard zoltu deployer. - address constant PARSER_DEPLOYED_ADDRESS = address(0x744d1fFF170FC824EcEDb4E220819682095dFE83); + address constant PARSER_DEPLOYED_ADDRESS = address(0x12Efbf18Ccec85818F0301Cfce7616297E1984B5); /// The code hash of the `RainterpreterParser` contract when deployed with /// the rain standard zoltu deployer. This can be used to verify that the /// deployed contract has the expected bytecode, which provides stronger /// guarantees than just checking the address. bytes32 constant PARSER_DEPLOYED_CODEHASH = - bytes32(0xcca04b4215c721df539f9a2525ea402fd5fc6905f4a0fee036a6e979b905ca18); + bytes32(0xde67804761c572b0c215bb2f8baf297d6ae7ff9b2690c076896856ee9c638cdb); /// The address of the `RainterpreterStore` contract when deployed with the /// rain standard zoltu deployer. @@ -33,34 +33,34 @@ library LibInterpreterDeploy { /// The address of the `Rainterpreter` contract when deployed with the rain /// standard zoltu deployer. - address constant INTERPRETER_DEPLOYED_ADDRESS = address(0xe758FACfeb8E02bfed2d8C53B731D687DDD88A68); + address constant INTERPRETER_DEPLOYED_ADDRESS = address(0xbECD4E58d657f40d9851013C75431B4CB8D6cd04); /// The code hash of the `Rainterpreter` contract when deployed with the rain /// standard zoltu deployer. This can be used to verify that the deployed /// contract has the expected bytecode, which provides stronger guarantees /// than just checking the address. bytes32 constant INTERPRETER_DEPLOYED_CODEHASH = - bytes32(0x66a2e1c7c62ac40c6dc3c281364370e8fd18e2cce1a2819dc38b01880cbce0fa); + bytes32(0x6ed341c240b58c7e314deee17aec3f1fc7ef5165c722350ac479aed9c1003db8); /// The address of the `RainterpreterExpressionDeployer` contract when /// deployed with the rain standard zoltu deployer. - address constant EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS = address(0x3Dcbe436dC61dd38635bB32aFc8C9487F6EFa5b6); + address constant EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS = address(0xB6151493CF4683c03241Be0D5Ac698447963cb15); /// The code hash of the `RainterpreterExpressionDeployer` contract when /// deployed with the rain standard zoltu deployer. This can be used to /// verify that the deployed contract has the expected bytecode, which /// provides stronger guarantees than just checking the address. bytes32 constant EXPRESSION_DEPLOYER_DEPLOYED_CODEHASH = - bytes32(0x58937b2f2590a07a3c3f93b766fd9dc1890de7a85239282e9a0928068d329b6c); + bytes32(0x445c1b70d019ad0fda5b60cd2ac7e04c5fcd1f59afff4adea3db4e37d829a38d); /// The address of the `RainterpreterDISPaiRegistry` contract when deployed /// with the rain standard zoltu deployer. - address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0x877e3e8D0860235f4F4F771a3E71B076f47b23Ac); + address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0x050Bbc64dd19d2A99C457B186Ad4dEA20439b21C); /// The code hash of the `RainterpreterDISPaiRegistry` contract when /// deployed with the rain standard zoltu deployer. This can be used to /// verify that the deployed contract has the expected bytecode, which /// provides stronger guarantees than just checking the address. bytes32 constant DISPAIR_REGISTRY_DEPLOYED_CODEHASH = - bytes32(0xaa0a08b32797ce90d646ca05dcc64b53b0bb34ba7b15834619a51b567294470f); + bytes32(0x31fde5a7e1b43718786c1eab42786520c947e9a35cb870769497111e2ec933db); } diff --git a/src/lib/op/LibAllStandardOps.sol b/src/lib/op/LibAllStandardOps.sol index a4e335d61..e1750b0c2 100644 --- a/src/lib/op/LibAllStandardOps.sol +++ b/src/lib/op/LibAllStandardOps.sol @@ -17,40 +17,39 @@ import {LibOpContext} from "./00/LibOpContext.sol"; import {LibOpExtern} from "./00/LibOpExtern.sol"; import {LibOpBitwiseAnd} from "./bitwise/LibOpBitwiseAnd.sol"; +import {LibOpBitwiseCountOnes} from "./bitwise/LibOpBitwiseCountOnes.sol"; +import {LibOpBitwiseDecode} from "./bitwise/LibOpBitwiseDecode.sol"; +import {LibOpBitwiseEncode} from "./bitwise/LibOpBitwiseEncode.sol"; import {LibOpBitwiseOr} from "./bitwise/LibOpBitwiseOr.sol"; -import {LibOpCtPop} from "./bitwise/LibOpCtPop.sol"; -import {LibOpDecodeBits} from "./bitwise/LibOpDecodeBits.sol"; -import {LibOpEncodeBits} from "./bitwise/LibOpEncodeBits.sol"; -import {LibOpShiftBitsLeft} from "./bitwise/LibOpShiftBitsLeft.sol"; -import {LibOpShiftBitsRight} from "./bitwise/LibOpShiftBitsRight.sol"; +import {LibOpBitwiseShiftLeft} from "./bitwise/LibOpBitwiseShiftLeft.sol"; +import {LibOpBitwiseShiftRight} from "./bitwise/LibOpBitwiseShiftRight.sol"; import {LibOpCall} from "./call/LibOpCall.sol"; import {LibOpHash} from "./crypto/LibOpHash.sol"; +import {LibOpERC20Allowance} from "./erc20/LibOpERC20Allowance.sol"; +import {LibOpERC20BalanceOf} from "./erc20/LibOpERC20BalanceOf.sol"; +import {LibOpERC20TotalSupply} from "./erc20/LibOpERC20TotalSupply.sol"; import {LibOpUint256ERC20Allowance} from "./erc20/uint256/LibOpUint256ERC20Allowance.sol"; import {LibOpUint256ERC20BalanceOf} from "./erc20/uint256/LibOpUint256ERC20BalanceOf.sol"; import {LibOpUint256ERC20TotalSupply} from "./erc20/uint256/LibOpUint256ERC20TotalSupply.sol"; -import {LibOpERC20Allowance} from "./erc20/LibOpERC20Allowance.sol"; -import {LibOpERC20BalanceOf} from "./erc20/LibOpERC20BalanceOf.sol"; -import {LibOpERC20TotalSupply} from "./erc20/LibOpERC20TotalSupply.sol"; +import {LibOpERC5313Owner} from "./erc5313/LibOpERC5313Owner.sol"; -import {LibOpUint256ERC721BalanceOf} from "./erc721/uint256/LibOpUint256ERC721BalanceOf.sol"; import {LibOpERC721BalanceOf} from "./erc721/LibOpERC721BalanceOf.sol"; import {LibOpERC721OwnerOf} from "./erc721/LibOpERC721OwnerOf.sol"; - -import {LibOpERC5313Owner} from "./erc5313/LibOpERC5313Owner.sol"; +import {LibOpUint256ERC721BalanceOf} from "./erc721/uint256/LibOpUint256ERC721BalanceOf.sol"; import {LibOpBlockNumber} from "./evm/LibOpBlockNumber.sol"; +import {LibOpBlockTimestamp} from "./evm/LibOpBlockTimestamp.sol"; import {LibOpChainId} from "./evm/LibOpChainId.sol"; -import {LibOpTimestamp} from "./evm/LibOpTimestamp.sol"; import {LibOpAny} from "./logic/LibOpAny.sol"; +import {LibOpBinaryEqualTo} from "./logic/LibOpBinaryEqualTo.sol"; import {LibOpConditions} from "./logic/LibOpConditions.sol"; import {LibOpEnsure} from "./logic/LibOpEnsure.sol"; import {LibOpEqualTo} from "./logic/LibOpEqualTo.sol"; -import {LibOpBinaryEqualTo} from "./logic/LibOpBinaryEqualTo.sol"; import {LibOpEvery} from "./logic/LibOpEvery.sol"; import {LibOpGreaterThan} from "./logic/LibOpGreaterThan.sol"; import {LibOpGreaterThanOrEqualTo} from "./logic/LibOpGreaterThanOrEqualTo.sol"; @@ -59,21 +58,10 @@ import {LibOpIsZero} from "./logic/LibOpIsZero.sol"; import {LibOpLessThan} from "./logic/LibOpLessThan.sol"; import {LibOpLessThanOrEqualTo} from "./logic/LibOpLessThanOrEqualTo.sol"; -import {LibOpExponentialGrowth} from "./math/growth/LibOpExponentialGrowth.sol"; -import {LibOpLinearGrowth} from "./math/growth/LibOpLinearGrowth.sol"; - -import {LibOpMaxUint256} from "./math/uint256/LibOpMaxUint256.sol"; -import {LibOpUint256Add} from "./math/uint256/LibOpUint256Add.sol"; -import {LibOpUint256Div} from "./math/uint256/LibOpUint256Div.sol"; -import {LibOpUint256Mul} from "./math/uint256/LibOpUint256Mul.sol"; -import {LibOpUint256Pow} from "./math/uint256/LibOpUint256Pow.sol"; -import {LibOpUint256Sub} from "./math/uint256/LibOpUint256Sub.sol"; - import {LibOpAbs} from "./math/LibOpAbs.sol"; import {LibOpAdd} from "./math/LibOpAdd.sol"; import {LibOpAvg} from "./math/LibOpAvg.sol"; import {LibOpCeil} from "./math/LibOpCeil.sol"; -import {LibOpMul} from "./math/LibOpMul.sol"; import {LibOpDiv} from "./math/LibOpDiv.sol"; import {LibOpE} from "./math/LibOpE.sol"; import {LibOpExp} from "./math/LibOpExp.sol"; @@ -89,10 +77,21 @@ import {LibOpMaxPositiveValue} from "./math/LibOpMaxPositiveValue.sol"; import {LibOpMin} from "./math/LibOpMin.sol"; import {LibOpMinNegativeValue} from "./math/LibOpMinNegativeValue.sol"; import {LibOpMinPositiveValue} from "./math/LibOpMinPositiveValue.sol"; -import {LibOpPow} from "./math/LibOpPow.sol"; +import {LibOpMul} from "./math/LibOpMul.sol"; +import {LibOpPower} from "./math/LibOpPower.sol"; import {LibOpSqrt} from "./math/LibOpSqrt.sol"; import {LibOpSub} from "./math/LibOpSub.sol"; +import {LibOpExponentialGrowth} from "./math/growth/LibOpExponentialGrowth.sol"; +import {LibOpLinearGrowth} from "./math/growth/LibOpLinearGrowth.sol"; + +import {LibOpUint256Add} from "./math/uint256/LibOpUint256Add.sol"; +import {LibOpUint256Div} from "./math/uint256/LibOpUint256Div.sol"; +import {LibOpUint256MaxValue} from "./math/uint256/LibOpUint256MaxValue.sol"; +import {LibOpUint256Mul} from "./math/uint256/LibOpUint256Mul.sol"; +import {LibOpUint256Power} from "./math/uint256/LibOpUint256Power.sol"; +import {LibOpUint256Sub} from "./math/uint256/LibOpUint256Sub.sol"; + import {LibOpGet} from "./store/LibOpGet.sol"; import {LibOpSet} from "./store/LibOpSet.sol"; @@ -134,14 +133,11 @@ library LibAllStandardOps { "Copies a value from the context. The first operand is the context column and second is the context row." ), // These are all ordered according to how they appear in the file system. + // bitwise/ AuthoringMetaV2( "bitwise-and", "Bitwise AND the top two items on the stack. Probably does NOT do what you expect for decimal numbers." ), - AuthoringMetaV2( - "bitwise-or", - "Bitwise OR the top two items on the stack. Probably does NOT do what you expect for decimal numbers." - ), AuthoringMetaV2( "bitwise-count-ones", "Counts the number of binary bits set to 1 in the input. Probably does NOT do what you expect for decimal numbers." @@ -154,6 +150,10 @@ library LibAllStandardOps { "bitwise-encode", "Encodes a value into a 256 bit value. The first operand is the start bit and the second is the length. Probably does NOT do what you expect for decimal numbers." ), + AuthoringMetaV2( + "bitwise-or", + "Bitwise OR the top two items on the stack. Probably does NOT do what you expect for decimal numbers." + ), AuthoringMetaV2( "bitwise-shift-left", "Shifts the input left by the number of bits specified in the operand. Probably does NOT do what you expect for decimal numbers." @@ -162,38 +162,44 @@ library LibAllStandardOps { "bitwise-shift-right", "Shifts the input right by the number of bits specified in the operand. Probably does NOT do what you expect for decimal numbers." ), + // call/ AuthoringMetaV2( "call", "Calls a source by index in the same Rain bytecode. The inputs to call are copied to the top of the called stack and the outputs are copied back to the calling stack according to the LHS items. The first operand is the source index." ), + // crypto/ AuthoringMetaV2("hash", "Hashes all inputs into a single 32 byte value using keccak256."), + // erc20/ AuthoringMetaV2( - "uint256-erc20-allowance", - "Gets the allowance of an erc20 token for an account as a uint256 value. The first input is the token address, the second is the owner address, and the third is the spender address." + "erc20-allowance", + "Gets the allowance of an erc20 token for an account. The first input is the token address, the second is the owner address, and the third is the spender address. Lossy conversion to float so that \"infinite approve\" doesn't error." ), AuthoringMetaV2( - "uint256-erc20-balance-of", - "Gets the balance of an erc20 token for an account as a uint256 value. The first input is the token address and the second is the account address." + "erc20-balance-of", + "Gets the balance of an erc20 token for an account. The first input is the token address and the second is the account address." ), AuthoringMetaV2( - "uint256-erc20-total-supply", - "Gets the total supply of an erc20 token as a uint256 value. The input is the token address." + "erc20-total-supply", "Gets the total supply of an erc20 token. The input is the token address." ), + // erc20/uint256/ AuthoringMetaV2( - "erc20-allowance", - "Gets the allowance of an erc20 token for an account. The first input is the token address, the second is the owner address, and the third is the spender address. Lossy conversion to float so that \"infinite approve\" doesn't error." + "uint256-erc20-allowance", + "Gets the allowance of an erc20 token for an account as a uint256 value. The first input is the token address, the second is the owner address, and the third is the spender address." ), AuthoringMetaV2( - "erc20-balance-of", - "Gets the balance of an erc20 token for an account. The first input is the token address and the second is the account address." + "uint256-erc20-balance-of", + "Gets the balance of an erc20 token for an account as a uint256 value. The first input is the token address and the second is the account address." ), AuthoringMetaV2( - "erc20-total-supply", "Gets the total supply of an erc20 token. The input is the token address." + "uint256-erc20-total-supply", + "Gets the total supply of an erc20 token as a uint256 value. The input is the token address." ), + // erc5313/ AuthoringMetaV2( - "uint256-erc721-balance-of", - "Gets the balance of an erc721 token for an account as a uint256 value. The first input is the token address and the second is the account address. Returns a uint256 rather than a float." + "erc5313-owner", + "Gets the owner of an erc5313 compatible contract. Note that erc5313 specifically DOES NOT do any onchain compatibility checks, so the expression author is responsible for ensuring the contract is compatible. The input is the contract address to get the owner of." ), + // erc721/ AuthoringMetaV2( "erc721-balance-of", "Gets the balance of an erc721 token for an account. The first input is the token address and the second is the account address." @@ -202,15 +208,20 @@ library LibAllStandardOps { "erc721-owner-of", "Gets the owner of an erc721 token. The first input is the token address and the second is the token id." ), + // erc721/uint256/ AuthoringMetaV2( - "erc5313-owner", - "Gets the owner of an erc5313 compatible contract. Note that erc5313 specifically DOES NOT do any onchain compatibility checks, so the expression author is responsible for ensuring the contract is compatible. The input is the contract address to get the owner of." + "uint256-erc721-balance-of", + "Gets the balance of an erc721 token for an account as a uint256 value. The first input is the token address and the second is the account address. Returns a uint256 rather than a float." ), + // evm/ AuthoringMetaV2("block-number", "The current block number."), - AuthoringMetaV2("chain-id", "The current chain id."), AuthoringMetaV2("block-timestamp", "The current block timestamp."), + // now is an alias for block-timestamp. AuthoringMetaV2("now", "The current block timestamp."), + AuthoringMetaV2("chain-id", "The current chain id."), + // logic/ AuthoringMetaV2("any", "The first non-zero value out of all inputs, or 0 if every input is 0."), + AuthoringMetaV2("binary-equal-to", "1 if all inputs are equal, 0 otherwise. Equality is binary."), AuthoringMetaV2( "conditions", "Treats inputs as pairwise condition/value pairs. The first nonzero condition's value is used. If no conditions are nonzero, the expression reverts. Provide a constant nonzero value to define a fallback case. If the number of inputs is odd, the final value is used as an error string in the case that no conditions match." @@ -220,7 +231,6 @@ library LibAllStandardOps { "Reverts if the first input is 0. This has to be exactly binary 0 (i.e. NOT the number 0). The second input is a string that is used as the revert reason if the first input is 0. Has 0 outputs." ), AuthoringMetaV2("equal-to", "1 if all inputs are equal, 0 otherwise. Equality is numerical."), - AuthoringMetaV2("binary-equal-to", "1 if all inputs are equal, 0 otherwise. Equality is binary."), AuthoringMetaV2("every", "The last nonzero value out of all inputs, or 0 if any input is 0."), AuthoringMetaV2( "greater-than", "true if the first input is greater than the second input, false otherwise." @@ -241,36 +251,7 @@ library LibAllStandardOps { AuthoringMetaV2( "less-than-or-equal-to", "1 if the first input is less than or equal to the second input, 0 otherwise." ), - AuthoringMetaV2( - "exponential-growth", - "Calculates an exponential growth curve as `base(1 + rate)^t` where `base` is the initial value, `rate` is the rate of growth and `t` is units of time. Inputs in order are `base`, `rate`, and `t` respectively." - ), - AuthoringMetaV2( - "linear-growth", - "Calculates a linear growth curve as `base + (rate * t)` where `base` is the initial value, `rate` is the rate of growth and `t` is units of time. Inputs in order are `base`, `rate`, and `t` respectively." - ), - AuthoringMetaV2( - "uint256-max-value", "The maximum possible unsigned integer value (all binary bits are 1)." - ), - AuthoringMetaV2( - "uint256-add", - "Adds all inputs together as uint256 values. Errors if the addition exceeds `uint256-max-value()`." - ), - AuthoringMetaV2( - "uint256-div", - "Divides the first input by all other inputs as uint256 values. Errors if any divisor is zero. Rounds down." - ), - AuthoringMetaV2( - "uint256-mul", - "Multiplies all inputs together as uint256 values. Errors if the multiplication exceeds `uint256-max-value()`." - ), - AuthoringMetaV2( - "uint256-power", - "Raises the first input to the power of all other inputs as uint256 values. Errors if the exponentiation exceeds `uint256-max-value()`." - ), - AuthoringMetaV2( - "uint256-sub", "Subtracts all inputs from the first input as uint256 values. Errors on underflow." - ), + // math/ AuthoringMetaV2("abs", "The absolute value of a number."), AuthoringMetaV2("add", "Adds all numbers together."), AuthoringMetaV2("avg", "Arithmetic average (mean) of two numbers."), @@ -309,6 +290,39 @@ library LibAllStandardOps { AuthoringMetaV2("power", "Raises the first number to the power of the second number."), AuthoringMetaV2("sqrt", "Calculates the square root of the input. Errors if the input is negative."), AuthoringMetaV2("sub", "Subtracts all numbers from the first number."), + // math/growth/ + AuthoringMetaV2( + "exponential-growth", + "Calculates an exponential growth curve as `base(1 + rate)^t` where `base` is the initial value, `rate` is the rate of growth and `t` is units of time. Inputs in order are `base`, `rate`, and `t` respectively." + ), + AuthoringMetaV2( + "linear-growth", + "Calculates a linear growth curve as `base + (rate * t)` where `base` is the initial value, `rate` is the rate of growth and `t` is units of time. Inputs in order are `base`, `rate`, and `t` respectively." + ), + // math/uint256/ + AuthoringMetaV2( + "uint256-add", + "Adds all inputs together as uint256 values. Errors if the addition exceeds `uint256-max-value()`." + ), + AuthoringMetaV2( + "uint256-div", + "Divides the first input by all other inputs as uint256 values. Errors if any divisor is zero. Rounds down." + ), + AuthoringMetaV2( + "uint256-max-value", "The maximum possible unsigned integer value (all binary bits are 1)." + ), + AuthoringMetaV2( + "uint256-mul", + "Multiplies all inputs together as uint256 values. Errors if the multiplication exceeds `uint256-max-value()`." + ), + AuthoringMetaV2( + "uint256-power", + "Raises the first input to the power of all other inputs as uint256 values. Errors if the exponentiation exceeds `uint256-max-value()`." + ), + AuthoringMetaV2( + "uint256-sub", "Subtracts all inputs from the first input as uint256 values. Errors on underflow." + ), + // store/ AuthoringMetaV2("get", "Gets a value from storage. The first operand is the key to lookup."), AuthoringMetaV2( "set", @@ -380,14 +394,14 @@ library LibAllStandardOps { LibParseOperand.handleOperandDoublePerByteNoDefault, // bitwise-and LibParseOperand.handleOperandDisallowed, - // bitwise-or - LibParseOperand.handleOperandDisallowed, // bitwise-count-ones LibParseOperand.handleOperandDisallowed, // bitwise-decode LibParseOperand.handleOperandDoublePerByteNoDefault, // bitwise-encode LibParseOperand.handleOperandDoublePerByteNoDefault, + // bitwise-or + LibParseOperand.handleOperandDisallowed, // bitwise-shift-left LibParseOperand.handleOperandSingleFull, // bitwise-shift-right @@ -396,44 +410,44 @@ library LibAllStandardOps { LibParseOperand.handleOperandSingleFull, // hash LibParseOperand.handleOperandDisallowed, - // uint256-erc20-allowance - LibParseOperand.handleOperandDisallowed, - // uint256-erc20-balance-of - LibParseOperand.handleOperandDisallowed, - // uint256-erc20-total-supply - LibParseOperand.handleOperandDisallowed, // erc20-allowance LibParseOperand.handleOperandDisallowed, // erc20-balance-of LibParseOperand.handleOperandDisallowed, // erc20-total-supply LibParseOperand.handleOperandDisallowed, - // uint256-erc721-balance-of + // uint256-erc20-allowance + LibParseOperand.handleOperandDisallowed, + // uint256-erc20-balance-of + LibParseOperand.handleOperandDisallowed, + // uint256-erc20-total-supply + LibParseOperand.handleOperandDisallowed, + // erc5313-owner LibParseOperand.handleOperandDisallowed, // erc721-balance-of LibParseOperand.handleOperandDisallowed, // erc721-owner-of LibParseOperand.handleOperandDisallowed, - // erc5313-owner + // uint256-erc721-balance-of LibParseOperand.handleOperandDisallowed, // block-number LibParseOperand.handleOperandDisallowed, - // chain-id - LibParseOperand.handleOperandDisallowed, // block-timestamp LibParseOperand.handleOperandDisallowed, // now LibParseOperand.handleOperandDisallowed, + // chain-id + LibParseOperand.handleOperandDisallowed, // any LibParseOperand.handleOperandDisallowed, + // binary-equal-to + LibParseOperand.handleOperandDisallowed, // conditions LibParseOperand.handleOperandDisallowed, // ensure LibParseOperand.handleOperandDisallowed, // equal-to LibParseOperand.handleOperandDisallowed, - // binary-equal-to - LibParseOperand.handleOperandDisallowed, // every LibParseOperand.handleOperandDisallowed, // greater-than @@ -448,22 +462,6 @@ library LibAllStandardOps { LibParseOperand.handleOperandDisallowed, // less-than-or-equal-to LibParseOperand.handleOperandDisallowed, - // exponential-growth - LibParseOperand.handleOperandDisallowed, - // linear-growth - LibParseOperand.handleOperandDisallowed, - // uint256-max-value - LibParseOperand.handleOperandDisallowed, - // uint256-add - LibParseOperand.handleOperandDisallowed, - // uint256-div - LibParseOperand.handleOperandDisallowed, - // uint256-mul - LibParseOperand.handleOperandDisallowed, - // uint256-power - LibParseOperand.handleOperandDisallowed, - // uint256-sub - LibParseOperand.handleOperandDisallowed, // abs LibParseOperand.handleOperandDisallowed, // add @@ -510,6 +508,22 @@ library LibAllStandardOps { LibParseOperand.handleOperandDisallowed, // sub LibParseOperand.handleOperandDisallowed, + // exponential-growth + LibParseOperand.handleOperandDisallowed, + // linear-growth + LibParseOperand.handleOperandDisallowed, + // uint256-add + LibParseOperand.handleOperandDisallowed, + // uint256-div + LibParseOperand.handleOperandDisallowed, + // uint256-max-value + LibParseOperand.handleOperandDisallowed, + // uint256-mul + LibParseOperand.handleOperandDisallowed, + // uint256-power + LibParseOperand.handleOperandDisallowed, + // uint256-sub + LibParseOperand.handleOperandDisallowed, // get LibParseOperand.handleOperandDisallowed, // set @@ -551,34 +565,34 @@ library LibAllStandardOps { LibOpContext.integrity, // Everything else is alphabetical, including folders. LibOpBitwiseAnd.integrity, + LibOpBitwiseCountOnes.integrity, + LibOpBitwiseDecode.integrity, + LibOpBitwiseEncode.integrity, LibOpBitwiseOr.integrity, - LibOpCtPop.integrity, - LibOpDecodeBits.integrity, - LibOpEncodeBits.integrity, - LibOpShiftBitsLeft.integrity, - LibOpShiftBitsRight.integrity, + LibOpBitwiseShiftLeft.integrity, + LibOpBitwiseShiftRight.integrity, LibOpCall.integrity, LibOpHash.integrity, - LibOpUint256ERC20Allowance.integrity, - LibOpUint256ERC20BalanceOf.integrity, - LibOpUint256ERC20TotalSupply.integrity, LibOpERC20Allowance.integrity, LibOpERC20BalanceOf.integrity, LibOpERC20TotalSupply.integrity, - LibOpUint256ERC721BalanceOf.integrity, + LibOpUint256ERC20Allowance.integrity, + LibOpUint256ERC20BalanceOf.integrity, + LibOpUint256ERC20TotalSupply.integrity, + LibOpERC5313Owner.integrity, LibOpERC721BalanceOf.integrity, LibOpERC721OwnerOf.integrity, - LibOpERC5313Owner.integrity, + LibOpUint256ERC721BalanceOf.integrity, LibOpBlockNumber.integrity, - LibOpChainId.integrity, - LibOpTimestamp.integrity, + LibOpBlockTimestamp.integrity, // now - LibOpTimestamp.integrity, + LibOpBlockTimestamp.integrity, + LibOpChainId.integrity, LibOpAny.integrity, + LibOpBinaryEqualTo.integrity, LibOpConditions.integrity, LibOpEnsure.integrity, LibOpEqualTo.integrity, - LibOpBinaryEqualTo.integrity, LibOpEvery.integrity, LibOpGreaterThan.integrity, LibOpGreaterThanOrEqualTo.integrity, @@ -586,14 +600,6 @@ library LibAllStandardOps { LibOpIsZero.integrity, LibOpLessThan.integrity, LibOpLessThanOrEqualTo.integrity, - LibOpExponentialGrowth.integrity, - LibOpLinearGrowth.integrity, - LibOpMaxUint256.integrity, - LibOpUint256Add.integrity, - LibOpUint256Div.integrity, - LibOpUint256Mul.integrity, - LibOpUint256Pow.integrity, - LibOpUint256Sub.integrity, LibOpAbs.integrity, LibOpAdd.integrity, LibOpAvg.integrity, @@ -614,9 +620,17 @@ library LibAllStandardOps { LibOpMinNegativeValue.integrity, LibOpMinPositiveValue.integrity, LibOpMul.integrity, - LibOpPow.integrity, + LibOpPower.integrity, LibOpSqrt.integrity, LibOpSub.integrity, + LibOpExponentialGrowth.integrity, + LibOpLinearGrowth.integrity, + LibOpUint256Add.integrity, + LibOpUint256Div.integrity, + LibOpUint256MaxValue.integrity, + LibOpUint256Mul.integrity, + LibOpUint256Power.integrity, + LibOpUint256Sub.integrity, LibOpGet.integrity, LibOpSet.integrity ]; @@ -655,34 +669,34 @@ library LibAllStandardOps { LibOpContext.run, // Everything else is alphabetical, including folders. LibOpBitwiseAnd.run, + LibOpBitwiseCountOnes.run, + LibOpBitwiseDecode.run, + LibOpBitwiseEncode.run, LibOpBitwiseOr.run, - LibOpCtPop.run, - LibOpDecodeBits.run, - LibOpEncodeBits.run, - LibOpShiftBitsLeft.run, - LibOpShiftBitsRight.run, + LibOpBitwiseShiftLeft.run, + LibOpBitwiseShiftRight.run, LibOpCall.run, LibOpHash.run, - LibOpUint256ERC20Allowance.run, - LibOpUint256ERC20BalanceOf.run, - LibOpUint256ERC20TotalSupply.run, LibOpERC20Allowance.run, LibOpERC20BalanceOf.run, LibOpERC20TotalSupply.run, - LibOpUint256ERC721BalanceOf.run, + LibOpUint256ERC20Allowance.run, + LibOpUint256ERC20BalanceOf.run, + LibOpUint256ERC20TotalSupply.run, + LibOpERC5313Owner.run, LibOpERC721BalanceOf.run, LibOpERC721OwnerOf.run, - LibOpERC5313Owner.run, + LibOpUint256ERC721BalanceOf.run, LibOpBlockNumber.run, - LibOpChainId.run, - LibOpTimestamp.run, + LibOpBlockTimestamp.run, // now - LibOpTimestamp.run, + LibOpBlockTimestamp.run, + LibOpChainId.run, LibOpAny.run, + LibOpBinaryEqualTo.run, LibOpConditions.run, LibOpEnsure.run, LibOpEqualTo.run, - LibOpBinaryEqualTo.run, LibOpEvery.run, LibOpGreaterThan.run, LibOpGreaterThanOrEqualTo.run, @@ -690,14 +704,6 @@ library LibAllStandardOps { LibOpIsZero.run, LibOpLessThan.run, LibOpLessThanOrEqualTo.run, - LibOpExponentialGrowth.run, - LibOpLinearGrowth.run, - LibOpMaxUint256.run, - LibOpUint256Add.run, - LibOpUint256Div.run, - LibOpUint256Mul.run, - LibOpUint256Pow.run, - LibOpUint256Sub.run, LibOpAbs.run, LibOpAdd.run, LibOpAvg.run, @@ -718,9 +724,17 @@ library LibAllStandardOps { LibOpMinNegativeValue.run, LibOpMinPositiveValue.run, LibOpMul.run, - LibOpPow.run, + LibOpPower.run, LibOpSqrt.run, LibOpSub.run, + LibOpExponentialGrowth.run, + LibOpLinearGrowth.run, + LibOpUint256Add.run, + LibOpUint256Div.run, + LibOpUint256MaxValue.run, + LibOpUint256Mul.run, + LibOpUint256Power.run, + LibOpUint256Sub.run, LibOpGet.run, LibOpSet.run ]; diff --git a/src/lib/op/bitwise/LibOpCtPop.sol b/src/lib/op/bitwise/LibOpBitwiseCountOnes.sol similarity index 86% rename from src/lib/op/bitwise/LibOpCtPop.sol rename to src/lib/op/bitwise/LibOpBitwiseCountOnes.sol index 62bc265d3..897687c94 100644 --- a/src/lib/op/bitwise/LibOpCtPop.sol +++ b/src/lib/op/bitwise/LibOpBitwiseCountOnes.sol @@ -8,14 +8,11 @@ import {InterpreterState} from "../../state/LibInterpreterState.sol"; import {IntegrityCheckState} from "../../integrity/LibIntegrityCheck.sol"; import {LibCtPop} from "rain.math.binary/lib/LibCtPop.sol"; -/// @title LibOpCtPop -/// @notice An opcode that counts the number of bits set in a word. This is -/// called ctpop because that's the name of this kind of thing elsewhere, but -/// the more common name is "population count" or "Hamming weight". The word -/// in the standard ops lib is called `bitwise-count-ones`, which follows the -/// Rust naming convention. +/// @title LibOpBitwiseCountOnes +/// @notice An opcode that counts the number of bits set in a word. Also known +/// as "population count", "Hamming weight", or "ctpop". /// There is no evm opcode for this, so we have to implement it ourselves. -library LibOpCtPop { +library LibOpBitwiseCountOnes { /// @notice ctpop unconditionally takes one value and returns one value. /// @return The number of inputs. /// @return The number of outputs. diff --git a/src/lib/op/bitwise/LibOpDecodeBits.sol b/src/lib/op/bitwise/LibOpBitwiseDecode.sol similarity index 94% rename from src/lib/op/bitwise/LibOpDecodeBits.sol rename to src/lib/op/bitwise/LibOpBitwiseDecode.sol index df2deeccd..6d4bd04c5 100644 --- a/src/lib/op/bitwise/LibOpDecodeBits.sol +++ b/src/lib/op/bitwise/LibOpBitwiseDecode.sol @@ -6,12 +6,12 @@ import {IntegrityCheckState} from "../../integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {InterpreterState} from "../../state/LibInterpreterState.sol"; import {Pointer} from "rain.solmem/lib/LibPointer.sol"; -import {LibOpEncodeBits} from "./LibOpEncodeBits.sol"; +import {LibOpBitwiseEncode} from "./LibOpBitwiseEncode.sol"; -/// @title LibOpDecodeBits +/// @title LibOpBitwiseDecode /// @notice Opcode for decoding binary data from a 256 bit value that was encoded -/// with LibOpEncodeBits. -library LibOpDecodeBits { +/// with LibOpBitwiseEncode. +library LibOpBitwiseDecode { /// @notice Decode takes a single value and returns the decoded value. /// @param state The current integrity check state. /// @param operand The operand for this opcode. @@ -21,7 +21,7 @@ library LibOpDecodeBits { // Use exact same integrity check as encode other than the return values. // All we're interested in is the errors that might be thrown. //slither-disable-next-line unused-return - LibOpEncodeBits.integrity(state, operand); + LibOpBitwiseEncode.integrity(state, operand); return (1, 1); } diff --git a/src/lib/op/bitwise/LibOpEncodeBits.sol b/src/lib/op/bitwise/LibOpBitwiseEncode.sol similarity index 98% rename from src/lib/op/bitwise/LibOpEncodeBits.sol rename to src/lib/op/bitwise/LibOpBitwiseEncode.sol index 89bd60db0..a554e68e7 100644 --- a/src/lib/op/bitwise/LibOpEncodeBits.sol +++ b/src/lib/op/bitwise/LibOpBitwiseEncode.sol @@ -8,9 +8,9 @@ import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterp import {InterpreterState} from "../../state/LibInterpreterState.sol"; import {Pointer} from "rain.solmem/lib/LibPointer.sol"; -/// @title LibOpEncodeBits +/// @title LibOpBitwiseEncode /// @notice Opcode for encoding binary data into a 256 bit value. -library LibOpEncodeBits { +library LibOpBitwiseEncode { /// @notice Encode takes two values and returns one value. The first value is the /// source, the second value is the target. /// @param operand The operand encoding the start bit and length. diff --git a/src/lib/op/bitwise/LibOpShiftBitsLeft.sol b/src/lib/op/bitwise/LibOpBitwiseShiftLeft.sol similarity index 97% rename from src/lib/op/bitwise/LibOpShiftBitsLeft.sol rename to src/lib/op/bitwise/LibOpBitwiseShiftLeft.sol index c4b6ed779..660b5e987 100644 --- a/src/lib/op/bitwise/LibOpShiftBitsLeft.sol +++ b/src/lib/op/bitwise/LibOpBitwiseShiftLeft.sol @@ -8,10 +8,10 @@ import {InterpreterState} from "../../state/LibInterpreterState.sol"; import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {UnsupportedBitwiseShiftAmount} from "../../../error/ErrBitwise.sol"; -/// @title LibOpShiftBitsLeft +/// @title LibOpBitwiseShiftLeft /// @notice Opcode for shifting bits left. The shift amount is taken from the /// operand so it is compile time constant. -library LibOpShiftBitsLeft { +library LibOpBitwiseShiftLeft { /// @notice Shift bits left by the amount specified in the operand. /// @param operand The operand encoding the shift amount. /// @return The number of inputs. diff --git a/src/lib/op/bitwise/LibOpShiftBitsRight.sol b/src/lib/op/bitwise/LibOpBitwiseShiftRight.sol similarity index 97% rename from src/lib/op/bitwise/LibOpShiftBitsRight.sol rename to src/lib/op/bitwise/LibOpBitwiseShiftRight.sol index 1d6e699a1..752c7a354 100644 --- a/src/lib/op/bitwise/LibOpShiftBitsRight.sol +++ b/src/lib/op/bitwise/LibOpBitwiseShiftRight.sol @@ -8,10 +8,10 @@ import {InterpreterState} from "../../state/LibInterpreterState.sol"; import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {UnsupportedBitwiseShiftAmount} from "../../../error/ErrBitwise.sol"; -/// @title LibOpShiftBitsRight +/// @title LibOpBitwiseShiftRight /// @notice Opcode for shifting bits right. The shift amount is taken from the /// operand so it is compile time constant. -library LibOpShiftBitsRight { +library LibOpBitwiseShiftRight { /// @notice Shift bits right by the amount specified in the operand. /// @param operand The operand encoding the shift amount. /// @return The number of inputs. diff --git a/src/lib/op/evm/LibOpTimestamp.sol b/src/lib/op/evm/LibOpBlockTimestamp.sol similarity index 97% rename from src/lib/op/evm/LibOpTimestamp.sol rename to src/lib/op/evm/LibOpBlockTimestamp.sol index d15e9755e..98ee6f9a7 100644 --- a/src/lib/op/evm/LibOpTimestamp.sol +++ b/src/lib/op/evm/LibOpBlockTimestamp.sol @@ -8,9 +8,9 @@ import {InterpreterState} from "../../state/LibInterpreterState.sol"; import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {Float, LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol"; -/// @title LibOpTimestamp +/// @title LibOpBlockTimestamp /// @notice Implementation of the EVM `TIMESTAMP` opcode as a standard Rainlang opcode. -library LibOpTimestamp { +library LibOpBlockTimestamp { using LibDecimalFloat for Float; /// @notice `block-timestamp` integrity check. Requires 0 inputs and produces 1 output. diff --git a/src/lib/op/math/LibOpPow.sol b/src/lib/op/math/LibOpPower.sol similarity index 94% rename from src/lib/op/math/LibOpPow.sol rename to src/lib/op/math/LibOpPower.sol index 236e54d20..5c417307f 100644 --- a/src/lib/op/math/LibOpPow.sol +++ b/src/lib/op/math/LibOpPower.sol @@ -8,9 +8,9 @@ import {InterpreterState} from "../../state/LibInterpreterState.sol"; import {IntegrityCheckState} from "../../integrity/LibIntegrityCheck.sol"; import {LibDecimalFloat, Float} from "rain.math.float/lib/LibDecimalFloat.sol"; -/// @title LibOpPow -/// @notice Opcode to pow a decimal floating point value to a float decimal power. -library LibOpPow { +/// @title LibOpPower +/// @notice Opcode to raise a decimal floating point value to a float decimal power. +library LibOpPower { using LibDecimalFloat for Float; /// @notice `pow` integrity check. Requires exactly 2 inputs and produces 1 output. diff --git a/src/lib/op/math/uint256/LibOpMaxUint256.sol b/src/lib/op/math/uint256/LibOpUint256MaxValue.sol similarity index 96% rename from src/lib/op/math/uint256/LibOpMaxUint256.sol rename to src/lib/op/math/uint256/LibOpUint256MaxValue.sol index bd2b5f1e4..4dc8c8dca 100644 --- a/src/lib/op/math/uint256/LibOpMaxUint256.sol +++ b/src/lib/op/math/uint256/LibOpUint256MaxValue.sol @@ -7,9 +7,9 @@ import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterp import {InterpreterState} from "../../../state/LibInterpreterState.sol"; import {Pointer} from "rain.solmem/lib/LibPointer.sol"; -/// @title LibOpMaxUint256 +/// @title LibOpUint256MaxValue /// @notice Exposes `type(uint256).max` as a Rainlang opcode. -library LibOpMaxUint256 { +library LibOpUint256MaxValue { /// `max-uint256` integrity check. Requires 0 inputs and produces 1 output. function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { return (0, 1); diff --git a/src/lib/op/math/uint256/LibOpUint256Pow.sol b/src/lib/op/math/uint256/LibOpUint256Power.sol similarity index 98% rename from src/lib/op/math/uint256/LibOpUint256Pow.sol rename to src/lib/op/math/uint256/LibOpUint256Power.sol index 8f8acd420..2a500060b 100644 --- a/src/lib/op/math/uint256/LibOpUint256Pow.sol +++ b/src/lib/op/math/uint256/LibOpUint256Power.sol @@ -7,9 +7,9 @@ import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {InterpreterState} from "../../../state/LibInterpreterState.sol"; import {IntegrityCheckState} from "../../../integrity/LibIntegrityCheck.sol"; -/// @title LibOpUint256Pow +/// @title LibOpUint256Power /// @notice Opcode to raise x successively to N integers. Errors on overflow. -library LibOpUint256Pow { +library LibOpUint256Power { /// @notice `uint256-pow` integrity check. Requires at least 2 inputs and produces 1 output. /// @param operand Low 4 bits of the high byte encode the input count. /// @return The number of inputs. diff --git a/test/lib/string/LibCamelToKebab.sol b/test/lib/string/LibCamelToKebab.sol new file mode 100644 index 000000000..ec4c96d9d --- /dev/null +++ b/test/lib/string/LibCamelToKebab.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +library LibCamelToKebab { + /// @notice Converts a CamelCase string to kebab-case. + /// Rules (applied per character, matching sed behaviour): + /// 1. Insert hyphen between a run of uppercase and an uppercase followed + /// by lowercase (e.g. "HTMLParser" → "HTML-Parser"). + /// 2. Insert hyphen between a lowercase/digit and an uppercase + /// (e.g. "camelCase" → "camel-Case"). + /// All characters are lowercased in the output. + function camelToKebab(string memory input) internal pure returns (string memory) { + bytes memory src = bytes(input); + // Worst case: every char gets a hyphen before it. + bytes memory buf = new bytes(src.length * 2); + uint256 len; + + for (uint256 i; i < src.length; i++) { + uint8 c = uint8(src[i]); + + if (i > 0 && isUpper(c)) { + uint8 prev = uint8(src[i - 1]); + // Rule 2: lowercase/digit followed by uppercase. + if (isLower(prev) || isDigit(prev)) { + buf[len++] = bytes1("-"); + } + // Rule 1: uppercase followed by uppercase+lowercase + // (split before the last uppercase in a run). + else if (isUpper(prev) && i + 1 < src.length && isLower(uint8(src[i + 1]))) { + buf[len++] = bytes1("-"); + } + } + + buf[len++] = bytes1(toLower(c)); + } + + // Truncate buf to actual length. + assembly ("memory-safe") { + mstore(buf, len) + } + return string(buf); + } + + function isUpper(uint8 c) private pure returns (bool) { + return c >= 0x41 && c <= 0x5A; + } + + function isLower(uint8 c) private pure returns (bool) { + return c >= 0x61 && c <= 0x7A; + } + + function isDigit(uint8 c) private pure returns (bool) { + return c >= 0x30 && c <= 0x39; + } + + function toLower(uint8 c) private pure returns (uint8) { + if (isUpper(c)) return c + 0x20; + return c; + } +} diff --git a/test/lib/string/LibCamelToKebab.t.sol b/test/lib/string/LibCamelToKebab.t.sol new file mode 100644 index 000000000..99d254445 --- /dev/null +++ b/test/lib/string/LibCamelToKebab.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {LibCamelToKebab} from "./LibCamelToKebab.sol"; + +contract LibCamelToKebabTest is Test { + function k(string memory s) internal pure returns (string memory) { + return LibCamelToKebab.camelToKebab(s); + } + + function testSimple() external pure { + assertEq(k("BitwiseAnd"), "bitwise-and"); + } + + function testAcronymBeforeWord() external pure { + assertEq(k("ERC20Allowance"), "erc20-allowance"); + } + + function testAcronymWithDigitsBeforeWord() external pure { + assertEq(k("ERC721BalanceOf"), "erc721-balance-of"); + } + + function testLeadingUint() external pure { + assertEq(k("Uint256ERC20Allowance"), "uint256-erc20-allowance"); + } + + function testSingleChar() external pure { + assertEq(k("E"), "e"); + } + + function testShortWord() external pure { + assertEq(k("If"), "if"); + } + + function testMultiWord() external pure { + assertEq(k("GreaterThanOrEqualTo"), "greater-than-or-equal-to"); + } + + function testTrailingDigit() external pure { + assertEq(k("Exp2"), "exp2"); + } + + function testAllCaps() external pure { + assertEq(k("ERC5313Owner"), "erc5313-owner"); + } + + function testCtPop() external pure { + assertEq(k("CtPop"), "ct-pop"); + } + + function testMaxUint256() external pure { + assertEq(k("MaxUint256"), "max-uint256"); + } +} diff --git a/test/src/lib/op/LibAllStandardOps.filesystemOrdering.t.sol b/test/src/lib/op/LibAllStandardOps.filesystemOrdering.t.sol new file mode 100644 index 000000000..0548df590 --- /dev/null +++ b/test/src/lib/op/LibAllStandardOps.filesystemOrdering.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {LibAllStandardOps, ALL_STANDARD_OPS_LENGTH} from "src/lib/op/LibAllStandardOps.sol"; +import {AuthoringMetaV2} from "rain.interpreter.interface/interface/IParserV2.sol"; +import {LibCamelToKebab} from "test/lib/string/LibCamelToKebab.sol"; + +/// @title LibAllStandardOpsFilesystemOrderingTest +/// @notice Verifies that authoring meta word names (indices 4+) match the +/// filesystem ordering of LibOp*.sol files. The first 4 opcodes (stack, +/// constant, extern, context) have a fixed order for parsing and are excluded. +/// Alias words (multiple words sharing a single file, e.g. "now" aliasing +/// "block-timestamp") are skipped in the comparison. +contract LibAllStandardOpsFilesystemOrderingTest is Test { + /// Authoring meta words at indices 4+ must match the filesystem ordering + /// of LibOp*.sol files when converted from CamelCase to kebab-case. + function testAuthoringMetaMatchesFilesystemOrdering() external { + string[] memory cmd = new string[](3); + cmd[0] = "bash"; + cmd[1] = "-c"; + cmd[2] = "find src/lib/op -name 'LibOp*.sol' " "-not -name 'LibAllStandardOps.sol' " + "-not -name 'LibOpConstant.sol' " "-not -name 'LibOpContext.sol' " "-not -name 'LibOpExtern.sol' " + "-not -name 'LibOpStack.sol' " "| LC_ALL=C sort " + "| while read f; do basename \"$f\" .sol | sed 's/^LibOp//'; done"; + bytes memory raw = vm.ffi(cmd); + + bytes memory authoringMeta = LibAllStandardOps.authoringMetaV2(); + AuthoringMetaV2[] memory words = abi.decode(authoringMeta, (AuthoringMetaV2[])); + + // Walk both lists in parallel. When a word is an alias (does not + // correspond to its own file), skip it in the word list. + // vm.ffi() trims trailing whitespace so the last entry may lack a + // trailing newline — handle that with a final segment check. + uint256 start; + uint256 wordIdx = 4; + for (uint256 i; i <= raw.length; i++) { + bool isEnd = i == raw.length; + bool isNewline = !isEnd && raw[i] == 0x0a; + if (!isNewline && !isEnd) continue; + if (i == start) { + // Empty line. + start = i + 1; + continue; + } + bytes memory segment = new bytes(i - start); + for (uint256 j; j < segment.length; j++) { + segment[j] = raw[start + j]; + } + string memory camelName = string(segment); + string memory kebab = LibCamelToKebab.camelToKebab(camelName); + + // Skip alias words that share a file with the previous word. + while (wordIdx < words.length && isAlias(words[wordIdx].word)) { + wordIdx++; + } + assertTrue(wordIdx < words.length, "more files than words"); + + string memory word = bytes32ToString(words[wordIdx].word); + assertEq( + word, + kebab, + string.concat( + "word[", vm.toString(wordIdx), "] '", word, "' != file '", camelName, "' -> '", kebab, "'" + ) + ); + wordIdx++; + start = i + 1; + } + + // After matching all files, only alias words should remain. + while (wordIdx < words.length) { + assertTrue( + isAlias(words[wordIdx].word), string.concat("trailing non-alias word[", vm.toString(wordIdx), "]") + ); + wordIdx++; + } + } + + /// @notice Returns true if the word is a known alias (shares a file with + /// another word). Currently only "now" (alias for "block-timestamp"). + function isAlias(bytes32 word) internal pure returns (bool) { + return word == bytes32("now"); + } + + function bytes32ToString(bytes32 b) internal pure returns (string memory) { + uint256 len; + for (len = 0; len < 32; len++) { + if (b[len] == 0) break; + } + bytes memory s = new bytes(len); + for (uint256 i; i < len; i++) { + s[i] = b[i]; + } + return string(s); + } +} diff --git a/test/src/lib/op/LibAllStandardOps.t.sol b/test/src/lib/op/LibAllStandardOps.t.sol index b548cc98d..9c8b45b93 100644 --- a/test/src/lib/op/LibAllStandardOps.t.sol +++ b/test/src/lib/op/LibAllStandardOps.t.sol @@ -79,62 +79,72 @@ contract LibAllStandardOpsTest is Test { assertEq(words[3].word, bytes32("context")); // Verify every word name and ordering. + // bitwise/ //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[4].word, bytes32("bitwise-and")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[5].word, bytes32("bitwise-or")); + assertEq(words[5].word, bytes32("bitwise-count-ones")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[6].word, bytes32("bitwise-count-ones")); + assertEq(words[6].word, bytes32("bitwise-decode")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[7].word, bytes32("bitwise-decode")); + assertEq(words[7].word, bytes32("bitwise-encode")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[8].word, bytes32("bitwise-encode")); + assertEq(words[8].word, bytes32("bitwise-or")); //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[9].word, bytes32("bitwise-shift-left")); //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[10].word, bytes32("bitwise-shift-right")); + // call/ //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[11].word, bytes32("call")); + // crypto/ //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[12].word, bytes32("hash")); + // erc20/ //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[13].word, bytes32("uint256-erc20-allowance")); + assertEq(words[13].word, bytes32("erc20-allowance")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[14].word, bytes32("uint256-erc20-balance-of")); + assertEq(words[14].word, bytes32("erc20-balance-of")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[15].word, bytes32("uint256-erc20-total-supply")); + assertEq(words[15].word, bytes32("erc20-total-supply")); + // erc20/uint256/ //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[16].word, bytes32("erc20-allowance")); + assertEq(words[16].word, bytes32("uint256-erc20-allowance")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[17].word, bytes32("erc20-balance-of")); + assertEq(words[17].word, bytes32("uint256-erc20-balance-of")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[18].word, bytes32("erc20-total-supply")); + assertEq(words[18].word, bytes32("uint256-erc20-total-supply")); + // erc5313/ //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[19].word, bytes32("uint256-erc721-balance-of")); + assertEq(words[19].word, bytes32("erc5313-owner")); + // erc721/ //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[20].word, bytes32("erc721-balance-of")); //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[21].word, bytes32("erc721-owner-of")); + // erc721/uint256/ //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[22].word, bytes32("erc5313-owner")); + assertEq(words[22].word, bytes32("uint256-erc721-balance-of")); + // evm/ //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[23].word, bytes32("block-number")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[24].word, bytes32("chain-id")); + assertEq(words[24].word, bytes32("block-timestamp")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[25].word, bytes32("block-timestamp")); + assertEq(words[25].word, bytes32("now")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[26].word, bytes32("now")); + assertEq(words[26].word, bytes32("chain-id")); + // logic/ //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[27].word, bytes32("any")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[28].word, bytes32("conditions")); + assertEq(words[28].word, bytes32("binary-equal-to")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[29].word, bytes32("ensure")); + assertEq(words[29].word, bytes32("conditions")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[30].word, bytes32("equal-to")); + assertEq(words[30].word, bytes32("ensure")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[31].word, bytes32("binary-equal-to")); + assertEq(words[31].word, bytes32("equal-to")); //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[32].word, bytes32("every")); //forge-lint: disable-next-line(unsafe-typecast) @@ -149,68 +159,72 @@ contract LibAllStandardOpsTest is Test { assertEq(words[37].word, bytes32("less-than")); //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[38].word, bytes32("less-than-or-equal-to")); + // math/ //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[39].word, bytes32("exponential-growth")); + assertEq(words[39].word, bytes32("abs")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[40].word, bytes32("linear-growth")); + assertEq(words[40].word, bytes32("add")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[41].word, bytes32("uint256-max-value")); + assertEq(words[41].word, bytes32("avg")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[42].word, bytes32("uint256-add")); + assertEq(words[42].word, bytes32("ceil")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[43].word, bytes32("uint256-div")); + assertEq(words[43].word, bytes32("div")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[44].word, bytes32("uint256-mul")); + assertEq(words[44].word, bytes32("e")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[45].word, bytes32("uint256-power")); + assertEq(words[45].word, bytes32("exp")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[46].word, bytes32("uint256-sub")); + assertEq(words[46].word, bytes32("exp2")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[47].word, bytes32("abs")); + assertEq(words[47].word, bytes32("floor")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[48].word, bytes32("add")); + assertEq(words[48].word, bytes32("frac")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[49].word, bytes32("avg")); + assertEq(words[49].word, bytes32("gm")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[50].word, bytes32("ceil")); + assertEq(words[50].word, bytes32("headroom")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[51].word, bytes32("div")); + assertEq(words[51].word, bytes32("inv")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[52].word, bytes32("e")); + assertEq(words[52].word, bytes32("max")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[53].word, bytes32("exp")); + assertEq(words[53].word, bytes32("max-negative-value")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[54].word, bytes32("exp2")); + assertEq(words[54].word, bytes32("max-positive-value")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[55].word, bytes32("floor")); + assertEq(words[55].word, bytes32("min")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[56].word, bytes32("frac")); + assertEq(words[56].word, bytes32("min-negative-value")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[57].word, bytes32("gm")); + assertEq(words[57].word, bytes32("min-positive-value")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[58].word, bytes32("headroom")); + assertEq(words[58].word, bytes32("mul")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[59].word, bytes32("inv")); + assertEq(words[59].word, bytes32("power")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[60].word, bytes32("max")); + assertEq(words[60].word, bytes32("sqrt")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[61].word, bytes32("max-negative-value")); + assertEq(words[61].word, bytes32("sub")); + // math/growth/ //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[62].word, bytes32("max-positive-value")); + assertEq(words[62].word, bytes32("exponential-growth")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[63].word, bytes32("min")); + assertEq(words[63].word, bytes32("linear-growth")); + // math/uint256/ //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[64].word, bytes32("min-negative-value")); + assertEq(words[64].word, bytes32("uint256-add")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[65].word, bytes32("min-positive-value")); + assertEq(words[65].word, bytes32("uint256-div")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[66].word, bytes32("mul")); + assertEq(words[66].word, bytes32("uint256-max-value")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[67].word, bytes32("power")); + assertEq(words[67].word, bytes32("uint256-mul")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[68].word, bytes32("sqrt")); + assertEq(words[68].word, bytes32("uint256-power")); //forge-lint: disable-next-line(unsafe-typecast) - assertEq(words[69].word, bytes32("sub")); + assertEq(words[69].word, bytes32("uint256-sub")); + // store/ //forge-lint: disable-next-line(unsafe-typecast) assertEq(words[70].word, bytes32("get")); //forge-lint: disable-next-line(unsafe-typecast) diff --git a/test/src/lib/op/bitwise/LibOpCtPop.t.sol b/test/src/lib/op/bitwise/LibOpBitwiseCountOnes.t.sol similarity index 79% rename from test/src/lib/op/bitwise/LibOpCtPop.t.sol rename to test/src/lib/op/bitwise/LibOpBitwiseCountOnes.t.sol index 15de1b17a..548e55f6e 100644 --- a/test/src/lib/op/bitwise/LibOpCtPop.t.sol +++ b/test/src/lib/op/bitwise/LibOpBitwiseCountOnes.t.sol @@ -4,7 +4,7 @@ pragma solidity =0.8.25; import {OpTest} from "test/abstract/OpTest.sol"; import {IntegrityCheckState} from "src/lib/integrity/LibIntegrityCheck.sol"; -import {LibOpCtPop} from "src/lib/op/bitwise/LibOpCtPop.sol"; +import {LibOpBitwiseCountOnes} from "src/lib/op/bitwise/LibOpBitwiseCountOnes.sol"; import {InterpreterState} from "src/lib/state/LibInterpreterState.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {LibCtPop} from "rain.math.binary/lib/LibCtPop.sol"; @@ -13,23 +13,30 @@ import {UnexpectedOperand} from "src/error/ErrParse.sol"; import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -contract LibOpCtPopTest is OpTest { - /// Directly test the integrity logic of LibOpCtPop. All possible operands +contract LibOpBitwiseCountOnesTest is OpTest { + /// Directly test the integrity logic of LibOpBitwiseCountOnes. All possible operands /// result in the same number of inputs and outputs, (1, 1). function testOpCtPopIntegrity(IntegrityCheckState memory state, OperandV2 operand) external pure { - (uint256 calcInputs, uint256 calcOutputs) = LibOpCtPop.integrity(state, operand); + (uint256 calcInputs, uint256 calcOutputs) = LibOpBitwiseCountOnes.integrity(state, operand); assertEq(calcInputs, 1); assertEq(calcOutputs, 1); } - /// Directly test the runtime logic of LibOpCtPop. This tests that the + /// Directly test the runtime logic of LibOpBitwiseCountOnes. This tests that the /// opcode correctly pushes the ct pop onto the stack. function testOpCtPopRun(StackItem x) external view { InterpreterState memory state = opTestDefaultInterpreterState(); StackItem[] memory inputs = new StackItem[](1); inputs[0] = x; OperandV2 operand = LibOperand.build(1, 1, 0); - opReferenceCheck(state, operand, LibOpCtPop.referenceFn, LibOpCtPop.integrity, LibOpCtPop.run, inputs); + opReferenceCheck( + state, + operand, + LibOpBitwiseCountOnes.referenceFn, + LibOpBitwiseCountOnes.integrity, + LibOpBitwiseCountOnes.run, + inputs + ); } /// Test the eval of a ct pop opcode parsed from a string. diff --git a/test/src/lib/op/bitwise/LibOpDecodeBits.t.sol b/test/src/lib/op/bitwise/LibOpBitwiseDecode.t.sol similarity index 89% rename from test/src/lib/op/bitwise/LibOpDecodeBits.t.sol rename to test/src/lib/op/bitwise/LibOpBitwiseDecode.t.sol index 0ebd61f5e..a6d6b5bb1 100644 --- a/test/src/lib/op/bitwise/LibOpDecodeBits.t.sol +++ b/test/src/lib/op/bitwise/LibOpBitwiseDecode.t.sol @@ -7,19 +7,19 @@ import {IntegrityCheckState} from "src/lib/integrity/LibIntegrityCheck.sol"; import {InterpreterState} from "src/lib/state/LibInterpreterState.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {TruncatedBitwiseEncoding, ZeroLengthBitwiseEncoding} from "src/error/ErrBitwise.sol"; -import {LibOpDecodeBits} from "src/lib/op/bitwise/LibOpDecodeBits.sol"; +import {LibOpBitwiseDecode} from "src/lib/op/bitwise/LibOpBitwiseDecode.sol"; import {LibOperand} from "test/lib/operand/LibOperand.sol"; -contract LibOpDecodeBitsTest is OpTest { +contract LibOpBitwiseDecodeTest is OpTest { function integrityExternal(IntegrityCheckState memory state, OperandV2 operand) external pure returns (uint256, uint256) { - return LibOpDecodeBits.integrity(state, operand); + return LibOpBitwiseDecode.integrity(state, operand); } - /// Directly test the integrity logic of LibOpDecodeBits. All possible + /// Directly test the integrity logic of LibOpBitwiseDecode. All possible /// operands result in the same number of inputs and outputs, (2, 1). /// However, lengths can overflow and error so we bound the operand to avoid /// that here. @@ -39,12 +39,12 @@ contract LibOpDecodeBitsTest is OpTest { // Bounds ensure the typecast is safe. //forge-lint: disable-next-line(unsafe-typecast) OperandV2 operand = LibOperand.build(inputs, outputs, uint16((uint256(length) << 8) | uint256(start))); - (uint256 calcInputs, uint256 calcOutputs) = LibOpDecodeBits.integrity(state, operand); + (uint256 calcInputs, uint256 calcOutputs) = LibOpBitwiseDecode.integrity(state, operand); assertEq(calcInputs, 1); assertEq(calcOutputs, 1); } - /// Directly test the integrity logic of LibOpDecodeBits. This tests the + /// Directly test the integrity logic of LibOpBitwiseDecode. This tests the /// error when the length overflows. function testOpDecodeBitsIntegrityFail(IntegrityCheckState memory state, uint8 start8Bit, uint8 length8Bit) external @@ -58,7 +58,7 @@ contract LibOpDecodeBitsTest is OpTest { (calcInputs, calcOutputs); } - /// Directly test the integrity logic of LibOpDecodeBits. This tests the + /// Directly test the integrity logic of LibOpBitwiseDecode. This tests the /// error when the length is zero. function testOpDecodeBitsIntegrityFailZeroLength(IntegrityCheckState memory state, uint8 start) external { OperandV2 operand = OperandV2.wrap(bytes32(2 << 0x10 | 0 << 8 | uint256(start))); @@ -67,7 +67,7 @@ contract LibOpDecodeBitsTest is OpTest { (calcInputs, calcOutputs); } - /// Directly test the runtime logic of LibOpDecodeBits. This tests that the + /// Directly test the runtime logic of LibOpBitwiseDecode. This tests that the /// opcode correctly pushes the decoded bits onto the stack. function testOpDecodeBitsRun(StackItem value, uint8 start8Bit, uint8 length8Bit) external view { uint256 start = uint256(start8Bit); @@ -80,7 +80,7 @@ contract LibOpDecodeBitsTest is OpTest { inputs[0] = value; InterpreterState memory state = opTestDefaultInterpreterState(); opReferenceCheck( - state, operand, LibOpDecodeBits.referenceFn, LibOpDecodeBits.integrity, LibOpDecodeBits.run, inputs + state, operand, LibOpBitwiseDecode.referenceFn, LibOpBitwiseDecode.integrity, LibOpBitwiseDecode.run, inputs ); } diff --git a/test/src/lib/op/bitwise/LibOpEncodeBits.t.sol b/test/src/lib/op/bitwise/LibOpBitwiseEncode.t.sol similarity index 89% rename from test/src/lib/op/bitwise/LibOpEncodeBits.t.sol rename to test/src/lib/op/bitwise/LibOpBitwiseEncode.t.sol index a5d095d7d..818ac2687 100644 --- a/test/src/lib/op/bitwise/LibOpEncodeBits.t.sol +++ b/test/src/lib/op/bitwise/LibOpBitwiseEncode.t.sol @@ -7,19 +7,19 @@ import {IntegrityCheckState} from "src/lib/integrity/LibIntegrityCheck.sol"; import {InterpreterState} from "src/lib/state/LibInterpreterState.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {TruncatedBitwiseEncoding, ZeroLengthBitwiseEncoding} from "src/error/ErrBitwise.sol"; -import {LibOpEncodeBits} from "src/lib/op/bitwise/LibOpEncodeBits.sol"; +import {LibOpBitwiseEncode} from "src/lib/op/bitwise/LibOpBitwiseEncode.sol"; import {LibOperand} from "test/lib/operand/LibOperand.sol"; -contract LibOpEncodeBitsTest is OpTest { +contract LibOpBitwiseEncodeTest is OpTest { function integrityExternal(IntegrityCheckState memory state, OperandV2 operand) external pure returns (uint256 inputs, uint256 outputs) { - return LibOpEncodeBits.integrity(state, operand); + return LibOpBitwiseEncode.integrity(state, operand); } - /// Directly test the integrity logic of LibOpEncodeBits. All possible + /// Directly test the integrity logic of LibOpBitwiseEncode. All possible /// operands result in the same number of inputs and outputs, (2, 1). /// However, lengths can overflow and error so we bound the operand to avoid /// that here. @@ -34,12 +34,12 @@ contract LibOpEncodeBitsTest is OpTest { // Bounds ensure the typecast is safe. //forge-lint: disable-next-line(unsafe-typecast) OperandV2 operand = LibOperand.build(2, 1, uint16((uint256(length) << 8) | uint256(start))); - (uint256 calcInputs, uint256 calcOutputs) = LibOpEncodeBits.integrity(state, operand); + (uint256 calcInputs, uint256 calcOutputs) = LibOpBitwiseEncode.integrity(state, operand); assertEq(calcInputs, 2); assertEq(calcOutputs, 1); } - /// Directly test the integrity logic of LibOpEncodeBits. This tests the + /// Directly test the integrity logic of LibOpBitwiseEncode. This tests the /// error when the length overflows. function testOpEncodeBitsIntegrityFail(IntegrityCheckState memory state, uint8 start8Bit, uint8 length8Bit) external @@ -55,7 +55,7 @@ contract LibOpEncodeBitsTest is OpTest { (calcInputs, calcOutputs); } - /// Directly test the integrity logic of LibOpEncodeBits. This tests the + /// Directly test the integrity logic of LibOpBitwiseEncode. This tests the /// error when the length is zero. function testOpEncodeBitsIntegrityFailZeroLength(IntegrityCheckState memory state, uint8 start) external { OperandV2 operand = LibOperand.build(2, 1, uint16(0 << 8 | uint256(start))); @@ -64,7 +64,7 @@ contract LibOpEncodeBitsTest is OpTest { (calcInputs, calcOutputs); } - /// Directly test the runtime logic of LibOpEncodeBits. This tests that the + /// Directly test the runtime logic of LibOpBitwiseEncode. This tests that the /// opcode correctly pushes the encoded bits onto the stack. function testOpEncodeBitsRun(StackItem source, StackItem target, uint8 start8Bit, uint8 length8Bit) external view { uint256 start = uint256(start8Bit); @@ -79,7 +79,7 @@ contract LibOpEncodeBitsTest is OpTest { inputs[1] = target; InterpreterState memory state = opTestDefaultInterpreterState(); opReferenceCheck( - state, operand, LibOpEncodeBits.referenceFn, LibOpEncodeBits.integrity, LibOpEncodeBits.run, inputs + state, operand, LibOpBitwiseEncode.referenceFn, LibOpBitwiseEncode.integrity, LibOpBitwiseEncode.run, inputs ); } diff --git a/test/src/lib/op/bitwise/LibOpShiftBitsLeft.t.sol b/test/src/lib/op/bitwise/LibOpBitwiseShiftLeft.t.sol similarity index 89% rename from test/src/lib/op/bitwise/LibOpShiftBitsLeft.t.sol rename to test/src/lib/op/bitwise/LibOpBitwiseShiftLeft.t.sol index 352f66b28..3a31bcd6a 100644 --- a/test/src/lib/op/bitwise/LibOpShiftBitsLeft.t.sol +++ b/test/src/lib/op/bitwise/LibOpBitwiseShiftLeft.t.sol @@ -4,23 +4,23 @@ pragma solidity =0.8.25; import {OpTest} from "test/abstract/OpTest.sol"; import {IntegrityCheckState} from "src/lib/integrity/LibIntegrityCheck.sol"; -import {LibOpShiftBitsLeft} from "src/lib/op/bitwise/LibOpShiftBitsLeft.sol"; +import {LibOpBitwiseShiftLeft} from "src/lib/op/bitwise/LibOpBitwiseShiftLeft.sol"; import {InterpreterState} from "src/lib/state/LibInterpreterState.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {UnsupportedBitwiseShiftAmount} from "src/error/ErrBitwise.sol"; import {LibOperand} from "test/lib/operand/LibOperand.sol"; import {OperandOverflow} from "src/error/ErrParse.sol"; -contract LibOpShiftBitsLeftTest is OpTest { +contract LibOpBitwiseShiftLeftTest is OpTest { function integrityExternal(IntegrityCheckState memory state, OperandV2 operand) external pure returns (uint256, uint256) { - return LibOpShiftBitsLeft.integrity(state, operand); + return LibOpBitwiseShiftLeft.integrity(state, operand); } - /// Directly test the integrity logic of LibOpShiftBitsLeft. Tests the + /// Directly test the integrity logic of LibOpBitwiseShiftLeft. Tests the /// happy path where the integrity check does not error due to an unsupported /// shift amount. function testOpShiftBitsLeftIntegrityHappy( @@ -33,12 +33,12 @@ contract LibOpShiftBitsLeftTest is OpTest { inputs = uint8(bound(inputs, 1, 0x0F)); outputs = uint8(bound(outputs, 1, 0x0F)); OperandV2 operand = LibOperand.build(inputs, outputs, shiftAmount); - (uint256 calcInputs, uint256 calcOutputs) = LibOpShiftBitsLeft.integrity(state, operand); + (uint256 calcInputs, uint256 calcOutputs) = LibOpBitwiseShiftLeft.integrity(state, operand); assertEq(calcInputs, 1); assertEq(calcOutputs, 1); } - /// Directly test the execution logic of LibOpShiftBitsLeft. Tests that + /// Directly test the execution logic of LibOpBitwiseShiftLeft. Tests that /// any shift amount that always results in an output of 0 will error as /// an unsupported shift amount. function testOpShiftBitsLeftIntegrityZero(IntegrityCheckState memory state, uint8 inputs, uint16 shiftAmount16) @@ -54,7 +54,7 @@ contract LibOpShiftBitsLeftTest is OpTest { (calcInputs, calcOutputs); } - /// Directly test the execution logic of LibOpShiftBitsLeft. Tests that + /// Directly test the execution logic of LibOpBitwiseShiftLeft. Tests that /// any shift amount that is a noop (0) will error as an unsupported shift /// amount. function testOpShiftBitsLeftIntegrityNoop(IntegrityCheckState memory state, uint8 inputs) external { @@ -64,7 +64,7 @@ contract LibOpShiftBitsLeftTest is OpTest { (calcInputs, calcOutputs); } - /// Directly test the runtime logic of LibOpShiftBitsLeft. This tests that + /// Directly test the runtime logic of LibOpBitwiseShiftLeft. This tests that /// the opcode correctly shifts bits left. function testOpShiftBitsLeftRun(StackItem x, uint8 shiftAmount) external view { vm.assume(shiftAmount != 0); @@ -73,7 +73,12 @@ contract LibOpShiftBitsLeftTest is OpTest { inputs[0] = x; OperandV2 operand = LibOperand.build(1, 1, shiftAmount); opReferenceCheck( - state, operand, LibOpShiftBitsLeft.referenceFn, LibOpShiftBitsLeft.integrity, LibOpShiftBitsLeft.run, inputs + state, + operand, + LibOpBitwiseShiftLeft.referenceFn, + LibOpBitwiseShiftLeft.integrity, + LibOpBitwiseShiftLeft.run, + inputs ); } diff --git a/test/src/lib/op/bitwise/LibOpShiftBitsRight.t.sol b/test/src/lib/op/bitwise/LibOpBitwiseShiftRight.t.sol similarity index 89% rename from test/src/lib/op/bitwise/LibOpShiftBitsRight.t.sol rename to test/src/lib/op/bitwise/LibOpBitwiseShiftRight.t.sol index ea555a391..215b03228 100644 --- a/test/src/lib/op/bitwise/LibOpShiftBitsRight.t.sol +++ b/test/src/lib/op/bitwise/LibOpBitwiseShiftRight.t.sol @@ -4,23 +4,23 @@ pragma solidity =0.8.25; import {OpTest} from "test/abstract/OpTest.sol"; import {IntegrityCheckState} from "src/lib/integrity/LibIntegrityCheck.sol"; -import {LibOpShiftBitsRight} from "src/lib/op/bitwise/LibOpShiftBitsRight.sol"; +import {LibOpBitwiseShiftRight} from "src/lib/op/bitwise/LibOpBitwiseShiftRight.sol"; import {InterpreterState} from "src/lib/state/LibInterpreterState.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {UnsupportedBitwiseShiftAmount} from "src/error/ErrBitwise.sol"; import {LibOperand} from "test/lib/operand/LibOperand.sol"; import {OperandOverflow} from "src/error/ErrParse.sol"; -contract LibOpShiftBitsRightTest is OpTest { +contract LibOpBitwiseShiftRightTest is OpTest { function integrityExternal(IntegrityCheckState memory state, OperandV2 operand) external pure returns (uint256, uint256) { - return LibOpShiftBitsRight.integrity(state, operand); + return LibOpBitwiseShiftRight.integrity(state, operand); } - /// Directly test the integrity logic of LibOpShiftBitsRight. Tests the + /// Directly test the integrity logic of LibOpBitwiseShiftRight. Tests the /// happy path where the integrity check does not error due to an unsupported /// shift amount. /// forge-config: default.fuzz.runs = 100 @@ -34,12 +34,12 @@ contract LibOpShiftBitsRightTest is OpTest { inputs = uint8(bound(inputs, 1, 0x0F)); outputs = uint8(bound(outputs, 1, 0x0F)); OperandV2 operand = LibOperand.build(inputs, outputs, shiftAmount); - (uint256 calcInputs, uint256 calcOutputs) = LibOpShiftBitsRight.integrity(state, operand); + (uint256 calcInputs, uint256 calcOutputs) = LibOpBitwiseShiftRight.integrity(state, operand); assertEq(calcInputs, 1); assertEq(calcOutputs, 1); } - /// Directly test the execution logic of LibOpShiftBitsRight. Tests that + /// Directly test the execution logic of LibOpBitwiseShiftRight. Tests that /// any shift amount that always results in an output of 0 will error as /// an unsupported shift amount. /// forge-config: default.fuzz.runs = 100 @@ -53,7 +53,7 @@ contract LibOpShiftBitsRightTest is OpTest { (calcInputs, calcOutputs); } - /// Directly test the execution logic of LibOpShiftBitsRight. Tests that + /// Directly test the execution logic of LibOpBitwiseShiftRight. Tests that /// any shift amount that is a noop (0) will error as an unsupported shift /// amount. /// forge-config: default.fuzz.runs = 100 @@ -64,7 +64,7 @@ contract LibOpShiftBitsRightTest is OpTest { (calcInputs, calcOutputs); } - /// Directly test the runtime logic of LibOpShiftBitsRight. This tests that + /// Directly test the runtime logic of LibOpBitwiseShiftRight. This tests that /// the opcode correctly shifts bits right. function testOpShiftBitsRightRun(StackItem x, uint8 shiftAmount) external view { vm.assume(shiftAmount != 0); @@ -75,9 +75,9 @@ contract LibOpShiftBitsRightTest is OpTest { opReferenceCheck( state, operand, - LibOpShiftBitsRight.referenceFn, - LibOpShiftBitsRight.integrity, - LibOpShiftBitsRight.run, + LibOpBitwiseShiftRight.referenceFn, + LibOpBitwiseShiftRight.integrity, + LibOpBitwiseShiftRight.run, inputs ); } diff --git a/test/src/lib/op/evm/LibOpTimestamp.t.sol b/test/src/lib/op/evm/LibOpBlockTimestamp.t.sol similarity index 87% rename from test/src/lib/op/evm/LibOpTimestamp.t.sol rename to test/src/lib/op/evm/LibOpBlockTimestamp.t.sol index 302a65ef6..c8ba6ee75 100644 --- a/test/src/lib/op/evm/LibOpTimestamp.t.sol +++ b/test/src/lib/op/evm/LibOpBlockTimestamp.t.sol @@ -13,12 +13,12 @@ import {FullyQualifiedNamespace} from "rain.interpreter.interface/interface/IInt import {SignedContextV1} from "rain.interpreter.interface/interface/IInterpreterCallerV4.sol"; import {LibContext} from "rain.interpreter.interface/lib/caller/LibContext.sol"; -import {LibOpTimestamp} from "src/lib/op/evm/LibOpTimestamp.sol"; +import {LibOpBlockTimestamp} from "src/lib/op/evm/LibOpBlockTimestamp.sol"; import {LibOperand} from "test/lib/operand/LibOperand.sol"; -/// @title LibOpTimestampTest -/// @notice Test the runtime and integrity time logic of LibOpTimestamp. -contract LibOpTimestampTest is OpTest { +/// @title LibOpBlockTimestampTest +/// @notice Test the runtime and integrity time logic of LibOpBlockTimestamp. +contract LibOpBlockTimestampTest is OpTest { using LibPointer for Pointer; using LibStackPointer for Pointer; using LibInterpreterState for InterpreterState; @@ -30,7 +30,7 @@ contract LibOpTimestampTest is OpTest { return words; } - /// Directly test the integrity logic of LibOpTimestamp. + /// Directly test the integrity logic of LibOpBlockTimestamp. function testOpTimestampIntegrity(IntegrityCheckState memory state, uint8 inputs, uint8 outputs, uint16 operandData) external pure @@ -38,13 +38,13 @@ contract LibOpTimestampTest is OpTest { inputs = uint8(bound(inputs, 0, 0x0F)); outputs = uint8(bound(outputs, 0, 0x0F)); (uint256 calcInputs, uint256 calcOutputs) = - LibOpTimestamp.integrity(state, LibOperand.build(inputs, outputs, operandData)); + LibOpBlockTimestamp.integrity(state, LibOperand.build(inputs, outputs, operandData)); assertEq(calcInputs, 0); assertEq(calcOutputs, 1); } - /// Directly test the runtime logic of LibOpTimestamp. This tests that the + /// Directly test the runtime logic of LibOpBlockTimestamp. This tests that the /// opcode correctly pushes the timestamp onto the stack. function testOpTimestampRun(uint256 blockTimestamp) external { blockTimestamp = bound(blockTimestamp, 0, uint128(type(int128).max)); @@ -53,7 +53,12 @@ contract LibOpTimestampTest is OpTest { StackItem[] memory inputs = new StackItem[](0); OperandV2 operand = LibOperand.build(0, 1, 0); opReferenceCheck( - state, operand, LibOpTimestamp.referenceFn, LibOpTimestamp.integrity, LibOpTimestamp.run, inputs + state, + operand, + LibOpBlockTimestamp.referenceFn, + LibOpBlockTimestamp.integrity, + LibOpBlockTimestamp.run, + inputs ); } diff --git a/test/src/lib/op/math/LibOpPow.t.sol b/test/src/lib/op/math/LibOpPower.t.sol similarity index 90% rename from test/src/lib/op/math/LibOpPow.t.sol rename to test/src/lib/op/math/LibOpPower.t.sol index ae565638d..9d145f69b 100644 --- a/test/src/lib/op/math/LibOpPow.t.sol +++ b/test/src/lib/op/math/LibOpPower.t.sol @@ -3,26 +3,26 @@ pragma solidity =0.8.25; import {OpTest, IntegrityCheckState, OperandV2, InterpreterState, UnexpectedOperand} from "test/abstract/OpTest.sol"; -import {LibOpPow} from "src/lib/op/math/LibOpPow.sol"; +import {LibOpPower} from "src/lib/op/math/LibOpPower.sol"; import {LibOperand} from "test/lib/operand/LibOperand.sol"; import {LibDecimalFloat, Float} from "rain.math.float/lib/LibDecimalFloat.sol"; import {StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {PowNegativeBase} from "rain.math.float/error/ErrDecimalFloat.sol"; -contract LibOpPowTest is OpTest { +contract LibOpPowerTest is OpTest { function beforeOpTestConstructor() internal virtual override { vm.createSelectFork(vm.envString("ETH_RPC_URL")); } - /// Directly test the integrity logic of LibOpPow. + /// Directly test the integrity logic of LibOpPower. /// Inputs are always 2, outputs are always 1. function testOpPowIntegrity(IntegrityCheckState memory state, OperandV2 operand) external pure { - (uint256 calcInputs, uint256 calcOutputs) = LibOpPow.integrity(state, operand); + (uint256 calcInputs, uint256 calcOutputs) = LibOpPower.integrity(state, operand); assertEq(calcInputs, 2); assertEq(calcOutputs, 1); } - /// Directly test the runtime logic of LibOpPow. + /// Directly test the runtime logic of LibOpPower. function testOpPowRun(int224 signedCoefficientA, int32 exponentA, int224 signedCoefficientB, int32 exponentB) public view @@ -40,7 +40,7 @@ contract LibOpPowTest is OpTest { inputs[0] = StackItem.wrap(Float.unwrap(a)); inputs[1] = StackItem.wrap(Float.unwrap(b)); - opReferenceCheck(state, operand, LibOpPow.referenceFn, LibOpPow.integrity, LibOpPow.run, inputs); + opReferenceCheck(state, operand, LibOpPower.referenceFn, LibOpPower.integrity, LibOpPower.run, inputs); } /// Test the eval of `power`. diff --git a/test/src/lib/op/math/uint256/LibOpMaxUint256.t.sol b/test/src/lib/op/math/uint256/LibOpUint256MaxValue.t.sol similarity index 75% rename from test/src/lib/op/math/uint256/LibOpMaxUint256.t.sol rename to test/src/lib/op/math/uint256/LibOpUint256MaxValue.t.sol index c2ed40a84..43c69972c 100644 --- a/test/src/lib/op/math/uint256/LibOpMaxUint256.t.sol +++ b/test/src/lib/op/math/uint256/LibOpUint256MaxValue.t.sol @@ -3,18 +3,18 @@ pragma solidity =0.8.25; import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; -import {LibOpMaxUint256} from "src/lib/op/math/uint256/LibOpMaxUint256.sol"; +import {LibOpUint256MaxValue} from "src/lib/op/math/uint256/LibOpUint256MaxValue.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {InterpreterState, LibInterpreterState} from "src/lib/state/LibInterpreterState.sol"; import {LibOperand} from "test/lib/operand/LibOperand.sol"; -/// @title LibOpMaxUint256Test -/// @notice Test the runtime and integrity time logic of LibOpMaxUint256. -contract LibOpMaxUint256Test is OpTest { +/// @title LibOpUint256MaxValueTest +/// @notice Test the runtime and integrity time logic of LibOpUint256MaxValue. +contract LibOpUint256MaxValueTest is OpTest { using LibInterpreterState for InterpreterState; - /// Directly test the integrity logic of LibOpMaxUint256. + /// Directly test the integrity logic of LibOpUint256MaxValue. function testOpMaxUint256Integrity( IntegrityCheckState memory state, uint8 inputs, @@ -24,24 +24,29 @@ contract LibOpMaxUint256Test is OpTest { inputs = uint8(bound(inputs, 0, 0x0F)); outputs = uint8(bound(outputs, 0, 0x0F)); (uint256 calcInputs, uint256 calcOutputs) = - LibOpMaxUint256.integrity(state, LibOperand.build(inputs, outputs, operandData)); + LibOpUint256MaxValue.integrity(state, LibOperand.build(inputs, outputs, operandData)); assertEq(calcInputs, 0); assertEq(calcOutputs, 1); } - /// Directly test the runtime logic of LibOpMaxUint256. This tests that the + /// Directly test the runtime logic of LibOpUint256MaxValue. This tests that the /// opcode correctly pushes the max uint256 onto the stack. function testOpMaxUint256Run() external view { InterpreterState memory state = opTestDefaultInterpreterState(); StackItem[] memory inputs = new StackItem[](0); OperandV2 operand = LibOperand.build(0, 1, 0); opReferenceCheck( - state, operand, LibOpMaxUint256.referenceFn, LibOpMaxUint256.integrity, LibOpMaxUint256.run, inputs + state, + operand, + LibOpUint256MaxValue.referenceFn, + LibOpUint256MaxValue.integrity, + LibOpUint256MaxValue.run, + inputs ); } - /// Test the eval of LibOpMaxUint256 parsed from a string. + /// Test the eval of LibOpUint256MaxValue parsed from a string. function testOpMaxUint256Eval() external view { checkHappy("_: uint256-max-value();", bytes32(type(uint256).max), ""); } diff --git a/test/src/lib/op/math/uint256/LibOpUint256Pow.t.sol b/test/src/lib/op/math/uint256/LibOpUint256Power.t.sol similarity index 94% rename from test/src/lib/op/math/uint256/LibOpUint256Pow.t.sol rename to test/src/lib/op/math/uint256/LibOpUint256Power.t.sol index eee8a1ea4..9797d0b34 100644 --- a/test/src/lib/op/math/uint256/LibOpUint256Pow.t.sol +++ b/test/src/lib/op/math/uint256/LibOpUint256Power.t.sol @@ -4,14 +4,14 @@ pragma solidity =0.8.25; import {stdError} from "forge-std/Test.sol"; import {OpTest} from "test/abstract/OpTest.sol"; -import {LibOpUint256Pow} from "src/lib/op/math/uint256/LibOpUint256Pow.sol"; +import {LibOpUint256Power} from "src/lib/op/math/uint256/LibOpUint256Power.sol"; import {IntegrityCheckState} from "src/lib/integrity/LibIntegrityCheck.sol"; import {InterpreterState} from "src/lib/state/LibInterpreterState.sol"; import {StackItem, OperandV2} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {LibOperand} from "test/lib/operand/LibOperand.sol"; -contract LibOpUint256PowTest is OpTest { - /// Directly test the integrity logic of LibOpUint256Pow. This tests the happy +contract LibOpUint256PowerTest is OpTest { + /// Directly test the integrity logic of LibOpUint256Power. This tests the happy /// path where the inputs input and calc match. function testOpUint256ExpIntegrityHappy(IntegrityCheckState memory state, uint8 inputs, uint16 operandData) external @@ -19,26 +19,26 @@ contract LibOpUint256PowTest is OpTest { { inputs = uint8(bound(inputs, 2, 0x0F)); (uint256 calcInputs, uint256 calcOutputs) = - LibOpUint256Pow.integrity(state, LibOperand.build(inputs, 1, operandData)); + LibOpUint256Power.integrity(state, LibOperand.build(inputs, 1, operandData)); assertEq(calcInputs, inputs); assertEq(calcOutputs, 1); } - /// Directly test the integrity logic of LibOpUint256Pow. This tests the unhappy + /// Directly test the integrity logic of LibOpUint256Power. This tests the unhappy /// path where the operand is invalid due to 0 inputs. function testOpUint256PowIntegrityUnhappyZeroInputs(IntegrityCheckState memory state) external pure { - (uint256 calcInputs, uint256 calcOutputs) = LibOpUint256Pow.integrity(state, OperandV2.wrap(0)); + (uint256 calcInputs, uint256 calcOutputs) = LibOpUint256Power.integrity(state, OperandV2.wrap(0)); // Calc inputs will be minimum 2. assertEq(calcInputs, 2); assertEq(calcOutputs, 1); } - /// Directly test the integrity logic of LibOpUint256Pow. This tests the unhappy + /// Directly test the integrity logic of LibOpUint256Power. This tests the unhappy /// path where the operand is invalid due to 1 inputs. function testOpUint256PowIntegrityUnhappyOneInput(IntegrityCheckState memory state) external pure { (uint256 calcInputs, uint256 calcOutputs) = - LibOpUint256Pow.integrity(state, OperandV2.wrap(bytes32(uint256(0x010000)))); + LibOpUint256Power.integrity(state, OperandV2.wrap(bytes32(uint256(0x010000)))); // Calc inputs will be minimum 2. assertEq(calcInputs, 2); assertEq(calcOutputs, 1); @@ -47,11 +47,11 @@ contract LibOpUint256PowTest is OpTest { function _testOpUint256PowRun(OperandV2 operand, StackItem[] memory inputs) external view { InterpreterState memory state = opTestDefaultInterpreterState(); opReferenceCheck( - state, operand, LibOpUint256Pow.referenceFn, LibOpUint256Pow.integrity, LibOpUint256Pow.run, inputs + state, operand, LibOpUint256Power.referenceFn, LibOpUint256Power.integrity, LibOpUint256Power.run, inputs ); } - /// Directly test the runtime logic of LibOpUint256Pow. + /// Directly test the runtime logic of LibOpUint256Power. function testOpUint256PowRun(StackItem[] memory inputs) external { vm.assume(inputs.length >= 2); vm.assume(inputs.length <= 0x0F); diff --git a/test/src/lib/parse/LibParseOperand.handleOperand.t.sol b/test/src/lib/parse/LibParseOperand.handleOperand.t.sol index 0029f2012..32d8673d7 100644 --- a/test/src/lib/parse/LibParseOperand.handleOperand.t.sol +++ b/test/src/lib/parse/LibParseOperand.handleOperand.t.sol @@ -18,7 +18,7 @@ contract LibParseOperandHandleOperandTest is Test { } /// Both handleOperandSingleFull (index 1, stack) and - /// handleOperandDisallowed (index 5, bitwise-and) return 0 for empty + /// handleOperandDisallowed (index 4, bitwise-and) return 0 for empty /// operand values. function testHandleOperandDispatchEmptyValues() external pure { ParseState memory state = LibParseState.newState("", "", "", ""); @@ -27,13 +27,13 @@ contract LibParseOperandHandleOperandTest is Test { // Index 1 (stack) -> handleOperandSingleFull -> returns 0. assertEq(OperandV2.unwrap(state.handleOperand(1)), 0, "stack empty"); - // Index 5 (bitwise-and) -> handleOperandDisallowed -> returns 0. - assertEq(OperandV2.unwrap(state.handleOperand(5)), 0, "bitwise-and empty"); + // Index 4 (bitwise-and) -> handleOperandDisallowed -> returns 0. + assertEq(OperandV2.unwrap(state.handleOperand(4)), 0, "bitwise-and empty"); } /// Prove different indices dispatch to different handlers: index 1 (stack, /// handleOperandSingleFull) accepts a single value and returns it; - /// index 5 (bitwise-and, handleOperandDisallowed) reverts. + /// index 4 (bitwise-and, handleOperandDisallowed) reverts. function testHandleOperandDispatchDifferentHandlers(uint256 value) external { value = bound(value, 0, type(uint16).max); ParseState memory state = LibParseState.newState("", "", "", ""); @@ -46,9 +46,9 @@ contract LibParseOperandHandleOperandTest is Test { // Index 1 (stack) -> handleOperandSingleFull -> returns the value. assertEq(OperandV2.unwrap(state.handleOperand(1)), bytes32(value), "stack single value"); - // Index 5 (bitwise-and) -> handleOperandDisallowed -> reverts. + // Index 4 (bitwise-and) -> handleOperandDisallowed -> reverts. vm.expectRevert(abi.encodeWithSelector(UnexpectedOperand.selector)); - this.handleOperandExternal(state, 5); + this.handleOperandExternal(state, 4); } /// Multiple indices that share the same handler produce the same result. @@ -77,15 +77,15 @@ contract LibParseOperandHandleOperandTest is Test { values[0] = bytes32(uint256(1)); state.operandValues = values; - // Index 5 (bitwise-and), 6 (bitwise-or), 13 (hash) all use + // Index 4 (bitwise-and), 8 (bitwise-or), 12 (hash) all use // handleOperandDisallowed. vm.expectRevert(abi.encodeWithSelector(UnexpectedOperand.selector)); - this.handleOperandExternal(state, 5); + this.handleOperandExternal(state, 4); vm.expectRevert(abi.encodeWithSelector(UnexpectedOperand.selector)); - this.handleOperandExternal(state, 6); + this.handleOperandExternal(state, 8); vm.expectRevert(abi.encodeWithSelector(UnexpectedOperand.selector)); - this.handleOperandExternal(state, 13); + this.handleOperandExternal(state, 12); } } From fce469f6a4f4f0de422226fbed7b55d9012e5a0a Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Tue, 3 Mar 2026 23:56:32 +0400 Subject: [PATCH 05/13] Add missing operand-disallowed tests for 15 opcodes P2-01: 10 logic opcodes (greater-than, less-than, greater-than-or-equal-to, less-than-or-equal-to, equal-to, binary-equal-to, is-zero, if, any, every) P2-02: 4 math opcodes (max-positive-value, max-negative-value, min-positive-value, min-negative-value) P2-03: hash Co-Authored-By: Claude Opus 4.6 --- audit/2026-03-01-01/triage.md | 6 +++--- test/src/lib/op/crypto/LibOpHash.t.sol | 7 ++++++- test/src/lib/op/logic/LibOpAny.t.sol | 7 ++++++- test/src/lib/op/logic/LibOpBinaryEqualTo.t.sol | 7 ++++++- test/src/lib/op/logic/LibOpEqualTo.t.sol | 7 ++++++- test/src/lib/op/logic/LibOpEvery.t.sol | 7 ++++++- test/src/lib/op/logic/LibOpGreaterThan.t.sol | 7 ++++++- test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol | 9 ++++++++- test/src/lib/op/logic/LibOpIf.t.sol | 7 ++++++- test/src/lib/op/logic/LibOpIsZero.t.sol | 7 ++++++- test/src/lib/op/logic/LibOpLessThan.t.sol | 7 ++++++- test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol | 7 ++++++- test/src/lib/op/math/LibOpMaxNegativeValue.t.sol | 7 ++++++- test/src/lib/op/math/LibOpMaxPositiveValue.t.sol | 7 ++++++- test/src/lib/op/math/LibOpMinNegativeValue.t.sol | 7 ++++++- test/src/lib/op/math/LibOpMinPositiveValue.t.sol | 7 ++++++- 16 files changed, 95 insertions(+), 18 deletions(-) diff --git a/audit/2026-03-01-01/triage.md b/audit/2026-03-01-01/triage.md index d92e5c5eb..3b8f8fe7b 100644 --- a/audit/2026-03-01-01/triage.md +++ b/audit/2026-03-01-01/triage.md @@ -56,9 +56,9 @@ - [FIXED] A44-1-P2: (LOW) subParseWordSlice no test for no-sub-parsers-registered path - [DISMISSED] P2-EAD-01: (LOW) BaseRainterpreterSubParser.subParseWord2 missing happy-path and no-match tests — both paths tested in RainterpreterReferenceExtern.intInc.t.sol (lines 78-81 happy, 122-127 no-match) - [FIXED] P2-EAD-02: (LOW) authoringMetaV2 word names not verified beyond index 3 -- [PENDING] P2-01: (LOW) Missing operand-disallowed tests for 10 logic opcodes -- [PENDING] P2-02: (LOW) Missing operand-disallowed tests for 5 math opcodes -- [PENDING] P2-03: (LOW) Missing operand-disallowed test for LibOpHash +- [FIXED] P2-01: (LOW) Missing operand-disallowed tests for 10 logic opcodes +- [FIXED] P2-02: (LOW) Missing operand-disallowed tests for 5 math opcodes — LibOpSub already had test; added tests for LibOpMaxPositiveValue, LibOpMaxNegativeValue, LibOpMinPositiveValue, LibOpMinNegativeValue +- [FIXED] P2-03: (LOW) Missing operand-disallowed test for LibOpHash - [PENDING] R02-PASS2-01: (LOW) No tests for error paths in Forker methods - [PENDING] R02-PASS2-02: (LOW) Forker::new() has no test - [PENDING] R02-PASS2-04: (LOW) RainSourceTrace::from_data() edge cases untested diff --git a/test/src/lib/op/crypto/LibOpHash.t.sol b/test/src/lib/op/crypto/LibOpHash.t.sol index acac6a262..28a7a5879 100644 --- a/test/src/lib/op/crypto/LibOpHash.t.sol +++ b/test/src/lib/op/crypto/LibOpHash.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpHash} from "src/lib/op/crypto/LibOpHash.sol"; import {LibContext} from "rain.interpreter.interface/lib/caller/LibContext.sol"; @@ -110,4 +110,9 @@ contract LibOpHashTest is OpTest { function testOpHashTwoOutputs() external { checkBadOutputs("_ _: hash();", 0, 1, 2); } + + /// Test that operand is disallowed. + function testOpHashEvalOperandDisallowed() external { + checkUnhappyParse("_: hash<0>(1);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/logic/LibOpAny.t.sol b/test/src/lib/op/logic/LibOpAny.t.sol index 2ed61224a..155ebaeea 100644 --- a/test/src/lib/op/logic/LibOpAny.t.sol +++ b/test/src/lib/op/logic/LibOpAny.t.sol @@ -6,7 +6,7 @@ import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {LibUint256Array} from "rain.solmem/lib/LibUint256Array.sol"; import {MemoryKV} from "rain.lib.memkv/lib/LibMemoryKV.sol"; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpAny} from "src/lib/op/logic/LibOpAny.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import { @@ -148,4 +148,9 @@ contract LibOpAnyTest is OpTest { function testOpAnyTwoOutputs() external { checkBadOutputs("_ _: any(0);", 1, 1, 2); } + + /// Test that operand is disallowed. + function testOpAnyEvalOperandDisallowed() external { + checkUnhappyParse("_: any<0>(1);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/logic/LibOpBinaryEqualTo.t.sol b/test/src/lib/op/logic/LibOpBinaryEqualTo.t.sol index acebfe2b5..017c6287e 100644 --- a/test/src/lib/op/logic/LibOpBinaryEqualTo.t.sol +++ b/test/src/lib/op/logic/LibOpBinaryEqualTo.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpBinaryEqualTo} from "src/lib/op/logic/LibOpBinaryEqualTo.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -101,4 +101,9 @@ contract LibOpBinaryEqualToTest is OpTest { function testOpBinaryEqualToTwoOutputs() external { checkBadOutputs("_ _: binary-equal-to(0 0);", 2, 1, 2); } + + /// Test that operand is disallowed. + function testOpBinaryEqualToEvalOperandDisallowed() external { + checkUnhappyParse("_: binary-equal-to<0>(1 2);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/logic/LibOpEqualTo.t.sol b/test/src/lib/op/logic/LibOpEqualTo.t.sol index 52c79a584..84f0f4adf 100644 --- a/test/src/lib/op/logic/LibOpEqualTo.t.sol +++ b/test/src/lib/op/logic/LibOpEqualTo.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpEqualTo} from "src/lib/op/logic/LibOpEqualTo.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -102,4 +102,9 @@ contract LibOpEqualToTest is OpTest { function testOpEqualToTwoOutputs() external { checkBadOutputs("_ _: equal-to(0 0);", 2, 1, 2); } + + /// Test that operand is disallowed. + function testOpEqualToEvalOperandDisallowed() external { + checkUnhappyParse("_: equal-to<0>(1 2);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/logic/LibOpEvery.t.sol b/test/src/lib/op/logic/LibOpEvery.t.sol index 36f1cb87d..46a72cb53 100644 --- a/test/src/lib/op/logic/LibOpEvery.t.sol +++ b/test/src/lib/op/logic/LibOpEvery.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpEvery} from "src/lib/op/logic/LibOpEvery.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -99,4 +99,9 @@ contract LibOpEveryTest is OpTest { function testOpEveryTwoOutputs() external { checkBadOutputs("_ _: every(5);", 1, 1, 2); } + + /// Test that operand is disallowed. + function testOpEveryEvalOperandDisallowed() external { + checkUnhappyParse("_: every<0>(1);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/logic/LibOpGreaterThan.t.sol b/test/src/lib/op/logic/LibOpGreaterThan.t.sol index f0373acd5..962194698 100644 --- a/test/src/lib/op/logic/LibOpGreaterThan.t.sol +++ b/test/src/lib/op/logic/LibOpGreaterThan.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpGreaterThan} from "src/lib/op/logic/LibOpGreaterThan.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -113,4 +113,9 @@ contract LibOpGreaterThanTest is OpTest { function testOpGreaterThanTwoOutputs() external { checkBadOutputs("_ _: greater-than(1 2);", 2, 1, 2); } + + /// Test that operand is disallowed. + function testOpGreaterThanEvalOperandDisallowed() external { + checkUnhappyParse("_: greater-than<0>(1 2);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol b/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol index ffee55d58..eaee221e1 100644 --- a/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol +++ b/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpGreaterThanOrEqualTo} from "src/lib/op/logic/LibOpGreaterThanOrEqualTo.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -111,4 +111,11 @@ contract LibOpGreaterThanOrEqualToTest is OpTest { function testOpGreaterThanOrEqualToTwoOutputs() external { checkBadOutputs("_ _: greater-than-or-equal-to(1 2);", 2, 1, 2); } + + /// Test that operand is disallowed. + function testOpGreaterThanOrEqualToEvalOperandDisallowed() external { + checkUnhappyParse( + "_: greater-than-or-equal-to<0>(1 2);", abi.encodeWithSelector(UnexpectedOperand.selector) + ); + } } diff --git a/test/src/lib/op/logic/LibOpIf.t.sol b/test/src/lib/op/logic/LibOpIf.t.sol index 085ee3d13..d6083a05c 100644 --- a/test/src/lib/op/logic/LibOpIf.t.sol +++ b/test/src/lib/op/logic/LibOpIf.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpIf} from "src/lib/op/logic/LibOpIf.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -139,4 +139,9 @@ contract LibOpIfTest is OpTest { function testOpIfEvalTwoOutputs() external { checkBadOutputs("_ _: if(5 0 0);", 3, 1, 2); } + + /// Test that operand is disallowed. + function testOpIfEvalOperandDisallowed() external { + checkUnhappyParse("_: if<0>(1 2 3);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/logic/LibOpIsZero.t.sol b/test/src/lib/op/logic/LibOpIsZero.t.sol index 4e3eb7383..4fec36cf0 100644 --- a/test/src/lib/op/logic/LibOpIsZero.t.sol +++ b/test/src/lib/op/logic/LibOpIsZero.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {LibOpIsZero} from "src/lib/op/logic/LibOpIsZero.sol"; import {InterpreterState} from "src/lib/state/LibInterpreterState.sol"; @@ -73,4 +73,9 @@ contract LibOpIsZeroTest is OpTest { function testOpIsZeroTwoOutputs() external { checkBadOutputs("_ _: is-zero(30);", 1, 1, 2); } + + /// Test that operand is disallowed. + function testOpIsZeroEvalOperandDisallowed() external { + checkUnhappyParse("_: is-zero<0>(1);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/logic/LibOpLessThan.t.sol b/test/src/lib/op/logic/LibOpLessThan.t.sol index 10d17f87e..996583f56 100644 --- a/test/src/lib/op/logic/LibOpLessThan.t.sol +++ b/test/src/lib/op/logic/LibOpLessThan.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpLessThan} from "src/lib/op/logic/LibOpLessThan.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {InterpreterState} from "src/lib/state/LibInterpreterState.sol"; @@ -111,4 +111,9 @@ contract LibOpLessThanTest is OpTest { function testOpLessThanTwoOutputs() external { checkBadOutputs("_ _: less-than(30 0);", 2, 1, 2); } + + /// Test that operand is disallowed. + function testOpLessThanEvalOperandDisallowed() external { + checkUnhappyParse("_: less-than<0>(1 2);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol b/test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol index bcc479e6c..ea5fda68b 100644 --- a/test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol +++ b/test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpLessThanOrEqualTo} from "src/lib/op/logic/LibOpLessThanOrEqualTo.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import { @@ -187,4 +187,9 @@ contract LibOpLessThanOrEqualToTest is OpTest { function testOpLessThanOrEqualToTwoOutputs() external { checkBadOutputs("_ _: less-than-or-equal-to(1 2);", 2, 1, 2); } + + /// Test that operand is disallowed. + function testOpLessThanOrEqualToEvalOperandDisallowed() external { + checkUnhappyParse("_: less-than-or-equal-to<0>(1 2);", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/math/LibOpMaxNegativeValue.t.sol b/test/src/lib/op/math/LibOpMaxNegativeValue.t.sol index 54ef89440..d3f942b46 100644 --- a/test/src/lib/op/math/LibOpMaxNegativeValue.t.sol +++ b/test/src/lib/op/math/LibOpMaxNegativeValue.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpMaxNegativeValue} from "src/lib/op/math/LibOpMaxNegativeValue.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -65,4 +65,9 @@ contract LibOpMaxNegativeValueTest is OpTest { function testOpMaxNegativeValueTwoOutputs() external { checkBadOutputs("_ _: max-negative-value();", 0, 1, 2); } + + /// Test that operand is disallowed. + function testOpMaxNegativeValueEvalOperandDisallowed() external { + checkUnhappyParse("_: max-negative-value<0>();", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/math/LibOpMaxPositiveValue.t.sol b/test/src/lib/op/math/LibOpMaxPositiveValue.t.sol index 53de2987c..f479815c3 100644 --- a/test/src/lib/op/math/LibOpMaxPositiveValue.t.sol +++ b/test/src/lib/op/math/LibOpMaxPositiveValue.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpMaxPositiveValue} from "src/lib/op/math/LibOpMaxPositiveValue.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -67,4 +67,9 @@ contract LibOpMaxPositiveValueTest is OpTest { function testOpMaxPositiveValueTwoOutputs() external { checkBadOutputs("_ _: max-positive-value();", 0, 1, 2); } + + /// Test that operand is disallowed. + function testOpMaxPositiveValueEvalOperandDisallowed() external { + checkUnhappyParse("_: max-positive-value<0>();", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/math/LibOpMinNegativeValue.t.sol b/test/src/lib/op/math/LibOpMinNegativeValue.t.sol index 2c0b7f574..3963a3421 100644 --- a/test/src/lib/op/math/LibOpMinNegativeValue.t.sol +++ b/test/src/lib/op/math/LibOpMinNegativeValue.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpMinNegativeValue} from "src/lib/op/math/LibOpMinNegativeValue.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -67,4 +67,9 @@ contract LibOpMinNegativeValueTest is OpTest { function testOpMinNegativeValueTwoOutputs() external { checkBadOutputs("_ _: min-negative-value();", 0, 1, 2); } + + /// Test that operand is disallowed. + function testOpMinNegativeValueEvalOperandDisallowed() external { + checkUnhappyParse("_: min-negative-value<0>();", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } diff --git a/test/src/lib/op/math/LibOpMinPositiveValue.t.sol b/test/src/lib/op/math/LibOpMinPositiveValue.t.sol index 0fb0479a1..6a55e0256 100644 --- a/test/src/lib/op/math/LibOpMinPositiveValue.t.sol +++ b/test/src/lib/op/math/LibOpMinPositiveValue.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {OpTest} from "test/abstract/OpTest.sol"; +import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol"; import {LibOpMinPositiveValue} from "src/lib/op/math/LibOpMinPositiveValue.sol"; import {IntegrityCheckState, BadOpInputsLength} from "src/lib/integrity/LibIntegrityCheck.sol"; import {OperandV2, StackItem} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; @@ -67,4 +67,9 @@ contract LibOpMinPositiveValueTest is OpTest { function testOpMinPositiveValueTwoOutputs() external { checkBadOutputs("_ _: min-positive-value();", 0, 1, 2); } + + /// Test that operand is disallowed. + function testOpMinPositiveValueEvalOperandDisallowed() external { + checkUnhappyParse("_: min-positive-value<0>();", abi.encodeWithSelector(UnexpectedOperand.selector)); + } } From 96003c457378fdbe6fca3c0fcdfc2ea201d969b8 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 4 Mar 2026 00:05:11 +0400 Subject: [PATCH 06/13] Fix NatSpec for Pass 5 findings and triage all 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P5-EXPGROWTH-01: Add @notice and @return tags to integrity in exponential-growth, linear-growth, uint256-max-value. P5-UINT256POW-01: Clarify left-to-right associativity in uint256-power library NatSpec. P5-HEADROOM-01: Dismissed — Rain-invented opcode, well tested. Co-Authored-By: Claude Opus 4.6 --- audit/2026-03-01-01/triage.md | 6 ++++++ src/lib/op/math/growth/LibOpExponentialGrowth.sol | 4 +++- src/lib/op/math/growth/LibOpLinearGrowth.sol | 4 +++- src/lib/op/math/uint256/LibOpUint256MaxValue.sol | 6 ++++-- src/lib/op/math/uint256/LibOpUint256Power.sol | 3 ++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/audit/2026-03-01-01/triage.md b/audit/2026-03-01-01/triage.md index 3b8f8fe7b..11e14595d 100644 --- a/audit/2026-03-01-01/triage.md +++ b/audit/2026-03-01-01/triage.md @@ -121,3 +121,9 @@ - [PENDING] P4-RUST-01: (LOW) Unused dependencies serde_json, reqwest, once_cell in eval Cargo.toml - [PENDING] P4-RUST-02: (LOW) Wildcard use alloy::primitives::* in dispair and parser - [PENDING] P4-RUST-03: (LOW) Duplicated error-handling logic in alloy_call/alloy_call_committing with inconsistent format + +## Pass 5: Correctness + +- [DISMISSED] P5-HEADROOM-01: (INFO) Headroom semantics for negative non-integers may surprise users — Rain-invented opcode, well tested including negative cases, semantics defined by NatSpec +- [FIXED] P5-EXPGROWTH-01: (INFO) integrity NatSpec in exponential-growth, linear-growth, uint256-max-value missing explicit @notice tag +- [FIXED] P5-UINT256POW-01: (INFO) uint256-power left-to-right associativity NatSpec could be clearer diff --git a/src/lib/op/math/growth/LibOpExponentialGrowth.sol b/src/lib/op/math/growth/LibOpExponentialGrowth.sol index 00b29639b..095002ca6 100644 --- a/src/lib/op/math/growth/LibOpExponentialGrowth.sol +++ b/src/lib/op/math/growth/LibOpExponentialGrowth.sol @@ -14,7 +14,9 @@ import {Float, LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol"; library LibOpExponentialGrowth { using LibDecimalFloat for Float; - /// `exponential-growth` integrity check. Requires exactly 3 inputs and produces 1 output. + /// @notice `exponential-growth` integrity check. Requires exactly 3 inputs and produces 1 output. + /// @return The number of inputs. + /// @return The number of outputs. function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { // There must be three inputs and one output. return (3, 1); diff --git a/src/lib/op/math/growth/LibOpLinearGrowth.sol b/src/lib/op/math/growth/LibOpLinearGrowth.sol index fb7f35e49..d2c1a4ee2 100644 --- a/src/lib/op/math/growth/LibOpLinearGrowth.sol +++ b/src/lib/op/math/growth/LibOpLinearGrowth.sol @@ -14,7 +14,9 @@ import {Float, LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol"; library LibOpLinearGrowth { using LibDecimalFloat for Float; - /// `linear-growth` integrity check. Requires exactly 3 inputs and produces 1 output. + /// @notice `linear-growth` integrity check. Requires exactly 3 inputs and produces 1 output. + /// @return The number of inputs. + /// @return The number of outputs. function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { // There must be three inputs and one output. return (3, 1); diff --git a/src/lib/op/math/uint256/LibOpUint256MaxValue.sol b/src/lib/op/math/uint256/LibOpUint256MaxValue.sol index 4dc8c8dca..359a0fe61 100644 --- a/src/lib/op/math/uint256/LibOpUint256MaxValue.sol +++ b/src/lib/op/math/uint256/LibOpUint256MaxValue.sol @@ -10,7 +10,9 @@ import {Pointer} from "rain.solmem/lib/LibPointer.sol"; /// @title LibOpUint256MaxValue /// @notice Exposes `type(uint256).max` as a Rainlang opcode. library LibOpUint256MaxValue { - /// `max-uint256` integrity check. Requires 0 inputs and produces 1 output. + /// @notice `uint256-max-value` integrity check. Requires 0 inputs and produces 1 output. + /// @return The number of inputs. + /// @return The number of outputs. function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { return (0, 1); } @@ -27,7 +29,7 @@ library LibOpUint256MaxValue { return stackTop; } - /// Reference implementation of `max-uint256` for testing. + /// @notice Reference implementation of `uint256-max-value` for testing. function referenceFn(InterpreterState memory, OperandV2, StackItem[] memory) internal pure diff --git a/src/lib/op/math/uint256/LibOpUint256Power.sol b/src/lib/op/math/uint256/LibOpUint256Power.sol index 2a500060b..b898f7d41 100644 --- a/src/lib/op/math/uint256/LibOpUint256Power.sol +++ b/src/lib/op/math/uint256/LibOpUint256Power.sol @@ -8,7 +8,8 @@ import {InterpreterState} from "../../../state/LibInterpreterState.sol"; import {IntegrityCheckState} from "../../../integrity/LibIntegrityCheck.sol"; /// @title LibOpUint256Power -/// @notice Opcode to raise x successively to N integers. Errors on overflow. +/// @notice Opcode for left-to-right uint256 exponentiation, i.e. `((a**b)**c)`. +/// Errors on overflow. library LibOpUint256Power { /// @notice `uint256-pow` integrity check. Requires at least 2 inputs and produces 1 output. /// @param operand Low 4 bits of the high byte encode the input count. From f5ef3a422ac5c96f13c6c1a6ecab20df50294333 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 4 Mar 2026 00:16:56 +0400 Subject: [PATCH 07/13] Fix Pass 3 NatSpec findings: @notice tags, @return tags, and parameter docs - P3-ERR-1: Add @notice to 17 errors across ErrBitwise, ErrEval, ErrExtern, ErrParse - P3-EA-01: Change "word dispatches" to "opcode dispatches" in BaseRainterpreterExtern - P3-EA-02: Add @notice and @return to buildOpcode/IntegrityFunctionPointers - P3-EA-03: Name parameters and add @param/@return to 3 context op subParser functions Co-Authored-By: Claude Opus 4.6 --- audit/2026-03-01-01/triage.md | 10 ++-- src/abstract/BaseRainterpreterExtern.sol | 4 +- .../extern/RainterpreterReferenceExtern.sol | 28 ++++++----- src/error/ErrBitwise.sol | 4 +- src/error/ErrEval.sol | 2 +- src/error/ErrExtern.sol | 2 +- src/error/ErrParse.sol | 48 +++++++++---------- .../op/LibExternOpContextCallingContract.sol | 17 +++++-- .../op/LibExternOpContextRainlen.sol | 18 +++++-- .../reference/op/LibExternOpContextSender.sol | 17 +++++-- 10 files changed, 94 insertions(+), 56 deletions(-) diff --git a/audit/2026-03-01-01/triage.md b/audit/2026-03-01-01/triage.md index 11e14595d..9c4bfface 100644 --- a/audit/2026-03-01-01/triage.md +++ b/audit/2026-03-01-01/triage.md @@ -66,10 +66,10 @@ ## Pass 3: Documentation -- [PENDING] P3-ERR-1: (LOW) 16 errors use plain /// without @notice while siblings use @notice -- [PENDING] P3-EA-01: (LOW) "word dispatches" should be "opcode dispatches" in BaseRainterpreterExtern NatSpec -- [PENDING] P3-EA-02: (LOW) Missing @return tags on buildOpcodeFunctionPointers/buildIntegrityFunctionPointers -- [PENDING] P3-EA-03: (LOW) Three context op subParser functions have unnamed parameters and no @param/@return +- [FIXED] P3-ERR-1: (LOW) 16 errors use plain /// without @notice while siblings use @notice — added @notice to 17 errors across ErrBitwise, ErrEval, ErrExtern, ErrParse +- [FIXED] P3-EA-01: (LOW) "word dispatches" should be "opcode dispatches" in BaseRainterpreterExtern NatSpec +- [FIXED] P3-EA-02: (LOW) Missing @return tags on buildOpcodeFunctionPointers/buildIntegrityFunctionPointers +- [FIXED] P3-EA-03: (LOW) Three context op subParser functions have unnamed parameters and no @param/@return - [PENDING] P3-EA-04: (LOW) Undocumented CONTEXT_CALLER_CONTEXT constants in LibExternOpContextRainlen - [PENDING] P3-EA-05: (LOW) Typo "determin" in SUB_PARSER_LITERAL_REPEAT_KEYWORD_MASK NatSpec - [PENDING] P3-LPS-01: (LOW) LibParseState library missing library-level NatSpec @@ -77,7 +77,7 @@ - [PENDING] P3-LPP-02: (LOW) 4 pragma keyword constants missing NatSpec - [PENDING] P3-LPST-02: (LOW) ParseStackTracker user-defined type missing NatSpec - [PENDING] P3-LPL-02: (LOW) 5 literal parser index constants missing NatSpec -- [PENDING] P3-ERR-01: (LOW) 13 errors in ErrParse.sol have doc comments but no explicit @notice tag +- [FIXED] P3-ERR-01: (LOW) 13 errors in ErrParse.sol have doc comments but no explicit @notice tag — fixed as part of P3-ERR-1 - [PENDING] P3-CC-01: (LOW) opcodeFunctionPointers has untagged lines before @return tag - [PENDING] P3-CC-02: (LOW) buildOperandHandlerFunctionPointers/buildLiteralParserFunctionPointers use bare /// instead of @inheritdoc - [PENDING] P3-CC-03: (LOW) Three internal virtual functions missing @return tags in RainterpreterParser diff --git a/src/abstract/BaseRainterpreterExtern.sol b/src/abstract/BaseRainterpreterExtern.sol index b1edbcafb..69d245fe1 100644 --- a/src/abstract/BaseRainterpreterExtern.sol +++ b/src/abstract/BaseRainterpreterExtern.sol @@ -115,8 +115,8 @@ abstract contract BaseRainterpreterExtern is IInterpreterExternV4, IIntegrityToo || super.supportsInterface(interfaceId); } - /// Overrideable function to provide the list of function pointers for - /// word dispatches. + /// @notice Overrideable function to provide the list of function pointers + /// for opcode dispatches. //slither-disable-next-line dead-code function opcodeFunctionPointers() internal view virtual returns (bytes memory) { return OPCODE_FUNCTION_POINTERS; diff --git a/src/concrete/extern/RainterpreterReferenceExtern.sol b/src/concrete/extern/RainterpreterReferenceExtern.sol index ba55a35b4..86d5b4365 100644 --- a/src/concrete/extern/RainterpreterReferenceExtern.sol +++ b/src/concrete/extern/RainterpreterReferenceExtern.sol @@ -354,14 +354,16 @@ contract RainterpreterReferenceExtern is BaseRainterpreterSubParser, BaseRainter } } - /// This mimics how LibAllStandardOps builds function pointers for the - /// Rainterpreter. The same pattern applies to externs but for a different - /// function signature for each opcode. Call this function somehow, e.g. from - /// within a test, and then copy the output into the + /// @notice Builds the opcode function pointer table for this extern + /// contract. This mimics how LibAllStandardOps builds function pointers for + /// the Rainterpreter. The same pattern applies to externs but for a + /// different function signature for each opcode. Call this function somehow, + /// e.g. from within a test, and then copy the output into the /// `OPCODE_FUNCTION_POINTERS` if there is a mismatch. This makes the /// function pointer lookup much more gas efficient. The reason this can't be /// done within the test itself is that the pointers need to be calculated /// relative to the bytecode of the current contract, not the test contract. + /// @return The packed 16-bit function pointers for each extern opcode. function buildOpcodeFunctionPointers() external pure returns (bytes memory) { unchecked { function(OperandV2, StackItem[] memory) internal view returns (StackItem[] memory) lengthPointer; @@ -386,14 +388,16 @@ contract RainterpreterReferenceExtern is BaseRainterpreterSubParser, BaseRainter } } - /// This applies the same pattern to integrity function pointers as the - /// opcode and parser function pointers on this same contract. Call this - /// function somehow, e.g. from within a test, and then check there is no - /// mismatch with the `INTEGRITY_FUNCTION_POINTERS` constant. This makes the - /// function pointer lookup at runtime much more gas efficient by allowing - /// it to be constant. The reason this can't be done within the test itself - /// is that the pointers need to be calculated relative to the bytecode of - /// the current contract, not the test contract. + /// @notice Builds the integrity function pointer table for this extern + /// contract. This applies the same pattern to integrity function pointers + /// as the opcode and parser function pointers on this same contract. Call + /// this function somehow, e.g. from within a test, and then check there is + /// no mismatch with the `INTEGRITY_FUNCTION_POINTERS` constant. This makes + /// the function pointer lookup at runtime much more gas efficient by + /// allowing it to be constant. The reason this can't be done within the + /// test itself is that the pointers need to be calculated relative to the + /// bytecode of the current contract, not the test contract. + /// @return The packed 16-bit function pointers for each integrity check. function buildIntegrityFunctionPointers() external pure returns (bytes memory) { unchecked { function(OperandV2, uint256, uint256) internal pure returns (uint256, uint256) lengthPointer; diff --git a/src/error/ErrBitwise.sol b/src/error/ErrBitwise.sol index acc62b241..3ba3a860c 100644 --- a/src/error/ErrBitwise.sol +++ b/src/error/ErrBitwise.sol @@ -18,6 +18,6 @@ error UnsupportedBitwiseShiftAmount(uint256 shiftAmount); /// @param length The length of the OOB encoding. error TruncatedBitwiseEncoding(uint256 startBit, uint256 length); -/// Thrown during integrity check when the length of a bitwise (en|de)coding -/// would be 0. +/// @notice Thrown during integrity check when the length of a bitwise +/// (en|de)coding would be 0. error ZeroLengthBitwiseEncoding(); diff --git a/src/error/ErrEval.sol b/src/error/ErrEval.sol index 63e1f1d44..5ffd1348d 100644 --- a/src/error/ErrEval.sol +++ b/src/error/ErrEval.sol @@ -10,6 +10,6 @@ contract ErrEval {} /// @param actual The actual number of inputs. error InputsLengthMismatch(uint256 expected, uint256 actual); -/// Thrown when the function pointer table is empty, which would cause +/// @notice Thrown when the function pointer table is empty, which would cause /// mod-by-zero in the eval loop opcode dispatch. error ZeroFunctionPointers(); diff --git a/src/error/ErrExtern.sol b/src/error/ErrExtern.sol index c43639bff..44dfafd60 100644 --- a/src/error/ErrExtern.sol +++ b/src/error/ErrExtern.sol @@ -24,5 +24,5 @@ error ExternPointersMismatch(uint256 opcodeCount, uint256 integrityCount); /// @param actualLength The number of outputs actually returned. error BadOutputsLength(uint256 expectedLength, uint256 actualLength); -/// Thrown at construction when there are no opcode function pointers. +/// @notice Thrown at construction when there are no opcode function pointers. error ExternOpcodePointersEmpty(); diff --git a/src/error/ErrParse.sol b/src/error/ErrParse.sol index fd5e62f9b..169282ccb 100644 --- a/src/error/ErrParse.sol +++ b/src/error/ErrParse.sol @@ -5,16 +5,16 @@ pragma solidity ^0.8.25; /// @dev Workaround for https://github.com/foundry-rs/foundry/issues/6572 contract ErrParse {} -/// Thrown when parsing a source string and an operand opening `<` paren is found -/// somewhere that we don't expect it or can't handle it. +/// @notice Thrown when parsing a source string and an operand opening `<` paren +/// is found somewhere that we don't expect it or can't handle it. error UnexpectedOperand(); -/// Thrown when there are more operand values in the operand than the handler -/// is expecting. +/// @notice Thrown when there are more operand values in the operand than the +/// handler is expecting. error UnexpectedOperandValue(); -/// Thrown when parsing an operand and some required component of the operand is -/// not found in the source string. +/// @notice Thrown when parsing an operand and some required component of the +/// operand is not found in the source string. error ExpectedOperand(); /// @notice Thrown when the number of values encountered in a single operand parsing is @@ -117,28 +117,28 @@ error WordSize(string word); /// @param word The word that was not found. error UnknownWord(string word); -/// The parser exceeded the maximum number of sources that it can build. +/// @notice The parser exceeded the maximum number of sources that it can build. error MaxSources(); -/// The parser encountered a dangling source. This is a bug in the parser. +/// @notice The parser encountered a dangling source. This is a bug in the parser. error DanglingSource(); -/// The parser moved past the end of the data. Defensive guard only — all +/// @notice The parser moved past the end of the data. Defensive guard only — all /// sub-parsers (parseInterstitial, parseLHS, parseRHS) receive `end` and /// respect it, so this condition is unreachable under normal operation. It /// exists to catch internal sub-parser bugs that advance the cursor past /// `end`. Cannot be tested without mocking a sub-parser to force overshoot. error ParserOutOfBounds(); -/// The parser encountered a stack deeper than it can process in the memory -/// region allocated for stack names. +/// @notice The parser encountered a stack deeper than it can process in the +/// memory region allocated for stack names. error ParseStackOverflow(); -/// The parser encountered a stack underflow. +/// @notice The parser encountered a stack underflow. error ParseStackUnderflow(); -/// The parser encountered a paren group deeper than it can process in the -/// memory region allocated for paren tracking. +/// @notice The parser encountered a paren group deeper than it can process in +/// the memory region allocated for paren tracking. error ParenOverflow(); /// @notice The parser did not find any whitespace after the pragma keyword. @@ -166,7 +166,7 @@ error BadSubParserResult(bytes bytecode); /// @param offset The byte offset in the source where the error occurred. error OpcodeIOOverflow(uint256 offset); -/// Thrown when an operand value is larger than the maximum allowed. +/// @notice Thrown when an operand value is larger than the maximum allowed. error OperandOverflow(); /// @notice The parser's free memory pointer exceeded 0x10000, which would corrupt @@ -174,20 +174,20 @@ error OperandOverflow(); /// @param freeMemoryPointer The free memory pointer value that exceeded the limit. error ParseMemoryOverflow(uint256 freeMemoryPointer); -/// A single top-level item exceeded 255 opcodes. The per-item byte counter -/// would silently wrap, corrupting source bytecode. +/// @notice A single top-level item exceeded 255 opcodes. The per-item byte +/// counter would silently wrap, corrupting source bytecode. error SourceItemOpsOverflow(); -/// The total number of opcodes across all top-level items in a single source -/// exceeded 255. The source prefix byte can only represent 0-255. +/// @notice The total number of opcodes across all top-level items in a single +/// source exceeded 255. The source prefix byte can only represent 0-255. error SourceTotalOpsOverflow(); -/// A paren group exceeded 255 inputs. The per-paren byte counter would -/// silently wrap, corrupting operand data. +/// @notice A paren group exceeded 255 inputs. The per-paren byte counter +/// would silently wrap, corrupting operand data. error ParenInputOverflow(); -/// A single line exceeded the maximum number of RHS top-level items that -/// can be tracked in the 256-bit lineTracker (14 items). +/// @notice A single line exceeded the maximum number of RHS top-level items +/// that can be tracked in the 256-bit lineTracker (14 items). error LineRHSItemsOverflow(); /// @notice Thrown when a numeric literal starts with `0X` (uppercase). Only @@ -196,7 +196,7 @@ error LineRHSItemsOverflow(); /// @param offset The byte offset in the source where the error occurred. error UppercaseHexPrefix(uint256 offset); -/// The number of LHS items overflowed the single-byte counter in +/// @notice The number of LHS items overflowed the single-byte counter in /// `lineTracker` (per line) or `topLevel1` (per source). Maximum 255 LHS /// items per line and per source. /// @param offset The byte offset in the source where the error occurred. diff --git a/src/lib/extern/reference/op/LibExternOpContextCallingContract.sol b/src/lib/extern/reference/op/LibExternOpContextCallingContract.sol index 6b4be7fc9..884651e92 100644 --- a/src/lib/extern/reference/op/LibExternOpContextCallingContract.sol +++ b/src/lib/extern/reference/op/LibExternOpContextCallingContract.sol @@ -13,10 +13,21 @@ import { /// @notice This op is a simple reference to the contract that called the interpreter. /// It is used to demonstrate how to implement context references. library LibExternOpContextCallingContract { - /// The sub parser for the calling contract context opcode. It has no special logic - /// so uses the default sub parser from `LibSubParse`. + /// @notice The sub parser for the calling contract context opcode. It has + /// no special logic so uses the default sub parser from `LibSubParse`. + /// @param constantsHeight The current height of the constants array (unused). + /// @param ioByte The IO byte encoding inputs and outputs (unused). + /// @param operand The operand for this opcode (unused). + /// @return Whether the sub parse succeeded. + /// @return The bytecode for the sub parse. + /// @return The constants for the sub parse. //slither-disable-next-line dead-code - function subParser(uint256, uint256, OperandV2) internal pure returns (bool, bytes memory, bytes32[] memory) { + function subParser(uint256 constantsHeight, uint256 ioByte, OperandV2 operand) + internal + pure + returns (bool, bytes memory, bytes32[] memory) + { + (constantsHeight, ioByte, operand); //slither-disable-next-line unused-return return LibSubParse.subParserContext(CONTEXT_BASE_COLUMN, CONTEXT_BASE_ROW_CALLING_CONTRACT); } diff --git a/src/lib/extern/reference/op/LibExternOpContextRainlen.sol b/src/lib/extern/reference/op/LibExternOpContextRainlen.sol index a414afb22..1fbbe6b04 100644 --- a/src/lib/extern/reference/op/LibExternOpContextRainlen.sol +++ b/src/lib/extern/reference/op/LibExternOpContextRainlen.sol @@ -21,10 +21,22 @@ uint256 constant CONTEXT_CALLER_CONTEXT_ROW_RAINLEN = 0; /// @notice This op is a simple reference to the length of the rainlang bytes. It is /// used to demonstrate how to implement context references. library LibExternOpContextRainlen { - /// The sub parser for the rainlen context opcode. It has no special logic - /// so uses the default sub parser from `LibSubParse`. + /// @notice The sub parser for the rainlen context opcode. It has no special + /// logic so uses the default sub parser from `LibSubParse`. + /// @param constantsHeight The current height of the constants array (unused). + /// @param ioByte The IO byte encoding inputs and outputs (unused). + /// @param operand The operand for this opcode (unused). + /// @return Whether the sub parse succeeded. + /// @return The bytecode for the sub parse. + /// @return The constants for the sub parse. //slither-disable-next-line dead-code - function subParser(uint256, uint256, OperandV2) internal pure returns (bool, bytes memory, bytes32[] memory) { + function subParser(uint256 constantsHeight, uint256 ioByte, OperandV2 operand) + internal + pure + returns (bool, bytes memory, bytes32[] memory) + { + (constantsHeight, ioByte, operand); + //slither-disable-next-line unused-return return LibSubParse.subParserContext(CONTEXT_CALLER_CONTEXT_COLUMN, CONTEXT_CALLER_CONTEXT_ROW_RAINLEN); } diff --git a/src/lib/extern/reference/op/LibExternOpContextSender.sol b/src/lib/extern/reference/op/LibExternOpContextSender.sol index 311415fed..6086172df 100644 --- a/src/lib/extern/reference/op/LibExternOpContextSender.sol +++ b/src/lib/extern/reference/op/LibExternOpContextSender.sol @@ -11,10 +11,21 @@ import {CONTEXT_BASE_COLUMN, CONTEXT_BASE_ROW_SENDER} from "rain.interpreter.int /// the interpreter. It is used to demonstrate how to implement context /// references. library LibExternOpContextSender { - /// The sub parser for the sender context opcode. It has no special logic - /// so uses the default sub parser from `LibSubParse`. + /// @notice The sub parser for the sender context opcode. It has no special + /// logic so uses the default sub parser from `LibSubParse`. + /// @param constantsHeight The current height of the constants array (unused). + /// @param ioByte The IO byte encoding inputs and outputs (unused). + /// @param operand The operand for this opcode (unused). + /// @return Whether the sub parse succeeded. + /// @return The bytecode for the sub parse. + /// @return The constants for the sub parse. //slither-disable-next-line dead-code - function subParser(uint256, uint256, OperandV2) internal pure returns (bool, bytes memory, bytes32[] memory) { + function subParser(uint256 constantsHeight, uint256 ioByte, OperandV2 operand) + internal + pure + returns (bool, bytes memory, bytes32[] memory) + { + (constantsHeight, ioByte, operand); //slither-disable-next-line unused-return return LibSubParse.subParserContext(CONTEXT_BASE_COLUMN, CONTEXT_BASE_ROW_SENDER); } From 554a3b3c6f05dfa87283893a7f1258e079a6f64d Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 4 Mar 2026 00:18:46 +0400 Subject: [PATCH 08/13] Fix forge fmt in LibOpGreaterThanOrEqualTo test Co-Authored-By: Claude Opus 4.6 --- test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol b/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol index eaee221e1..3b764620b 100644 --- a/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol +++ b/test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol @@ -114,8 +114,6 @@ contract LibOpGreaterThanOrEqualToTest is OpTest { /// Test that operand is disallowed. function testOpGreaterThanOrEqualToEvalOperandDisallowed() external { - checkUnhappyParse( - "_: greater-than-or-equal-to<0>(1 2);", abi.encodeWithSelector(UnexpectedOperand.selector) - ); + checkUnhappyParse("_: greater-than-or-equal-to<0>(1 2);", abi.encodeWithSelector(UnexpectedOperand.selector)); } } From aaed1777350f814ac55f6fefee3c45c2f4d585e7 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 4 Mar 2026 00:32:13 +0400 Subject: [PATCH 09/13] Fix CodeRabbit review findings: comments, fuzz overrides, test gaps - CR-7: Fix boundary test to use exactly 128+128=256 ops (was 129+129=258) - CR-8: Update stale comment describing old silent truncation behavior - CR-9: Remove redundant literalParsers reassignment in pragma test - CR-10: Assert specific UnknownWord("") error instead of bare expectRevert - CR-12: Rename test functions from CtPop to BitwiseCountOnes - Fix cargo fmt in fork.rs Co-Authored-By: Claude Opus 4.6 --- audit/2026-03-01-01/triage.md | 13 +++++++++++++ crates/eval/src/fork.rs | 14 ++++++-------- .../lib/op/bitwise/LibOpBitwiseCountOnes.t.sol | 16 ++++++++-------- test/src/lib/parse/LibParsePragma.keyword.t.sol | 1 - ...LibParseState.endSourceTotalOpsOverflow.t.sol | 2 +- .../lib/parse/LibSubParse.subParseLiteral.t.sol | 6 +++--- .../lib/parse/LibSubParse.subParseWords.t.sol | 9 +++------ 7 files changed, 34 insertions(+), 27 deletions(-) diff --git a/audit/2026-03-01-01/triage.md b/audit/2026-03-01-01/triage.md index 9c4bfface..8c230b71e 100644 --- a/audit/2026-03-01-01/triage.md +++ b/audit/2026-03-01-01/triage.md @@ -127,3 +127,16 @@ - [DISMISSED] P5-HEADROOM-01: (INFO) Headroom semantics for negative non-integers may surprise users — Rain-invented opcode, well tested including negative cases, semantics defined by NatSpec - [FIXED] P5-EXPGROWTH-01: (INFO) integrity NatSpec in exponential-growth, linear-growth, uint256-max-value missing explicit @notice tag - [FIXED] P5-UINT256POW-01: (INFO) uint256-power left-to-right associativity NatSpec could be clearer + +## CodeRabbit (PR #438) + +- [FIXED] CR-7: (LOW) endSourceTotalOpsOverflow test boundary off by one — tree128 was 129 ops not 128, testing 258 not 256 +- [FIXED] CR-8: (LOW) Stale comment in subParseLiteral test — described old silent truncation behavior, now reverts +- [FIXED] CR-12: (LOW) Test function names in LibOpBitwiseCountOnes.t.sol still used old CtPop prefix +- [DISMISSED] CR-3: (LOW) Per-test fuzz run overrides in LibOpBitwiseShiftRight.t.sol — intentional for slow tests +- [FIXED] CR-10: (LOW) Broad vm.expectRevert() in subParseWords test — now asserts UnknownWord("") specifically +- [FIXED] CR-9: (LOW) Redundant state.literalParsers reassignment in LibParsePragma.keyword.t.sol +- [DISMISSED] CR-1/2: (LOW) Pragma ^ vs = in library files — intentional for downstream compatibility +- [DISMISSED] CR-5: (LOW) LibCamelToKebab NatSpec — false positive, continuation lines are valid +- [DISMISSED] CR-6: (LOW) LibIntegrityCheck NatSpec — false positive, correctly tagged +- [DISMISSED] CR-11: (INFO) Alias detection docs — suggestion only, not a bug diff --git a/crates/eval/src/fork.rs b/crates/eval/src/fork.rs index c1d77a7fc..72173b2ee 100644 --- a/crates/eval/src/fork.rs +++ b/crates/eval/src/fork.rs @@ -448,14 +448,12 @@ impl Forker { self.add_or_select( NewForkedEvm { fork_url: fork_url.clone(), - fork_block_number: Some( - block_number - .checked_sub(1) - .ok_or(ReplayTransactionError::GenesisBlockReplay( - tx_hash.to_string(), - fork_url.clone(), - ))?, - ), + fork_block_number: Some(block_number.checked_sub(1).ok_or( + ReplayTransactionError::GenesisBlockReplay( + tx_hash.to_string(), + fork_url.clone(), + ), + )?), }, None, ) diff --git a/test/src/lib/op/bitwise/LibOpBitwiseCountOnes.t.sol b/test/src/lib/op/bitwise/LibOpBitwiseCountOnes.t.sol index 548e55f6e..2a7ca420e 100644 --- a/test/src/lib/op/bitwise/LibOpBitwiseCountOnes.t.sol +++ b/test/src/lib/op/bitwise/LibOpBitwiseCountOnes.t.sol @@ -16,7 +16,7 @@ import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; contract LibOpBitwiseCountOnesTest is OpTest { /// Directly test the integrity logic of LibOpBitwiseCountOnes. All possible operands /// result in the same number of inputs and outputs, (1, 1). - function testOpCtPopIntegrity(IntegrityCheckState memory state, OperandV2 operand) external pure { + function testOpBitwiseCountOnesIntegrity(IntegrityCheckState memory state, OperandV2 operand) external pure { (uint256 calcInputs, uint256 calcOutputs) = LibOpBitwiseCountOnes.integrity(state, operand); assertEq(calcInputs, 1); assertEq(calcOutputs, 1); @@ -24,7 +24,7 @@ contract LibOpBitwiseCountOnesTest is OpTest { /// Directly test the runtime logic of LibOpBitwiseCountOnes. This tests that the /// opcode correctly pushes the ct pop onto the stack. - function testOpCtPopRun(StackItem x) external view { + function testOpBitwiseCountOnesRun(StackItem x) external view { InterpreterState memory state = opTestDefaultInterpreterState(); StackItem[] memory inputs = new StackItem[](1); inputs[0] = x; @@ -40,7 +40,7 @@ contract LibOpBitwiseCountOnesTest is OpTest { } /// Test the eval of a ct pop opcode parsed from a string. - function testOpCtPopEval(StackItem x) external view { + function testOpBitwiseCountOnesEval(StackItem x) external view { StackItem[] memory stack = new StackItem[](1); stack[0] = StackItem.wrap(bytes32(LibCtPop.ctpop(uint256(StackItem.unwrap(x))))); checkHappy( @@ -51,24 +51,24 @@ contract LibOpBitwiseCountOnesTest is OpTest { } /// Test that a bitwise count with bad inputs fails integrity. - function testOpCtPopZeroInputs() external { + function testOpBitwiseCountOnesZeroInputs() external { checkBadInputs("_: bitwise-count-ones();", 0, 1, 0); } - function testOpCtPopTwoInputs() external { + function testOpBitwiseCountOnesTwoInputs() external { checkBadInputs("_: bitwise-count-ones(0 0);", 2, 1, 2); } - function testOpCtPopZeroOutputs() external { + function testOpBitwiseCountOnesZeroOutputs() external { checkBadOutputs(": bitwise-count-ones(0);", 1, 1, 0); } - function testOpCtPopTwoOutputs() external { + function testOpBitwiseCountOnesTwoOutputs() external { checkBadOutputs("_ _: bitwise-count-ones(0);", 1, 1, 2); } /// Test that operand is disallowed. - function testOpCtPopEvalBadOperand() external { + function testOpBitwiseCountOnesEvalOperandDisallowed() external { checkUnhappyParse("_: bitwise-count-ones<0>(0);", abi.encodeWithSelector(UnexpectedOperand.selector)); } } diff --git a/test/src/lib/parse/LibParsePragma.keyword.t.sol b/test/src/lib/parse/LibParsePragma.keyword.t.sol index 7fe93a5b5..14b3a075e 100644 --- a/test/src/lib/parse/LibParsePragma.keyword.t.sol +++ b/test/src/lib/parse/LibParsePragma.keyword.t.sol @@ -296,7 +296,6 @@ contract LibParsePragmaKeywordTest is Test { ParseState memory state = LibParseState.newState(data, "", "", LibAllStandardOps.literalParserFunctionPointers()); - state.literalParsers = LibAllStandardOps.literalParserFunctionPointers(); uint256 cursor = Pointer.unwrap(data.dataPointer()); // Set end to 60: just past the trailing space after the address. diff --git a/test/src/lib/parse/LibParseState.endSourceTotalOpsOverflow.t.sol b/test/src/lib/parse/LibParseState.endSourceTotalOpsOverflow.t.sol index 3016740eb..cbc64459c 100644 --- a/test/src/lib/parse/LibParseState.endSourceTotalOpsOverflow.t.sol +++ b/test/src/lib/parse/LibParseState.endSourceTotalOpsOverflow.t.sol @@ -35,7 +35,7 @@ contract LibParseStateEndSourceTotalOpsOverflowTest is ParseTest { /// 256 total ops across two items MUST overflow. /// Two items: 128 ops + 128 ops = 256 total. function testTotalOpsOverflow256() external { - bytes memory tree128 = bytes.concat("a(", buildTree(6), " a())"); + bytes memory tree128 = bytes.concat("a(", buildTree(6), ")"); string memory s = string(bytes.concat("_: ", tree128, ",\n_: ", tree128, ";")); vm.expectRevert(abi.encodeWithSelector(SourceTotalOpsOverflow.selector)); this.parseExternal(s); diff --git a/test/src/lib/parse/LibSubParse.subParseLiteral.t.sol b/test/src/lib/parse/LibSubParse.subParseLiteral.t.sol index 369a623f1..97e596e86 100644 --- a/test/src/lib/parse/LibSubParse.subParseLiteral.t.sol +++ b/test/src/lib/parse/LibSubParse.subParseLiteral.t.sol @@ -164,9 +164,9 @@ contract LibSubParseSubParseLiteralTest is Test { assertEq(result, expectedValue); } - /// Dispatch region exceeding 0xFFFF bytes causes silent truncation of - /// the 2-byte encoded length. The sub-parser receives corrupted data - /// where the dispatch/body boundary is wrong. + /// Dispatch region exceeding 0xFFFF bytes reverts with + /// SubParseLiteralDispatchLengthOverflow to prevent silent truncation + /// of the 2-byte encoded length. function testSubParseLiteralDispatchLengthOverflow() external { address subParser = makeAddr("subParser"); // Create a dispatch of 0x10001 bytes — one byte past the 16-bit max. diff --git a/test/src/lib/parse/LibSubParse.subParseWords.t.sol b/test/src/lib/parse/LibSubParse.subParseWords.t.sol index 05ddc68c6..9d7990448 100644 --- a/test/src/lib/parse/LibSubParse.subParseWords.t.sol +++ b/test/src/lib/parse/LibSubParse.subParseWords.t.sol @@ -12,6 +12,7 @@ import {ISubParserV4} from "rain.interpreter.interface/interface/ISubParserV4.so import {IERC165} from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; import {OPCODE_UNKNOWN, OPCODE_CONSTANT, OPCODE_CONTEXT} from "rain.interpreter.interface/interface/IInterpreterV4.sol"; import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; +import {UnknownWord} from "src/error/ErrParse.sol"; /// @dev A sub parser that resolves any word by returning a context opcode with /// no constants. Used to verify that subParseWords iterates multiple sources. @@ -142,11 +143,7 @@ contract LibSubParseSubParseWordsTest is Test { } /// @notice External wrapper for subParseWords so reverts can be caught. - function externalSubParseWords(bytes memory bytecode) - external - view - returns (bytes memory, bytes32[] memory) - { + function externalSubParseWords(bytes memory bytecode) external view returns (bytes memory, bytes32[] memory) { ParseState memory state = LibParseState.newState("", "", "", ""); return state.subParseWords(bytecode); } @@ -176,7 +173,7 @@ contract LibSubParseSubParseWordsTest is Test { //forge-lint: disable-next-line(unsafe-typecast) bytes memory bytecode = buildSingleOpBytecode(uint8(OPCODE_UNKNOWN), 0x10, 0x0000); - vm.expectRevert(); + vm.expectRevert(abi.encodeWithSelector(UnknownWord.selector, "")); this.externalSubParseWords(bytecode); } From 07289af24a5489844bb4fde927ba6050876dfeb2 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 4 Mar 2026 00:32:20 +0400 Subject: [PATCH 10/13] Enable ffi in foundry.toml for test support Co-Authored-By: Claude Opus 4.6 --- foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry.toml b/foundry.toml index ad618fdf6..ec35a7359 100644 --- a/foundry.toml +++ b/foundry.toml @@ -22,6 +22,7 @@ optimizer = true # code size cap to be hit. optimizer_runs = 1000 +ffi = true bytecode_hash = "none" cbor_metadata = false From d2cce985e16788b3f8defca8e5c7f66b81941775 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 4 Mar 2026 00:33:15 +0400 Subject: [PATCH 11/13] Apply forge fmt to LibEval test files Co-Authored-By: Claude Opus 4.6 --- .../lib/eval/LibEval.inputsLengthMismatch.t.sol | 14 ++++++-------- test/src/lib/eval/LibEval.remainderOnly.t.sol | 6 ++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/test/src/lib/eval/LibEval.inputsLengthMismatch.t.sol b/test/src/lib/eval/LibEval.inputsLengthMismatch.t.sol index 7343bfa0d..4dfb835a3 100644 --- a/test/src/lib/eval/LibEval.inputsLengthMismatch.t.sol +++ b/test/src/lib/eval/LibEval.inputsLengthMismatch.t.sol @@ -20,11 +20,11 @@ import {InputsLengthMismatch} from "src/error/ErrEval.sol"; /// LibEval.eval4, using hand-built bytecode and InterpreterState. contract LibEvalInputsLengthMismatchTest is Test { /// External wrapper so vm.expectRevert works with the library call. - function externalEval4( - InterpreterState memory state, - StackItem[] memory inputs, - uint256 maxOutputs - ) external view returns (StackItem[] memory, bytes32[] memory) { + function externalEval4(InterpreterState memory state, StackItem[] memory inputs, uint256 maxOutputs) + external + view + returns (StackItem[] memory, bytes32[] memory) + { return LibEval.eval4(state, inputs, maxOutputs); } @@ -35,9 +35,7 @@ contract LibEvalInputsLengthMismatchTest is Test { // Bytecode: 1 source, 0 offset, 0 ops, sourceInputs stack allocation, // sourceInputs inputs, 0 outputs. - bytes memory bytecode = abi.encodePacked( - uint8(1), uint16(0), uint8(0), sourceInputs, sourceInputs, uint8(0) - ); + bytes memory bytecode = abi.encodePacked(uint8(1), uint16(0), uint8(0), sourceInputs, sourceInputs, uint8(0)); StackItem[][] memory stacks = new StackItem[][](1); stacks[0] = new StackItem[](sourceInputs); diff --git a/test/src/lib/eval/LibEval.remainderOnly.t.sol b/test/src/lib/eval/LibEval.remainderOnly.t.sol index 824663894..d3295b0cb 100644 --- a/test/src/lib/eval/LibEval.remainderOnly.t.sol +++ b/test/src/lib/eval/LibEval.remainderOnly.t.sol @@ -33,8 +33,7 @@ contract LibEvalRemainderOnlyTest is RainterpreterExpressionDeployerDeploymentTe bytes32 c6 ) external view { // 7 distinct hex literals produce 7 constant opcodes. - (bytes memory bytecode,) = - I_PARSER.unsafeParse(bytes("_ _ _ _ _ _ _: 0xaa 0xbb 0xcc 0xdd 0xee 0xff 0x11;")); + (bytes memory bytecode,) = I_PARSER.unsafeParse(bytes("_ _ _ _ _ _ _: 0xaa 0xbb 0xcc 0xdd 0xee 0xff 0x11;")); bytes32[] memory constants = new bytes32[](7); constants[0] = c0; @@ -60,8 +59,7 @@ contract LibEvalRemainderOnlyTest is RainterpreterExpressionDeployerDeploymentTe LibAllStandardOps.opcodeFunctionPointers() ); - (StackItem[] memory outputs, bytes32[] memory kvs) = - LibEval.eval4(state, new StackItem[](0), type(uint256).max); + (StackItem[] memory outputs, bytes32[] memory kvs) = LibEval.eval4(state, new StackItem[](0), type(uint256).max); assertEq(outputs.length, 7); // Stack outputs are top-first: last pushed constant is first output. From c634d4e959fc2d449a07d2a1e91cc7157eb21e99 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 4 Mar 2026 00:49:04 +0400 Subject: [PATCH 12/13] Fix remaining Pass 3 Solidity NatSpec findings - P3-EA-05: Fix typo "determin" -> "determine" - P3-LPS-01: Add @title/@notice to LibParseState library - P3-LPS-02: Add @notice to checkParseMemoryOverflow - P3-LPP-02: Add @dev NatSpec to 4 pragma keyword constants - P3-LPST-02: Add @dev NatSpec to ParseStackTracker type - P3-LPL-02: Add @dev NatSpec to 5 literal parser index constants - P3-CC-01: Merge untagged lines into @notice on opcodeFunctionPointers - P3-CC-02: Use @inheritdoc on buildOperandHandler/LiteralParser - P3-CC-03: Add @notice/@return to 3 virtual functions in RainterpreterParser - P3-CC-04: Add @notice to 4 IDISPaiRegistry functions - P3-OPALL-01/02: Add @notice/@return to integrity() in 4 ERC uint256 files - P3-OPALL-03: Add @return to referenceFn() in LibOpUint256MaxValue - P3-OPALL-04: Add @notice to integrity() in LibOpEnsure Co-Authored-By: Claude Opus 4.6 --- audit/2026-03-01-01/triage.md | 30 +++++++++---------- src/concrete/Rainterpreter.sol | 6 ++-- src/concrete/RainterpreterParser.sol | 15 ++++++---- .../extern/RainterpreterReferenceExtern.sol | 2 +- src/interface/IDISPaiRegistry.sol | 9 +++--- .../uint256/LibOpUint256ERC20Allowance.sol | 5 +++- .../uint256/LibOpUint256ERC20BalanceOf.sol | 5 +++- .../uint256/LibOpUint256ERC20TotalSupply.sol | 5 +++- .../uint256/LibOpUint256ERC721BalanceOf.sol | 5 +++- src/lib/op/logic/LibOpEnsure.sol | 3 +- .../op/math/uint256/LibOpUint256MaxValue.sol | 1 + src/lib/parse/LibParsePragma.sol | 5 ++++ src/lib/parse/LibParseStackTracker.sol | 3 ++ src/lib/parse/LibParseState.sol | 4 ++- src/lib/parse/literal/LibParseLiteral.sol | 5 ++++ 15 files changed, 69 insertions(+), 34 deletions(-) diff --git a/audit/2026-03-01-01/triage.md b/audit/2026-03-01-01/triage.md index 8c230b71e..0af3dbd8f 100644 --- a/audit/2026-03-01-01/triage.md +++ b/audit/2026-03-01-01/triage.md @@ -70,22 +70,22 @@ - [FIXED] P3-EA-01: (LOW) "word dispatches" should be "opcode dispatches" in BaseRainterpreterExtern NatSpec - [FIXED] P3-EA-02: (LOW) Missing @return tags on buildOpcodeFunctionPointers/buildIntegrityFunctionPointers - [FIXED] P3-EA-03: (LOW) Three context op subParser functions have unnamed parameters and no @param/@return -- [PENDING] P3-EA-04: (LOW) Undocumented CONTEXT_CALLER_CONTEXT constants in LibExternOpContextRainlen -- [PENDING] P3-EA-05: (LOW) Typo "determin" in SUB_PARSER_LITERAL_REPEAT_KEYWORD_MASK NatSpec -- [PENDING] P3-LPS-01: (LOW) LibParseState library missing library-level NatSpec -- [PENDING] P3-LPS-02: (LOW) checkParseMemoryOverflow missing explicit @notice tag -- [PENDING] P3-LPP-02: (LOW) 4 pragma keyword constants missing NatSpec -- [PENDING] P3-LPST-02: (LOW) ParseStackTracker user-defined type missing NatSpec -- [PENDING] P3-LPL-02: (LOW) 5 literal parser index constants missing NatSpec +- [FIXED] P3-EA-04: (LOW) Undocumented CONTEXT_CALLER_CONTEXT constants in LibExternOpContextRainlen — already documented as part of A49-8 +- [FIXED] P3-EA-05: (LOW) Typo "determin" in SUB_PARSER_LITERAL_REPEAT_KEYWORD_MASK NatSpec +- [FIXED] P3-LPS-01: (LOW) LibParseState library missing library-level NatSpec +- [FIXED] P3-LPS-02: (LOW) checkParseMemoryOverflow missing explicit @notice tag +- [FIXED] P3-LPP-02: (LOW) 4 pragma keyword constants missing NatSpec +- [FIXED] P3-LPST-02: (LOW) ParseStackTracker user-defined type missing NatSpec +- [FIXED] P3-LPL-02: (LOW) 5 literal parser index constants missing NatSpec - [FIXED] P3-ERR-01: (LOW) 13 errors in ErrParse.sol have doc comments but no explicit @notice tag — fixed as part of P3-ERR-1 -- [PENDING] P3-CC-01: (LOW) opcodeFunctionPointers has untagged lines before @return tag -- [PENDING] P3-CC-02: (LOW) buildOperandHandlerFunctionPointers/buildLiteralParserFunctionPointers use bare /// instead of @inheritdoc -- [PENDING] P3-CC-03: (LOW) Three internal virtual functions missing @return tags in RainterpreterParser -- [PENDING] P3-CC-04: (LOW) IDISPaiRegistry functions have untagged description + @return tag -- [PENDING] P3-OPALL-01: (LOW) Missing @notice tag on integrity() in 7 files -- [PENDING] P3-OPALL-02: (LOW) Missing @return tags on integrity() in 7 files -- [PENDING] P3-OPALL-03: (LOW) Missing @notice and @return on referenceFn() in LibOpMaxUint256 -- [PENDING] P3-OPALL-04: (LOW) Missing @notice on integrity() in LibOpEnsure +- [FIXED] P3-CC-01: (LOW) opcodeFunctionPointers has untagged lines before @return tag +- [FIXED] P3-CC-02: (LOW) buildOperandHandlerFunctionPointers/buildLiteralParserFunctionPointers use bare /// instead of @inheritdoc +- [FIXED] P3-CC-03: (LOW) Three internal virtual functions missing @return tags in RainterpreterParser +- [FIXED] P3-CC-04: (LOW) IDISPaiRegistry functions have untagged description + @return tag +- [FIXED] P3-OPALL-01: (LOW) Missing @notice tag on integrity() in 7 files — 4 ERC uint256 files fixed; 3 others already fixed in P5-EXPGROWTH-01 +- [FIXED] P3-OPALL-02: (LOW) Missing @return tags on integrity() in 7 files — same as P3-OPALL-01 +- [FIXED] P3-OPALL-03: (LOW) Missing @return on referenceFn() in LibOpUint256MaxValue +- [FIXED] P3-OPALL-04: (LOW) Missing @notice on integrity() in LibOpEnsure - [PENDING] P3-RC-01: (LOW) No crate-level //! documentation on any Rust crate - [PENDING] P3-RC-02: (LOW) CLI crate all 13 public items completely undocumented - [PENDING] P3-RC-03: (LOW) eval crate 10 undocumented public items diff --git a/src/concrete/Rainterpreter.sol b/src/concrete/Rainterpreter.sol index dfae15d23..c7c66a906 100644 --- a/src/concrete/Rainterpreter.sol +++ b/src/concrete/Rainterpreter.sol @@ -39,9 +39,9 @@ contract Rainterpreter is IInterpreterV4, IOpcodeToolingV1, ERC165 { if (opcodeFunctionPointers().length == 0) revert ZeroFunctionPointers(); } - /// Returns the packed 2-byte function pointer table used by the eval loop - /// to dispatch each opcode. Virtual so subclasses can override the table. - /// @notice Overrides MUST return the same non-empty value at construction + /// @notice Returns the packed 2-byte function pointer table used by the + /// eval loop to dispatch each opcode. Virtual so subclasses can override + /// the table. Overrides MUST return the same non-empty value at construction /// time and at runtime. Returning empty bytes at runtime would cause /// division-by-zero in the eval loop's modulo-based dispatch, leading to /// reads from arbitrary memory interpreted as function pointers. diff --git a/src/concrete/RainterpreterParser.sol b/src/concrete/RainterpreterParser.sol index bfac1a32e..911e8a183 100644 --- a/src/concrete/RainterpreterParser.sol +++ b/src/concrete/RainterpreterParser.sol @@ -88,27 +88,32 @@ contract RainterpreterParser is ERC165, IParserToolingV1 { return PragmaV1(parseState.exportSubParsers()); } - /// Virtual function to return the parse meta. + /// @notice Virtual function to return the parse meta. + /// @return The parse meta bytes. function parseMeta() internal pure virtual returns (bytes memory) { return PARSE_META; } - /// Virtual function to return the operand handler function pointers. + /// @notice Virtual function to return the operand handler function + /// pointers. + /// @return The packed operand handler function pointers. function operandHandlerFunctionPointers() internal pure virtual returns (bytes memory) { return OPERAND_HANDLER_FUNCTION_POINTERS; } - /// Virtual function to return the literal parser function pointers. + /// @notice Virtual function to return the literal parser function + /// pointers. + /// @return The packed literal parser function pointers. function literalParserFunctionPointers() internal pure virtual returns (bytes memory) { return LITERAL_PARSER_FUNCTION_POINTERS; } - /// External function to build the operand handler function pointers. + /// @inheritdoc IParserToolingV1 function buildOperandHandlerFunctionPointers() external pure override returns (bytes memory) { return LibAllStandardOps.operandHandlerFunctionPointers(); } - /// External function to build the literal parser function pointers. + /// @inheritdoc IParserToolingV1 function buildLiteralParserFunctionPointers() external pure override returns (bytes memory) { return LibAllStandardOps.literalParserFunctionPointers(); } diff --git a/src/concrete/extern/RainterpreterReferenceExtern.sol b/src/concrete/extern/RainterpreterReferenceExtern.sol index 86d5b4365..c675d759a 100644 --- a/src/concrete/extern/RainterpreterReferenceExtern.sol +++ b/src/concrete/extern/RainterpreterReferenceExtern.sol @@ -60,7 +60,7 @@ bytes32 constant SUB_PARSER_LITERAL_REPEAT_KEYWORD_BYTES32 = bytes32(SUB_PARSER_ /// @dev The number of bytes in the repeat literal keyword. uint256 constant SUB_PARSER_LITERAL_REPEAT_KEYWORD_BYTES_LENGTH = 18; -/// @dev The mask to apply to the dispatch bytes when parsing to determin whether +/// @dev The mask to apply to the dispatch bytes when parsing to determine whether /// the dispatch is for the repeat literal parser. bytes32 constant SUB_PARSER_LITERAL_REPEAT_KEYWORD_MASK = //forge-lint: disable-next-line(incorrect-shift) diff --git a/src/interface/IDISPaiRegistry.sol b/src/interface/IDISPaiRegistry.sol index 261a04272..30ac6c193 100644 --- a/src/interface/IDISPaiRegistry.sol +++ b/src/interface/IDISPaiRegistry.sol @@ -7,19 +7,20 @@ pragma solidity ^0.8.25; /// registry that exposes the deterministic deploy addresses of the four core /// interpreter components. interface IDISPaiRegistry { - /// Returns the deterministic deploy address of the expression deployer. + /// @notice Returns the deterministic deploy address of the expression + /// deployer. /// @return The expression deployer address. function expressionDeployerAddress() external pure returns (address); - /// Returns the deterministic deploy address of the interpreter. + /// @notice Returns the deterministic deploy address of the interpreter. /// @return The interpreter address. function interpreterAddress() external pure returns (address); - /// Returns the deterministic deploy address of the store. + /// @notice Returns the deterministic deploy address of the store. /// @return The store address. function storeAddress() external pure returns (address); - /// Returns the deterministic deploy address of the parser. + /// @notice Returns the deterministic deploy address of the parser. /// @return The parser address. function parserAddress() external pure returns (address); } diff --git a/src/lib/op/erc20/uint256/LibOpUint256ERC20Allowance.sol b/src/lib/op/erc20/uint256/LibOpUint256ERC20Allowance.sol index e6ff088b3..3c8c7fb2c 100644 --- a/src/lib/op/erc20/uint256/LibOpUint256ERC20Allowance.sol +++ b/src/lib/op/erc20/uint256/LibOpUint256ERC20Allowance.sol @@ -12,7 +12,10 @@ import {NotAnAddress} from "../../../../error/ErrRainType.sol"; /// @title LibOpUint256ERC20Allowance /// @notice Opcode for getting the current erc20 allowance of an account. library LibOpUint256ERC20Allowance { - /// `uint256-erc20-allowance` integrity check. Requires 3 inputs and produces 1 output. + /// @notice `uint256-erc20-allowance` integrity check. Requires 3 inputs + /// and produces 1 output. + /// @return The number of inputs. + /// @return The number of outputs. function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { // Always 3 inputs, the token, the owner and the spender. // Always 1 output, the allowance. diff --git a/src/lib/op/erc20/uint256/LibOpUint256ERC20BalanceOf.sol b/src/lib/op/erc20/uint256/LibOpUint256ERC20BalanceOf.sol index 3115117ba..c0e1144f3 100644 --- a/src/lib/op/erc20/uint256/LibOpUint256ERC20BalanceOf.sol +++ b/src/lib/op/erc20/uint256/LibOpUint256ERC20BalanceOf.sol @@ -12,7 +12,10 @@ import {NotAnAddress} from "../../../../error/ErrRainType.sol"; /// @title LibOpUint256ERC20BalanceOf /// @notice Opcode for getting the current erc20 balance of an account. library LibOpUint256ERC20BalanceOf { - /// `uint256-erc20-balance-of` integrity check. Requires 2 inputs and produces 1 output. + /// @notice `uint256-erc20-balance-of` integrity check. Requires 2 inputs + /// and produces 1 output. + /// @return The number of inputs. + /// @return The number of outputs. function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { // Always 2 inputs, the token and the account. // Always 1 output, the balance. diff --git a/src/lib/op/erc20/uint256/LibOpUint256ERC20TotalSupply.sol b/src/lib/op/erc20/uint256/LibOpUint256ERC20TotalSupply.sol index 2f597fcdb..75e05cd8a 100644 --- a/src/lib/op/erc20/uint256/LibOpUint256ERC20TotalSupply.sol +++ b/src/lib/op/erc20/uint256/LibOpUint256ERC20TotalSupply.sol @@ -12,7 +12,10 @@ import {NotAnAddress} from "../../../../error/ErrRainType.sol"; /// @title LibOpUint256ERC20TotalSupply /// @notice Opcode for ERC20 `totalSupply`. library LibOpUint256ERC20TotalSupply { - /// `uint256-erc20-total-supply` integrity check. Requires 1 input and produces 1 output. + /// @notice `uint256-erc20-total-supply` integrity check. Requires 1 input + /// and produces 1 output. + /// @return The number of inputs. + /// @return The number of outputs. function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { // Always 1 input, the contract. // Always 1 output, the total supply. diff --git a/src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol b/src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol index f22bf2445..cddc36307 100644 --- a/src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol +++ b/src/lib/op/erc721/uint256/LibOpUint256ERC721BalanceOf.sol @@ -12,7 +12,10 @@ import {NotAnAddress} from "../../../../error/ErrRainType.sol"; /// @title LibOpUint256ERC721BalanceOf /// @notice Opcode for getting the current erc721 balance of an account. library LibOpUint256ERC721BalanceOf { - /// `uint256-erc721-balance-of` integrity check. Requires 2 inputs and produces 1 output. + /// @notice `uint256-erc721-balance-of` integrity check. Requires 2 inputs + /// and produces 1 output. + /// @return The number of inputs. + /// @return The number of outputs. function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { // Always 2 inputs, the token and the account. // Always 1 output, the balance. diff --git a/src/lib/op/logic/LibOpEnsure.sol b/src/lib/op/logic/LibOpEnsure.sol index 758b5c84e..b73e781d6 100644 --- a/src/lib/op/logic/LibOpEnsure.sol +++ b/src/lib/op/logic/LibOpEnsure.sol @@ -15,10 +15,11 @@ library LibOpEnsure { using LibDecimalFloat for Float; using LibIntOrAString for IntOrAString; + /// @notice `ensure` integrity check. Requires exactly 2 inputs and + /// produces 0 outputs. /// @return The number of inputs. /// @return The number of outputs. function integrity(IntegrityCheckState memory, OperandV2) internal pure returns (uint256, uint256) { - // There must be exactly 2 inputs. return (2, 0); } diff --git a/src/lib/op/math/uint256/LibOpUint256MaxValue.sol b/src/lib/op/math/uint256/LibOpUint256MaxValue.sol index 359a0fe61..87d701715 100644 --- a/src/lib/op/math/uint256/LibOpUint256MaxValue.sol +++ b/src/lib/op/math/uint256/LibOpUint256MaxValue.sol @@ -30,6 +30,7 @@ library LibOpUint256MaxValue { } /// @notice Reference implementation of `uint256-max-value` for testing. + /// @return The stack item outputs. function referenceFn(InterpreterState memory, OperandV2, StackItem[] memory) internal pure diff --git a/src/lib/parse/LibParsePragma.sol b/src/lib/parse/LibParsePragma.sol index 1797a3053..003ad0c7e 100644 --- a/src/lib/parse/LibParsePragma.sol +++ b/src/lib/parse/LibParsePragma.sol @@ -9,11 +9,16 @@ import {LibParseError} from "./LibParseError.sol"; import {LibParseInterstitial} from "./LibParseInterstitial.sol"; import {LibParseLiteral} from "./literal/LibParseLiteral.sol"; +/// @dev The raw bytes of the pragma keyword "using-words-from". bytes constant PRAGMA_KEYWORD_BYTES = bytes("using-words-from"); +/// @dev The pragma keyword as a left-aligned bytes32 for single-word comparison. // Constant is safe to typecast. //forge-lint: disable-next-line(unsafe-typecast) bytes32 constant PRAGMA_KEYWORD_BYTES32 = bytes32(PRAGMA_KEYWORD_BYTES); +/// @dev The byte length of the pragma keyword (16 bytes for "using-words-from"). uint256 constant PRAGMA_KEYWORD_BYTES_LENGTH = 16; +/// @dev Bitmask that isolates the first `PRAGMA_KEYWORD_BYTES_LENGTH` bytes of +/// a bytes32 value, used to compare only the keyword portion. //forge-lint: disable-next-line(incorrect-shift) bytes32 constant PRAGMA_KEYWORD_MASK = bytes32(~((1 << (32 - PRAGMA_KEYWORD_BYTES_LENGTH) * 8) - 1)); diff --git a/src/lib/parse/LibParseStackTracker.sol b/src/lib/parse/LibParseStackTracker.sol index e2fde62fb..670d0803b 100644 --- a/src/lib/parse/LibParseStackTracker.sol +++ b/src/lib/parse/LibParseStackTracker.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.25; import {ParseStackUnderflow, ParseStackOverflow} from "../../error/ErrParse.sol"; +/// @dev Tracks the current and high-water stack heights during parsing. The +/// low 128 bits hold the current stack height; the high 128 bits hold the +/// maximum height seen so far (used to size the runtime stack allocation). type ParseStackTracker is uint256; library LibParseStackTracker { diff --git a/src/lib/parse/LibParseState.sol b/src/lib/parse/LibParseState.sol index 092627be6..287022c1d 100644 --- a/src/lib/parse/LibParseState.sol +++ b/src/lib/parse/LibParseState.sol @@ -183,6 +183,8 @@ struct ParseState { bytes meta; } +/// @title LibParseState +/// @notice Utilities for constructing and managing `ParseState` during parsing. library LibParseState { using LibParseState for ParseState; using LibParseStackTracker for ParseStackTracker; @@ -1039,7 +1041,7 @@ library LibParseState { } } - /// The parse system packs memory pointers into 16 bits throughout its + /// @notice The parse system packs memory pointers into 16 bits throughout its /// linked list structures (active source slots, paren tracker, line /// tracker, sources builder, constants builder, stack names). This is /// safe as long as all memory allocated during parsing stays below diff --git a/src/lib/parse/literal/LibParseLiteral.sol b/src/lib/parse/literal/LibParseLiteral.sol index 77cd404a5..140d66688 100644 --- a/src/lib/parse/literal/LibParseLiteral.sol +++ b/src/lib/parse/literal/LibParseLiteral.sol @@ -15,11 +15,16 @@ import {UnsupportedLiteralType, UppercaseHexPrefix} from "../../../error/ErrPars import {ParseState} from "../LibParseState.sol"; import {LibParseError} from "../LibParseError.sol"; +/// @dev The number of built-in literal parser types. uint256 constant LITERAL_PARSERS_LENGTH = 4; +/// @dev Index of the hexadecimal literal parser (e.g. `0xDEAD`). uint256 constant LITERAL_PARSER_INDEX_HEX = 0; +/// @dev Index of the decimal literal parser (e.g. `42`, `1e18`). uint256 constant LITERAL_PARSER_INDEX_DECIMAL = 1; +/// @dev Index of the string literal parser (e.g. `"hello"`). uint256 constant LITERAL_PARSER_INDEX_STRING = 2; +/// @dev Index of the sub-parseable literal parser (e.g. `[dispatch body]`). uint256 constant LITERAL_PARSER_INDEX_SUB_PARSE = 3; library LibParseLiteral { From 14b95dc482260a15444257473e9eef979d870b7c Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 4 Mar 2026 01:48:11 +0400 Subject: [PATCH 13/13] Fix all remaining audit findings across Solidity and Rust Solidity fixes: - Remove unused IERC165 import, LibUint256Array import/using - Add virtual/override keywords to concrete contracts - Extract magic numbers to named constants (MAX_PAREN_OFFSET, MAX_STACK_RHS_OFFSET) - Replace raw hex masks with type(uint8).max/type(uint16).max - Add @title NatSpec to 7 parse libraries - Remove redundant casts and double parens - Suppress forge-lint unsafe-typecast warnings in test files - Update deploy constants after pointer cascade Rust fixes: - Add error path tests for Forker::call, call_committing, roll_fork - Add Forker::new() + add_or_select empty-forks test - Add RainSourceTrace::from_data() edge case tests (empty, partial, boundary) - Add CLI Parse command test - Add crate-level //! docs to all 6 crates - Add /// doc comments to all undocumented public items in cli, eval, parser, dispair - Fix "Rainalang" typo, wrong param names in new_with_fork doc, missing .await - Add missing decode_error param to alloy_call/alloy_call_committing docs - Remove unused serde_json/reqwest/once_cell dependencies from eval - Replace wildcard imports with specific imports in dispair and parser - Fix inconsistent TypedError format string in alloy_call_committing Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 3 + Cargo.lock | 3 - audit/2026-03-01-01/triage.md | 72 ++++----- crates/bindings/src/lib.rs | 2 + crates/cli/src/commands/eval.rs | 2 + crates/cli/src/commands/parse.rs | 32 ++++ crates/cli/src/execute.rs | 2 + crates/cli/src/lib.rs | 6 + crates/cli/src/output.rs | 4 + crates/dispair/src/lib.rs | 11 +- crates/eval/Cargo.toml | 3 - crates/eval/src/error.rs | 2 + crates/eval/src/eval.rs | 2 +- crates/eval/src/fork.rs | 146 ++++++++++++++++-- crates/eval/src/lib.rs | 2 + crates/eval/src/trace.rs | 81 ++++++++++ crates/parser/src/lib.rs | 2 + crates/parser/src/v2.rs | 12 +- crates/test_fixtures/src/lib.rs | 2 + src/concrete/RainterpreterDISPaiRegistry.sol | 10 +- .../RainterpreterExpressionDeployer.sol | 6 +- src/concrete/RainterpreterParser.sol | 1 + ...interpreterExpressionDeployer.pointers.sol | 2 +- .../RainterpreterParser.pointers.sol | 6 +- src/lib/deploy/LibInterpreterDeploy.sol | 12 +- src/lib/extern/LibExtern.sol | 2 +- src/lib/parse/LibParse.sol | 20 ++- src/lib/parse/LibParseOperand.sol | 5 +- src/lib/parse/LibParsePragma.sol | 3 + src/lib/parse/LibParseStackTracker.sol | 3 + src/lib/parse/LibParseState.sol | 11 +- src/lib/parse/LibSubParse.sol | 10 +- src/lib/parse/literal/LibParseLiteral.sol | 3 + .../parse/literal/LibParseLiteralDecimal.sol | 3 + src/lib/parse/literal/LibParseLiteralHex.sol | 3 + .../literal/LibParseLiteralSubParseable.sol | 3 + test/lib/string/LibCamelToKebab.sol | 4 + ...LibAllStandardOps.filesystemOrdering.t.sol | 2 + test/src/lib/parse/LibParseInterstitial.t.sol | 3 + 39 files changed, 400 insertions(+), 101 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 82ad26062..4d2ad02a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,8 @@ nix develop -c cargo doc # Rust docs 3. `nix develop -c forge script --silent ./script/BuildPointers.sol` deploys contracts in local EVM, extracts function pointer tables, and writes `src/generated/*.pointers.sol` 4. `nix develop -c forge build` compiles everything using the generated pointers +After `forge build`, check for warnings. Address all warnings before proceeding to pointer rebuild, tests, or the next task. + The `src/generated/` directory contains build-time generated constants (bytecode hashes, function pointer tables, parse meta). These are regenerated by `BuildPointers.sol`. After any source change affecting bytecode: run `nix develop -c i9r-prelude` → `nix develop -c forge script --silent ./script/BuildPointers.sol` → `nix develop -c forge fmt`, then run `LibInterpreterDeployTest` to get new deploy addresses/codehashes. Update `LibInterpreterDeploy.sol` and repeat until stable (constants cascade through the deploy chain). @@ -113,6 +115,7 @@ Testing patterns and conventions are in `TESTING.md`. Read that file before writ - Test files are in `test/` mirroring `src/` structure, suffixed `.t.sol` - Rust test fixtures (`crates/test_fixtures/`) deploy all four contracts on a local Anvil instance - Always run test commands with `run_in_background: true` so work continues in parallel +- While background builds or tests run, continue with other work that doesn't depend on the build result — e.g. triage presentation, code review, documentation edits ## Process (Jidoka) diff --git a/Cargo.lock b/Cargo.lock index f85982563..4c32a91df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5452,15 +5452,12 @@ dependencies = [ "alloy", "eyre", "foundry-evm", - "once_cell", "rain-error-decoding 0.1.0 (git+https://github.com/rainlanguage/rain.error?rev=3d2ed70fb2f7c6156706846e10f163d1e493a8d3)", "rain_interpreter_bindings", "rain_interpreter_test_fixtures", - "reqwest 0.11.27", "revm 24.0.1", "revm 25.0.0", "serde", - "serde_json", "thiserror 1.0.69", "tokio", "tracing", diff --git a/audit/2026-03-01-01/triage.md b/audit/2026-03-01-01/triage.md index 0af3dbd8f..31456b1ad 100644 --- a/audit/2026-03-01-01/triage.md +++ b/audit/2026-03-01-01/triage.md @@ -59,10 +59,10 @@ - [FIXED] P2-01: (LOW) Missing operand-disallowed tests for 10 logic opcodes - [FIXED] P2-02: (LOW) Missing operand-disallowed tests for 5 math opcodes — LibOpSub already had test; added tests for LibOpMaxPositiveValue, LibOpMaxNegativeValue, LibOpMinPositiveValue, LibOpMinNegativeValue - [FIXED] P2-03: (LOW) Missing operand-disallowed test for LibOpHash -- [PENDING] R02-PASS2-01: (LOW) No tests for error paths in Forker methods -- [PENDING] R02-PASS2-02: (LOW) Forker::new() has no test -- [PENDING] R02-PASS2-04: (LOW) RainSourceTrace::from_data() edge cases untested -- [PENDING] R02-PASS2-07: (LOW) CLI Parse command entirely untested +- [FIXED] R02-PASS2-01: (LOW) No tests for error paths in Forker methods — added 11 tests for call/call_committing invalid address lengths and roll_fork with no active fork +- [FIXED] R02-PASS2-02: (LOW) Forker::new() has no test — added test_forker_new_then_add_or_select covering constructor and empty-forks add_or_select branch +- [FIXED] R02-PASS2-04: (LOW) RainSourceTrace::from_data() edge cases untested — added 7 tests covering empty, 1/3/4 bytes, trailing partial word, one full word, one full word plus partial, two full words +- [FIXED] R02-PASS2-07: (LOW) CLI Parse command entirely untested — added test_execute for Parse command ## Pass 3: Documentation @@ -86,41 +86,41 @@ - [FIXED] P3-OPALL-02: (LOW) Missing @return tags on integrity() in 7 files — same as P3-OPALL-01 - [FIXED] P3-OPALL-03: (LOW) Missing @return on referenceFn() in LibOpUint256MaxValue - [FIXED] P3-OPALL-04: (LOW) Missing @notice on integrity() in LibOpEnsure -- [PENDING] P3-RC-01: (LOW) No crate-level //! documentation on any Rust crate -- [PENDING] P3-RC-02: (LOW) CLI crate all 13 public items completely undocumented -- [PENDING] P3-RC-03: (LOW) eval crate 10 undocumented public items -- [PENDING] P3-RC-04: (LOW) parser crate 3 undocumented public items -- [PENDING] P3-RC-05: (LOW) dispair crate DISPaiR::new constructor lacks doc comment -- [PENDING] P3-RC-06: (LOW) "Rainalang" typo in ForkEvalArgs.rainlang_string field doc -- [PENDING] P3-RC-07: (LOW) Forker::new_with_fork doc lists wrong params and missing await -- [PENDING] P3-RC-08: (LOW) alloy_call/alloy_call_committing docs omit decode_error param +- [FIXED] P3-RC-01: (LOW) No crate-level //! documentation on any Rust crate — added //! docs to all 6 crates +- [FIXED] P3-RC-02: (LOW) CLI crate all 13 public items completely undocumented — added /// doc comments to Interpreter enum/variants, Execute trait, Parse/Eval/ForkParseArgsCli/ForkEvalCliArgs structs, SupportedOutputEncoding enum/variants, output fn +- [FIXED] P3-RC-03: (LOW) eval crate 10 undocumented public items — added /// docs to ForkTypedReturn, NewForkedEvm (structs + fields), ForkCallError, ReplayTransactionError enums +- [FIXED] P3-RC-04: (LOW) parser crate 3 undocumented public items — added docs to Parser2 trait (both wasm/non-wasm), ParserV2 struct + field, new() +- [FIXED] P3-RC-05: (LOW) dispair crate DISPaiR::new constructor lacks doc comment — replaced bare struct doc, added doc to new() +- [FIXED] P3-RC-06: (LOW) "Rainalang" typo in ForkEvalArgs.rainlang_string field doc — changed to "Rainlang" +- [FIXED] P3-RC-07: (LOW) Forker::new_with_fork doc lists wrong params and missing await — updated param names to match signature, added .await to example +- [FIXED] P3-RC-08: (LOW) alloy_call/alloy_call_committing docs omit decode_error param — added decode_error to both # Arguments sections ## Pass 4: Code Quality -- [PENDING] P4-CC-01: (LOW) Unused IERC165 import in RainterpreterExpressionDeployer -- [PENDING] P4-CC-02: (LOW) No virtual on any function in RainterpreterDISPaiRegistry -- [PENDING] P4-CC-03: (LOW) buildIntegrityFunctionPointers missing override in RainterpreterExpressionDeployer -- [PENDING] P4-CC-04: (LOW) describedByMetaV1 missing virtual in RainterpreterExpressionDeployer -- [PENDING] P4-CC-05: (LOW) unsafeParse missing virtual in RainterpreterParser -- [PENDING] P4-EVLP-1: (LOW) Unused using LibUint256Array and import in LibParse.sol -- [PENDING] P4-EVLP-2: (LOW) Unused return value suppressed with (index); instead of blank destructuring -- [PENDING] P4-EVLP-5: (LOW) Magic number 59 for paren depth limit -- [PENDING] P4-EVLP-6: (LOW) Magic number 0x3f for stack RHS overflow -- [PENDING] P4-PARSE-2: (LOW) handleOperandSingleFull type(uint16).max vs uint256() inconsistency -- [PENDING] P4-PARSE-3: (LOW) Redundant double-parentheses in LibSubParse.sol -- [PENDING] P4-PARSE-4: (LOW) type(uint8).max vs raw 0xFF bounds check inconsistency -- [PENDING] P4-PARSE-5: (LOW) 7 of 10 parse libraries missing @title NatSpec -- [PENDING] P4-EA-01: (LOW) Dispatch decoding duplicated inline instead of reusing LibExtern -- [PENDING] P4-EA-02: (LOW) Inconsistent bitmask style type(uint16).max vs 0xFFFF -- [PENDING] P4-EA-03: (LOW) Context position constants defined locally instead of shared -- [PENDING] P4-EA-04: (LOW) Inconsistent subParser parameter naming across context ops -- [PENDING] P4-EA-05: (LOW) Five build* functions in RainterpreterReferenceExtern repeat boilerplate -- [PENDING] PASS4-LIBOP-1: (LOW) Missing @notice tag on integrity/referenceFn in 8 files -- [PENDING] PASS4-LIBOP-2: (LOW) LibOpCall only standard opcode without referenceFn -- [PENDING] P4-ERR-01: (LOW) Inconsistent @notice tags across error files -- [PENDING] P4-RUST-01: (LOW) Unused dependencies serde_json, reqwest, once_cell in eval Cargo.toml -- [PENDING] P4-RUST-02: (LOW) Wildcard use alloy::primitives::* in dispair and parser -- [PENDING] P4-RUST-03: (LOW) Duplicated error-handling logic in alloy_call/alloy_call_committing with inconsistent format +- [FIXED] P4-CC-01: (LOW) Unused IERC165 import in RainterpreterExpressionDeployer +- [FIXED] P4-CC-02: (LOW) No virtual on any function in RainterpreterDISPaiRegistry — added virtual to all 5 functions +- [FIXED] P4-CC-03: (LOW) buildIntegrityFunctionPointers missing override in RainterpreterExpressionDeployer +- [FIXED] P4-CC-04: (LOW) describedByMetaV1 missing virtual in RainterpreterExpressionDeployer +- [FIXED] P4-CC-05: (LOW) unsafeParse missing virtual in RainterpreterParser +- [FIXED] P4-EVLP-1: (LOW) Unused using LibUint256Array and import in LibParse.sol +- [FIXED] P4-EVLP-2: (LOW) Unused return value suppressed with (index); instead of blank destructuring +- [FIXED] P4-EVLP-5: (LOW) Magic number 59 for paren depth limit — extracted to MAX_PAREN_OFFSET constant +- [FIXED] P4-EVLP-6: (LOW) Magic number 0x3f for stack RHS overflow — extracted to MAX_STACK_RHS_OFFSET constant +- [FIXED] P4-PARSE-2: (LOW) handleOperandSingleFull type(uint16).max vs uint256() inconsistency +- [FIXED] P4-PARSE-3: (LOW) Redundant double-parentheses in LibSubParse.sol +- [FIXED] P4-PARSE-4: (LOW) type(uint8).max vs raw 0xFF bounds check inconsistency +- [FIXED] P4-PARSE-5: (LOW) 7 of 10 parse libraries missing @title NatSpec +- [DISMISSED] P4-EA-01: (LOW) Dispatch decoding duplicated inline instead of reusing LibExtern — inline code is interleaved with surrounding logic (unchecked mod, bounds check); refactoring adds bytecode risk for no functional benefit +- [FIXED] P4-EA-02: (LOW) Inconsistent bitmask style type(uint16).max vs 0xFFFF — updated LibExtern.decodeExternDispatch +- [UPSTREAM] P4-EA-03: (LOW) Context position constants defined locally instead of shared — belongs in rain.interpreter.interface; local constants already have NatSpec (A49-8) +- [FIXED] P4-EA-04: (LOW) Inconsistent subParser parameter naming across context ops — already fixed in P3-EA-03 +- [DISMISSED] P4-EA-05: (LOW) Five build* functions in RainterpreterReferenceExtern repeat boilerplate — extraction blocked by varying function pointer types; accepted duplication +- [FIXED] PASS4-LIBOP-1: (LOW) Missing @notice tag on integrity/referenceFn in 8 files — already fixed in P3-OPALL-01/02/03/04 and P5-EXPGROWTH-01 +- [DISMISSED] PASS4-LIBOP-2: (LOW) LibOpCall only standard opcode without referenceFn — intentional; cross-source call semantics cannot be expressed in standard referenceFn signature +- [FIXED] P4-ERR-01: (LOW) Inconsistent @notice tags across error files — already fixed in P3-ERR-1 +- [FIXED] P4-RUST-01: (LOW) Unused dependencies serde_json, reqwest, once_cell in eval Cargo.toml — removed all three +- [FIXED] P4-RUST-02: (LOW) Wildcard use alloy::primitives::* in dispair and parser — replaced with use alloy::primitives::Address in both +- [FIXED] P4-RUST-03: (LOW) Duplicated error-handling logic in alloy_call/alloy_call_committing with inconsistent format — added Raw:{:?} to alloy_call_committing to match alloy_call ## Pass 5: Correctness diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 38fcf4d3f..8dec2ea97 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -1,3 +1,5 @@ +//! Alloy Solidity contract bindings for rain.interpreter. + use alloy::sol; sol!( diff --git a/crates/cli/src/commands/eval.rs b/crates/cli/src/commands/eval.rs index b277f6c8e..d95947c53 100644 --- a/crates/cli/src/commands/eval.rs +++ b/crates/cli/src/commands/eval.rs @@ -11,6 +11,7 @@ use rain_interpreter_eval::trace::RainEvalResult; use rain_interpreter_eval::{eval::ForkEvalArgs, fork::Forker}; use std::path::PathBuf; +/// CLI arguments for evaluating a Rainlang expression. #[derive(Args, Clone, Debug)] pub struct ForkEvalCliArgs { #[arg(short, long, help = "The Rainlang string to parse")] @@ -101,6 +102,7 @@ fn parse_int_or_hex(value: &str) -> Result { } } +/// CLI subcommand that evaluates a Rainlang expression against a forked EVM. #[derive(Args, Clone)] pub struct Eval { /// Output path. If not specified, the output is written to stdout. diff --git a/crates/cli/src/commands/parse.rs b/crates/cli/src/commands/parse.rs index b18ff7077..76e2e4ebd 100644 --- a/crates/cli/src/commands/parse.rs +++ b/crates/cli/src/commands/parse.rs @@ -9,6 +9,7 @@ use rain_interpreter_eval::eval::ForkParseArgs; use rain_interpreter_eval::fork::Forker; use std::path::PathBuf; +/// CLI arguments for parsing a Rainlang expression. #[derive(Args, Clone, Debug)] pub struct ForkParseArgsCli { #[arg(short, long, help = "The address of the deployer")] @@ -21,6 +22,7 @@ pub struct ForkParseArgsCli { decode_errors: bool, } +/// CLI subcommand that parses a Rainlang expression into bytecode. #[derive(Args, Clone)] pub struct Parse { /// Output path. If not specified, the output is written to stdout. @@ -62,3 +64,33 @@ impl Execute for Parse { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::fork::NewForkedEvmCliArgs; + use rain_interpreter_test_fixtures::LocalEvm; + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_execute() { + let local_evm = LocalEvm::new().await; + let deployer = *local_evm.deployer.address(); + + let parse = Parse { + output_path: None, + output_encoding: SupportedOutputEncoding::Binary, + forked_evm: NewForkedEvmCliArgs { + fork_url: local_evm.url(), + fork_block_number: None, + }, + fork_parse_args: ForkParseArgsCli { + deployer, + rainlang_string: "_: 1;".into(), + decode_errors: false, + }, + }; + + let result = parse.execute().await; + assert!(result.is_ok()); + } +} diff --git a/crates/cli/src/execute.rs b/crates/cli/src/execute.rs index 95810519e..8a67a4913 100644 --- a/crates/cli/src/execute.rs +++ b/crates/cli/src/execute.rs @@ -1,5 +1,7 @@ use anyhow::Result; +/// Trait for CLI subcommands that can be executed asynchronously. pub trait Execute { + /// Runs the subcommand, returning an error on failure. async fn execute(&self) -> Result<()>; } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 62923a81e..38870cb34 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,3 +1,5 @@ +//! CLI tool for parsing and evaluating Rainlang expressions. + use crate::commands::Parse; use crate::execute::Execute; use anyhow::Result; @@ -9,13 +11,17 @@ mod execute; mod fork; mod output; +/// Top-level CLI command enum dispatching to `Parse` or `Eval` subcommands. #[derive(Parser)] pub enum Interpreter { + /// Parse a Rainlang expression into bytecode. Parse(Parse), + /// Evaluate a Rainlang expression against a forked EVM. Eval(Eval), } impl Interpreter { + /// Dispatches to the selected subcommand's `execute` implementation. pub async fn execute(self) -> Result<()> { match self { Interpreter::Parse(parse) => parse.execute().await, diff --git a/crates/cli/src/output.rs b/crates/cli/src/output.rs index 137fe54a9..a2b2e483f 100644 --- a/crates/cli/src/output.rs +++ b/crates/cli/src/output.rs @@ -1,12 +1,16 @@ use std::io::Write; use std::path::PathBuf; +/// Output encoding formats supported by the CLI. #[derive(clap::ValueEnum, Clone)] pub enum SupportedOutputEncoding { + /// Raw binary bytes. Binary, + /// 0x-prefixed hex string. Hex, } +/// Writes `bytes` to `output_path` (or stdout) using the given encoding. pub fn output( output_path: &Option, output_encoding: SupportedOutputEncoding, diff --git a/crates/dispair/src/lib.rs b/crates/dispair/src/lib.rs index 4735f93f7..cc3485290 100644 --- a/crates/dispair/src/lib.rs +++ b/crates/dispair/src/lib.rs @@ -1,7 +1,11 @@ -use alloy::primitives::*; +//! DISPaiR (Deployer/Interpreter/Store/Parser) address tuple. -/// DISPaiR -/// Struct representing Deployer/Interpreter/Store/Parser instances. +use alloy::primitives::Address; + +/// Deployer/Interpreter/Store/Parser address tuple. +/// +/// Groups the four contract addresses that together form a complete +/// Rain interpreter deployment. #[derive(Debug, Clone, Default)] pub struct DISPaiR { pub deployer: Address, @@ -11,6 +15,7 @@ pub struct DISPaiR { } impl DISPaiR { + /// Creates a new `DISPaiR` from the four component addresses. pub fn new(deployer: Address, interpreter: Address, store: Address, parser: Address) -> Self { DISPaiR { deployer, diff --git a/crates/eval/Cargo.toml b/crates/eval/Cargo.toml index efefb896d..47439b659 100644 --- a/crates/eval/Cargo.toml +++ b/crates/eval/Cargo.toml @@ -10,9 +10,6 @@ alloy = { workspace = true } thiserror = { workspace = true } rain_interpreter_bindings = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } -reqwest = { workspace = true } -once_cell = { workspace = true } eyre = { workspace = true } rain-error-decoding = { workspace = true } diff --git a/crates/eval/src/error.rs b/crates/eval/src/error.rs index 136e72122..ad28235bd 100644 --- a/crates/eval/src/error.rs +++ b/crates/eval/src/error.rs @@ -4,6 +4,7 @@ use foundry_evm::{backend::DatabaseError, executors::RawCallResult}; use rain_error_decoding::{AbiDecodeFailedErrors, AbiDecodedErrorType}; use thiserror::Error; +/// Errors that can occur when calling a forked EVM. #[derive(Debug, Error)] pub enum ForkCallError { #[error("Executor error: {0}")] @@ -27,6 +28,7 @@ pub enum ForkCallError { ReplayTransactionError(#[from] ReplayTransactionError), } +/// Errors specific to replaying a historical transaction. #[derive(Debug, Error)] pub enum ReplayTransactionError { #[error("Transaction not found for hash {0} and fork url {1}")] diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 8cfa1376c..47380367f 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -8,7 +8,7 @@ use rain_interpreter_bindings::IParserV2::parse2Call; /// Arguments for evaluating a Rainlang string in a forked EVM context #[derive(Debug, Clone)] pub struct ForkEvalArgs { - /// The Rainalang string to evaluate + /// The Rainlang string to evaluate pub rainlang_string: String, /// The source index of the rainlang to evaluate pub source_index: u16, diff --git a/crates/eval/src/fork.rs b/crates/eval/src/fork.rs index 72173b2ee..e586ac6f5 100644 --- a/crates/eval/src/fork.rs +++ b/crates/eval/src/fork.rs @@ -28,14 +28,21 @@ pub struct Forker { forks: HashMap, } +/// Result of an alloy-typed call containing both the raw EVM result and the +/// ABI-decoded return value. pub struct ForkTypedReturn { + /// The raw EVM call result including traces and exit reason. pub raw: RawCallResult, + /// The ABI-decoded return value. pub typed_return: C::Return, } +/// Configuration for creating a new forked EVM instance. #[derive(Debug, Clone)] pub struct NewForkedEvm { + /// RPC URL of the network to fork. pub fork_url: String, + /// Optional block number to fork from. Uses latest if `None`. pub fork_block_number: Option, } @@ -71,24 +78,24 @@ impl Forker { /// /// # Arguments /// - /// * `fork_url` - The URL of the fork to connect to. - /// * `fork_block_number` - Optional fork block number to start from. - /// * `env` - Optional fork environment. - /// * `gas_limit` - Optional fork gas limit. + /// * `args` - Fork configuration containing URL and optional block number. + /// * `env` - Optional EVM environment override. + /// * `gas_limit` - Optional gas limit override. /// /// # Returns /// /// A new instance of `Forker`. + /// /// # Examples /// - /// ``` + /// ```ignore /// use rain_interpreter_eval::fork::{Forker, NewForkedEvm}; /// - /// let fork_url = "https://example.com/fork".to_owned(); - /// let fork_block_number = Some(12345u64); - /// let args = NewForkedEvm { fork_url, fork_block_number }; - /// - /// let forker = Forker::new_with_fork(args, None, None); + /// let args = NewForkedEvm { + /// fork_url: "https://example.com/fork".to_owned(), + /// fork_block_number: Some(12345u64), + /// }; + /// let forker = Forker::new_with_fork(args, None, None).await; /// ``` pub async fn new_with_fork( args: NewForkedEvm, @@ -227,6 +234,7 @@ impl Forker { /// * `from_address` - The address to call from. /// * `to_address` - The address to call to. /// * `call` - The call to make. + /// * `decode_error` - Whether to decode revert data using the error selector registry. /// # Returns /// A result containing the raw call result and the typed return. pub async fn alloy_call( @@ -270,6 +278,7 @@ impl Forker { /// * `to_address` - The address to call to. /// * `call` - The call to make. /// * `value` - The value to send with the call. + /// * `decode_error` - Whether to decode revert data using the error selector registry. /// # Returns /// A result containing the raw call result and the typed return. pub async fn alloy_call_committing( @@ -299,7 +308,12 @@ impl Forker { } let typed_return = T::abi_decode_returns(&raw.result.0).map_err(|e| { - ForkCallError::TypedError(format!("Call:{:?} Error:{:?}", type_name::(), e)) + ForkCallError::TypedError(format!( + "Call:{:?} Error:{:?} Raw:{:?}", + type_name::(), + e, + raw + )) })?; Ok(ForkTypedReturn { raw, typed_return }) } @@ -801,4 +815,114 @@ mod tests { assert!(replay_result.env.tx.caller == local_evm.anvil.addresses()[0]); assert!(replay_result.exit_reason.is_ok()); } + + #[test] + fn test_call_invalid_from_address_too_short() { + let forker = Forker::new().unwrap(); + let result = forker.call(&[0u8; 19], &[0u8; 20], &[]); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "invalid address!")); + } + + #[test] + fn test_call_invalid_from_address_too_long() { + let forker = Forker::new().unwrap(); + let result = forker.call(&[0u8; 21], &[0u8; 20], &[]); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "invalid address!")); + } + + #[test] + fn test_call_invalid_from_address_empty() { + let forker = Forker::new().unwrap(); + let result = forker.call(&[], &[0u8; 20], &[]); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "invalid address!")); + } + + #[test] + fn test_call_invalid_to_address_too_short() { + let forker = Forker::new().unwrap(); + let result = forker.call(&[0u8; 20], &[0u8; 19], &[]); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "invalid address!")); + } + + #[test] + fn test_call_invalid_to_address_too_long() { + let forker = Forker::new().unwrap(); + let result = forker.call(&[0u8; 20], &[0u8; 21], &[]); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "invalid address!")); + } + + #[test] + fn test_call_invalid_to_address_empty() { + let forker = Forker::new().unwrap(); + let result = forker.call(&[0u8; 20], &[], &[]); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "invalid address!")); + } + + #[test] + fn test_call_committing_invalid_from_address() { + let mut forker = Forker::new().unwrap(); + let result = forker.call_committing(&[0u8; 19], &[0u8; 20], &[], U256::ZERO); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "invalid address!")); + } + + #[test] + fn test_call_committing_invalid_to_address() { + let mut forker = Forker::new().unwrap(); + let result = forker.call_committing(&[0u8; 20], &[0u8; 21], &[], U256::ZERO); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "invalid address!")); + } + + #[test] + fn test_call_committing_both_addresses_invalid() { + let mut forker = Forker::new().unwrap(); + let result = forker.call_committing(&[], &[], &[], U256::ZERO); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "invalid address!")); + } + + #[test] + fn test_roll_fork_no_active_fork() { + let mut forker = Forker::new().unwrap(); + let result = forker.roll_fork(None, None); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "no active fork!")); + } + + #[test] + fn test_roll_fork_no_active_fork_with_block_number() { + let mut forker = Forker::new().unwrap(); + let result = forker.roll_fork(Some(100), None); + assert!(matches!(result, Err(ForkCallError::ExecutorError(ref msg)) if msg == "no active fork!")); + } + + /// Forker::new() creates a valid empty forker, and add_or_select on an + /// empty forker populates it via the is_empty branch. + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_forker_new_then_add_or_select() { + let local_evm = LocalEvm::new().await; + let deployer = *local_evm.deployer.address(); + let mut forker = Forker::new().unwrap(); + assert!(forker.forks.is_empty()); + + forker + .add_or_select( + NewForkedEvm { + fork_url: local_evm.url(), + fork_block_number: None, + }, + None, + ) + .await + .unwrap(); + + assert!(!forker.forks.is_empty()); + + // Verify the fork is usable by making a call. + let call = IERC165::supportsInterfaceCall { + interfaceId: alloy::primitives::FixedBytes([0x01, 0xff, 0xc9, 0xa7]), + }; + let result = forker + .alloy_call(Address::default(), deployer, call, false) + .await + .unwrap(); + assert!(result.typed_return); + } } diff --git a/crates/eval/src/lib.rs b/crates/eval/src/lib.rs index 49ce6b462..cc7601676 100644 --- a/crates/eval/src/lib.rs +++ b/crates/eval/src/lib.rs @@ -1,3 +1,5 @@ +//! Evaluation runtime for Rainlang expressions using forked EVM contexts. + pub mod error; #[cfg(not(target_family = "wasm"))] pub mod eval; diff --git a/crates/eval/src/trace.rs b/crates/eval/src/trace.rs index f8f5053de..15e77cf63 100644 --- a/crates/eval/src/trace.rs +++ b/crates/eval/src/trace.rs @@ -540,6 +540,87 @@ mod tests { )); } + #[test] + fn test_from_data_empty() { + assert!(RainSourceTrace::from_data(&[]).is_none()); + } + + #[test] + fn test_from_data_one_byte() { + assert!(RainSourceTrace::from_data(&[0x01]).is_none()); + } + + #[test] + fn test_from_data_three_bytes() { + assert!(RainSourceTrace::from_data(&[0x00, 0x01, 0x02]).is_none()); + } + + #[test] + fn test_from_data_exactly_four_bytes() { + let data = [0x00, 0x01, 0x00, 0x02]; + let trace = RainSourceTrace::from_data(&data).unwrap(); + assert_eq!(trace.parent_source_index, 1); + assert_eq!(trace.source_index, 2); + assert!(trace.stack.is_empty()); + } + + #[test] + fn test_from_data_trailing_partial_word() { + // 4 header bytes + 31 payload bytes = 35 total. + // 31 bytes is less than 32, so no stack item is produced. + let mut data = vec![0x00, 0x03, 0x00, 0x04]; + data.extend_from_slice(&[0xFF; 31]); + let trace = RainSourceTrace::from_data(&data).unwrap(); + assert_eq!(trace.parent_source_index, 3); + assert_eq!(trace.source_index, 4); + assert!(trace.stack.is_empty()); + } + + #[test] + fn test_from_data_one_full_word() { + // 4 header bytes + 32 payload bytes = 36 total. + let mut data = vec![0x00, 0x00, 0x00, 0x01]; + let mut word = [0u8; 32]; + word[31] = 0x42; + data.extend_from_slice(&word); + let trace = RainSourceTrace::from_data(&data).unwrap(); + assert_eq!(trace.parent_source_index, 0); + assert_eq!(trace.source_index, 1); + assert_eq!(trace.stack.len(), 1); + assert_eq!(trace.stack[0], U256::from(0x42)); + } + + #[test] + fn test_from_data_one_full_word_plus_partial() { + // 4 header + 32 full word + 15 trailing = 51 total. + // Only 1 stack item should be produced; the 15 trailing bytes are dropped. + let mut data = vec![0x00, 0x05, 0x00, 0x06]; + let mut word = [0u8; 32]; + word[31] = 0x07; + data.extend_from_slice(&word); + data.extend_from_slice(&[0xFF; 15]); + let trace = RainSourceTrace::from_data(&data).unwrap(); + assert_eq!(trace.parent_source_index, 5); + assert_eq!(trace.source_index, 6); + assert_eq!(trace.stack.len(), 1); + assert_eq!(trace.stack[0], U256::from(0x07)); + } + + #[test] + fn test_from_data_two_full_words() { + let mut data = vec![0x00, 0x00, 0x00, 0x00]; + let mut word1 = [0u8; 32]; + word1[31] = 0x0A; + let mut word2 = [0u8; 32]; + word2[31] = 0x0B; + data.extend_from_slice(&word1); + data.extend_from_slice(&word2); + let trace = RainSourceTrace::from_data(&data).unwrap(); + assert_eq!(trace.stack.len(), 2); + assert_eq!(trace.stack[0], U256::from(0x0A)); + assert_eq!(trace.stack[1], U256::from(0x0B)); + } + #[test] fn test_rain_eval_result_into_flattened_table() { let trace1 = RainSourceTrace { diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index 802a7089b..486047fd6 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -1,3 +1,5 @@ +//! Rust client for the on-chain Rainlang parser contract. + pub mod error; pub mod v2; diff --git a/crates/parser/src/v2.rs b/crates/parser/src/v2.rs index f8ed612ce..1c374fe77 100644 --- a/crates/parser/src/v2.rs +++ b/crates/parser/src/v2.rs @@ -1,10 +1,11 @@ use crate::error::ParserError; -use alloy::primitives::*; +use alloy::primitives::Address; use alloy_ethers_typecast::{ReadContractParametersBuilder, ReadableClient}; use rain_interpreter_bindings::IParserPragmaV1::*; use rain_interpreter_bindings::IParserV2::*; use rain_interpreter_dispair::DISPaiR; +/// Trait for interacting with the on-chain Rainlang parser contract. #[cfg(not(target_family = "wasm"))] pub trait Parser2 { /// Call Parser contract to parse the provided rainlang text. @@ -51,6 +52,7 @@ pub trait Parser2 { } } +/// Trait for interacting with the on-chain Rainlang parser contract. #[cfg(target_family = "wasm")] pub trait Parser2 { /// Call Parser contract to parse the provided rainlang text. @@ -97,10 +99,11 @@ pub trait Parser2 { } } -/// ParserV2 -/// Struct representing ParserV2 instances. +/// Client-side wrapper around a deployer address that implements [`Parser2`] +/// by making read calls to the on-chain parser contract. #[derive(Clone, Default)] pub struct ParserV2 { + /// The address of the expression deployer whose parser will be called. pub deployer_address: Address, } @@ -121,6 +124,7 @@ impl From
for ParserV2 { } impl ParserV2 { + /// Creates a new `ParserV2` for the given deployer address. pub fn new(deployer_address: Address) -> Self { Self { deployer_address } } @@ -169,7 +173,7 @@ impl Parser2 for ParserV2 { #[cfg(test)] mod tests { use super::*; - use alloy::{primitives::Address, providers::mock::Asserter}; + use alloy::{hex, primitives::Address, providers::mock::Asserter}; #[tokio::test] async fn test_from_dispair() { diff --git a/crates/test_fixtures/src/lib.rs b/crates/test_fixtures/src/lib.rs index 472bc558b..33d728518 100644 --- a/crates/test_fixtures/src/lib.rs +++ b/crates/test_fixtures/src/lib.rs @@ -1,3 +1,5 @@ +//! Shared test fixtures for rain.interpreter Rust crates. + use alloy::{ contract::SolCallBuilder, network::{AnyNetwork, AnyReceiptEnvelope, EthereumWallet}, diff --git a/src/concrete/RainterpreterDISPaiRegistry.sol b/src/concrete/RainterpreterDISPaiRegistry.sol index df44b8d7d..d072638d9 100644 --- a/src/concrete/RainterpreterDISPaiRegistry.sol +++ b/src/concrete/RainterpreterDISPaiRegistry.sol @@ -14,27 +14,27 @@ import {ERC165} from "openzeppelin-contracts/contracts/utils/introspection/ERC16 /// address. contract RainterpreterDISPaiRegistry is IDISPaiRegistry, ERC165 { /// @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return interfaceId == type(IDISPaiRegistry).interfaceId || super.supportsInterface(interfaceId); } /// @inheritdoc IDISPaiRegistry - function expressionDeployerAddress() external pure override returns (address) { + function expressionDeployerAddress() external pure virtual override returns (address) { return LibInterpreterDeploy.EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS; } /// @inheritdoc IDISPaiRegistry - function interpreterAddress() external pure override returns (address) { + function interpreterAddress() external pure virtual override returns (address) { return LibInterpreterDeploy.INTERPRETER_DEPLOYED_ADDRESS; } /// @inheritdoc IDISPaiRegistry - function storeAddress() external pure override returns (address) { + function storeAddress() external pure virtual override returns (address) { return LibInterpreterDeploy.STORE_DEPLOYED_ADDRESS; } /// @inheritdoc IDISPaiRegistry - function parserAddress() external pure override returns (address) { + function parserAddress() external pure virtual override returns (address) { return LibInterpreterDeploy.PARSER_DEPLOYED_ADDRESS; } } diff --git a/src/concrete/RainterpreterExpressionDeployer.sol b/src/concrete/RainterpreterExpressionDeployer.sol index 8842f4a02..c483e69c0 100644 --- a/src/concrete/RainterpreterExpressionDeployer.sol +++ b/src/concrete/RainterpreterExpressionDeployer.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {ERC165, IERC165} from "openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; +import {ERC165} from "openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {IParserV2} from "rain.interpreter.interface/interface/IParserV2.sol"; import {IParserPragmaV1, PragmaV1} from "rain.interpreter.interface/interface/IParserPragmaV1.sol"; @@ -70,12 +70,12 @@ contract RainterpreterExpressionDeployer is } /// @inheritdoc IIntegrityToolingV1 - function buildIntegrityFunctionPointers() external view virtual returns (bytes memory) { + function buildIntegrityFunctionPointers() external view virtual override returns (bytes memory) { return LibAllStandardOps.integrityFunctionPointers(); } /// @inheritdoc IDescribedByMetaV1 - function describedByMetaV1() external pure override returns (bytes32) { + function describedByMetaV1() external pure virtual override returns (bytes32) { return DESCRIBED_BY_META_HASH; } } diff --git a/src/concrete/RainterpreterParser.sol b/src/concrete/RainterpreterParser.sol index 911e8a183..51a413e00 100644 --- a/src/concrete/RainterpreterParser.sol +++ b/src/concrete/RainterpreterParser.sol @@ -57,6 +57,7 @@ contract RainterpreterParser is ERC165, IParserToolingV1 { function unsafeParse(bytes memory data) external view + virtual checkParseMemoryOverflow returns (bytes memory, bytes32[] memory) { diff --git a/src/generated/RainterpreterExpressionDeployer.pointers.sol b/src/generated/RainterpreterExpressionDeployer.pointers.sol index 9dbee4697..2209e47c3 100644 --- a/src/generated/RainterpreterExpressionDeployer.pointers.sol +++ b/src/generated/RainterpreterExpressionDeployer.pointers.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0x445c1b70d019ad0fda5b60cd2ac7e04c5fcd1f59afff4adea3db4e37d829a38d); +bytes32 constant BYTECODE_HASH = bytes32(0x282ab179f2b1bbfe4b5fc50938ee379070dc8731afa3b931b54b2b9d819527f2); /// @dev The hash of the meta that describes the contract. bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0x0ae1ecb6c0f6314beaf4d4cd803ba14c900b0eecb1ecd39a52739cff9ae2c34a); diff --git a/src/generated/RainterpreterParser.pointers.sol b/src/generated/RainterpreterParser.pointers.sol index 96c353f2b..b2cbb84b2 100644 --- a/src/generated/RainterpreterParser.pointers.sol +++ b/src/generated/RainterpreterParser.pointers.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.25; // file needs the contract to exist so that it can be compiled. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0xde67804761c572b0c215bb2f8baf297d6ae7ff9b2690c076896856ee9c638cdb); +bytes32 constant BYTECODE_HASH = bytes32(0x62e660fd8299bfd11fd6aa5f1cc931b5f3574ebe6f1fd0f9bedc8400f2f6b379); /// @dev The parse meta that is used to lookup word definitions. /// The structure of the parse meta is: @@ -39,11 +39,11 @@ uint8 constant PARSE_META_BUILD_DEPTH = 2; /// These positional indexes all map to the same indexes looked up in the parse /// meta. bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = - hex"1a5c1a5c1a5c1afa1bcb1bcb1afa1afa1bcb1a5c1a5c1a5c1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb1bcb"; + hex"1a591a591a591af71bc81bc81af71af71bc81a591a591a591bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc81bc8"; /// @dev Every two bytes is a function pointer for a literal parser. /// Literal dispatches are determined by the first byte(s) of the literal /// rather than a full word lookup, and are done with simple conditional /// jumps as the possibilities are limited compared to the number of words we /// have. -bytes constant LITERAL_PARSER_FUNCTION_POINTERS = hex"16511824186418a3"; +bytes constant LITERAL_PARSER_FUNCTION_POINTERS = hex"164e1821186118a0"; diff --git a/src/lib/deploy/LibInterpreterDeploy.sol b/src/lib/deploy/LibInterpreterDeploy.sol index 8316b27e5..689c4b68a 100644 --- a/src/lib/deploy/LibInterpreterDeploy.sol +++ b/src/lib/deploy/LibInterpreterDeploy.sol @@ -11,14 +11,14 @@ pragma solidity ^0.8.25; library LibInterpreterDeploy { /// The address of the `RainterpreterParser` contract when deployed with the /// rain standard zoltu deployer. - address constant PARSER_DEPLOYED_ADDRESS = address(0x12Efbf18Ccec85818F0301Cfce7616297E1984B5); + address constant PARSER_DEPLOYED_ADDRESS = address(0x2fc4EE5b4985b19a49ebF05F0cD2b4afa81F3CdE); /// The code hash of the `RainterpreterParser` contract when deployed with /// the rain standard zoltu deployer. This can be used to verify that the /// deployed contract has the expected bytecode, which provides stronger /// guarantees than just checking the address. bytes32 constant PARSER_DEPLOYED_CODEHASH = - bytes32(0xde67804761c572b0c215bb2f8baf297d6ae7ff9b2690c076896856ee9c638cdb); + bytes32(0x62e660fd8299bfd11fd6aa5f1cc931b5f3574ebe6f1fd0f9bedc8400f2f6b379); /// The address of the `RainterpreterStore` contract when deployed with the /// rain standard zoltu deployer. @@ -44,23 +44,23 @@ library LibInterpreterDeploy { /// The address of the `RainterpreterExpressionDeployer` contract when /// deployed with the rain standard zoltu deployer. - address constant EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS = address(0xB6151493CF4683c03241Be0D5Ac698447963cb15); + address constant EXPRESSION_DEPLOYER_DEPLOYED_ADDRESS = address(0xa7381d809C2ad239859E0261F2a75DbEF4259c18); /// The code hash of the `RainterpreterExpressionDeployer` contract when /// deployed with the rain standard zoltu deployer. This can be used to /// verify that the deployed contract has the expected bytecode, which /// provides stronger guarantees than just checking the address. bytes32 constant EXPRESSION_DEPLOYER_DEPLOYED_CODEHASH = - bytes32(0x445c1b70d019ad0fda5b60cd2ac7e04c5fcd1f59afff4adea3db4e37d829a38d); + bytes32(0x282ab179f2b1bbfe4b5fc50938ee379070dc8731afa3b931b54b2b9d819527f2); /// The address of the `RainterpreterDISPaiRegistry` contract when deployed /// with the rain standard zoltu deployer. - address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0x050Bbc64dd19d2A99C457B186Ad4dEA20439b21C); + address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0x2c703c1087b760E6b593B61018AE680ba2351a9a); /// The code hash of the `RainterpreterDISPaiRegistry` contract when /// deployed with the rain standard zoltu deployer. This can be used to /// verify that the deployed contract has the expected bytecode, which /// provides stronger guarantees than just checking the address. bytes32 constant DISPAIR_REGISTRY_DEPLOYED_CODEHASH = - bytes32(0x31fde5a7e1b43718786c1eab42786520c947e9a35cb870769497111e2ec933db); + bytes32(0x0a8af02edb9d20f7ba60f9d3c50f0c13020dafd1f4d2c20464b9130072a9c096); } diff --git a/src/lib/extern/LibExtern.sol b/src/lib/extern/LibExtern.sol index 1fb9c5e53..1aac1378a 100644 --- a/src/lib/extern/LibExtern.sol +++ b/src/lib/extern/LibExtern.sol @@ -35,7 +35,7 @@ library LibExtern { function decodeExternDispatch(ExternDispatchV2 dispatch) internal pure returns (uint256, OperandV2) { return ( uint256(ExternDispatchV2.unwrap(dispatch) >> 0x10), - OperandV2.wrap(ExternDispatchV2.unwrap(dispatch) & bytes32(uint256(0xFFFF))) + OperandV2.wrap(ExternDispatchV2.unwrap(dispatch) & bytes32(uint256(type(uint16).max))) ); } diff --git a/src/lib/parse/LibParse.sol b/src/lib/parse/LibParse.sol index 0e0575ca8..f2c8f68b5 100644 --- a/src/lib/parse/LibParse.sol +++ b/src/lib/parse/LibParse.sol @@ -51,7 +51,6 @@ import {LibParseInterstitial} from "./LibParseInterstitial.sol"; import {LibParseError} from "./LibParseError.sol"; import {LibSubParse} from "./LibSubParse.sol"; import {LibBytes} from "rain.solmem/lib/LibBytes.sol"; -import {LibUint256Array} from "rain.solmem/lib/LibUint256Array.sol"; import {LibBytes32Array} from "rain.solmem/lib/LibBytes32Array.sol"; /// @dev Size in bytes of the fixed header prepended to sub-parser bytecode. @@ -59,6 +58,13 @@ import {LibBytes32Array} from "rain.solmem/lib/LibBytes32Array.sol"; /// tail pointer (2 bytes), and the word length (1 byte). uint256 constant SUB_PARSER_BYTECODE_HEADER_SIZE = 5; +/// @dev Maximum paren offset before the paren tracker overflows. The tracker +/// has 62 bytes of group data (3 bytes each, fitting 20 groups), but +/// `pushOpToSource` writes a phantom counter at `parenOffset + 4`, so the +/// effective limit is 19 groups (offset 57). The check rejects offset 60 +/// because `newParenOffset` is already incremented by 3. +uint256 constant MAX_PAREN_OFFSET = 59; + /// @title LibParse /// @notice Core parsing library for Rainlang source text. /// @@ -78,7 +84,6 @@ library LibParse { using LibParseOperand for ParseState; using LibSubParse for ParseState; using LibBytes for bytes; - using LibUint256Array for uint256[]; using LibBytes32Array for bytes32[]; /// @notice Parses a word that matches a tail mask between cursor and end. The caller @@ -153,8 +158,7 @@ library LibParse { // Named stack item. if (char & CMASK_IDENTIFIER_HEAD > 0) { (cursor, word) = parseWord(cursor, end, CMASK_LHS_STACK_TAIL); - (bool exists, uint256 index) = state.pushStackName(word); - (index); + (bool exists,) = state.pushStackName(word); // If the stack name already exists, then we // revert as shadowing is not allowed. if (exists) { @@ -343,13 +347,7 @@ library LibParse { newParenOffset := add(byte(0, mload(add(state, parenTracker0Offset))), 3) mstore8(add(state, parenTracker0Offset), newParenOffset) } - // 62 bytes of group data (3 bytes each) fit 20 - // groups by size, but pushOpToSource zeroes a - // phantom counter at parenOffset + 4, so the - // effective max is 19 groups (offset 57). The - // check must reject offset 60 to prevent that - // phantom write from corrupting lineTracker. - if (newParenOffset > 59) { + if (newParenOffset > MAX_PAREN_OFFSET) { revert ParenOverflow(); } cursor++; diff --git a/src/lib/parse/LibParseOperand.sol b/src/lib/parse/LibParseOperand.sol index ef8514cb8..6790ba9e9 100644 --- a/src/lib/parse/LibParseOperand.sol +++ b/src/lib/parse/LibParseOperand.sol @@ -18,6 +18,9 @@ import {LibParseError} from "./LibParseError.sol"; import {LibParseInterstitial} from "./LibParseInterstitial.sol"; import {LibDecimalFloat, Float} from "rain.math.float/lib/LibDecimalFloat.sol"; +/// @title LibParseOperand +/// @notice Parses operand values from Rainlang source text and dispatches +/// to type-specific operand handlers. library LibParseOperand { using LibParseError for ParseState; using LibParseLiteral for ParseState; @@ -206,7 +209,7 @@ library LibParseOperand { } (int256 signedCoefficient, int256 exponent) = Float.wrap(OperandV2.unwrap(operand)).unpack(); uint256 operandUint = LibDecimalFloat.toFixedDecimalLossless(signedCoefficient, exponent, 0); - if (operandUint > uint256(type(uint16).max)) { + if (operandUint > type(uint16).max) { revert OperandOverflow(); } operand = OperandV2.wrap(bytes32(operandUint)); diff --git a/src/lib/parse/LibParsePragma.sol b/src/lib/parse/LibParsePragma.sol index 003ad0c7e..0dcdb028e 100644 --- a/src/lib/parse/LibParsePragma.sol +++ b/src/lib/parse/LibParsePragma.sol @@ -22,6 +22,9 @@ uint256 constant PRAGMA_KEYWORD_BYTES_LENGTH = 16; //forge-lint: disable-next-line(incorrect-shift) bytes32 constant PRAGMA_KEYWORD_MASK = bytes32(~((1 << (32 - PRAGMA_KEYWORD_BYTES_LENGTH) * 8) - 1)); +/// @title LibParsePragma +/// @notice Parses the `using-words-from` pragma from Rainlang source text +/// and registers sub-parser contract addresses on the parse state. library LibParsePragma { using LibParseError for ParseState; using LibParseInterstitial for ParseState; diff --git a/src/lib/parse/LibParseStackTracker.sol b/src/lib/parse/LibParseStackTracker.sol index 670d0803b..5d38046ab 100644 --- a/src/lib/parse/LibParseStackTracker.sol +++ b/src/lib/parse/LibParseStackTracker.sol @@ -9,6 +9,9 @@ import {ParseStackUnderflow, ParseStackOverflow} from "../../error/ErrParse.sol" /// maximum height seen so far (used to size the runtime stack allocation). type ParseStackTracker is uint256; +/// @title LibParseStackTracker +/// @notice Tracks current and high-water stack heights during parsing to +/// size the runtime stack allocation. library LibParseStackTracker { using LibParseStackTracker for ParseStackTracker; diff --git a/src/lib/parse/LibParseState.sol b/src/lib/parse/LibParseState.sol index 287022c1d..d5672d098 100644 --- a/src/lib/parse/LibParseState.sol +++ b/src/lib/parse/LibParseState.sol @@ -78,6 +78,12 @@ uint256 constant PARSE_STATE_PAREN_TRACKER0_OFFSET = 0x60; /// Used in assembly to snapshot source head pointers per line. uint256 constant PARSE_STATE_LINE_TRACKER_OFFSET = 0xa0; +/// @dev Maximum RHS offset value. The RHS offset is stored as a single +/// byte within the topLevel0 field but shares the word with other packed +/// fields, so it must not exceed 62 (0x3e). The check uses >= 0x3f (63) +/// to reject offset 63 and above. +uint256 constant MAX_STACK_RHS_OFFSET = 0x3f; + /// @notice The parser is stateful. This struct keeps track of the entire state. /// @param activeSourcePtr The pointer to the current source being built. /// The active source being pointed to is: @@ -534,10 +540,7 @@ library LibParseState { newStackRHSOffset := add(byte(0, mload(stackRHSOffsetPtr)), 1) mstore8(stackRHSOffsetPtr, newStackRHSOffset) } - // 0x3f = 63: the RHS offset is stored as a single byte within - // the topLevel0 field, but shares the word with other packed - // fields, so it must not exceed 62 (0x3e). - if (newStackRHSOffset >= 0x3f) { + if (newStackRHSOffset >= MAX_STACK_RHS_OFFSET) { revert ParseStackOverflow(); } } diff --git a/src/lib/parse/LibSubParse.sol b/src/lib/parse/LibSubParse.sol index 2422d4573..c8066b933 100644 --- a/src/lib/parse/LibSubParse.sol +++ b/src/lib/parse/LibSubParse.sol @@ -51,7 +51,7 @@ library LibSubParse { pure returns (bool, bytes memory, bytes32[] memory) { - if (column > 0xFF || row > 0xFF) { + if (column > type(uint8).max || row > type(uint8).max) { revert ContextGridOverflow(column, row); } bytes memory bytecode; @@ -99,7 +99,7 @@ library LibSubParse { pure returns (bool, bytes memory, bytes32[] memory) { - if (constantsHeight > 0xFFFF) { + if (constantsHeight > type(uint16).max) { revert ConstantOpcodeConstantsHeightOverflow(constantsHeight); } // Build a constant opcode that the interpreter will run itself. @@ -169,7 +169,7 @@ library LibSubParse { // The constants height is an error check because the main parser can // provide two bytes for it. Everything else is expected to be more // directly controlled by the subparser itself. - if (constantsHeight > 0xFFFF) { + if (constantsHeight > type(uint16).max) { revert ExternDispatchConstantsHeightOverflow(constantsHeight); } // Build an extern call that dials back into the current contract at eval @@ -223,7 +223,7 @@ library LibSubParse { if (memoryAtCursor >> 0xf8 == OPCODE_UNKNOWN) { bytes32 deref = state.subParsers; while (deref != 0) { - ISubParserV4 subParser = ISubParserV4(address(uint160(uint256((deref))))); + ISubParserV4 subParser = ISubParserV4(address(uint160(uint256(deref)))); assembly ("memory-safe") { deref := mload(shr(0xf0, deref)) } @@ -360,7 +360,7 @@ library LibSubParse { { uint256 copyPointer; uint256 dispatchLength = dispatchEnd - dispatchStart; - if (dispatchLength > 0xFFFF) { + if (dispatchLength > type(uint16).max) { revert SubParseLiteralDispatchLengthOverflow(dispatchLength); } uint256 bodyLength = bodyEnd - bodyStart; diff --git a/src/lib/parse/literal/LibParseLiteral.sol b/src/lib/parse/literal/LibParseLiteral.sol index 140d66688..f12e80aba 100644 --- a/src/lib/parse/literal/LibParseLiteral.sol +++ b/src/lib/parse/literal/LibParseLiteral.sol @@ -27,6 +27,9 @@ uint256 constant LITERAL_PARSER_INDEX_STRING = 2; /// @dev Index of the sub-parseable literal parser (e.g. `[dispatch body]`). uint256 constant LITERAL_PARSER_INDEX_SUB_PARSE = 3; +/// @title LibParseLiteral +/// @notice Dispatches literal parsing to the appropriate type-specific parser +/// (hex, decimal, string, or sub-parseable) based on the head character. library LibParseLiteral { using LibParseLiteral for ParseState; using LibParseError for ParseState; diff --git a/src/lib/parse/literal/LibParseLiteralDecimal.sol b/src/lib/parse/literal/LibParseLiteralDecimal.sol index a96b4a692..1dd732eb0 100644 --- a/src/lib/parse/literal/LibParseLiteralDecimal.sol +++ b/src/lib/parse/literal/LibParseLiteralDecimal.sol @@ -7,6 +7,9 @@ import {LibParseError} from "../LibParseError.sol"; import {LibParseDecimalFloat, Float} from "rain.math.float/lib/parse/LibParseDecimalFloat.sol"; import {LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol"; +/// @title LibParseLiteralDecimal +/// @notice Parses decimal numeric literals from Rainlang source text into +/// packed decimal float values. library LibParseLiteralDecimal { using LibParseError for ParseState; diff --git a/src/lib/parse/literal/LibParseLiteralHex.sol b/src/lib/parse/literal/LibParseLiteralHex.sol index 3bd1bf6d3..ca4d33d4d 100644 --- a/src/lib/parse/literal/LibParseLiteralHex.sol +++ b/src/lib/parse/literal/LibParseLiteralHex.sol @@ -17,6 +17,9 @@ import { } from "rain.string/lib/parse/LibParseCMask.sol"; import {LibParseError} from "../LibParseError.sol"; +/// @title LibParseLiteralHex +/// @notice Parses hexadecimal literals from Rainlang source text into +/// bytes32 values. library LibParseLiteralHex { using LibParseLiteralHex for ParseState; using LibParseError for ParseState; diff --git a/src/lib/parse/literal/LibParseLiteralSubParseable.sol b/src/lib/parse/literal/LibParseLiteralSubParseable.sol index 104c7f865..a1d0431e5 100644 --- a/src/lib/parse/literal/LibParseLiteralSubParseable.sol +++ b/src/lib/parse/literal/LibParseLiteralSubParseable.sol @@ -11,6 +11,9 @@ import {LibParseError} from "../LibParseError.sol"; import {LibSubParse} from "../LibSubParse.sol"; import {LibParseChar} from "rain.string/lib/parse/LibParseChar.sol"; +/// @title LibParseLiteralSubParseable +/// @notice Parses sub-parseable literals delimited by `[` and `]` by +/// delegating to registered sub-parser contracts. library LibParseLiteralSubParseable { using LibParse for ParseState; using LibParseInterstitial for ParseState; diff --git a/test/lib/string/LibCamelToKebab.sol b/test/lib/string/LibCamelToKebab.sol index ec4c96d9d..2807cf03e 100644 --- a/test/lib/string/LibCamelToKebab.sol +++ b/test/lib/string/LibCamelToKebab.sol @@ -23,11 +23,15 @@ library LibCamelToKebab { uint8 prev = uint8(src[i - 1]); // Rule 2: lowercase/digit followed by uppercase. if (isLower(prev) || isDigit(prev)) { + // Casting "-" to bytes1 is safe because it is a single ASCII byte. + //forge-lint: disable-next-line(unsafe-typecast) buf[len++] = bytes1("-"); } // Rule 1: uppercase followed by uppercase+lowercase // (split before the last uppercase in a run). else if (isUpper(prev) && i + 1 < src.length && isLower(uint8(src[i + 1]))) { + // Casting "-" to bytes1 is safe because it is a single ASCII byte. + //forge-lint: disable-next-line(unsafe-typecast) buf[len++] = bytes1("-"); } } diff --git a/test/src/lib/op/LibAllStandardOps.filesystemOrdering.t.sol b/test/src/lib/op/LibAllStandardOps.filesystemOrdering.t.sol index 0548df590..e4d6d186b 100644 --- a/test/src/lib/op/LibAllStandardOps.filesystemOrdering.t.sol +++ b/test/src/lib/op/LibAllStandardOps.filesystemOrdering.t.sol @@ -81,6 +81,8 @@ contract LibAllStandardOpsFilesystemOrderingTest is Test { /// @notice Returns true if the word is a known alias (shares a file with /// another word). Currently only "now" (alias for "block-timestamp"). function isAlias(bytes32 word) internal pure returns (bool) { + // "now" is 3 bytes; left-aligning into bytes32 is safe. + //forge-lint: disable-next-line(unsafe-typecast) return word == bytes32("now"); } diff --git a/test/src/lib/parse/LibParseInterstitial.t.sol b/test/src/lib/parse/LibParseInterstitial.t.sol index ffa6dbe61..b2af8ff4c 100644 --- a/test/src/lib/parse/LibParseInterstitial.t.sol +++ b/test/src/lib/parse/LibParseInterstitial.t.sol @@ -156,7 +156,10 @@ contract LibParseInterstitialTest is Test { function testSkipCommentFuzzBody(bytes memory body) external pure { // Replace all `*` in body with `~` so no `*/` can appear. for (uint256 i = 0; i < body.length; i++) { + // Casting single ASCII chars to bytes1 is safe. + //forge-lint: disable-next-line(unsafe-typecast) if (body[i] == bytes1("*")) { + //forge-lint: disable-next-line(unsafe-typecast) body[i] = bytes1("~"); } }