test: pin ICloneableV2's once-only initialize and always-reverting typed overload - #76
Conversation
…pe gaps Probe pass 1 against the pre-existing suite found the whole declaration-only half of this repo unguarded: the three deprecated interfaces, ICloneableFactoryV2 and ICloneableV2.InitializeSignatureFn are imported by nothing, and even ICloneableFactoryV3.NewClone survives having two of its three address parameters transposed, because that is invisible to every topic and every log data byte while telling every indexer the deployer is the clone. - test/src/lib/LibPublishedAbi.sol reads the compiled artifact, the only oracle for parameter names, same-typed parameter order, indexed flags and return types. - Declaration pins for ICloneableFactoryV3, ICloneableV2, ICloneableFactoryV2 and the three deprecated interfaces. - TestCloneableConformant, TestCloneableEmitter, TestCloneableReverter and TestCloneableV1Shaped: fixtures whose shape makes the once-only MUST, the typed-overload MUST, NewClone log ordering and initialize revert bubbling observable at all. - TestCloneFactory.t.sol pins the pure-delegation claim the concrete exists for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…undeployable code testCheckImplementationCodeEtched fails on UNMUTATED source whenever the fuzzer draws code whose first byte is 0xEF: vm.etch reads that as an EIP-7702 delegation designator and rejects it. EIP-3541 forbids deploying such code at all, so it can never be an implementation's code on chain, and excluding it narrows the fuzz domain to inputs the property is about rather than weakening it - the guard only ever reads code LENGTH. Reported independently as #64 and #68; fixed here because a baseline that goes red on a fuzz draw makes every mutation verdict in this campaign unreliable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. WalkthroughThe pull request adds tests for ChangesICloneableV2 initialization coverage
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Other · Severity of issue fixed: Medium 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
Three parallel AMT branches each invented their own `ICloneableV2` fixture on top of the two that already existed. The merge from `2026-08-24-test-fixtures-out-of-src-mirror` resolves this branch onto the shared set: - `TestCloneableCallRecorder` and `TestCloneableRevert` move to `test/concrete/`, out of the `test/src` mirror, and are the canonical versions — #76 carries the same two files byte for byte, having dropped its own `TestCloneableEmitter` and `TestCloneableReverter` duplicates, so the two branches merge without touching each other. - Both now return the success sentinel as a LITERAL rather than importing `ICLONEABLE_V2_SUCCESS`, matching `TestCloneable` after the base branch: a fixture that imports the constant the library compares against moves in lockstep with it and cannot discriminate a drift. - The `0xef` fuzz-domain narrowing on `testCheckImplementationCodeEtched` is dropped from this branch. All four AMT branches had patched that same line; it is a `main` defect and now lives there once. Gas snapshot regenerated: it moves for the extra `SSTORE` the base branch's initialization guard costs on every `TestCloneable` clone, as well as for this branch's new tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # test/concrete/TestCloneFactory.t.sol # test/concrete/TestCloneableConformant.sol # test/concrete/TestCloneableEmitter.sol # test/concrete/TestCloneableReverter.sol # test/concrete/TestCloneableV1Shaped.sol # test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol
…what they test The merge from `2026-08-24-test-fixtures-out-of-src-mirror` already relocated this branch's fixtures out of the `test/src` mirror, which only mirrors `src`. This finishes the job. Fixtures — three of this branch's five are gone, none of the coverage with them: - `TestCloneableConformant` was `TestCloneable` plus a one-shot init guard and the typed overload. The base branch gives `TestCloneable` both, for the whole suite rather than for two tests, so the second fixture had nothing left to add. `testInitializeOnlyOnce` and `testTypedOverloadRevertsInitializeSignatureFn` move onto it unchanged apart from the error name. - `TestCloneableReverter` and `TestCloneableEmitter` are replaced by `TestCloneableRevert` and `TestCloneableCallRecorder`, byte for byte the same files #78 carries, so the two branches merge without touching each other. - `TestCloneableRevert` and `TestCloneableFailure` stay SEPARATE. A fixture that returns a non-success sentinel cannot also revert with a typed error carrying data; collapsing them would weaken `testInitializeRevertBubblesVerbatim` to a bare decode revert. `testNothingCalledBeforeInitialize` gains a second, stronger oracle in the swap. `TestCloneableEmitter` announced a stray call by emitting from its fallback, so the whole assertion rode on a log count. `TestCloneableCallRecorder` records the SELECTOR SEQUENCE instead, and the test now asserts that exactly one call reached the clone and that it was `initialize(bytes)`. Mutation-checked: a `child.call(...)` inserted into `cloneAndInitialize` between `NewClone` and `initialize` is caught by the selector assertion ("2 != 1") and is INVISIBLE to the log-count assertion, because the recorder's fallback deliberately does not emit. `TestCloneableV1Shaped` was imported by nothing on this branch — a fixture built for a test that was never written. It now has it. `testInitializeSelectorSharedWithV1` pins that V1 and V2 publish the SAME `initialize(bytes)` selector, which is only half a hazard statement: the question it raises is whether a V4 factory silently accepts a legacy implementation and leaves a live clone that was never initialized. It does not — the `bytes32` return decode finds an empty returndata buffer and reverts before the sentinel comparison, with NO revert data, which the test asserts as observed behaviour rather than assuming `InitializationFailed`. Mutation-checked: give the fixture a `bytes32` return and the test fails. Layout, so a file name says what it tests: - `LibPublishedAbi.sol` is test support, not a mirror of any `src/lib` file, so it moves to `test/lib/`. - `ICloneableFactoryV2.sol.t.sol` and `ICloneableFactoryV3.sol.t.sol` lose the doubled extension. - `ICloneableV2.sol.t.sol` becomes `ICloneableV2.initialize.t.sol`, matching the `<unit>.<function>.t.sol` convention `test/src/lib` already uses. Every test in it is about `initialize` or its return sentinel. - `DeprecatedInterfaces.t.sol` splits into `ICloneableV1.t.sol`, `ICloneableFactoryV1.t.sol` and `IFactory.t.sol`, one per file in `src/interface/deprecated/`, which is what the mirror is for. The three tests were independent and shared nothing but the notice, which each file keeps. The `0xef` fuzz-domain narrowing this branch carried is resolved onto the base branch's version of the same line, which now holds it once for all four AMT branches instead of four times. Gas snapshot regenerated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # .gas-snapshot
|
@coderabbitai review Queuing a review — the original attempt hit the OSS rate limit before reading the diff, so this PR has never actually been reviewed. Orientation, since this is an audit deliverable rather than a feature:
|
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Changed files ==='
git diff --name-status HEAD^ HEAD
printf '%s\n' '=== Diff summary ==='
git diff --stat HEAD^ HEAD
printf '%s\n' '=== Full pull request diff ==='
git diff --find-renames --find-copies --unified=80 HEAD^ HEADLength of output: 18575 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Pull request metadata ==='
gh pr view 76 --repo rainlanguage/rain.factory \
--json baseRefName,baseRefOid,headRefName,headRefOid,commits \
--jq '{baseRefName, baseRefOid, headRefName, headRefOid, commits: [.commits[] | {oid, messageHeadline}]}'
base_oid="$(gh pr view 76 --repo rainlanguage/rain.factory --json baseRefOid --jq '.baseRefOid')"
merge_base="$(git merge-base HEAD "$base_oid")"
printf '%s\n' '=== Review range ==='
printf 'merge_base=%s\nhead=%s\n' "$merge_base" "$(git rev-parse HEAD)"
printf '%s\n' '=== Changed files in complete pull request ==='
git diff --name-status "$merge_base" HEAD
printf '%s\n' '=== Complete Solidity diff ==='
git diff --find-renames --find-copies --unified=60 "$merge_base" HEAD -- '*.sol'
printf '%s\n' '=== Changed non-Solidity files ==='
git diff --name-only "$merge_base" HEAD -- ':!*.sol'Length of output: 68655 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Foundry configuration diff and active configuration ==='
git diff c1c2afd3d88405d3228cb3b21e55c9d63ba8f5be HEAD -- foundry.toml
cat -n foundry.toml
printf '%s\n' '=== Relevant source and test outlines ==='
ast-grep outline src/lib/LibICloneableFactoryV4.sol --items all
ast-grep outline test/src/interface/ICloneableV2.initialize.t.sol --items all
ast-grep outline test/src/interface/ICloneableFactoryV3.t.sol --items all
ast-grep outline test/lib/LibPublishedAbi.sol --items all
printf '%s\n' '=== Clone initialization implementation and factory forwarding ==='
rg -n -A70 -B15 'function cloneAndInitialize|cloneAndInitialize\s*\(' src/lib/LibICloneableFactoryV4.sol test/concrete/TestCloneFactory.sol
rg -n -A45 -B10 'function cloneDeterministic|function cloneDeterministicOpenSalt|function predictDeterministic' test/concrete/TestCloneFactory.sol
printf '%s\n' '=== ABI declarations under test ==='
cat -n src/interface/ICloneableV2.sol
cat -n src/interface/ICloneableFactoryV2.sol
cat -n src/interface/ICloneableFactoryV3.sol
cat -n src/interface/deprecated/ICloneableV1.sol
cat -n src/interface/deprecated/ICloneableFactoryV1.sol
cat -n src/interface/deprecated/IFactory.sol
printf '%s\n' '=== Complete changed initialization test ==='
cat -n test/src/interface/ICloneableV2.initialize.t.sol
printf '%s\n' '=== Complete changed V3 declaration test ==='
cat -n test/src/interface/ICloneableFactoryV3.t.solLength of output: 49730 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Obsolete fixture-path references ==='
rg -n -C2 'test/src/concrete/(TestCloneFactory|TestCloneable|TestCloneableFailure|TestCloneableEmitter|TestCloneableReverter|TestCloneableConformant|TestCloneableV1Shaped)\.sol' test || true
printf '%s\n' '=== Solidity test discovery paths ==='
fd -e sol . test | sort
printf '%s\n' '=== Imports in changed Solidity files whose targets do not exist ==='
python3 - <<'PY'
from pathlib import Path
import re
import subprocess
base = subprocess.check_output(
["git", "merge-base", "HEAD", "c1c2afd3d88405d3228cb3b21e55c9d63ba8f5be"],
text=True,
).strip()
changed = subprocess.check_output(
["git", "diff", "--name-only", base, "HEAD", "--", "*.sol"],
text=True,
).splitlines()
missing = []
for filename in changed:
path = Path(filename)
for line_no, line in enumerate(path.read_text().splitlines(), 1):
match = re.match(r'\s*import\s+.*?from\s+"([^"]+)";', line)
if not match:
continue
target = match.group(1)
if target.startswith(("src/", "test/")) and not Path(target).exists():
missing.append(f"{filename}:{line_no}: {target}")
print("\n".join(missing) if missing else "No missing src/ or test/ import targets.")
PY
printf '%s\n' '=== Pull request check status ==='
gh pr checks 76 --repo rainlanguage/rain.factory || trueLength of output: 3342 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Imports in changed, present Solidity files whose targets do not exist ==='
python3 - <<'PY'
from pathlib import Path
import re
import subprocess
base = "c1c2afd3d88405d3228cb3b21e55c9d63ba8f5be"
changed = subprocess.check_output(
["git", "diff", "--diff-filter=AMR", "--name-only", base, "HEAD", "--", "*.sol"],
text=True,
).splitlines()
missing = []
checked = []
for filename in changed:
path = Path(filename)
if not path.is_file():
continue
checked.append(filename)
for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
match = re.match(r'\s*import\s+.*?from\s+"([^"]+)";', line)
if not match:
continue
target = match.group(1)
if target.startswith(("src/", "test/")) and not Path(target).is_file():
missing.append(f"{filename}:{line_no}: {target}")
print("Checked:")
print("\n".join(checked))
print("\nMissing targets:")
print("\n".join(missing) if missing else "None")
PY
printf '%s\n' '=== Pull request check status ==='
gh pr checks 76 --repo rainlanguage/rain.factoryLength of output: 2615
I reviewed the complete PR range, not only the final commit.
The moved fixtures have no stale imports or missing internal import targets. The
|
The split into one file per deprecated interface made `testICloneableV1AbiPinned` and friends look redundant against their new contract names, so the previous commit shortened all three to `testAbiPinned`. That was wrong twice over. The PR's `## QA` block names those three tests as the killers of M45, M41, M42, M43, M36 and M46. That block is a record of a mutation run that actually happened; renaming its subjects makes it unfollowable, and editing it to match would be rewriting the evidence rather than keeping it true. The short name also collided. Two `testAbiPinned` already existed, so the rename made five identically named tests in one suite and `--match-test testAbiPinned` select all of them. The file split stands; only the function names go back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolves the .gas-snapshot conflict against main after #78 by regenerating the snapshot with `forge snapshot` on the merge; the result is the exact union of both sides' 76 tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keeps testInitializeOnlyOnce and testTypedOverloadRevertsInitializeSignatureFn. Removes the ABI pinning, TestCloneFactory delegation, NewClone topic and V1-shaped tests and fixtures from this branch, and restores main's foundry.toml and .gas-snapshot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@coderabbitai review |
|
|
@coderabbitai review |
|
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #67.
The fixture half of #67 is on
mainvia #80:test/concrete/TestCloneable.solrejects a secondinitializewithTestCloneableAlreadyInitialized, and its typedinitialize(uint256)overload always revertsInitializeSignatureFn. No test onmainexercises either behaviour. This PR adds the two tests that do, on clonesTestCloneFactorydeployed and initialized.Change
One file,
test/src/interface/ICloneableV2.initialize.t.sol(ICloneableV2InitializeTest):testInitializeOnlyOnce: after the factory'scloneDeterministic, a secondinitialize(otherData)on the clone revertsTestCloneableAlreadyInitializedandsData()still equals the factory'sdata.testTypedOverloadRevertsInitializeSignatureFn:initialize(uint256)revertsICloneableV2.InitializeSignatureFnon the never-initialized implementation and on an initialized clone..gas-snapshotandfoundry.tomlaremain's.main(f0154e1) is merged in.Other work that was on this branch is now elsewhere:
TestCloneableV1Shaped+testV1ShapedImplementationIsRejected: Raise InitializationFailed for every non-conforming initialize answer #89 (→ InitializationFailed is not raised when the implementation does not implement ICloneableV2.initialize — caller gets bare 0x #61).NewClonetopic pins: Document and pin the wire identity of every published NewClone #85 (→ NewClone is declared by three published interfaces under two signatures; ICloneableFactoryV1 and V2 share one topic0 #65).testInitializeRevertBubblesVerbatim(duplicatestestCloneDeterministicInitializeRevertBubbles),testNothingCalledBeforeInitialize(duplicatestestCloneDeterministicInitializeIsTheOnlyCall+testCloneDeterministicEventPrecedesInitialize),testNewCloneSharedByBothEntryPoints(each entry point's fullNewClonelog is asserted bytestCloneDeterministicEvent/testCloneDeterministicOpenSaltEvent),testPredictionsAreStatic(cannot fail: the compiler enforcesview),TestCloneFactory.t.sol(tests the test harness; the deployed concrete is in rain.factory.deploy).QA
Discriminating tests:
testInitializeOnlyOnce,testTypedOverloadRevertsInitializeSignatureFn. Each fails onmain's suite plus a mutated fixture and passes unmutated.Mutations (
mutation-probe, rainlanguage/adversarial-mutation-test@4e59ec0,nix develop -c forge test, whole suite per mutant), all intest/concrete/TestCloneable.sol:mainf0154e1 (48 tests)if (sInitialized)→if (false)testInitializeOnlyOnceinitializereturns the sentinel instead of revertingtestInitializeOnlyOncesInitialized = true→falsetestInitializeOnlyOncetestTypedOverloadRevertsInitializeSignatureFnTestCloneableAlreadyInitializedtestTypedOverloadRevertsInitializeSignatureFnmain:0/5 killed; survived: 5; no-run: 0; harness errors: 0. This PR:5/5 killed; survived: 0; no-run: 0; harness errors: 0. Configs and logs:/home/gildlab/artifacts/factory/76/(mutants-{base,head}.toml,probe-{base,head}.{log,json}).Oracle: the two MUSTs in
src/interface/ICloneableV2.sol—initializecan NOT be called more than once; a typed overload revertsInitializeSignatureFnalways. Expected reverts are the fixture'sTestCloneableAlreadyInitializedand the interface'sInitializeSignatureFn, not values read back from the clone.Category check: TestCloneable, the only worked ICloneableV2 fixture, violates the interface's once-only initialize MUST that ICloneableFactoryV4's open-salt safety argument rests on #67 names two fixtures and two MUSTs. Both MUSTs are covered on
TestCloneable, the conforming fixture every successful-initialization flow test uses.TestCloneableFailureis the failure fixture: itsinitializeispure, holds no state and has no once-only guard or typed overload, and these tests do not cover it.🤖 Generated with Claude Code
Summary by CodeRabbit