Skip to content

AddressRegistry: the write-once IAddressRegistryV1 concrete - #7

Open
thedavidmeister wants to merge 2 commits into
mainfrom
2026-08-08-address-registry-concrete
Open

AddressRegistry: the write-once IAddressRegistryV1 concrete#7
thedavidmeister wants to merge 2 commits into
mainfrom
2026-08-08-address-registry-concrete

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The concrete half of
rainlanguage/rain.deploy#25. The interface, the
reader library and the cross-network deploy gate are in
rainlanguage/rain.deploy#26; this is the contract that
implements the interface. Merging both completes the issue.

Blocked on rain.deploy#26

This imports IAddressRegistryV1 from rain-deploy 0.1.6, which is not
published yet — rain.deploy's [package].version is the next, unpublished
version, and merging
rainlanguage/rain.deploy#26 is what autopublishes it
(latest on the registry today is 0.1.5).

So until that merges:

  • CI here is red at forge soldeer install, because the version does not exist.
  • soldeer.lock still carries rain-deploy 0.1.3; its 0.1.6 entry needs the
    published artifact's checksum, which cannot be fabricated. Once 0.1.6 is on
    the registry, forge soldeer install regenerates it and the result wants
    committing here.

The interface is deliberately not vendored or re-declared locally to dodge
that: the whole point of the split is that one declaration lives in the library
repo and the concrete implements it.

If the merge of rain.deploy#26 publishes something other than 0.1.6 (i.e. if
another PR lands there first), the version pin and the four import prefixes here
move with it.

The root authority is a placeholder — a human must supply it

ADDRESS_REGISTRY_ROOT in src/concrete/AddressRegistry.sol is
0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD. I do not have the real root and
will not invent one.

The root is a constant in the creation code, so it is part of the contract's
identity: changing it changes the deterministic Zoltu address and the code hash
on every network. Consequently:

  • No deploy-pin snapshot is generated in this PR. No
    src/generated/<tag>/AddressRegistry.pointers.sol, no pin lib, no
    Deploy.sol suite. Generating one now would freeze an address nobody can use,
    into a directory that CI enforces as append-only.
  • rain-deploy's LibAddressRegistry.ADDRESS_REGISTRY /
    ADDRESS_REGISTRY_CODEHASH are derived from the placeholder build and must be
    re-derived in the same change that supplies the real root.
  • AddressRegistryDeployPinsTest here fails the moment those two disagree, so
    the two repos cannot drift apart silently. Changing only the root and pushing
    turns this repo red until the pins follow — that is the intended forcing
    function, demonstrated as a mutation below.

The contract

An immutable root binds an opaque bytes32 name to an address, once; nothing,
root included, can change one after; reading an unbound name reverts. Two
functions, four errors, one event, and nothing else — no rotation, no removal,
no upgrade, no pause, no admin surface, and no second reader.

Two details that are not decoration:

  • register rejects the zero address. An unbound name reads as zero
    internally, so binding zero would produce a name that is both bound and
    unreadable — and that register would accept a second time. Without this,
    write-once is violable.
  • The bindings mapping is internal, not public. A public mapping's
    generated getter answers an unbound name with the zero address, which is
    exactly the silent failure the reverting get exists to prevent.

Also in this PR, and why

  • The repo now holds two unrelated concretes, so README and CLAUDE.md no
    longer describe it as only "the deployment half of rain.factory". The
    GitHub repo description still says that and wants updating too — I cannot
    change it.
    Suggested: "Rain's Zoltu-deployed concrete contracts and their
    deploy pins."
  • rain-deploy 0.1.3 → 0.1.6. Required — soldeer resolves one version per
    package, so the interface has to come from the same version LibRainDeploy
    does. The bump also drops deployAndBroadcast's trailing dependency
    code-hash mapping, which no longer exists upstream, so script/Deploy.sol
    loses that argument and its now-unused sDepCodeHashes field. Behaviour for
    CloneFactory is unchanged: it passed an empty dependency list already.
  • .gas-snapshot regenerated for the new tests.

QA

Discriminating tests

test/src/concrete/AddressRegistryRegister.t.sol (8) —
testRegisterOnlyRoot, testRegisterOnlyRootWhenAlreadyBound (authority is
checked even when the name is already bound), testRegisterWriteOnce,
testRegisterWriteOnceSameAccount (a no-op rebind is not a special case that
slips through), testRegisterZeroAccount (and that the name stays unbound
after), testRegisterDistinctNames, testRegisterEvent (topics and empty data,
so the indexing is pinned), testRegisterNoEventOnRevert.

test/src/concrete/AddressRegistryGet.t.sol (5) — testGetUnsetReverts,
testGetReturnsRegistered (twice, so reading does not consume),
testGetOpaqueNames (bytes32(0), 1, type(uint256).max — names a
string-hashing convention would never produce),
testGetNoGeneratedMappingGetter, testGetNoOtherEntryPoint (no fallback, no
receive, nothing outside the two interface selectors).

test/src/concrete/AddressRegistryDeployPins.t.sol (2) — the cross-repo gate:
the address LibRainDeploy.zoltuAddress derives from this source and the code
hash of its runtime code MUST equal rain-deploy's pins, and actually deploying
the creation code through the etched Zoltu factory MUST land there.

Mutations applied, and what killed each

Applied to the committed tree, one at a time, whole suite run, tree restored.
Baseline green. Killers below list only the behavioural test — every mutation
also trips the two pin tests, because any source change moves the bytecode,
which is itself the point of those two.

mutation killed by
register: root check removed testRegisterOnlyRoot, testRegisterOnlyRootWhenAlreadyBound, testRegisterNoEventOnRevert
register: root check inverted (!===) all eight register tests + testGetReturnsRegistered, testGetOpaqueNames
register: zero-address check removed testRegisterZeroAccount
register: write-once check removed testRegisterWriteOnce, testRegisterWriteOnceSameAccount
register: write-once carve-out so an identical rebind is allowed testRegisterWriteOnce, testRegisterWriteOnceSameAccount
register: Register not emitted testRegisterEvent
register: Register emitted with msg.sender instead of account testRegisterEvent
get: unbound name returns zero instead of reverting testGetUnsetReverts, testRegisterDistinctNames, testRegisterZeroAccount
bindings mapping made public testGetNoGeneratedMappingGetter, testGetNoOtherEntryPoint
root constant changed to a different placeholder testAddressRegistryPinsDeriveFromThisSource, testAddressRegistryDeploysToPinnedAddress

No survivors.

Oracle

The issue is the oracle, not the code: immutable root, write-once
bytes32 => address, revert on unset, nothing else. The pins have an
independent second oracle — the Zoltu factory itself, deployed to and read back,
rather than a recomputation of the same formula the library uses.

Category check

The issue's motivating example is initial ownership, but it says the problem is
not owner-specific, and nothing here is: the key is an opaque bytes32, the
value is an address, and the contract has no notion of an owner, a role or an
initializer. The "no second reader" requirement is covered as a property (no
entry point outside the two interface selectors) rather than only as the one
named instance (the generated mapping getter). Every error the contract can
revert with has a test, and every check has a test for the case where it is the
first check to fire.

n/a

  • Screenshot — n/a, no GUI.
  • Green CI on this PR — n/a until rain.deploy#26 merges and autopublishes
    rain-deploy 0.1.6, as described above. Locally, with 0.1.6 materialised in
    dependencies/ (gitignored) from that PR's branch, all 30 non-fork tests pass,
    forge fmt --check, slither . (0 findings), reuse lint and
    rainix-sol-single-contract are clean. The five LibCloneFactoryDeployProdTest
    fork tests need the RPC secrets CI has.
  • A live deployment of AddressRegistry — n/a, it cannot be deployed until
    the real root is supplied.

Summary by CodeRabbit

  • New Features

    • Added an address registry for write-once name-to-address bindings.
    • Restricted registrations to the designated root authority and rejected invalid or duplicate entries.
    • Added read access for registered names with clear handling for unregistered names.
    • Added deterministic deployment support and validation for the address registry.
  • Documentation

    • Expanded project documentation with address registry behavior, deployment considerations, and validation guidance.
  • Tests

    • Added coverage for registration, lookup, access control, events, deployment pins, and gas measurements.

…ncrete

An immutable root authority binds an opaque `bytes32` name to an address, once;
nothing, root included, can change one after; and reading an unbound name
reverts rather than answering with the zero address, so no caller has to
remember to check.

That is the whole contract. No rotation, no removal, no upgrade, no admin
surface and no second reader — each of those turns a binding from a constant
back into a value that can move, which is the one property the registry exists
to provide, and the property that makes a deploy-time check of a binding worth
anything.

`ADDRESS_REGISTRY_ROOT` is a PLACEHOLDER. The root is a constant in the
creation code, so it is part of the contract's identity: changing it moves the
deterministic deploy address and code hash on every network. No deploy-pin
snapshot, deploy suite or pin lib is generated here for that reason, and
`AddressRegistryDeployPinsTest` fails if this source and `rain-deploy`'s
`LibAddressRegistry` pins ever disagree.

The interface comes from `rain-deploy`, which the bump from 0.1.3 to 0.1.6
brings in. That bump also drops `deployAndBroadcast`'s trailing dependency
code-hash mapping, which no longer exists upstream.
@thedavidmeister thedavidmeister self-assigned this Aug 8, 2026
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b4bc2231-4983-44ae-87e5-39c8133ceeb6

📥 Commits

Reviewing files that changed from the base of the PR and between c48170a and 1f4cc0a.

📒 Files selected for processing (1)
  • CLAUDE.md

Walkthrough

Added AddressRegistry with immutable root-controlled bindings, tests, deterministic deployment checks, updated deployment tooling, dependency versions, gas snapshots, and repository documentation.

Changes

AddressRegistry implementation and deployment

Layer / File(s) Summary
Registry contract and behavioral validation
src/concrete/AddressRegistry.sol, test/src/concrete/AddressRegistry*.t.sol
Added root-only, write-once bytes32 name registration. Added validation, events, lookup behavior, and entry-point tests.
Deterministic deployment pin validation
test/src/concrete/AddressRegistryDeployPins.t.sol, .gas-snapshot
Added pinned address and runtime code hash checks for derivation and factory deployment. Added AddressRegistry gas snapshots.
Deployment tooling and dependency alignment
foundry.toml, script/*.sol, test/src/lib/*.t.sol, .gas-snapshot
Updated rain-deploy to 0.1.6, removed the deployment-code-hash mapping, and updated CloneFactory deployment measurements.
Repository architecture and deployment documentation
CLAUDE.md, README.md
Documented AddressRegistry behavior, dependency sources, placeholder root authority, deployment state, and pin-generation requirements.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • rainlanguage/rain.deploy#25 — Covers the immutable-root, write-once bytes32 address registry implemented and tested in this PR.

Possibly related PRs

🚥 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 identifies the new AddressRegistry concrete implementation and its write-once behavior.
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 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-08-08-address-registry-concrete

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.

@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

🤖 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 `@CLAUDE.md`:
- Around line 8-21: The deployment documentation must distinguish approved
production artifacts from test deployments and external rain-deploy pins. In
CLAUDE.md lines 8-21, state that this repository has no approved local
production snapshot yet, while LibAddressRegistry pins and pin tests already
exist. In CLAUDE.md lines 123-128, state that AddressRegistry is excluded from
production script/Deploy.sol and has no approved local generated snapshot; do
not characterize it as lacking deployment or pin support entirely.

In `@foundry.toml`:
- Line 37: Revert the rain-deploy version in foundry.toml to the released 0.1.3
version and keep soldeer.lock consistent; do not use 0.1.6 until it is
officially published.
🪄 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: 5a3617f4-e1fc-4ee3-9540-3296f7f04694

📥 Commits

Reviewing files that changed from the base of the PR and between 6173d3e and c48170a.

📒 Files selected for processing (13)
  • .gas-snapshot
  • CLAUDE.md
  • README.md
  • foundry.toml
  • script/BuildPointers.sol
  • script/Deploy.sol
  • src/concrete/AddressRegistry.sol
  • test/src/concrete/AddressRegistryDeployPins.t.sol
  • test/src/concrete/AddressRegistryGet.t.sol
  • test/src/concrete/AddressRegistryRegister.t.sol
  • test/src/lib/LibCloneFactoryDeploy.t.sol
  • test/src/lib/LibCloneFactoryDeployProd.t.sol
  • test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol

Comment thread CLAUDE.md Outdated
Comment thread foundry.toml
…repo

Said it had 'no deploy pins' and 'no pin lib', which is wrong: rain-deploy's
LibAddressRegistry pins its address and code hash, and AddressRegistryDeployPins
tests them here. What it has none of is a src/generated/<tag>/ snapshot and a
Deploy.sol suite, and the reason those must wait is that the directory is
append-only, so a placeholder-derived snapshot could never be corrected.
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Superseded by rainlanguage/rain.deploy#26. Leaving this open rather than closing it — that call is the human's.

rain.deploy is now treated as a concrete repo for publishing purposes and moves to manual sol-v* tag publish, which matches its *.deploy name. AddressRegistry moved into it, so the interface, the concrete, the reader lib and the deploy-time check all live in one repo. Nothing from this PR is lost — the contract, its tests and the pin tests are all in rain.deploy#26, with two design changes on top:

  • Bindings are mutable. Write-once could not express an ordinary owning-Safe rotation: the name is in the consumer's creation code, so a name welded to one address forever forces a new name, new creation code and a new deterministic address. Root may now re-register. The immutable root, the revert on unset and the zero-address rejection all stand.
  • The cross-network check moved after the deploy. A pre-deploy check against a mutable registry is TOCTOU. Verification now reads what the deployed contract already snapshotted in its constructor, which is settled state.

Two problems this PR had are removed by the move rather than reasoned around:

  • No soldeer cycle. Nothing depends on rain-factory-deploy for the pins any more.
  • No compiler-settings divergence. The address and codehash are a function of the creation code, which is a function of the settings that compiled it. Those settings belong to whichever repo owns the concrete, so deriving the constants in one repo and compiling the contract in another could silently diverge. Now both are in rain.deploy, which also pins solc/optimizer/evm_version exactly so the pins cannot move under a compiler default change.

The one thing here that does not carry over and may be worth keeping is the rain-deploy 0.1.30.1.6 bump on this branch, which drops deployAndBroadcast's trailing dependency code-hash mapping (removed upstream) from script/Deploy.sol. That is an unrelated, independently useful upgrade if you want it as its own PR.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:producer
Blocked-on: All three red checks (static, legal, test) fail at ONE shared step, not three causes: every rainix-sol job runs forge soldeer install (gated on soldeer.lock existing) BEFORE its actual check command, so none of slither / forge fmt / rainix-sol-single-contract / reuse lint / forge test ever executes.

Reproduced verbatim in this checkout's own CI toolchain (nix develop github:rainlanguage/rainix/53e96a7d0a97d7c7c75c3b2412521324776fdac6#sol-shell -c forge soldeer install):

Error: Failed to run soldeer: error during remappings operation: dependency not found: rain-deploy~0.1.6

Ground truth, read off the soldeer registry rather than the PR prose: the latest published rain-deploy is 0.1.5 (2026-07-30). 0.1.6 does not exist. foundry.toml pins rain-deploy = 0.1.6; soldeer.lock still carries 0.1.3.

No code fix exists on this branch. A downgrade to 0.1.5 is not available: rain.deploy at tag sol-v0.1.5 AND at main today ships only src/lib/LibRainDeploy.sol -- there is no IAddressRegistryV1 and no LibAddressRegistry in any published version. Both symbols exist only on rain.deploy#26's branch, which is still OPEN and unmerged. Vendoring or re-declaring the interface locally is the design this split exists to prevent, so it is not a repair either.

This is NOT the deploy-pin / migration shape despite the .deploy repo: no prod-pin, testProdDeploy* or deploy-pinned-constant check is red. AddressRegistryDeployPins.t.sol never runs at all. Nothing here is deployable and no deploy is wanted.

This is also NOT environmental: the registry is up and answered correctly; it simply does not hold the requested version.

Clearance: merging rain.deploy#26 autopublishes rain-deploy 0.1.6. Then forge soldeer install regenerates soldeer.lock's 0.1.6 entry (checksum cannot be fabricated before publish) and that regenerated lock wants committing here. If something other than 0.1.6 publishes, the foundry.toml pin and the four versioned import prefixes move with it.

SECOND, INDEPENDENT GATE once the above clears -- human judgement, not machine work: ADDRESS_REGISTRY_ROOT in src/concrete/AddressRegistry.sol is the placeholder 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD. It is a constant in the creation code, so it is part of the contract's deterministic identity on every network; a human must supply the real root, and rain-deploy's LibAddressRegistry pins must be re-derived in that same change.
blocked-by rainlanguage/rain.deploy#26

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

Labels

ai:blocked-on AI producer: blocked on a dependency PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant