diff --git a/.devcontainer.json b/.devcontainer.json index 272e49571..97ee18458 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -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" + ] + } + } } diff --git a/.envrc b/.envrc index a3693d2d5..a93d020e9 100644 --- a/.envrc +++ b/.envrc @@ -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= diff --git a/.github/workflows/pr-assessment.yaml b/.github/workflows/pr-assessment.yaml index 71c5461a9..18e484f44 100644 --- a/.github/workflows/pr-assessment.yaml +++ b/.github/workflows/pr-assessment.yaml @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json index 99982f2f2..0661a93b4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,5 @@ { - "editor.rulers": [ - 80 - ], - "solidity.compileUsingRemoteVersion": "v0.8.25+commit.b61c2a91", - "solidity.formatter": "forge" -} \ No newline at end of file + "editor.rulers": [80], + "solidity.compileUsingRemoteVersion": "v0.8.25+commit.b61c2a91", + "solidity.formatter": "forge" +} diff --git a/TESTING.md b/TESTING.md index 7024316db..d353401c4 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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. diff --git a/audit/2026-03-23-01/triage.md b/audit/2026-03-23-01/triage.md index dd3bb230f..179125ce0 100644 --- a/audit/2026-03-23-01/triage.md +++ b/audit/2026-03-23-01/triage.md @@ -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. diff --git a/audit/known-false-positives.md b/audit/known-false-positives.md index 7ee104902..3af48b939 100644 --- a/audit/known-false-positives.md +++ b/audit/known-false-positives.md @@ -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