Uh oh!
There was an error while loading. Please reload this page.
LibHexString.bytesToHex gets a test file - #37
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Warning Review limit reached
Next review available in:55 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thedavidmeister
commented
Aug 16, 2026
Reviewed 0814859: ready. Tests only — 18 mutants, 17 killed. The survivor is M16, removing the The adversarial half refuted the candidate I seeded it with, on evidence rather than argument. The What survived as a finding is filed as #43 and deliberately not blessed here: the length arithmetic guards nothing against a caller-supplied Second helper contract in its own file under All five checks green. They ran before #36 merged, so this head has not been exercised against #36's new committed-artifact staleness test; the diff is two new test files touching neither CodeRabbit reports |
Uh oh!
There was an error while loading. Please reload this page.
src/lib/LibHexString.solhad no test file. Its one function,bytesToHex, isthe only raw assembly pointer arithmetic in the repo: it takes the string
vm.toString(bytes)returns, moves the pointer forward two bytes, and rewritesthe length word two bytes lower so the leading
0xfalls off. Every part ofthat — the offset, the subtraction, the pointer reassignment, and the promise in
the
memory-safeannotation — was unpinned.This adds
test/lib/LibHexString.bytesToHex.t.sol. Tests only; no source change.What the tests pin
The emitted characters. Exact strings for a known vector, a zero byte, a
single high byte, leading zero bytes, both sides of the first word boundary, and
the end of the allocated buffer. Fuzzed: the result is exactly two characters per
input byte, every character is a lower case hex nibble, no
xsurvives anywhere,the result round trips back through
vm.parseBytesto the input bytes, and theresult is
vm.toString(data)with its first two characters removed and nothingelse touched.
The length property is load bearing rather than cosmetic. The output is spliced
straight into a
hex"..."literal in generated source, and solc 0.8.25 rejectsan odd nibble count outright (
hex"abc"→ "Expected even number ofhex-nibbles"). An off-by-one in the subtraction emits source that does not
compile. Case is not load bearing — solc accepts
hex"AABB"and even mixed casehex"aAbB"— but stability under regeneration is, so the exact lower case outputis pinned.
Empty bytes, end to end.
vm.toString(bytes(""))is"0x", so the wholestring is prefix and
2 - 2is the tightest input the subtraction ever sees.testBytesToHexEmptyreads the raw length word rather than comparing strings, soan underflow to
2**256 - 2cannot pass as"".testBytesToHexEmptyReachesCallerAsEmptyHexLiteralfollows it intoLibCodeGen.bytesConstantStringand pins the emitted declaration asbytes constant NOTHING = hex"";, which solc accepts.The memory-safe promise. The annotation claims the block touches nothing
outside the string it was handed. Memory allocated before the call survives it,
memory allocated after it is not clobbered, the free memory pointer never moves
backwards, the caller's input bytes come back unchanged, and the returned string
lies entirely inside memory allocated during the call. The returned pointer is
deliberately not word aligned (
ptr % 32 == 2), so the result is also forcedacross an ABI boundary and through a
string.concatcopy atdata.length % 16 == 15— the lengths where the payload ends flush with the endof the buffer
vm.toStringallocated, which is where a copier reading past thatbuffer would surface.
The annotation is a promise to the optimiser, so the suite was also run under the
pipelines a consumer might build this library with:
--via-ir, and--evm-version shanghaiwhere there is nomcopyand the compiler falls back toa word-rounded copy loop — the case where reading two bytes past the buffer would
actually happen. All 20 pass under each, and under
--via-ir --evm-version paris.QA
Discriminating tests: 20 new tests, all asserting exact values or exact memory
facts rather than "it did not revert". Full suite 29 tests / 5 suites, 0
failures;
forge fmt --checkclean. Every test passes on the clean tree, andeach behaviour it claims to cover has a mutation below that it fails under.
Mutations applied: 18, one behaviour each, run with
mutation-probeagainstthe whole suite from a green 29-test baseline. 17 KILLED, 1 SURVIVED. Killer
lists are the first five the probe reports, not the complete set:
add(hexString, 2)→add(hexString, 0)testBytesToHexAllocationBoundary,testBytesToHexCharset,testBytesToHexConcatenatesIntoHexLiteral,testBytesToHexHasNoPrefix,testBytesToHexIsVmToStringWithoutPrefixadd(hexString, 2)→add(hexString, 1)add(hexString, 2)→add(hexString, 3)testBytesToHexAllocationBoundary,testBytesToHexCharset,testBytesToHexConcatenatesIntoHexLiteral,testBytesToHexIsVmToStringWithoutPrefix,testBytesToHexKnownadd(hexString, 2)→add(hexString, 32)sub(mload(hexString), 2)→mload(hexString)(term dropped)testBytesToHexAllocationBoundary,testBytesToHexCharset,testBytesToHexConcatenatesIntoHexLiteral,testBytesToHexEmpty,testBytesToHexIsVmToStringWithoutPrefixsub(…, 2)→sub(…, 1)(odd nibble count)sub(…, 2)→sub(…, 3)testBytesToHexAllocationBoundary,testBytesToHexCharset,testBytesToHexConcatenatesIntoHexLiteral,testBytesToHexEmpty,testBytesToHexEmptyReachesCallerAsEmptyHexLiteralsub(…, 2)→add(…, 2)newHexStringinstead ofhexStringtestBytesToHexAllocationBoundary,testBytesToHexCharset,testBytesToHexConcatenatesIntoHexLiteral,testBytesToHexEmpty,testBytesToHexHasNoPrefixmstoredeleted entirelyhexString := newHexString→hexString := hexStringtestBytesToHexAllocationBoundary,testBytesToHexConcatenatesIntoHexLiteral,testBytesToHexIsVmToStringWithoutPrefix,testBytesToHexKnown,testBytesToHexLeadingZeroBytesvm.toString(data)→vm.toString(bytes(""))(input ignored)return hexString;→return "";mstore(0x80, 0)— writes memory the block does not owntestBytesToHexDoesNotMutateInput,testBytesToHexLeavesNeighbouringMemoryAlone,testBytesToHexLength,testBytesToHexLongData,testBytesToHexRoundTripsassembly ("memory-safe")→assemblytestBytesToHexAllocationBoundary,testBytesToHexCharset,testBytesToHexConcatenatesIntoHexLiteral,testBytesToHexEmpty,testBytesToHexHasNoPrefixtestBytesToHexDoesNotMutateInput,testBytesToHexLongData,testBytesToHexRoundTripsM16 is a non-behaviour control and its survival is the correct verdict, not a
gap: the annotation is a promise made to the optimiser, and removing it only
makes the optimiser more conservative, so nothing observable changes at
runtime. The promise itself is checked directly instead, by M15, M17 and M18 —
each injects a distinct violation of what the annotation claims, and each dies.
Oracle: the generated text has to compile, so intent was derived from what solc
accepts rather than from what the library returns. Checked against solc 0.8.25
directly:
hex""compiles,hex"AABB"and mixed casehex"aAbB"compile,hex"abc"does not. That is what makes "exactly two characters per byte" theproperty worth pinning, and makes case a stability concern rather than a
correctness one. The docstring's claim about
vm.toString("the leading 0xwhich is unconditionally added") is checked rather than assumed:
testBytesToHexIsVmToStringWithoutPrefixasserts the prefix is present and thelength is
2 * data.length + 2for every fuzz input, and it holds — includingfor empty bytes, where
vm.toStringreturns exactly"0x"and the subtractionlands on zero rather than underflowing.
Category check: the ask is coverage for
LibHexString, and the category isevery behaviour in it, not the interesting ones. The file is one function with
six behaviours — the conversion call, the pointer offset, the length
subtraction, the length store, the pointer reassignment, and the return — plus
the memory-safe promise and the handoff to
bytesConstantString. All eight arein the matrix above, each probed by at least one mutation. A second pass added
input immutability and the buffer boundary; a third re-survey of the function
surfaced no behaviour that is not already probed.