Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions .devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"image": "rainprotocol/devcontainer:foundry",
"customizations": {
"vscode": {
"extensions": [
"JuanBlanco.solidity",
"ms-azuretools.vscode-docker",
"bungcip.better-toml"
]
}
}
"image": "rainprotocol/devcontainer:foundry",
"customizations": {
"vscode": {
"extensions": [
"JuanBlanco.solidity",
"ms-azuretools.vscode-docker",
"bungcip.better-toml"
]
}
}
}
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# shellcheck shell=bash
if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then
URL=https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc
HASH=sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/pr-assessment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ on:
pull_request:
types:
- closed

jobs:
assess-pr-size-on-merge:
uses: rainlanguage/github-chore/.github/workflows/pr-assessment.yml@main
Expand Down
10 changes: 4 additions & 6 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"editor.rulers": [
80
],
"solidity.compileUsingRemoteVersion": "v0.8.25+commit.b61c2a91",
"solidity.formatter": "forge"
}
"editor.rulers": [80],
"solidity.compileUsingRemoteVersion": "v0.8.25+commit.b61c2a91",
"solidity.formatter": "forge"
}
53 changes: 38 additions & 15 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,65 @@

Test base contracts in `test/abstract/`:

- **`RainlangExpressionDeployerDeploymentTest`** (`test/abstract/RainlangExpressionDeployerDeploymentTest.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()`.
- **`RainlangExpressionDeployerDeploymentTest`**
(`test/abstract/RainlangExpressionDeployerDeploymentTest.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

- Use `bound()` to constrain fuzz inputs, not `vm.assume()`. `vm.assume()` wastes runs by discarding inputs. `vm.assume()` is acceptable when the rejection rate is low or `bound()` cannot express the constraint.
- When fuzzing over a non-contiguous set (e.g., non-hex bytes), `bound()` to the count of valid values, then map with arithmetic to skip excluded ranges.
- When a fuzz parameter affects expression structure, build rainlang dynamically. The fuzz variable must match what the rainlang produces.
- Use `bound()` to constrain fuzz inputs, not `vm.assume()`. `vm.assume()`
wastes runs by discarding inputs. `vm.assume()` is acceptable when the
rejection rate is low or `bound()` cannot express the constraint.
- When fuzzing over a non-contiguous set (e.g., non-hex bytes), `bound()` to the
count of valid values, then map with arithmetic to skip excluded ranges.
- When a fuzz parameter affects expression structure, build rainlang
dynamically. The fuzz variable must match what the rainlang produces.

## Library Internals

Internal library functions need an external wrapper in the test contract. Construct `ParseState` inside the wrapper so memory pointers are valid. Call via `this.externalFoo()`.
Internal library functions need an external wrapper in the test contract.
Construct `ParseState` inside the wrapper so memory pointers are valid. Call via
`this.externalFoo()`.

## Revert Paths

Use `vm.expectRevert` with `abi.encodeWithSelector` and the custom error type. Call through `this.externalFoo()` for library functions or directly on `I_PARSER`/`I_INTERPRETER` for integration tests.
Use `vm.expectRevert` with `abi.encodeWithSelector` and the custom error type.
Call through `this.externalFoo()` for library functions or directly on
`I_PARSER`/`I_INTERPRETER` for integration tests.

## Bytecode Construction

Use the parse library to generate bytecode from rainlang when the test needs valid bytecode. Only hand-encode bytecode when the test intentionally needs invalid or malformed bytecode that the parser cannot produce.
Use the parse library to generate bytecode from rainlang when the test needs
valid bytecode. Only hand-encode bytecode when the test intentionally needs
invalid or malformed bytecode that the parser cannot produce.

## Bytecode Inspection

Use `LibBytecode` from `rain.interpreter.interface/lib/bytecode/LibBytecode.sol`. Do not manually index into bytecode bytes.
Use `LibBytecode` from
`rain.interpreter.interface/lib/bytecode/LibBytecode.sol`. Do not manually index
into bytecode bytes.

## Opcode Testing

Use `opReferenceCheck` to validate that `run` output matches a pure reference implementation and that `integrity` correctly declares inputs/outputs.
Use `opReferenceCheck` to validate that `run` output matches a pure reference
implementation and that `integrity` correctly declares inputs/outputs.

## Boundary Tests

Always test both sides: the max valid value (should succeed) and one past it (should revert).
Always test both sides: the max valid value (should succeed) and one past it
(should revert).

## One Test at a Time

Write one test function per edit-compile-run cycle. Do not batch multiple new tests into a single edit. Writing one test at a time produces higher quality code — each test gets full attention, compilation errors are caught immediately, and failures are unambiguous.
Write one test function per edit-compile-run cycle. Do not batch multiple new
tests into a single edit. Writing one test at a time produces higher quality
code — each test gets full attention, compilation errors are caught immediately,
and failures are unambiguous.
85 changes: 67 additions & 18 deletions audit/2026-03-23-01/triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,73 @@ Source: Report_rain.interpreter_2.0_mar_2026.pdf

## External: Security

- [FIXED] H01: (HIGH) Off-by-one in MAX_STACK_RHS_OFFSET causes LHS item count corruption — 63rd RHS item writes into LHS counter byte at offset 0x5F. Path: src/lib/parse/LibParseState.sol
- [FIXED] M01: (MEDIUM) Out-of-bounds second-byte read causes valid decimals to revert — look-ahead past buffer boundary when literal ends with single digit. Path: src/lib/parse/literal/LibParseLiteral.sol. Fix at line 110, test: LibParseLiteral.dispatch.t.sol testTryParseLiteralOOBSecondBytePoison
- [FIXED] M02: (MEDIUM) Out-of-bounds memory read and garbage literal parsing in pragma — tryParseLiteral called when cursor == end after trailing whitespace. Path: src/lib/parse/LibParsePragma.sol. Fix at line 88, test: LibParsePragma.keyword.t.sol testParsePragmaOOBAfterInterstitial
- [FIXED] M03: (MEDIUM) Silent truncation of sub-parser dispatch length — dispatchLength >0xFFFF silently truncated to 16 bits during packing. Path: src/lib/parse/LibSubParse.sol. Fix at line 363, test: LibSubParse.subParseLiteral.t.sol testSubParseLiteralDispatchLengthOverflow
- [FIXED] M04: (MEDIUM) LHS item count overflow causes bitwise carry-over and parser state corruption — 256 LHS items overflows packed byte in unchecked block. Path: src/lib/parse/LibParse.sol. Fix at line 182, test: LibParse.lhsOverflow.t.sol testLHSItemCountOverflow256
- [FIXED] M05: (MEDIUM) Unbounded LHS count in endLine() for empty-RHS lines — lineLHSItems added to totalRHSTopLevel without bounds check. Path: src/lib/parse/LibParseState.sol
- [FIXED] M06: (MEDIUM) Semantic manipulation and implicit validation via malicious extern contracts — LibOpExtern.integrity() blindly trusts external externIntegrity() return values. Path: src/lib/op/00/LibOpExtern.sol
- [FIXED] L01: (LOW) Uppercase hexadecimal prefix bypasses hex parser and fails confusingly — 0X not recognized, routed to decimal parser. Path: src/lib/parse/literal/LibParseLiteral.sol. Fix at line 121, test: LibParseLiteral.dispatch.t.sol testTryParseLiteralUppercaseXReverts
- [FIXED] L02: (LOW) Missing bitwise mask on outputs in LibOpCall — unmasked right shift relies on upstream truncation. Path: src/lib/op/call/LibOpCall.sol
- [FIXED] L03: (LOW) Implicit operand/bytecode synchronisation in LibOpCall — integrity() ignores operand-encoded inputs, relies on external integrityCheck2. Path: src/lib/op/call/LibOpCall.sol
- [FIXED] H01: (HIGH) Off-by-one in MAX_STACK_RHS_OFFSET causes LHS item count
corruption — 63rd RHS item writes into LHS counter byte at offset 0x5F. Path:
src/lib/parse/LibParseState.sol
- [FIXED] M01: (MEDIUM) Out-of-bounds second-byte read causes valid decimals to
revert — look-ahead past buffer boundary when literal ends with single digit.
Path: src/lib/parse/literal/LibParseLiteral.sol. Fix at line 110, test:
LibParseLiteral.dispatch.t.sol testTryParseLiteralOOBSecondBytePoison
- [FIXED] M02: (MEDIUM) Out-of-bounds memory read and garbage literal parsing in
pragma — tryParseLiteral called when cursor == end after trailing whitespace.
Path: src/lib/parse/LibParsePragma.sol. Fix at line 88, test:
LibParsePragma.keyword.t.sol testParsePragmaOOBAfterInterstitial
- [FIXED] M03: (MEDIUM) Silent truncation of sub-parser dispatch length —
dispatchLength >0xFFFF silently truncated to 16 bits during packing. Path:
src/lib/parse/LibSubParse.sol. Fix at line 363, test:
LibSubParse.subParseLiteral.t.sol testSubParseLiteralDispatchLengthOverflow
- [FIXED] M04: (MEDIUM) LHS item count overflow causes bitwise carry-over and
parser state corruption — 256 LHS items overflows packed byte in unchecked
block. Path: src/lib/parse/LibParse.sol. Fix at line 182, test:
LibParse.lhsOverflow.t.sol testLHSItemCountOverflow256
- [FIXED] M05: (MEDIUM) Unbounded LHS count in endLine() for empty-RHS lines —
lineLHSItems added to totalRHSTopLevel without bounds check. Path:
src/lib/parse/LibParseState.sol
- [FIXED] M06: (MEDIUM) Semantic manipulation and implicit validation via
malicious extern contracts — LibOpExtern.integrity() blindly trusts external
externIntegrity() return values. Path: src/lib/op/00/LibOpExtern.sol
- [FIXED] L01: (LOW) Uppercase hexadecimal prefix bypasses hex parser and fails
confusingly — 0X not recognized, routed to decimal parser. Path:
src/lib/parse/literal/LibParseLiteral.sol. Fix at line 121, test:
LibParseLiteral.dispatch.t.sol testTryParseLiteralUppercaseXReverts
- [FIXED] L02: (LOW) Missing bitwise mask on outputs in LibOpCall — unmasked
right shift relies on upstream truncation. Path: src/lib/op/call/LibOpCall.sol
- [FIXED] L03: (LOW) Implicit operand/bytecode synchronisation in LibOpCall —
integrity() ignores operand-encoded inputs, relies on external
integrityCheck2. Path: src/lib/op/call/LibOpCall.sol

## External: Informational

- [DOCUMENTED] I01: (INFO) Dead code: MalformedHexLiteral error is unreachable after boundHex filtering. Path: src/lib/parse/literal/LibParseLiteralHex.sol. Added comment noting defensive fallback.
- [DISMISSED] I02: (INFO) Unused ParseState parameter in boundHex. Path: src/lib/parse/literal/LibParseLiteralHex.sol. The parameter is the implicit receiver from `using for` — called as `state.boundHex(cursor, end)`. Cannot be removed.
- [FIXED] I03: (INFO) Misleading documentation comment regarding non-ASCII characters — behavior is actually deterministic revert, not undefined. Path: src/lib/parse/literal/LibParseLiteralSubParseable.sol. Misleading comment removed.
- [FIXED] I04: (INFO) Missing explicit constants index bounds check in LibOpExtern.integrity(). Path: src/lib/op/00/LibOpExtern.sol. Added OutOfBoundsConstantRead check matching LibOpConstant pattern.
- [FIXED] I05: (INFO) Architectural fragility: hardcoded InterpreterState memory layout — mload(state) assumes stackBottoms is first struct field. Path: src/lib/op/00/LibOpStack.sol. Now uses explicit field access matching LibOpCall pattern.
- [FIXED] I06: (INFO) Float identity testing relies on implicit referenceFn() divergence in EVM block opcodes. Path: src/lib/op/evm/LibOpBlockNumber.sol, LibOpBlockTimestamp.sol, LibOpChainId.sol. Identity fuzz test merged in rain.math.float, propagated via interface dep update.
- [DOCUMENTED] I07: (INFO) Asymmetry between integrity() and run() bounds in variable-length logic opcodes — min-input clamp not duplicated in run(). Path: src/lib/op/logic/LibOpAny.sol, LibOpEvery.sol, LibOpConditions.sol. By design: integrity reports IO, integrityCheck2 validates, run() trusts the result for gas efficiency. Documented in CLAUDE.md, README.md, and integrityCheck2 NatSpec.
- [FIXED] I08: (INFO) Missing internal enforcement of memory bounds in parser — checkParseMemoryOverflow() never called by LibParse.parse(). Path: src/lib/parse/LibParse.sol, src/lib/parse/LibParseState.sol. Moved check inside parse() after buildBytecode/subParseWords, removed redundant modifier from unsafeParse.
- [DOCUMENTED] I01: (INFO) Dead code: MalformedHexLiteral error is unreachable
after boundHex filtering. Path: src/lib/parse/literal/LibParseLiteralHex.sol.
Added comment noting defensive fallback.
- [DISMISSED] I02: (INFO) Unused ParseState parameter in boundHex. Path:
src/lib/parse/literal/LibParseLiteralHex.sol. The parameter is the implicit
receiver from `using for` — called as `state.boundHex(cursor, end)`. Cannot be
removed.
- [FIXED] I03: (INFO) Misleading documentation comment regarding non-ASCII
characters — behavior is actually deterministic revert, not undefined. Path:
src/lib/parse/literal/LibParseLiteralSubParseable.sol. Misleading comment
removed.
- [FIXED] I04: (INFO) Missing explicit constants index bounds check in
LibOpExtern.integrity(). Path: src/lib/op/00/LibOpExtern.sol. Added
OutOfBoundsConstantRead check matching LibOpConstant pattern.
- [FIXED] I05: (INFO) Architectural fragility: hardcoded InterpreterState memory
layout — mload(state) assumes stackBottoms is first struct field. Path:
src/lib/op/00/LibOpStack.sol. Now uses explicit field access matching
LibOpCall pattern.
- [FIXED] I06: (INFO) Float identity testing relies on implicit referenceFn()
divergence in EVM block opcodes. Path: src/lib/op/evm/LibOpBlockNumber.sol,
LibOpBlockTimestamp.sol, LibOpChainId.sol. Identity fuzz test merged in
rain.math.float, propagated via interface dep update.
- [DOCUMENTED] I07: (INFO) Asymmetry between integrity() and run() bounds in
variable-length logic opcodes — min-input clamp not duplicated in run(). Path:
src/lib/op/logic/LibOpAny.sol, LibOpEvery.sol, LibOpConditions.sol. By design:
integrity reports IO, integrityCheck2 validates, run() trusts the result for
gas efficiency. Documented in CLAUDE.md, README.md, and integrityCheck2
NatSpec.
- [FIXED] I08: (INFO) Missing internal enforcement of memory bounds in parser —
checkParseMemoryOverflow() never called by LibParse.parse(). Path:
src/lib/parse/LibParse.sol, src/lib/parse/LibParseState.sol. Moved check
inside parse() after buildBytecode/subParseWords, removed redundant modifier
from unsafeParse.
14 changes: 7 additions & 7 deletions audit/known-false-positives.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# Known False Positives

Audit findings that have been triaged and dismissed. Documented here so
future audits do not re-flag the same issues.
Audit findings that have been triaged and dismissed. Documented here so future
audits do not re-flag the same issues.

## LibOpGet — read-only key persistence (gas tradeoff)

**File:** `src/lib/op/store/LibOpGet.sol`

When `get` has a cache miss it writes the fetched value into the in-memory
`stateKV` so that subsequent reads hit the cache. Because `stateKV` is
persisted at the end of eval, read-only keys pay an unnecessary `SSTORE`.
`stateKV` so that subsequent reads hit the cache. Because `stateKV` is persisted
at the end of eval, read-only keys pay an unnecessary `SSTORE`.

This is a deliberate design tradeoff: caching repeated reads saves more gas
than the extra `SSTORE` costs for read-only keys. Documented inline and in
commit `25c7c56f`.
This is a deliberate design tradeoff: caching repeated reads saves more gas than
the extra `SSTORE` costs for read-only keys. Documented inline and in commit
`25c7c56f`.

## ERC20 float opcodes — `decimals()` is optional

Expand Down
Loading