Skip to content

feat(deploy): deterministic-clone toolchain + deterministic V4 authoriser deploy capability (#292 step 1) - #250

Open
thedavidmeister wants to merge 15 commits into
mainfrom
stox/deterministic-authoriser-clone
Open

feat(deploy): deterministic-clone toolchain + deterministic V4 authoriser deploy capability (#292 step 1)#250
thedavidmeister wants to merge 15 commits into
mainfrom
stox/deterministic-authoriser-clone

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Step 1 of #292 plus the deterministic deploy capability: everything needed to deploy the deterministic V4 authoriser clones on all configured chains post-merge. The only follow-up left is the migration itself (grant mirroring, setAuthorizer, live-pin flip — #292 step 3).

What this PR contains

Toolchain — rain-factory 0.1.1→0.1.5 (deterministic-only CloneFactory), rain-vats 0.1.6→0.1.7 (0.1.6 pins rain-factory 0.1.1 internally; 0.1.7 keeps a single factory in the tree). Import prefixes repointed repo-wide; unit fixtures migrated clone()cloneDeterministic().

src/lib/LibNonceCloneFactory.sol — pins the nonce-based CloneFactory (0x444acC29…) every LIVE clone was deployed through: address + codehash + full runtime bytecode (#293 pin shape), keccak-asserted in the 20260619 suite. The executed 20260619 script keeps targeting it unchanged.

Migration target pin (BuildPointers-generated into LibProdDeployV4):

  • STOX_PROD_AUTHORISER_V4_CLONE_DETERMINISTIC = 0xE322d7D0d366cE0bddfACF6D25d11DdA1B13A86b — CREATE2 prediction against the 0.1.5 factory, chain-invariant (Zoltu factory + Zoltu impl + pinned salt/deployer). Moves off the original proposal's 0x9746…, which was predicted for the retired 0x8E4bdeec… EOA.
  • …_SALT (bytes32(0)), …_DEPLOYER (0xE8c6eDE2…, the EOA the CI deploy key resolves to — the creator of the live Base clone via the nonce factory, tx 0x26519d1c…; the factory namespaces salts by sender), …_CODE (EIP-1167 runtime bytes) and …_CODEHASH (Hash-only and address-only pins across the repo: embed runtime bytecode on every deployed-artifact pin (ruling from #250) #293 shape).
  • The LIVE pins (STOX_PROD_AUTHORISER_V4_CLONE = 0x315b16…, the Ethereum clone) are byte-identical to main and stay authoritative until the migration.

script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol — per-chain broadcast (Base + Ethereum config on main; HyperEVM arrives with its own wave):

  • Pre-flight: token-owner Safe intact; deterministic factory present with the 0.1.5-pinned codehash; V4 impl present with the audited codehash; the factory's own predictDeterministicAddress equals the pin; idempotent — no-op when the pinned clone already occupies the target, typed revert if foreign code does.
  • Broadcast: cloneDeterministic(impl, abi.encode({initialAdmin: tokenOwnerSafe}), salt) — admin lands directly on the Safe, the deployer never holds a role, no renounce ceremony.
  • Post-state: clone landed exactly at the pin, EIP-1167 codehash matches, Safe holds all seven auto-granted _ADMIN roles.
  • The deployed-but-unmigrated clone is inert: initialized with Safe-held admin only, no operational grants, and no vault references it.
  • Note: the factory is live on Base; on Ethereum it is not yet Zoltu-deployed, so the script fail-safes there with a typed error until that (permissionless) deploy happens.

Tests (test/script/20260804-…t.sol, 11 tests): offline CREATE2 re-derivation of the pin, EIP-1167 bytecode re-derivation, keccak(code)==codehash, live-Base-fork prediction check, a real-factory fork deploy from the pinned deployer landing exactly on the pin, idempotent no-op, and typed-revert coverage for foreign-code/missing-factory/codehash-drift pre-flights — tolerant of the not-yet-deployed target ("no live code EXPECTED" pattern).

Dropped from the original diff

The up-front flip of the LIVE pin to the predicted address (contradicts the deployed clone) and the rewrite of the executed 20260619 script — the deterministic deploy now lives in its own dated script above.

🤖 Generated with Claude Code

QA

  • Discriminating tests: testTargetPinRederivesOffline, testTargetPinCodeRederives, testTargetPinCodehashMatchesCode, testDeployerPinIsTheLiveCloneCreator, testBaseLiveFactoryPredictsPin, testCloneFromPinnedDeployerLandsOnPin, testRunNoOpsWhenTargetAlreadyHydrated, testRunRejectsUninitializedCloneAtTarget, testRunRejectsForeignCodeAtTarget, testRunRejectsMissingFactory, testRunRejectsFactoryCodehashDrift, testRunRejectsImplCodehashDrift, testEthereumFactoryGate (all 13 in test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol), plus testNonceCloneFactoryCodehashMatchesEmbeddedCode, testAssertKeysHoldNoRolesPassesLive, testAssertKeysHoldNoRolesRejectsHotKeyRole, testAssertKeysHoldNoRolesRejectsRetiredKeyDefaultAdmin — each fails on base, verified STRUCTURALLY rather than by running the base suite: git diff --name-status origin/main...HEAD shows script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol, src/lib/LibNonceCloneFactory.sol and the 20260804 test file are Added by this PR, and LibAuthoriserInvariants.assertKeysHoldNoRoles does not exist on origin/main, so on base none of these tests have a subject to compile against. All 17 pass at the head in CI (rainix-sol / test / test, which runs the Base-fork suite).
  • Mutations applied: no forge mutation pass was run, and the reason is a toolchain one, not a coverage one — pr-review-report sol-toolchain returns mode: conflict (exit 3) for this checkout, naming two gating shells (the rainix reusable @main and github:rainlanguage/rainix#sol-shell via git-clean.yaml), so no single local forge is the one CI judges with and picking either would measure the wrong toolchain. In its place the deploy-critical derivation was mutated OFFLINE, one input at a time, against the independent keccak in the Oracle line, and each mutant re-predicted: deployer 0xE8c6eDE2…→retired 0x8E4bdeec…0x9746648ef5…; salt bytes32(0)bytes32(1)0x898899433b…; factory 0x7053c1D0… (0.1.5)→0x444acC29… (nonce) ⇒ 0xc3df700b63…; impl 0x2ea0d35d…0x315b16faa6…0xff53cd7a92…; drop the sender-namespacing of the salt ⇒ 0x0dfaca23c2…. Every mutant moves the address off the pin 0xE322d7D0…, so testTargetPinRederivesOffline and testBaseLiveFactoryPredictsPin are discriminating on each of the four inputs and on the namespacing itself. The deployer mutant reproduces 0x9746… exactly — the original proposal's address, predicted for the retired EOA — which is the drift the deployer correction in this PR fixes.
  • Oracle: independent of this repo's Solidity in both halves. (a) Offline — a hand-written pure-python keccak-256, self-tested against keccak256("") = 0xc5d2460186f7… and keccak256("abc") = 0x4e03657aea45…, re-derived from first principles: the EIP-1167 runtime for impl 0x2ea0d35d… (45 bytes, byte-equal to the pinned …_DETERMINISTIC_CODE); keccak256(…_DETERMINISTIC_CODE) = 0x2089950d3cc1112dd66a58adcfadeadc490b50053ac67be8bc676b4a2dcd1717 = …_DETERMINISTIC_CODEHASH; the effective salt keccak256(abi.encode(deployer, bytes32(0))) = 0x55e90699ce… using the formula read out of the rain-factory 0.1.5 soldeer artifact itself (src/concrete/CloneFactory.sol:48 _effectiveSalt, downloaded from the URL in soldeer.lock, not read from this repo); the CREATE2 address 0xe322d7d0d366ce0bddfacf6d25d11dda1b13a86b = the pinned STOX_PROD_AUTHORISER_V4_CLONE_DETERMINISTIC; and keccak256(CLONE_FACTORY_DEPLOYED_CODE) over the embedded 1012 bytes = 0xf21b813c7075a1621285df3a8369d0652c31ea80cb807be1aaadafeecd134475 = CLONE_FACTORY_DEPLOYED_CODEHASH. (b) Live chain — raw eth_getCode / eth_call over public RPCs, no fork harness: Base 0x444acC29… runtime is BYTE-IDENTICAL to the embedded CLONE_FACTORY_DEPLOYED_CODE (1012 bytes); Base 0x7053c1D0… (0.1.5 factory) is live with codehash 0x3ec02e2f27d73f603f5b37a7dc228956004e9154c6d1f9c5335bd60036379d00, equal to that artifact's own BYTECODE_HASH, and is ABSENT on Ethereum (0x on three independent Ethereum RPCs — publicnode, drpc, mevblocker), so the script's typed Ethereum fail-safe describes a real condition; the deterministic target 0xE322d7D0… is empty on Base and on Ethereum; the V4 authoriser impl 0x2ea0d35d… is present on BOTH chains at 5958 bytes, which is what makes the predicted address chain-invariant; the live STOX_PROD_AUTHORISER_V4_CLONE 0x315b16faa6… carries codehash 0x2089950d3c…, equal to STOX_PROD_AUTHORISER_V4_CLONE_CODEHASH; and a batched hasRole sweep of that live clone shows the token-owner Safe holding all 10 distinct pinned roles (the positive control that proves the call encoding) while the hot deploy key 0xE8c6eDE2… and the retired key 0x8E4bdeec… hold 0 of 22 (role, key) pairs — the "deploy keys hold NOTHING" invariant, measured rather than asserted.
  • Category check: closingIssuesReferences is EMPTY — this PR closes no issue; it is step 1 of 2 of Deterministic authoriser clones everywhere: chain-invariant address, pinned up-front, migrate Base off the nonce-based clone #292, which stays open for step 2. Deterministic authoriser clones everywhere: chain-invariant address, pinned up-front, migrate Base off the nonce-based clone #292's step-1 list asks for six things and this covers all six: rain-factory 0.1.5 + forced rain-vats 0.1.7 bump (foundry.toml "rain-factory" = "0.1.5", "rain-vats" = "0.1.7", prefixes repointed in remappings.txt); clone()cloneDeterministic fixture migration (58 files changed with 0 file deletions in the diff — fixtures migrated, not dropped); a bytecode-complete nonce-factory pin per Hash-only and address-only pins across the repo: embed runtime bytecode on every deployed-artifact pin (ruling from #250) #293 (src/lib/LibNonceCloneFactory.sol: address + codehash + full 1012-byte runtime, keccak-asserted); the dated cloneDeterministic deploy script for every chain carrying prod config (script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol, Base + Ethereum); the predicted address pinned in the Hash-only and address-only pins across the repo: embed runtime bytecode on every deployed-artifact pin (ruling from #250) #293 shape with offline predicted==pinned and keccak asserts (the five …_DETERMINISTIC* constants in LibProdDeployV4 plus the 13 tests above); and live pins untouched (the LibProdDeployV4 diff against origin/main is additions only — STOX_PROD_AUTHORISER_V4_CLONE and …_ETHEREUM are byte-identical to main). Deliberately NOT covered, because Deterministic authoriser clones everywhere: chain-invariant address, pinned up-front, migrate Base off the nonce-based clone #292 scopes them elsewhere: step 2 (grant replay from RoleGranted/RoleRevoked logs, balance sweep, per-vault setAuthorizer bundles, pin flip) and the HyperEVM wave (feat(multichain): HyperEVM V4 authoriser clone pin plumbing (RAI-1511) #274/ops(script): HyperEVM beacon-owner migration + 0.1.1 beacon lib generalisation (RAI-1511) #276/ops(script): HyperEVM token deploy (RAI-1511) #278), which deploys deterministically from the start and needs no migration.

Summary by CodeRabbit

  • New Features
    • Added deterministic deployment for V4 authorizer clones with predictable addresses and safe initialization.
    • Existing valid deployments are recognized without duplication, while unexpected code or configuration stops deployment safely.
    • Added post-deployment verification for authorizer roles, implementation integrity, and deployment state.
  • Security
    • Added safeguards ensuring deployment keys retain no administrative or authorizer roles.
  • Maintenance
    • Updated supporting Rain Factory and Rain Vats components to newer versions.

…posal, #248)

Replace the nondeterministic CloneFactory.clone() V4-authoriser deploy with the
deterministic clone primitive (rain-factory 0.1.5 CloneFactory.cloneDeterministic
+ predictDeterministicAddress). The clone address is now a pure function of
(factory, impl, salt, deployer), so:

- BuildPointers PINS it up-front: STOX_PROD_AUTHORISER_V4_CLONE is computed via
  predictDeterministicAddress (was address(0)) instead of backfilled post-deploy.
- The deploy script asserts predict == pin BEFORE deploying and that the clone
  lands exactly there — no post-deploy hydration. Retires #211.
- Distinct salts give distinct clones of one impl, so future authorisers get
  their own salt (why cloneDeterministic, not a salt-free Zoltu-direct clone).

The msg.sender-namespaced salt means the pin encodes the deployer
(V4_AUTHORISER_CLONE_DEPLOYER = 0x8E4bdeec...), so the prod broadcast must come
from that key; the script's predict==pin assert enforces it.

Proposal for review (josh + team).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Jul 12, 2026
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR upgrades Rain dependencies, pins the nonce-based factory, adds deterministic V4 authoriser clone generation and deployment, updates clone fixtures, and adds deploy-key role invariants.

Changes

V4 deterministic clone flow

Layer / File(s) Summary
Rain dependency and interface updates
foundry.toml, remappings.txt, src/..., script/..., test/...
Updates rain-factory to 0.1.5 and rain-vats to 0.1.7. Test fixtures use deterministic clones with a zero salt.
Nonce-based factory pin generation
src/lib/LibNonceCloneFactory.sol, script/20260619-deploy-v4-authoriser-clone.s.sol, script/20260623-upgrade-receipt-vaults-to-v4.s.sol, test/script/...
Adds pinned factory address, runtime bytecode, and codehash constants. Existing validation uses these constants.
Deterministic clone pointer generation
script/BuildPointers.sol
Generates deterministic V4 authoriser clone deployment constants.
Deterministic clone deployment and validation
script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol, test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol
Adds Safe initialization, target prediction, idempotent execution, codehash checks, role checks, typed errors, and fork tests.
Deploy-key role invariants
src/lib/LibAuthoriserInvariants.sol, test/src/lib/LibAuthoriserInvariants*.sol
Rejects pinned roles held by the deterministic deployer or retired deploy key. Tests cover valid and invalid role assignments.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant DeployScript
  participant CloneFactory
  participant V4AuthoriserClone
  participant Safe
  DeployScript->>CloneFactory: validate factory and predict deterministic target
  DeployScript->>CloneFactory: deploy clone with pinned implementation and salt
  CloneFactory->>V4AuthoriserClone: create clone with Safe as initialAdmin
  DeployScript->>V4AuthoriserClone: verify codehash and seven admin roles
  V4AuthoriserClone->>Safe: establish automatically granted roles
Loading

Poem

A rabbit pins the factory tight,
And checks the clone by hash.
Zero salt guides the V4 hop,
Safe roles bloom in a flash.
Rain imports now match.

🚥 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 summarizes the main changes: deterministic clone tooling and deterministic V4 authoriser deployment capability.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stox/deterministic-authoriser-clone

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 and others added 2 commits July 12, 2026 14:55
The dep bump removed rain-factory-0.1.1; src contracts + tests still imported
ICloneableV2 from it. ICloneableV2/ICLONEABLE_V2_SUCCESS are unchanged across
0.1.1->0.1.5, so this is bytecode-neutral (frozen snapshots unaffected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The V4 authoriser clone pin is now computed up-front (deterministic), so the
comments describing an address(0) placeholder, post-execution pin-PR hydration,
and 'placeholder until deployed' no longer describe current behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thedavidmeister thedavidmeister added the ai:needs-work Needs rework — the producer's inbox (vetter verdict or human ruling) label Jul 12, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
Reviewed 3853038: reject — closes #248 (retires #211) — INCOMPLETE at its own footprint: hydrates STOX_PROD_AUTHORISER_V4_CLONE to 0x9746 but leaves testAuthoriserV4ClonePlaceholder (LibProdDeployV4.t.sol) asserting the pin is address(0) — that guard's own message demands replacing it with a real address+codehash assertion, which this PR omits; StoxProdV4.t.sol coverage notes also left stale ('non-deterministic, not checked here'); body has NO QA evidence block. Salt mirror vs CloneFactory._effectiveSalt and impl pin verified correct; deployer-encoded-address design point stays live for the human on rework
cost 655 — authoriser deploy path, dep-repo salt verification

rain-vats 0.1.7 consumes rain-factory 0.1.5, so the whole tree collapses to a
single rain-factory version. Drops the rain-factory 0.1.1 pin that rain-vats
0.1.6 dragged in, resolving the two-version conflict that broke the build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
script/BuildPointers.sol (1)

355-359: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Compute the clone codehash instead of hardcoding it.

STOX_PROD_AUTHORISER_V4_CLONE_CODEHASH is hardcoded to 0x2089950d3cc1112dd66a58adcfadeadc490b50053ac67be8bc676b4a2dcd1717. This is the EIP-1167 minimal proxy codehash, which is a function of the impl address. If V4_AUTHORISER_CLONE_IMPL changes but this codehash isn't updated, the upgrade script's cloneCodehash != LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE_CODEHASH check will fail in production.

Consider computing it as keccak256 abi.encodePacked of the EIP-1167 bytecode with V4_AUTHORISER_CLONE_IMPL embedded, so it stays consistent automatically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/BuildPointers.sol` around lines 355 - 359, Replace the hardcoded
STOX_PROD_AUTHORISER_V4_CLONE_CODEHASH generation in BuildPointers with a
computed keccak256 hash of the EIP-1167 minimal-proxy bytecode embedding
V4_AUTHORISER_CLONE_IMPL, using abi.encodePacked so the generated value stays
synchronized when the implementation address changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@remappings.txt`:
- Around line 7-14: Remove the unused rain-factory-0.1.1 remapping, and resolve
the rain-vats version mismatch by aligning the remapping and foundry.toml
dependency with the rain-vats-0.1.6 imports still used throughout src, script,
and test; keep the imports and declared dependency consistent.

In `@script/BuildPointers.sol`:
- Line 159: Update V4_AUTHORISER_CLONE_DEPLOYER to reference the existing
GEN_OWNER constant instead of duplicating its address, preserving the current
value while keeping both symbols synchronized.
- Line 157: Update BuildPointers and the buildContractPointers flow so
V4_AUTHORISER_CLONE_IMPL is validated against, or generated from,
LibProdDeployV4.STOX_OFFCHAIN_ASSET_RECEIPT_VAULT_AUTHORIZER_V1_0_1_1, ensuring
the hardcoded clone implementation cannot diverge from the generated pointer
address.

---

Outside diff comments:
In `@script/BuildPointers.sol`:
- Around line 355-359: Replace the hardcoded
STOX_PROD_AUTHORISER_V4_CLONE_CODEHASH generation in BuildPointers with a
computed keccak256 hash of the EIP-1167 minimal-proxy bytecode embedding
V4_AUTHORISER_CLONE_IMPL, using abi.encodePacked so the generated value stays
synchronized when the implementation address changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1e64f5fb-26b0-45ed-a3e6-af6cb7184d0f

📥 Commits

Reviewing files that changed from the base of the PR and between e45b728 and 43af1d4.

⛔ Files ignored due to path filters (2)
  • soldeer.lock is excluded by !**/*.lock
  • src/generated/LibProdDeployV4.sol is excluded by !**/generated/**
📒 Files selected for processing (17)
  • foundry.toml
  • remappings.txt
  • script/20260619-deploy-v4-authoriser-clone.s.sol
  • script/20260623-upgrade-receipt-vaults-to-v4.s.sol
  • script/BuildPointers.sol
  • src/concrete/StoxWrappedTokenVault.sol
  • src/concrete/authorize/StoxOffchainAssetReceiptVaultAuthorizerV1.sol
  • src/concrete/deploy/StoxWrappedTokenVaultBeaconSetDeployer.sol
  • test/script/20260619-deploy-v4-authoriser-clone.t.sol
  • test/src/concrete/StoxCorporateActionsFacet.authorizerIntegration.t.sol
  • test/src/concrete/StoxReceiptVault.setAuthorizerGuard.t.sol
  • test/src/concrete/StoxWrappedTokenVault.t.sol
  • test/src/concrete/StoxWrappedTokenVaultV1.prod.base.t.sol
  • test/src/concrete/authorize/StoxOffchainAssetReceiptVaultAuthorizerV1.initializeGuard.t.sol
  • test/src/concrete/authorize/StoxOffchainAssetReceiptVaultPaymentMintAuthorizerV1.corporateActionPairingGap.t.sol
  • test/src/concrete/integration/OrchestratorIntegrationTest.sol
  • test/src/concrete/upgrade/V3UpgradeShadowFork.t.sol

Comment thread remappings.txt
Comment thread script/BuildPointers.sol Outdated
Comment thread script/BuildPointers.sol Outdated
… cloneDeterministic

rain-vats 0.1.7 (on rain-factory 0.1.5) replaces 0.1.6 across all src/test/script
imports. rain-factory 0.1.5 is deterministic-only (no clone()), so the test
fixtures that spun up authorizer/vault instances via CloneFactory.clone() migrate
to cloneDeterministic(impl, data, bytes32(0)) — each uses a fresh single-use
factory, so bytes32(0) cannot collide. V3UpgradeShadowFork clones via the
deployed factory cast to ICloneableFactoryV3.

Bytecode-neutral: deploy addresses/codehashes unchanged, generated pointer libs
regenerate identically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
Reviewed 1f061b7: reject — closes #248 (retires #211) — rework (43af1d4, 1f061b7) fixed only the rain-factory version conflict; the prior reject grounds all remain at head 1f061b7: testAuthoriserV4ClonePlaceholder (LibProdDeployV4.t.sol:233) still asserts the pin is address(0) while the diff pins 0x9746… (sole CI failure; the codehash assert behind it is dead code) — its own message demands a real address+codehash assertion this PR still omits; stale placeholder docs persist in the diff's own files (StoxProdV4.t.sol:26-29; 20260623-upgrade script:102-106 'fails red today … still address(0)' now false; 20260619 script:74 says clone() not cloneDeterministic); body still has NO QA §8 evidence block. New nit: remappings.txt:6 dangling rain-factory-0.1.1 mapping. Verified sound: salt mirror == rain-factory 0.1.5 _effectiveSalt, impl pin == frozen pointer, fresh-factory fixtures collision-free, 0.1.7 bump bytecode-neutral (all pin suites green); deployer-encoded-address design point stays live for the human
cost 670 — authoriser deploy path, fixture migration sweep

@thedavidmeister thedavidmeister added the ai:close-candidate AI vetter: candidate to close label Jul 16, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:producer
Close-candidate: superseded-by-main: the V4 authoriser clone was deployed on Base and pinned by MERGED PR #252 (0x315b16faa6…, codehash-verified vs live) — this PR's up-front deterministic pin 0x9746… conflicts with that deployed reality, so its core premise is dead. Auxiliary rain-factory 0.1.5 / rain-vats 0.1.7 bump + cloneDeterministic fixture migration remain unabsorbed by main and can be re-scoped separately if wanted. Issue #248 flagged close-candidate on the same evidence.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

human:reject on the close-candidate — rework ordered. #292 makes deterministic clones an explicit goal, which un-moots this PR's premise but rescopes it to #292 step 1: rain-factory 0.1.5 bump + clone()→cloneDeterministic fixture migration. Pin stays the live 0x315b16… clone in this PR; the deterministic pin flip lands with the Base migration script (#292 step 3).

@thedavidmeister thedavidmeister removed ai:needs-work Needs rework — the producer's inbox (vetter verdict or human ruling) ai:close-candidate AI vetter: candidate to close labels Aug 4, 2026
Conflict resolution takes main wholesale for the deploy surface —
BuildPointers.sol, LibProdDeployV4.sol, the 20260619 clone script and its
test, V3UpgradeShadowFork — so every deployed pin (Base clone 0x315b16…,
Ethereum clone, all impl codehashes) stays exactly as main pinned them.
The up-front 0x9746… deterministic pin and the cloneDeterministic rewrite
of the executed clone script are dropped; #292 owns that as steps 2-3.

What survives on top of main:
- rain-factory 0.1.5 + rain-vats 0.1.7, import prefixes repointed
  repo-wide (including main's post-June scripts/tests still on 0.1.6/0.1.1).
- src/lib/LibNonceCloneFactory.sol: pins the nonce-based CloneFactory
  (0x444acC29…) the live clones were deployed through; rain-factory 0.1.5's
  LibCloneFactoryDeploy pins the deterministic-only factory which has no
  clone(address,bytes), so the clone script keeps targeting the factory it
  always used.
- Unit fixtures migrated clone() -> cloneDeterministic (0.1.5 concrete is
  deterministic-only); fork tests still exercise the live nonce factory.
- src/generated/candidate/ regenerated via BuildPointers + forge fmt;
  frozen 0_1_1/ and LibProdDeployV4 deployed pins byte-identical to main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thedavidmeister thedavidmeister changed the title feat(deploy): deterministic V4 authoriser clone, pinned up-front (proposal, closes #248) chore(deps): rain-factory 0.1.5 + rain-vats 0.1.7 — deterministic-clone toolchain (#292 step 1) Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
script/20260722-deploy-missing-tokens-ethereum.s.sol (2)

129-143: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject partially hydrated token rows.

entryClear is true only when all three addresses are zero. A row with one or two non-zero addresses is skipped. If every row is partial, _selectMissing() reverts NoMissingTokens() even though the table is not fully hydrated.

Accept only all-zero or all-non-zero rows. Revert on a partial row.

Proposed guard
+            bool entryHydrated = table[i].receipt != address(0) && table[i].receiptVault != address(0)
+                && table[i].wrappedTokenVault != address(0);
             bool entryClear = table[i].receipt == address(0) && table[i].receiptVault == address(0)
                 && table[i].wrappedTokenVault == address(0);
-            if (!entryClear) {
+            if (!entryClear && !entryHydrated) {
+                revert TokenTablePartiallyHydrated(i);
+            }
+            if (entryHydrated) {
                 continue;
             }

Declare TokenTablePartiallyHydrated(uint256 index) with the existing errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/20260722-deploy-missing-tokens-ethereum.s.sol` around lines 129 - 143,
Update the row-selection logic in _selectMissing so each token table row is
classified as either fully empty or fully hydrated: if any of receipt,
receiptVault, or wrappedTokenVault is non-zero while not all are non-zero,
revert with a newly declared TokenTablePartiallyHydrated(uint256 index) error;
continue selecting only all-zero rows and preserve the existing NoMissingTokens
behavior when none qualify.

210-214: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Verify the authorizer and ownership handoff state before logging success.

The script still needs the same readback check used by 20260706-deploy-tokens-ethereum.s.sol after setAuthorizer and transferOwnership. Call assertHandoffLanded(receiptVault, authoriser, safe) before logging the tuple, and add or share the helper and matching errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/20260722-deploy-missing-tokens-ethereum.s.sol` around lines 210 - 214,
After the setAuthorizer and transferOwnership calls, invoke
assertHandoffLanded(receiptVault, authoriser, safe) before logging the success
tuple. Add or reuse the matching helper and validation errors from
20260706-deploy-tokens-ethereum.s.sol so the authorizer and ownership readbacks
are verified consistently.
script/20260706-deploy-tokens-ethereum.s.sol (1)

140-148: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make the Ethereum deployment scripts resume-safe.

vm.startBroadcast() is not atomic, and broadcast mode does not roll back earlier successful calls when a later external call reverts. If a partial run leaves deployed tokens while productionTokensEthereum() is still unhydrated, a rerun can treat them as already deployed or still missing and deploy duplicate production tokens.

  • script/20260706-deploy-tokens-ethereum.s.sol#L140-L148: use durable resume state or on-chain detection instead of the static input pin table as the run-once guard.
  • script/20260706-deploy-tokens-ethereum.s.sol#L245-L271: make each iteration durable before the next, or require explicit reconciliation/partial-run handling, and add a failure-after-first-iteration test.
  • script/20260722-deploy-missing-tokens-ethereum.s.sol#L115-L118: do not use only the unhydrated pin table to decide whether token configs are missing.
  • script/20260722-deploy-missing-tokens-ethereum.s.sol#L168-L189: skip/reconcile already deployed tokens or abort when partial state is detected, and add a failure-after-first-iteration test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/20260706-deploy-tokens-ethereum.s.sol` around lines 140 - 148, Replace
the static pin-table-based guard in _assertNotAlreadyDeployed() at
script/20260706-deploy-tokens-ethereum.s.sol#L140-L148 with durable resume state
or on-chain detection that survives partial runs, so partial deployments cannot
be mistaken as completed runs. In the deployment loop at
script/20260706-deploy-tokens-ethereum.s.sol#L245-L271, make each iteration
durable before advancing to the next iteration, add explicit reconciliation or
partial-run handling, and add a test that verifies correct behavior when a
failure occurs after the first iteration completes. In
script/20260722-deploy-missing-tokens-ethereum.s.sol#L115-L118, replace the
check that relies only on the unhydrated pin table with on-chain detection to
determine whether token configs are missing. In the loop at
script/20260722-deploy-missing-tokens-ethereum.s.sol#L168-L189, skip
already-deployed tokens by detecting them on-chain, abort when partial
deployment state is detected, and add a test that validates correct behavior
when a failure occurs after the first iteration.
script/20260623-upgrade-receipt-vaults-to-v4.s.sol (1)

132-161: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Do not reference LibProdDeployV1 from this production script.

UpgradeReceiptVaultsToV4 reads live beacon addresses and rollback implementations from LibProdDeployV1 in BEACON, V1_IMPL, beacons(), and v1Impls(). This couples the migration to an obsolete deployment library and violates the repository versioning rule.

Expose the required historical V1 values through the current versioned deployment surface or a current-version migration library. Do not replace them with V4 addresses.

As per coding guidelines, production source contracts must use the current versioned LibProdDeploy* library and must not reference obsolete deployment versions such as LibProdDeployV1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/20260623-upgrade-receipt-vaults-to-v4.s.sol` around lines 132 - 161,
Remove all LibProdDeployV1 references from UpgradeReceiptVaultsToV4, including
BEACON, V1_IMPL, beacons(), and v1Impls(). Re-source the required historical V1
beacon and implementation addresses through the current versioned deployment
surface or a current-version migration library, preserving their existing values
and index alignment; do not substitute V4 addresses.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@script/20260623-upgrade-receipt-vaults-to-v4.s.sol`:
- Around line 181-183: Update the documentation comment for V4_AUTHORISER_CLONE
to avoid claiming the existing clone was deployed deterministically; describe it
as pinned in LibProdDeployV4 or precomputed by BuildPointers, while preserving
the deployed nonce-based clone reference.

In `@src/lib/LibNonceCloneFactory.sol`:
- Line 3: Update the pragma declaration in LibNonceCloneFactory to pin the
compiler exactly to Solidity 0.8.25 by replacing the caret version constraint
with an exact version constraint. Keep the existing pragma location and
formatting unchanged.

---

Outside diff comments:
In `@script/20260623-upgrade-receipt-vaults-to-v4.s.sol`:
- Around line 132-161: Remove all LibProdDeployV1 references from
UpgradeReceiptVaultsToV4, including BEACON, V1_IMPL, beacons(), and v1Impls().
Re-source the required historical V1 beacon and implementation addresses through
the current versioned deployment surface or a current-version migration library,
preserving their existing values and index alignment; do not substitute V4
addresses.

In `@script/20260706-deploy-tokens-ethereum.s.sol`:
- Around line 140-148: Replace the static pin-table-based guard in
_assertNotAlreadyDeployed() at
script/20260706-deploy-tokens-ethereum.s.sol#L140-L148 with durable resume state
or on-chain detection that survives partial runs, so partial deployments cannot
be mistaken as completed runs. In the deployment loop at
script/20260706-deploy-tokens-ethereum.s.sol#L245-L271, make each iteration
durable before advancing to the next iteration, add explicit reconciliation or
partial-run handling, and add a test that verifies correct behavior when a
failure occurs after the first iteration completes. In
script/20260722-deploy-missing-tokens-ethereum.s.sol#L115-L118, replace the
check that relies only on the unhydrated pin table with on-chain detection to
determine whether token configs are missing. In the loop at
script/20260722-deploy-missing-tokens-ethereum.s.sol#L168-L189, skip
already-deployed tokens by detecting them on-chain, abort when partial
deployment state is detected, and add a test that validates correct behavior
when a failure occurs after the first iteration.

In `@script/20260722-deploy-missing-tokens-ethereum.s.sol`:
- Around line 129-143: Update the row-selection logic in _selectMissing so each
token table row is classified as either fully empty or fully hydrated: if any of
receipt, receiptVault, or wrappedTokenVault is non-zero while not all are
non-zero, revert with a newly declared TokenTablePartiallyHydrated(uint256
index) error; continue selecting only all-zero rows and preserve the existing
NoMissingTokens behavior when none qualify.
- Around line 210-214: After the setAuthorizer and transferOwnership calls,
invoke assertHandoffLanded(receiptVault, authoriser, safe) before logging the
success tuple. Add or reuse the matching helper and validation errors from
20260706-deploy-tokens-ethereum.s.sol so the authorizer and ownership readbacks
are verified consistently.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7ab59688-a6ea-48d3-b8c7-acb75921dd1b

📥 Commits

Reviewing files that changed from the base of the PR and between 43af1d4 and 58c7b80.

📒 Files selected for processing (47)
  • foundry.toml
  • remappings.txt
  • script/20260619-deploy-v4-authoriser-clone.s.sol
  • script/20260623-upgrade-receipt-vaults-to-v4.s.sol
  • script/20260706-deploy-tokens-ethereum.s.sol
  • script/20260722-deploy-missing-tokens-ethereum.s.sol
  • script/20260722-swap-remaining-vault-authorisers.s.sol
  • script/20260722-swap-rklb-authoriser.s.sol
  • src/concrete/ST0xOrchestrator.sol
  • src/concrete/StoxCorporateActionsFacet.sol
  • src/concrete/StoxReceipt.sol
  • src/concrete/StoxReceiptVault.sol
  • src/concrete/authorize/StoxOffchainAssetReceiptVaultAuthorizerV1.sol
  • src/concrete/authorize/StoxOffchainAssetReceiptVaultPaymentMintAuthorizerV1.sol
  • src/concrete/deploy/StoxOffchainAssetReceiptVaultBeaconSetDeployer.sol
  • src/concrete/deploy/StoxUnifiedDeployer.sol
  • src/interface/IStoxUnifiedDeployerV1.sol
  • src/lib/LibNonceCloneFactory.sol
  • test/script/20260619-deploy-v4-authoriser-clone.t.sol
  • test/script/20260623-upgrade-receipt-vaults-to-v4.t.sol
  • test/script/20260722-swap-remaining-vault-authorisers.t.sol
  • test/script/20260722-swap-rklb-authoriser.t.sol
  • test/src/concrete/DelegatecallHarness.sol
  • test/src/concrete/MockAuthorizer.sol
  • test/src/concrete/MockVault.sol
  • test/src/concrete/PermissiveAuthorizer.sol
  • test/src/concrete/ST0xOrchestrator.t.sol
  • test/src/concrete/StoxCorporateActionsFacet.authorizerIntegration.t.sol
  • test/src/concrete/StoxCorporateActionsFacet.t.sol
  • test/src/concrete/StoxReceiptRebaseIntegrationTest.t.sol
  • test/src/concrete/StoxReceiptVault.setAuthorizerGuard.t.sol
  • test/src/concrete/StoxReceiptVaultFallbackRouting.t.sol
  • test/src/concrete/StoxWrappedTokenVault.t.sol
  • test/src/concrete/authorize/FailingSuperInitAuthorizer.sol
  • test/src/concrete/authorize/StoxOffchainAssetReceiptVaultAuthorizerV1.initializeGuard.t.sol
  • test/src/concrete/authorize/StoxOffchainAssetReceiptVaultPaymentMintAuthorizerV1.corporateActionPairingGap.t.sol
  • test/src/concrete/deploy/StoxProdV2.t.sol
  • test/src/concrete/deploy/StoxProdV4.t.sol
  • test/src/concrete/deploy/StoxUnifiedDeployer.newTokenAndWrapperVault.t.sol
  • test/src/concrete/deploy/StoxUnifiedDeployer.prod.base.t.sol
  • test/src/concrete/deploy/StoxUnifiedDeployer.t.sol
  • test/src/concrete/integration/BurnInfoAuditTrail.t.sol
  • test/src/concrete/integration/CertificationLapse.t.sol
  • test/src/concrete/integration/MintWithEcdsaSignature.t.sol
  • test/src/concrete/integration/OrchestratorIntegrationTest.sol
  • test/src/concrete/upgrade/V3UpgradeShadowFork.t.sol
  • test/src/lib/LibTokenInvariants.addresses.t.sol
💤 Files with no reviewable changes (1)
  • remappings.txt

Comment thread script/20260623-upgrade-receipt-vaults-to-v4.s.sol
Comment thread src/lib/LibNonceCloneFactory.sol
thedavidmeister and others added 2 commits August 4, 2026 17:19
…ehash

A codehash pin alone cannot recreate historical behavior once compiler or
optimizer drift makes the source unable to reproduce it; the embedded
runtime code can. CLONE_FACTORY_DEPLOYED_CODE is the live Base bytecode of
0x444acC29..., asserted in the 20260619 suite as
CLONE_FACTORY_DEPLOYED_CODEHASH == keccak256(CLONE_FACTORY_DEPLOYED_CODE),
the same shape the prod pins assert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both the branch's 'pinned deterministically up-front' and main's
'placeholder until deployed' wording predate the deployed, hydrated pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in + dated script

BuildPointers now emits the chain-invariant migration target (#292) into
LibProdDeployV4: the CREATE2 prediction against the 0.1.5 CloneFactory
(0x9746648e...) with its salt, deployer, EIP-1167 runtime bytecode and
codehash (#293 pin shape). Live pins are untouched and stay authoritative
until the migration.

script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol broadcasts
the clone at the target on any configured chain: pre-flights factory/impl
codehashes and the prediction, no-ops when the pinned clone already
occupies the target, initialises with the chain's token-owner Safe as
initialAdmin so the deployer never holds a role, and post-asserts landing
address, code shape, and the Safe's seven admin roles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thedavidmeister thedavidmeister changed the title chore(deps): rain-factory 0.1.5 + rain-vats 0.1.7 — deterministic-clone toolchain (#292 step 1) feat(deploy): deterministic-clone toolchain + deterministic V4 authoriser deploy capability (#292 step 1) Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol`:
- Around line 119-129: Validate the idempotent clone’s initialization state by
calling assertPostState(target, safe) after the matching codehash check and
before the no-op return in
script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol (lines 119-129).
Update test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol
(lines 132-141) so the no-op idempotency test uses an initialized clone rather
than etching only the runtime bytecode.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 74db6ab8-9f11-4787-99ef-1e13c7550454

📥 Commits

Reviewing files that changed from the base of the PR and between 58c7b80 and 851ce5b.

⛔ Files ignored due to path filters (1)
  • src/generated/LibProdDeployV4.sol is excluded by !**/generated/**
📒 Files selected for processing (6)
  • script/20260623-upgrade-receipt-vaults-to-v4.s.sol
  • script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol
  • script/BuildPointers.sol
  • src/lib/LibNonceCloneFactory.sol
  • test/script/20260619-deploy-v4-authoriser-clone.t.sol
  • test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol

Comment thread script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol Outdated
thedavidmeister and others added 2 commits August 4, 2026 18:22
….skip

The codehash cannot see initialization, so a same-address clone could have
been initialized with different admin data — the no-op path now runs the
same post-state assertion as a fresh deploy. The static gate also bans
vm.skip: the fork tests now assert the deployed-world branch instead of
skipping it, and an etched-but-uninitialized clone is a typed revert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e retired EOA

0x8E4bdeec... is the retired beacon-owner EOA. The CI deploy key resolves
to 0xE8c6eDE2..., the sender that created the live Base clone via the
nonce factory (tx 0x26519d1c9090e6236cbd6e9c7f5d6eee7cf633da3a6653742914
b3c17fe7d236). The factory namespaces CREATE2 salts by sender, so the
migration target moves to 0xE322d7D0d366cE0bddfACF6D25d11DdA1B13A86b.
A regression guard ties the pin to the creation-tx sender and rejects the
retired EOA.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sertion

The deploy key is a hot wallet: it deploys and holds no role, ever. New
LibAuthoriserInvariants.assertKeysHoldNoRoles iterates the pinned role
universe (13 expectedGrants ids + DEFAULT_ADMIN_ROLE) and reverts
DeployKeyHoldsRole if the hot deploy key (the generated deterministic
deployer pin) or the retired deploy EOA (BEACON_INITIAL_OWNER) holds any
of them. Composed into assertExpectedGrants so every per-chain prod fork
assertion enforces it, and called from the deterministic clone deploy's
post-state so it binds at broadcast time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
vet-protocol 4
lens source@ed81ec8a28dd2940a2a0271fd1111a40463bcb75, invocation UNOBSERVED (this run names no lens ledger ($RUN_LENS_LEDGER is unset))
Reviewed ed81ec8: reject — recorded OUTSIDE any vetter run (vetter cron disabled 2026-08-02, unvetted=0) — hand-vet at human direction, no cron produced this. The 2026-08-04 rework order is met technically, and I re-derived the deploy-critical math offline (pure-python keccak, no forge): the pin 0xE322d7D0 re-derives exactly from Clones.predictDeterministicAddress(impl 0x2ea0d35d, keccak256(abi.encode(0xE8c6eDE2, bytes32(0))), factory 0x7053c1D0), mirroring rain-factory 0.1.5 _effectiveSalt verbatim (sender-namespaced, so salt bytes32(0) is unsquattable); LibNonceCloneFactory's 1012-byte CLONE_FACTORY_DEPLOYED_CODE keccaks to 0xf21b813c AND is byte-identical to live Base code at 0x444acC29 (#293 shape real); live tx 0x26519d1c from=0xE8c6eDE2 confirms the deployer correction; the 0.1.5 factory is live on Base and absent on Ethereum (3 independent RPCs), so the typed fail-safe is real; hot + retired deploy keys hold 0 of 22 pinned (role,key) pairs on the live Base clone; live pin 0x315b16 byte-identical to main; the idempotent path now asserts initialized state (a differently-initialized same-address clone reverts SafeMissingAdminRole rather than passing silently); clone()->cloneDeterministic fixtures were migrated not deleted (0 file deletions), each on a fresh CloneFactory so salt 0 cannot collide. BLOCKED on two grounds, both already cited in the 2026-07-12 rejects and both still at head after seven commits: (1) the PR body carries NO ## QA evidence block (0 occurrences) — QA-GUIDE section 8 is mandatory; repair-qa-block plus an --allow-empty commit fixes it. (2) script/20260623-upgrade-receipt-vaults-to-v4.s.sol:105-109 still states as CURRENT behaviour that the clone-pin check "fails red today because STOX_PROD_AUTHORISER_V4_CLONE is still address(0)" — false at head (pin = 0x315b16, live and codehash-verified), and e2e17b1, the commit whose whole purpose was this wording, corrected the sibling docs at :36-39 and :182 in that same file and missed this block. NOT grounds, for the record: four PRE-EXISTING hand-rolled expectedGrants() loops (20260623:315, 20260722-swap-remaining:228, 20260722-rklb:200, test/script/20260706:160) bypass the new assertKeysHoldNoRoles, so "every prod assertion" holds for LibAuthoriserInvariants but not for those — out of footprint, new-issue material; run()'s broadcast branch is untested end-to-end (documented forge-test limitation, mirrors the 20260619 suite); sol-conventions exits 2 on a pre-existing pragma at test/src/concrete/integration/OrchestratorIntegrationTest.sol:3. sol-toolchain reports conflict for this checkout (rainix reusable @main vs github:rainlanguage/rainix#sol-shell in git-clean), so there is no single shell to name — I ran no forge locally and leaned on CI (5/5 green at ed81ec8) plus the offline derivations above.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:producer
QA-block repair: appended QA-GUIDE section 8's evidence block in the PR body via pr-review-report repair-qa-block. Every byte outside the ## QA section is unchanged.

claude added 2 commits August 5, 2026 10:35
The pre-flight description still said the clone-pin check fails because
STOX_PROD_AUTHORISER_V4_CLONE is address(0). The pin names a deployed
clone that carries the pinned EIP-1167 codehash and the full grant map,
so both the impl- and clone-side pre-flights pass. The sibling blocks in
this file already describe that state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two claims in this file described pins as unhydrated. The codehash pin
is a real value, as the deploy log's own comment a few lines up already
says, and both chains the clone-pin selector serves return a hydrated
pin — every other chain reverts. `_assertPostState` still re-derives the
expected codehash from the impl address, which is what keeps that check
independent of the pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol`:
- Around line 178-193: Update testRunRejectsUninitializedCloneAtTarget so its
etched target cannot inherit forked Safe role state: mock the first Safe
hasRole(...) call to return false, or clear the relevant AccessControl storage
before invoking script.run(). Preserve the expected SafeMissingAdminRole revert
assertion.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a484c79f-d1e1-4a08-845e-28875e9d53a0

📥 Commits

Reviewing files that changed from the base of the PR and between 851ce5b and fe098ce.

⛔ Files ignored due to path filters (1)
  • src/generated/LibProdDeployV4.sol is excluded by !**/generated/**
📒 Files selected for processing (7)
  • script/20260623-upgrade-receipt-vaults-to-v4.s.sol
  • script/20260804-deploy-deterministic-v4-authoriser-clone.s.sol
  • script/BuildPointers.sol
  • src/lib/LibAuthoriserInvariants.sol
  • test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol
  • test/src/lib/LibAuthoriserInvariants.t.sol
  • test/src/lib/LibAuthoriserInvariantsHarness.sol

Comment thread test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol
…hydrated

vm.etch replaces code but preserves storage, so after the real broadcast
lands the etched clone at the deterministic target inherits the live
initialized grants and run() correctly takes the idempotent path — the
uninitialized shape this test fabricates only exists while the target is
empty. Guard with the same tolerant no-live-code pattern the sibling tests
use; the hydrated target's post-state stays covered by
testRunNoOpsWhenTargetAlreadyHydrated and
testCloneFromPinnedDeployerLandsOnPin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Reviewed 9f774c8: APPROVE
Rulings-conformance: repo CLAUDE.md has no rulings section (verified by scan); checked against every ruling governing this work. (1) #292's ruled goal — deterministic, chain-invariant, pinned up-front, no placeholder→backfill: the pin 0xE322d7D0… re-derives exactly under an independent pure-python keccak CREATE2 derivation from (impl, deployer-namespaced salt, factory), all three inputs Zoltu/chain-invariant, and is pinned before any broadcast. (2) #292's two-step shape — this PR is exactly step 1 (toolchain, fixtures, nonce-factory pin, target pin, dated per-chain script); step 2 (grant replay from RoleGranted/RoleRevoked logs, sweep, setAuthorizer, pin flip) is untouched. (3) #293 pin shape — LibNonceCloneFactory and the deterministic target both embed full runtime bytecode with keccak==codehash asserted in tests; live-Base nonce factory verified byte-identical to the embedded 1012 bytes. (4) Rework order on the rejected close-candidate — live pins stay authoritative: the LibProdDeployV4 diff against main has zero deletion lines. (5) Authority enumerated from chain, not repo — the deploy-key-holds-nothing invariant is measured via hasRole on live clones and composed into every prod fork assertion; step 2's grant replay is specified from logs. (6) Adversarial mutation on every code PR — the QA block's five offline input-mutations each move the CREATE2 address off the pin (formula sensitivity confirmed by my independent re-derivation), and the delta commit 9f774c8 is a test-only tolerant guard verified 13/13 on live Base+Ethereum forks. The artifact obeys every one.

Delta since 99b8760: testRunRejectsUninitializedCloneAtTarget now skips once the target is hydrated (vm.etch preserves storage, so the uninitialized shape it fabricates only exists while the target is empty — post-broadcast it would false-fail, not false-pass); CodeRabbit thread resolved with the reasoning on record. Independent verification highlights: CREATE2 pin, EIP-1167 code and codehash all re-derive offline; 0.1.5 factory live on Base with the pinned hash and absent on Ethereum (the typed fail-safe is real); target empty on both chains; impl byte-identical on both; and the Ethereum clone address 0x66566cc9… holds a proxy to a DIFFERENT impl on Base — live nonce-drift, the exact disease this migration kills.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol (1)

272-285: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the factory codehash on Ethereum.

When the factory exists, this branch trusts any contract that implements predictDeterministicAddress. It does not compare factory.codehash with LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_CODEHASH. A compatible but incorrect factory can satisfy the prediction while the deployment script rejects it.

Proposed assertion
         } else {
+            assertEq(
+                factory.codehash,
+                LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_CODEHASH,
+                "factory codehash drift on Ethereum"
+            );
             address predicted = ICloneableFactoryV3(factory)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol` around
lines 272 - 285, In the factory-present branch of the test, add an assertion
that factory.codehash equals
LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_CODEHASH before trusting
predictDeterministicAddress. Keep the existing prediction and pinned-address
assertion unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol`:
- Around line 272-285: In the factory-present branch of the test, add an
assertion that factory.codehash equals
LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_CODEHASH before trusting
predictDeterministicAddress. Keep the existing prediction and pinned-address
assertion unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bffb95d4-93d5-4c0a-92fa-39e72fbc0f55

📥 Commits

Reviewing files that changed from the base of the PR and between fe098ce and 9f774c8.

📒 Files selected for processing (2)
  • script/20260619-deploy-v4-authoriser-clone.s.sol
  • test/script/20260804-deploy-deterministic-v4-authoriser-clone.t.sol

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Scope addition: the admin and the salt both need to change here, not after

This PR pins STOX_PROD_AUTHORISER_V4_CLONE_DETERMINISTIC = 0xE322d7D0…, derived through CloneFactory.cloneDeterministic, whose CREATE2 salt is keccak256(abi.encode(msg.sender, salt)). So the pinned address depends on …_DEPLOYER = 0xE8c6eDE2….

That is the same failure this PR is already working around — the body notes the pin moved off 0x9746… because that one was "predicted for the retired 0x8E4bdeec… EOA". The deployer is an identity baked into an address derivation, so the next key rotation moves it again.

Why this belongs in this PR rather than a follow-up: the fix changes the address derivation. Land this as-is and deploy 0xE322d7D0…, and the follow-up immediately supersedes it — a second deploy and a second grant-mirroring migration for the same contract. Doing it here is one migration instead of two.

The change

Two halves, and neither is safe alone:

  1. Resolve the initial admin from the address registry (Address registry: immutable root, mutable bytes32 bindings on Zoltu, plus a lib to read it and a post-deploy check rainlanguage/rain.deploy#25) by name, rather than StoxOffchainAssetReceiptVaultAuthorizerV1.initialize taking config.initialAdmin from caller-supplied data.
  2. Deploy through the open-salt clone variant (Open-salt deterministic clone variant: keep the deployer out of the address derivation rainlanguage/rain.factory#50), so no msg.sender enters the salt and the address stops depending on the deploy EOA.

Open salt alone reopens squatting — anyone can land on the address with their own data, and since _initializeClone makes clone-and-initialize atomic, first mover sets the admin permanently. Registry resolution alone leaves the deployer in the derivation. Together there is nothing for a squatter to vary: the admin comes from the registry, and the salt commits to the name, so a different name is a different address and a squatter's deploy produces exactly the intended contract.

Blocked on

Tracking issue with the full rationale: #295 — this PR should close it.

The live pins (STOX_PROD_AUTHORISER_V4_CLONE = 0x315b16…) are unaffected either way; existing clones keep their granted admin. This is about what gets deployed next.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
vet-protocol 4
lens source@9f774c8221dbaf33e36168746eac9440820bce39 + audit skill invoked at pr:250
Reviewed 9f774c8: needs-work — Refs #292 step 1: pragma convention defect in the diff's own changed file — test/src/concrete/integration/OrchestratorIntegrationTest.sol:3 pins pragma solidity =0.8.25; while the file declares abstract contract OrchestratorIntegrationTest (line 55); the rainlanguage convention floats library/abstract files (^0.8.25) — fix this one file per its kind, never mass-pin. Everything else reads sound: the 20260804 deterministic deploy seats the Safe as initialAdmin atomically so the hot key never enters the role graph, assertKeysHoldNoRoles is enforced on both broadcast and idempotent paths, every derived pin carries an offline re-derivation test, fixtures migrated to cloneDeterministic on fresh per-fixture factories, live pins are additions-only vs main, QA block holds against the tree.
cost 815 — deterministic authoriser deploy, 58-file dep sweep

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:needs-work Needs rework — the producer's inbox (vetter verdict or human ruling)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants