- MemoryLayout L129 and L145 follow an
assertEq that already fixes w, so the inequality compares two compile-time literals and can never fail: delete them (the NatSpec already states the contrast). HashPattern L177 and HashPatternFold L108 are real checks written as assertTrue(a != b), which prints no operands: use assertNotEq (forge-std 1.16.1 has both overloads). Verified 31/31.
- HashPattern
testFooListIsWordList (L69-105) pins the README word-list claim and additionally new Foo[](n) allocation order and sizes — a codegen fact the README disclaims — but only for element 0 and without reading the elements; its doc says "zero-initialised" while each element's words are 0, 0, 0x60, 0x60 (dynamic members point at the zero slot; verified). Keep only the length and word-equals-pointer assertions here; move the allocator facts, strengthened per element (fooPointer == ptr + 0x20 + n*0x20 + i*0x80, words 0, 0, 0x60, 0x60, foo_.c.length == 0; verified 1024 runs) to MemoryLayout under their own name with "default-initialised (value members 0, dynamic members pointing at the zero slot)".
- HashPattern
testBytesTrueLength asserts only wordOne_ == wordTwo_, not the README's stated word (0x01 then 31 zero bytes) nor the rounded-length collision it contrasts: add assertEq(wordOne_, uint256(bytes32(hex"01"))) and hash both at keccak256(add(x_, 0x20), 0x20) asserting the rounded hashes collide.
- MemoryLayout
testDeeplyNestedStructIsOnePointerWordPerLevel(uint256 x, uint256 y): x and y reach no assertion (the depth-1 sibling asserts w0 == x). Adding two new locals hits stack-too-deep; the verified stack-safe form reuses the dead locals after L266: assembly { w1 := mload(outermost) midW1 := mload(midPtr) } then assertEq(w1, y); assertEq(midW1, x); (or drop the params).
Findings from the audit run
F57 [INFO] dim 5 cat — test/MemoryLayout.t.sol:129, 145 (test/HashPattern.t.sol:177; test/HashPatternFold.t.sol:108)
assertTrue(a != b) sites: two in MemoryLayout are tautologies on literals after the preceding assertEq; two elsewhere print no operands where forge-std assertNotEq exists
MemoryLayout L129/L145 follow an assertEq that already fixes w, so the inequality compares two compile-time literals and can never fail; HashPattern L177 and HashPatternFold L108 are real checks written as assertTrue, which on failure prints only 'assertion failed'.
EVIDENCE: MemoryLayout L128-129 assertEq(w, type(uint256).max); assertTrue(w != uint256(uint8(int8(-1))));, L144-145 assertEq(w, uint256(0x01020304) << 224); assertTrue(w != uint256(uint32(0x01020304)));; HashPattern L177 assertTrue(hashOne_ != hashTwo_);; HashPatternFold L108 assertTrue(folded != hashX);; forge-std 1.16.1 StdAssertions declares assertNotEq(bytes32,bytes32) and (uint256,uint256).
FIX: Delete MemoryLayout L129 and L145 (keep the wording in NatSpec); HashPattern L177 assertNotEq(hashOne_, hashTwo_);; HashPatternFold L108 assertNotEq(folded, hashX);.
Correctness of the claim: confirmed. MemoryLayout L129 follows assertEq(w, type(uint256).max), so w != uint256(uint8(int8(-1))) reduces to max != 255 — unreachable-false; L145 likewise (0x01020304<<224) != 0x01020304. Deleting them weakens nothing; the NatSpec on both tests (L124, L140-141) already states the "not the zero-padded form" contrast the deleted lines were trying to express. The two assertNotEq swaps are strict improvements (operands printed on failure) with identical pass/fail semantics. One nuance the finding does not state: HashPattern L177 is also implied by the two assertEq on L178-179 (hashOne_ == keccak(hex"01") and hashTwo_ == keccak(hex"0100") are distinct absent a keccak collision), so it is redundant rather than a "real check"; the swap is still harmless and the line documents the README's "MUST respect the true length" claim directly, so keeping it as assertNotEq is fine. HashPatternFold L108 is a genuine (fuzzed) check that folding a singleton differs from the item hash.
No new test is proposed and none is needed: nothing fails today, nothing should — this is a test-hygiene change, not a coverage gap.
Severity: INFO is correct. Skill scheme L120: "suggestion for improvement with no direct risk." Test-only edit in an internal-function hashing library; the tautological lines cannot mask a defect because the assertEq immediately above each one pins the exact word value, so no production behaviour (including rainlang.interface's signed-context digest) depends on the change. Not a Pass-6 finding, so L373 does not apply; if it were, it would still be INFO (convention kept by discipline, cheaply enforced).
F61 [INFO] dim 5 cat — test/HashPattern.t.sol:69-105
testFooListIsWordList mixes the README word-list claim with allocator-layout facts: those are asserted only for element 0, their 'zero-initialised' doc is wrong (element words are 0, 0, 0x60, 0x60), and the README says the pattern does not depend on the allocator
L93-101 pin the README claim (word i+1 == pointer to foos_[i]); L89/91/102-104 additionally pin new Foo[](n) allocation order and sizes, a solc codegen fact the README disclaims at L357-359, but only for element 0 and without reading the elements, whose dynamic members point at the zero slot (which is also why the NoAlloc tests guard 0x60 — pinned nowhere). Three agents disagree on direction (drop vs strengthen); the thorough option keeps the word-list claim here and moves the allocator facts, strengthened, to MemoryLayout under their own name.
EVIDENCE: L71-73 doc 'one 0x80 zero-initialised Foo per element'; L89 assertEq(ptr, fmpBefore); L91 assertEq(fmpAfter - ptr, 0x20 + n * 0x20 + n * 0x80); L102-104 if (i == 0) { assertEq(fooPointer, ptr + 0x20 + n * 0x20); }. README L317-318 (the named claim), L357-359 'the pattern never depends on what the allocator does beyond it'. Probe at HEAD: each element mload = 0, 0, 0x60, 0x60; assertEq(fooPointer, ptr + 0x20 + n * 0x20 + i * 0x80) PASS 1024 runs.
FIX: testFooListIsWordList: keep only len and word-equals-pointer assertions (drop L76-79, 89, 91, 102-104 and the doc sentence). Add to test/MemoryLayout.t.sol testNewFooArrayAllocatesListThenElements(uint8 length) asserting per element fooPointer == ptr + 0x20 + n*0x20 + i*0x80 and the four words 0, 0, 0x60, 0x60 with foo_.c.length == 0, doc 'default-initialised (value members 0, dynamic members pointing at the zero slot)'.
F62 [INFO] dim 5 cat — test/HashPattern.t.sol:160-173
testBytesTrueLength asserts the two data words are equal but not the README's stated value or the rounded-up collision
README says hex"01" and hex"0100" share the word 0x01 followed by 31 zero bytes; the test asserts only wordOne_ == wordTwo_, so two equal but differently padded words would pass and the collision hashing the rounded word is never shown.
EVIDENCE: L173 assertEq(wordOne_, wordTwo_);; README L354-356.
FIX: After L173: assertEq(wordOne_, uint256(bytes32(hex"01"))); and hash both at keccak256(add(x_, 0x20), 0x20) asserting the rounded hashes collide (text in d5:correctness:HashPattern.t.sol#4).
F64 [INFO] dim 5 cat — test/MemoryLayout.t.sol:234-267
Fuzz inputs x and y of testDeeplyNestedStructIsOnePointerWordPerLevel never reach an assertion
x and y flow into the constructors only; the signature advertises value-word coverage that does not exist (the sibling test asserts w0 == x).
EVIDENCE: L234 (uint256 x, uint256 y); assertions L261-266 on pointers/fmp only.
FIX: Add w0 := mload(outermost), midW0 := mload(w1) and assertEq(w0, y); assertEq(midW0, x); (or drop the params).
Whole-repo audit of rainlanguage/rain.lib.hash at 1e6e59f3d1fc4fd30c86a5836aa156eed382e729 (main, 2026-09-06; audit skill 0.35.0 at claude-audit-skills 9fefd09902d777aabc967ef6625d038e6c2101b7). Pass-2 gate: ledgered (adversarial-mutation-test record 2026-09-03 at 5055221444d547aba034b88fbd2525973b17d9a9; library code unchanged since). Fixes marked verified were applied to a scratch copy and run with forge in the rainix sol-shell (dbcd9d3c) during the audit; everything else is proposed only. Full run record: /home/gildlab/artifacts/rain.lib.hash-audit-2026-09-06/.
assertEqthat already fixesw, so the inequality compares two compile-time literals and can never fail: delete them (the NatSpec already states the contrast). HashPattern L177 and HashPatternFold L108 are real checks written asassertTrue(a != b), which prints no operands: useassertNotEq(forge-std 1.16.1 has both overloads). Verified 31/31.testFooListIsWordList(L69-105) pins the README word-list claim and additionallynew Foo[](n)allocation order and sizes — a codegen fact the README disclaims — but only for element 0 and without reading the elements; its doc says "zero-initialised" while each element's words are0, 0, 0x60, 0x60(dynamic members point at the zero slot; verified). Keep only the length and word-equals-pointer assertions here; move the allocator facts, strengthened per element (fooPointer == ptr + 0x20 + n*0x20 + i*0x80, words0, 0, 0x60, 0x60,foo_.c.length == 0; verified 1024 runs) to MemoryLayout under their own name with "default-initialised (value members 0, dynamic members pointing at the zero slot)".testBytesTrueLengthasserts onlywordOne_ == wordTwo_, not the README's stated word (0x01then 31 zero bytes) nor the rounded-length collision it contrasts: addassertEq(wordOne_, uint256(bytes32(hex"01")))and hash both atkeccak256(add(x_, 0x20), 0x20)asserting the rounded hashes collide.testDeeplyNestedStructIsOnePointerWordPerLevel(uint256 x, uint256 y):xandyreach no assertion (the depth-1 sibling assertsw0 == x). Adding two new locals hits stack-too-deep; the verified stack-safe form reuses the dead locals after L266:assembly { w1 := mload(outermost) midW1 := mload(midPtr) }thenassertEq(w1, y); assertEq(midW1, x);(or drop the params).Findings from the audit run
F57 [INFO] dim 5 cat — test/MemoryLayout.t.sol:129, 145 (test/HashPattern.t.sol:177; test/HashPatternFold.t.sol:108)
assertTrue(a != b)sites: two in MemoryLayout are tautologies on literals after the preceding assertEq; two elsewhere print no operands where forge-std assertNotEq existsMemoryLayout L129/L145 follow an assertEq that already fixes
w, so the inequality compares two compile-time literals and can never fail; HashPattern L177 and HashPatternFold L108 are real checks written as assertTrue, which on failure prints only 'assertion failed'.EVIDENCE: MemoryLayout L128-129
assertEq(w, type(uint256).max); assertTrue(w != uint256(uint8(int8(-1))));, L144-145assertEq(w, uint256(0x01020304) << 224); assertTrue(w != uint256(uint32(0x01020304)));; HashPattern L177assertTrue(hashOne_ != hashTwo_);; HashPatternFold L108assertTrue(folded != hashX);; forge-std 1.16.1 StdAssertions declares assertNotEq(bytes32,bytes32) and (uint256,uint256).FIX: Delete MemoryLayout L129 and L145 (keep the wording in NatSpec); HashPattern L177
assertNotEq(hashOne_, hashTwo_);; HashPatternFold L108assertNotEq(folded, hashX);.Correctness of the claim: confirmed. MemoryLayout L129 follows
assertEq(w, type(uint256).max), sow != uint256(uint8(int8(-1)))reduces tomax != 255— unreachable-false; L145 likewise(0x01020304<<224) != 0x01020304. Deleting them weakens nothing; the NatSpec on both tests (L124, L140-141) already states the "not the zero-padded form" contrast the deleted lines were trying to express. The two assertNotEq swaps are strict improvements (operands printed on failure) with identical pass/fail semantics. One nuance the finding does not state: HashPattern L177 is also implied by the two assertEq on L178-179 (hashOne_ == keccak(hex"01") and hashTwo_ == keccak(hex"0100") are distinct absent a keccak collision), so it is redundant rather than a "real check"; the swap is still harmless and the line documents the README's "MUST respect the true length" claim directly, so keeping it as assertNotEq is fine. HashPatternFold L108 is a genuine (fuzzed) check that folding a singleton differs from the item hash.No new test is proposed and none is needed: nothing fails today, nothing should — this is a test-hygiene change, not a coverage gap.
Severity: INFO is correct. Skill scheme L120: "suggestion for improvement with no direct risk." Test-only edit in an internal-function hashing library; the tautological lines cannot mask a defect because the assertEq immediately above each one pins the exact word value, so no production behaviour (including rainlang.interface's signed-context digest) depends on the change. Not a Pass-6 finding, so L373 does not apply; if it were, it would still be INFO (convention kept by discipline, cheaply enforced).
F61 [INFO] dim 5 cat — test/HashPattern.t.sol:69-105
testFooListIsWordList mixes the README word-list claim with allocator-layout facts: those are asserted only for element 0, their 'zero-initialised' doc is wrong (element words are 0, 0, 0x60, 0x60), and the README says the pattern does not depend on the allocator
L93-101 pin the README claim (word i+1 == pointer to foos_[i]); L89/91/102-104 additionally pin
new Foo[](n)allocation order and sizes, a solc codegen fact the README disclaims at L357-359, but only for element 0 and without reading the elements, whose dynamic members point at the zero slot (which is also why the NoAlloc tests guard 0x60 — pinned nowhere). Three agents disagree on direction (drop vs strengthen); the thorough option keeps the word-list claim here and moves the allocator facts, strengthened, to MemoryLayout under their own name.EVIDENCE: L71-73 doc 'one 0x80 zero-initialised
Fooper element'; L89assertEq(ptr, fmpBefore);L91assertEq(fmpAfter - ptr, 0x20 + n * 0x20 + n * 0x80);L102-104if (i == 0) { assertEq(fooPointer, ptr + 0x20 + n * 0x20); }. README L317-318 (the named claim), L357-359 'the pattern never depends on what the allocator does beyond it'. Probe at HEAD: each elementmload= 0, 0, 0x60, 0x60;assertEq(fooPointer, ptr + 0x20 + n * 0x20 + i * 0x80)PASS 1024 runs.FIX: testFooListIsWordList: keep only len and word-equals-pointer assertions (drop L76-79, 89, 91, 102-104 and the doc sentence). Add to test/MemoryLayout.t.sol
testNewFooArrayAllocatesListThenElements(uint8 length)asserting per elementfooPointer == ptr + 0x20 + n*0x20 + i*0x80and the four words0, 0, 0x60, 0x60withfoo_.c.length == 0, doc 'default-initialised (value members 0, dynamic members pointing at the zero slot)'.F62 [INFO] dim 5 cat — test/HashPattern.t.sol:160-173
testBytesTrueLength asserts the two data words are equal but not the README's stated value or the rounded-up collision
README says hex"01" and hex"0100" share the word
0x01followed by 31 zero bytes; the test asserts only wordOne_ == wordTwo_, so two equal but differently padded words would pass and the collision hashing the rounded word is never shown.EVIDENCE: L173
assertEq(wordOne_, wordTwo_);; README L354-356.FIX: After L173:
assertEq(wordOne_, uint256(bytes32(hex"01")));and hash both atkeccak256(add(x_, 0x20), 0x20)asserting the rounded hashes collide (text in d5:correctness:HashPattern.t.sol#4).F64 [INFO] dim 5 cat — test/MemoryLayout.t.sol:234-267
Fuzz inputs x and y of testDeeplyNestedStructIsOnePointerWordPerLevel never reach an assertion
x and y flow into the constructors only; the signature advertises value-word coverage that does not exist (the sibling test asserts
w0 == x).EVIDENCE: L234
(uint256 x, uint256 y); assertions L261-266 on pointers/fmp only.FIX: Add
w0 := mload(outermost),midW0 := mload(w1)andassertEq(w0, y); assertEq(midW0, x);(or drop the params).Whole-repo audit of rainlanguage/rain.lib.hash at
1e6e59f3d1fc4fd30c86a5836aa156eed382e729(main, 2026-09-06; audit skill 0.35.0 at claude-audit-skills9fefd09902d777aabc967ef6625d038e6c2101b7). Pass-2 gate:ledgered(adversarial-mutation-test record 2026-09-03 at5055221444d547aba034b88fbd2525973b17d9a9; library code unchanged since). Fixes marked verified were applied to a scratch copy and run withforgein the rainix sol-shell (dbcd9d3c) during the audit; everything else is proposed only. Full run record:/home/gildlab/artifacts/rain.lib.hash-audit-2026-09-06/.