Skip to content

test: pin ICloneableV2's once-only initialize and always-reverting typed overload - #76

Merged
thedavidmeister merged 9 commits into
mainfrom
2026-08-24-amt-g4-icloneablefactoryv3-newclone
Sep 19, 2026
Merged

thedavidmeister merged 9 commits into
mainfrom
2026-08-24-amt-g4-icloneablefactoryv3-newclone

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closes #67.

The fixture half of #67 is on main via #80: test/concrete/TestCloneable.sol rejects a second initialize with TestCloneableAlreadyInitialized, and its typed initialize(uint256) overload always reverts InitializeSignatureFn. No test on main exercises either behaviour. This PR adds the two tests that do, on clones TestCloneFactory deployed and initialized.

Change

One file, test/src/interface/ICloneableV2.initialize.t.sol (ICloneableV2InitializeTest):

  • testInitializeOnlyOnce: after the factory's cloneDeterministic, a second initialize(otherData) on the clone reverts TestCloneableAlreadyInitialized and sData() still equals the factory's data.
  • testTypedOverloadRevertsInitializeSignatureFn: initialize(uint256) reverts ICloneableV2.InitializeSignatureFn on the never-initialized implementation and on an initialized clone.

.gas-snapshot and foundry.toml are main's. main (f0154e1) is merged in.

Other work that was on this branch is now elsewhere:

QA

  • Discriminating tests: testInitializeOnlyOnce, testTypedOverloadRevertsInitializeSignatureFn. Each fails on main'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 in test/concrete/TestCloneable.sol:

    Mutant main f0154e1 (48 tests) this PR 4f3d48d (50 tests)
    F01 if (sInitialized)if (false) SURVIVED KILLED by testInitializeOnlyOnce
    F02 second initialize returns the sentinel instead of reverting SURVIVED KILLED by testInitializeOnlyOnce
    F03 sInitialized = truefalse SURVIVED KILLED by testInitializeOnlyOnce
    F04 typed overload returns the sentinel instead of reverting SURVIVED KILLED by testTypedOverloadRevertsInitializeSignatureFn
    F05 typed overload reverts TestCloneableAlreadyInitialized SURVIVED KILLED by testTypedOverloadRevertsInitializeSignatureFn

    main: 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.solinitialize can NOT be called more than once; a typed overload reverts InitializeSignatureFn always. Expected reverts are the fixture's TestCloneableAlreadyInitialized and the interface's InitializeSignatureFn, 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. TestCloneableFailure is the failure fixture: its initialize is pure, 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

  • Tests
    • Added coverage verifying clone initialization can only occur once.
    • Added coverage confirming initialized clones retain their original data after rejected reinitialization attempts.
    • Added coverage for expected failures when using the typed initialization overload on implementations and initialized clones.

baku-ccron and others added 2 commits August 24, 2026 07:48
…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>
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: accb0b5a-6174-4638-8fa7-dc2735d0acca

📥 Commits

Reviewing files that changed from the base of the PR and between ebef4a7 and 98d291d.

📒 Files selected for processing (1)
  • test/src/interface/ICloneableV2.initialize.t.sol

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The pull request adds tests for ICloneableV2 initialization. The tests cover single-use initialization and rejection of the typed initialize(uint256) overload on implementations and deterministic clones.

Changes

ICloneableV2 initialization coverage

Layer / File(s) Summary
Initialization behavior tests
test/src/interface/ICloneableV2.initialize.t.sol
Adds tests that verify clone data, reject repeated initialize calls with TestCloneableAlreadyInitialized, and reject typed initialization calls with ICloneableV2.InitializeSignatureFn.

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)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the two initialization requirements covered by the new tests: once-only initialization and the always-reverting typed overload.
Linked Issues check ✅ Passed Issue #67 requires coverage for the once-only initialize rule and the typed overload revert. The added testInitializeOnlyOnce deploys and initializes a factory clone, checks the initial data, expe…
Out of Scope Changes check ✅ Passed The whole-PR diff adds only test/src/interface/ICloneableV2.initialize.t.sol. Both tests directly cover the initialization requirements in issue #67. No unrelated production, fixture, ABI, topic, or…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

thedavidmeister pushed a commit that referenced this pull request Aug 24, 2026
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>
baku-ccron and others added 3 commits August 24, 2026 12:40
# 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>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

@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:

  • Nothing under src/ is touched. The diff is tests, fixtures and file layout.
  • Fixture count goes 5 to 3 on this branch. TestCloneableConformant folds into TestCloneable, which now carries the one-shot init guard and the typed overload for the whole suite. TestCloneableEmitter and TestCloneableReverter are replaced by TestCloneableCallRecorder and TestCloneableRevert, byte for byte the same two files AMT coverage: g2-libicloneablefactoryv4-predi #78 carries so the branches merge without touching each other.
  • Please look hardest at testNothingCalledBeforeInitialize. The swap from TestCloneableEmitter to TestCloneableCallRecorder would have been a silent REGRESSION on its own: the Emitter announced a stray call by emitting from its fallback, the CallRecorder deliberately does not emit from its fallback. I verified that directly — with the new selectors().length == 1 assertion removed, a child.call(...) inserted into cloneAndInitialize between NewClone and initialize PASSES; with it, that mutant is killed. If you think the log-stream assertions still carry weight I would rather hear it than assume.
  • TestCloneableV1Shaped was imported by nothing on this branch — a fixture with no test. testV1ShapedImplementationIsRejected is the test it was built for. It asserts an EMPTY revert (vm.expectRevert(bytes(""))), because the bytes32 return decode reverts with no data before the sentinel comparison is reached, rather than producing InitializationFailed. That is the observed behaviour, mutation-checked: give the fixture a bytes32 return and the test fails. Push back if you think pinning a data-less revert is too brittle.
  • TestCloneableFailure and TestCloneableRevert are deliberately NOT collapsed. A fixture that returns a non-success sentinel cannot also revert with a typed error carrying data.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
🧩 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^ HEAD

Length 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.sol

Length 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 || true

Length 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.factory

Length of output: 2615


@thedavidmeister No actionable findings.

I reviewed the complete PR range, not only the final commit.

testNothingCalledBeforeInitialize now has the required direct oracle. The selectors().length == 1 assertion detects a successful stray proxy call. The log assertions still add value because they verify that NewClone occurs before the clone emits Initializing, and that data reaches initialize unchanged.

testV1ShapedImplementationIsRejected correctly pins the observed empty revert. cloneAndInitialize performs a typed bytes32 decode at the external initialize call. A V1-shaped implementation returns empty data, so decoding reverts before the sentinel comparison.

The moved fixtures have no stale imports or missing internal import targets. The legal, static, and test GitHub checks pass.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

baku-ccron and others added 3 commits August 24, 2026 13:03
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>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thedavidmeister thedavidmeister self-assigned this Sep 19, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thedavidmeister thedavidmeister changed the title AMT coverage: g4-icloneablefactoryv3-newclone test: pin ICloneableV2's once-only initialize and always-reverting typed overload Sep 19, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant