Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
CloneFactoryCloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 421457, ~: 371564)
AddressRegistryDeployPinsTest:testAddressRegistryDeploysToPinnedAddress() (gas: 185870)
AddressRegistryDeployPinsTest:testAddressRegistryPinsDeriveFromThisSource() (gas: 1430)
AddressRegistryGetTest:testGetNoGeneratedMappingGetter(bytes32) (runs: 2048, μ: 5460, ~: 5460)
AddressRegistryGetTest:testGetNoOtherEntryPoint(bytes4,bytes32) (runs: 2048, μ: 8746, ~: 8746)
AddressRegistryGetTest:testGetOpaqueNames(address) (runs: 2048, μ: 624044, ~: 624044)
AddressRegistryGetTest:testGetReturnsRegistered(bytes32,address) (runs: 2048, μ: 34917, ~: 34917)
AddressRegistryGetTest:testGetUnsetReverts(bytes32) (runs: 2048, μ: 10868, ~: 10868)
AddressRegistryRegisterTest:testRegisterDistinctNames(bytes32,bytes32,address,address) (runs: 2048, μ: 65290, ~: 65290)
AddressRegistryRegisterTest:testRegisterEvent(bytes32,address) (runs: 2048, μ: 36420, ~: 36420)
AddressRegistryRegisterTest:testRegisterNoEventOnRevert(address,bytes32,address) (runs: 2048, μ: 10763, ~: 10763)
AddressRegistryRegisterTest:testRegisterOnlyRoot(address,bytes32,address) (runs: 2048, μ: 9672, ~: 9672)
AddressRegistryRegisterTest:testRegisterOnlyRootWhenAlreadyBound(address,bytes32,address,address) (runs: 2048, μ: 36210, ~: 36210)
AddressRegistryRegisterTest:testRegisterWriteOnce(bytes32,address,address) (runs: 2048, μ: 36439, ~: 36439)
AddressRegistryRegisterTest:testRegisterWriteOnceSameAccount(bytes32,address) (runs: 2048, μ: 35973, ~: 35973)
AddressRegistryRegisterTest:testRegisterZeroAccount(bytes32) (runs: 2048, μ: 12750, ~: 12750)
CloneFactoryCloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 420846, ~: 349101)
CloneFactoryCloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 161266, ~: 161266)
CloneFactoryCloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 569724, ~: 482275)
CloneFactoryCloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 423089, ~: 372826)
CloneFactoryCloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 571796, ~: 437419)
CloneFactoryCloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 422475, ~: 350218)
CloneFactoryCloneDeterministicTest:testCloneDeterministicSaltIsAbiEncodeHash(address,bytes32,address) (runs: 2048, μ: 4686, ~: 4686)
CloneFactoryCloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 585442, ~: 485815)
CloneFactoryCloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 590582, ~: 440959)
CloneFactoryCloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 10699, ~: 10678)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_3_CreationDeploysToPinnedAddress() (gas: 240145)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_3_RuntimeHashesToBytecodeHash() (gas: 755)
Expand Down
41 changes: 36 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@ code in this repository.

## Project Overview

rain.factory.deploy is the **deployment** half of `rain.factory`: the concrete
`CloneFactory` contract plus its deployed address + codehash pins. The core
contract `CloneFactory` clones any contract implementing `ICloneableV2` (an
interface it imports from the `rain-factory` Soldeer package) and atomically
initializes it.
rain.factory.deploy holds Rain's Zoltu-deployed concrete contracts and their
deploy pins. Two unrelated contracts live here, sharing only that deploy model:

- `CloneFactory` — the **deployment** half of `rain.factory`: the concrete
contract plus its deployed address + codehash pins. It clones any contract
implementing `ICloneableV2` (an interface it imports from the `rain-factory`
Soldeer package) and atomically initializes it.
- `AddressRegistry` — the implementation of `IAddressRegistryV1`, which arrives
from the `rain-deploy` Soldeer package alongside `LibAddressRegistry` (the
reader) and the cross-network deploy gate. An immutable root authority binds a
`bytes32` name to an address once and forever; reading an unbound name
reverts. Its address and code hash are pinned in `rain-deploy`'s
`LibAddressRegistry`, not in a `src/generated/<tag>/` snapshot here — those
pins come from the current placeholder build, because `ADDRESS_REGISTRY_ROOT`
is a constant in the creation code and no snapshot should be frozen until a
human supplies the real root.

License: LicenseRef-DCL-1.0 (DecentraLicense). All source files must include
SPDX headers.
Expand Down Expand Up @@ -68,6 +79,14 @@ as the `rain-factory` Soldeer dependency, so they are read under
- `src/concrete/CloneFactory.sol` — The single concrete implementation of
`ICloneableFactoryV3`. Uses OpenZeppelin `Clones.cloneDeterministic()`; there
is no plain `clone()`.
- `src/concrete/AddressRegistry.sol` — The single concrete implementation of
`IAddressRegistryV1` (from the `rain-deploy` Soldeer package). Two functions,
`register` and `get`, and nothing else: adding rotation, removal, an admin
surface or a non-reverting reader would destroy the write-once property the
contract exists for. `ADDRESS_REGISTRY_ROOT` is a placeholder until a human
supplies the real root; because it is a constant in the creation code, it is
part of the contract's identity, and `rain-deploy`'s `LibAddressRegistry` pins
must be re-derived whenever it changes.
- `src/lib/LibCloneFactoryDeploy.sol` — Deterministic deployment address and
codehash constants (generated; aliases the current tag's
`src/generated/<tag>/` snapshot).
Expand Down Expand Up @@ -103,6 +122,18 @@ A deploy is a human-dispatched run of the `Manual sol artifacts` workflow
(`workflow_dispatch` → `rainix-manual-sol-artifacts`), never a merge and never
part of the release workflow.

`AddressRegistry` is not deployable yet: it has no suite in `script/Deploy.sol`
and no `src/generated/<tag>/` snapshot. Its root authority is a placeholder, and
the root is part of the creation code, so a snapshot frozen now would pin an
address nobody can use — and `src/generated/<tag>/` is append-only, so it could
not be corrected afterwards.

Its address and code hash are pinned in `rain-deploy`'s `LibAddressRegistry`,
derived from the same placeholder build.
`test/src/concrete/AddressRegistryDeployPins.t.sol` fails if this source and
those pins disagree, so supplying the real root means re-deriving them in the
same change.

## Releases and versioning

This is a **deploy repo**, not a library repo, so nothing publishes on merge:
Expand Down
44 changes: 34 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
# rain.factory.deploy

The **deployment** half of `rain.factory`: the concrete `CloneFactory` contract,
its deployed address + codehash pins (`LibCloneFactoryDeploy`), the frozen
per-tag deploy-pin snapshots under `src/generated/<tag>/`, and the deploy
script.

The **library** half — the `ICloneable*` interfaces — lives in
[`rain.factory`](https://github.com/rainlanguage/rain.factory) and is imported
here as the `rain-factory` Soldeer package. Consumers that need only the
interfaces depend on `rain-factory`; consumers that need the deployed
address/codehash pins depend on `rain-factory-deploy`.
Rain's Zoltu-deployed concrete contracts and their deploy pins. Two unrelated
contracts live here, sharing only that deploy model:

- **`CloneFactory`** — the **deployment** half of `rain.factory`: the concrete
contract, its deployed address + codehash pins (`LibCloneFactoryDeploy`), the
frozen per-tag deploy-pin snapshots under `src/generated/<tag>/`, and the
deploy script. The **library** half — the `ICloneable*` interfaces — lives in
[`rain.factory`](https://github.com/rainlanguage/rain.factory) and is imported
here as the `rain-factory` Soldeer package. Consumers that need only the
interfaces depend on `rain-factory`; consumers that need the deployed
address/codehash pins depend on `rain-factory-deploy`.
- **`AddressRegistry`** — the implementation of `IAddressRegistryV1`, which is
the same split: the interface, the library that reads a registered address,
and the cross-network deploy gate all live in
[`rain.deploy`](https://github.com/rainlanguage/rain.deploy) and arrive here
as the `rain-deploy` Soldeer package.

## `AddressRegistry`

An immutable root authority binds a `bytes32` name to an address, once, forever;
anyone reads a bound name; reading an unbound name reverts. There is no
rotation, no removal, no upgrade and no admin surface, because a binding that
can move is not worth checking at deploy time.

Names are opaque. Nothing here says how one is derived, and nothing here should.

`ADDRESS_REGISTRY_ROOT` in `src/concrete/AddressRegistry.sol` is currently a
**placeholder**. The root is a constant in the creation code, so it is part of
the contract's identity: changing it changes the deterministic deploy address
and code hash on every network. A human must supply the real root before this
contract is deployed anywhere or given a deploy-pin snapshot, and
`rain-deploy`'s `LibAddressRegistry` pins must be re-derived from the resulting
creation code at the same time. `AddressRegistryDeployPinsTest` fails if those
two ever disagree.

## Releases

Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ runs = 2048
forge-std = "1.16.1"
"@openzeppelin-contracts" = "5.6.1"
"rain-extrospection" = "0.1.1"
"rain-deploy" = "0.1.3"
"rain-deploy" = "0.1.6"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"rain-sol-codegen" = "0.1.0"
"rain-factory" = "0.1.5"

Expand Down
2 changes: 1 addition & 1 deletion script/BuildPointers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity =0.8.25;
import {Script} from "forge-std-1.16.1/src/Script.sol";
import {LibCodeGen} from "rain-sol-codegen-0.1.0/src/lib/LibCodeGen.sol";
import {LibFs} from "rain-sol-codegen-0.1.0/src/lib/LibFs.sol";
import {LibRainDeploy} from "rain-deploy-0.1.3/src/lib/LibRainDeploy.sol";
import {LibRainDeploy} from "rain-deploy-0.1.6/src/lib/LibRainDeploy.sol";
import {CloneFactory} from "../src/concrete/CloneFactory.sol";

/// @title BuildPointers
Expand Down
7 changes: 2 additions & 5 deletions script/Deploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity =0.8.25;

import {Script} from "forge-std-1.16.1/src/Script.sol";
import {CloneFactory} from "../src/concrete/CloneFactory.sol";
import {LibRainDeploy} from "rain-deploy-0.1.3/src/lib/LibRainDeploy.sol";
import {LibRainDeploy} from "rain-deploy-0.1.6/src/lib/LibRainDeploy.sol";
import {LibCloneFactoryDeploy} from "../src/lib/LibCloneFactoryDeploy.sol";

/// @dev Hash of the "clone-factory" deployment suite string.
Expand All @@ -13,8 +13,6 @@ bytes32 constant DEPLOYMENT_SUITE_CLONE_FACTORY = keccak256("clone-factory");
/// @title Deploy
/// @notice A script that deploys a CloneFactory.
contract Deploy is Script {
mapping(string => mapping(address => bytes32)) internal sDepCodeHashes;

function run() external {
uint256 deployerPrivateKey = vm.envUint("DEPLOYMENT_KEY");

Expand All @@ -28,8 +26,7 @@ contract Deploy is Script {
"src/concrete/CloneFactory.sol:CloneFactory",
LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_ADDRESS,
LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_CODEHASH,
new address[](0),
sDepCodeHashes
new address[](0)
);
} else {
revert(
Expand Down
65 changes: 65 additions & 0 deletions src/concrete/AddressRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {IAddressRegistryV1} from "rain-deploy-0.1.6/src/interface/IAddressRegistryV1.sol";

/// @dev PLACEHOLDER ROOT AUTHORITY. THIS IS NOT A REAL ROOT.
///
/// The only account that may bind a name. It is a compile-time constant, not
/// storage, so it can never be rotated, and it is part of the creation code, so
/// changing it changes the deterministic deploy address and code hash of
/// `AddressRegistry` on every network.
///
/// A human MUST replace this value with the intended root before any deploy-pin
/// snapshot is generated for this contract, and the pins in `rain-deploy`'s
/// `LibAddressRegistry` MUST be regenerated from the resulting creation code.
address constant ADDRESS_REGISTRY_ROOT = address(0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD);

/// @title AddressRegistry
/// @notice The whole of `IAddressRegistryV1`: an immutable root authority binds
/// a `bytes32` name that is unbound, anyone reads a name that is bound, and a
/// read of an unbound name reverts.
///
/// There is deliberately nothing else. No rotation, no removal, no upgrade, no
/// pause, no admin surface, and no reader that returns the zero address for an
/// unbound name. Every one of those would turn a binding from a constant back
/// into a value that can move, which is the single property the registry
/// exists to provide.
///
/// The storage mapping is `internal` rather than `public` for that reason: a
/// public mapping's generated getter answers an unbound name with the zero
/// address, which is exactly the silent failure `get` reverts to prevent.
contract AddressRegistry is IAddressRegistryV1 {
/// The bindings. Not `public`: the only reader is `get`, which reverts on an
/// unbound name. A name maps to the zero address if and only if it is
/// unbound, which is why `register` rejects the zero address.
mapping(bytes32 name => address account) internal sAddresses;

/// @inheritdoc IAddressRegistryV1
function register(bytes32 name, address account) external {
if (msg.sender != ADDRESS_REGISTRY_ROOT) {
revert NotRoot(msg.sender);
}
if (account == address(0)) {
revert ZeroAccount(name);
}
address registered = sAddresses[name];
// Write-once. Root has no more authority here than anyone else: an
// existing binding is never overwritten, not even with the same
// address.
if (registered != address(0)) {
revert NameAlreadyRegistered(name, registered);
}
sAddresses[name] = account;
emit Register(name, account);
}

/// @inheritdoc IAddressRegistryV1
function get(bytes32 name) external view returns (address account) {
account = sAddresses[name];
if (account == address(0)) {
revert NameNotRegistered(name);
}
}
}
40 changes: 40 additions & 0 deletions test/src/concrete/AddressRegistryDeployPins.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {Test} from "forge-std-1.16.1/src/Test.sol";

import {LibRainDeploy} from "rain-deploy-0.1.6/src/lib/LibRainDeploy.sol";
import {LibAddressRegistry} from "rain-deploy-0.1.6/src/lib/LibAddressRegistry.sol";
import {AddressRegistry} from "../../../src/concrete/AddressRegistry.sol";

/// @title AddressRegistryDeployPinsTest
/// @notice `rain-deploy`'s `LibAddressRegistry` pins the deterministic address
/// and code hash of the `AddressRegistry` in this repo, and consumers resolve
/// names through those pins. The root authority is a constant in this contract's
/// creation code, so changing it moves both pins; this suite is what makes that
/// loud instead of silent.
contract AddressRegistryDeployPinsTest is Test {
/// The pins MUST be derivable from this source without deploying anything:
/// the Zoltu factory is `CREATE2` over its calldata with a zero salt, so the
/// address is a pure function of the creation code, and the code hash is
/// `keccak256` of the runtime code that creation code leaves behind.
function testAddressRegistryPinsDeriveFromThisSource() external pure {
assertEq(LibRainDeploy.zoltuAddress(type(AddressRegistry).creationCode), LibAddressRegistry.ADDRESS_REGISTRY);
assertEq(keccak256(type(AddressRegistry).runtimeCode), LibAddressRegistry.ADDRESS_REGISTRY_CODEHASH);
}

/// Actually deploying this contract's creation code through the Zoltu
/// factory MUST land at the pinned address with the pinned code hash, so the
/// derivation is checked against the factory rather than only against
/// itself.
function testAddressRegistryDeploysToPinnedAddress() external {
LibRainDeploy.etchZoltuFactory(vm);

address deployed = LibRainDeploy.deployZoltu(type(AddressRegistry).creationCode);

assertEq(deployed, LibAddressRegistry.ADDRESS_REGISTRY);
assertEq(deployed.codehash, LibAddressRegistry.ADDRESS_REGISTRY_CODEHASH);
assertEq(keccak256(deployed.code), LibAddressRegistry.ADDRESS_REGISTRY_CODEHASH);
}
}
75 changes: 75 additions & 0 deletions test/src/concrete/AddressRegistryGet.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {Test} from "forge-std-1.16.1/src/Test.sol";

import {IAddressRegistryV1} from "rain-deploy-0.1.6/src/interface/IAddressRegistryV1.sol";
import {AddressRegistry, ADDRESS_REGISTRY_ROOT} from "../../../src/concrete/AddressRegistry.sol";

/// @title AddressRegistryGetTest
/// @notice A test suite for `AddressRegistry.get`: it answers a bound name with
/// its address and an unbound name with a revert, and it is the only reader.
contract AddressRegistryGetTest is Test {
/// The registry under test. Stateful, so a fresh one per test.
AddressRegistry internal sRegistry;

function setUp() external {
sRegistry = new AddressRegistry();
}

/// A read of an unbound name reverts rather than returning the zero
/// address, so a caller cannot proceed on a name nobody bound by forgetting
/// to check.
function testGetUnsetReverts(bytes32 name) external {
vm.expectRevert(abi.encodeWithSelector(IAddressRegistryV1.NameNotRegistered.selector, name));
sRegistry.get(name);
}

/// A read of a bound name returns exactly what was bound, and reading does
/// not consume or alter the binding.
function testGetReturnsRegistered(bytes32 name, address account) external {
vm.assume(account != address(0));

vm.prank(ADDRESS_REGISTRY_ROOT);
sRegistry.register(name, account);

assertEq(sRegistry.get(name), account);
assertEq(sRegistry.get(name), account);
}

/// Names are opaque: nothing about a name's bytes changes how it is stored
/// or read, including names a string-hashing convention would never
/// produce.
function testGetOpaqueNames(address account) external {
vm.assume(account != address(0));

bytes32[3] memory names = [bytes32(0), bytes32(uint256(1)), bytes32(type(uint256).max)];
for (uint256 i = 0; i < names.length; i++) {
AddressRegistry registry = new AddressRegistry();
vm.prank(ADDRESS_REGISTRY_ROOT);
registry.register(names[i], account);
assertEq(registry.get(names[i]), account);
}
}

/// `get` is the only reader. The bindings mapping is not `public`, so the
/// getter a `public` mapping would generate — which answers an unbound name
/// with the zero address, the exact silent failure `get` reverts to prevent
/// — does not exist.
function testGetNoGeneratedMappingGetter(bytes32 name) external {
(bool success,) = address(sRegistry).call(abi.encodeWithSignature("sAddresses(bytes32)", name));
assertFalse(success);
}

/// There is no other entry point at all: no fallback, no receive, and
/// nothing beyond the two `IAddressRegistryV1` functions, so an unknown
/// selector reverts instead of being silently absorbed.
function testGetNoOtherEntryPoint(bytes4 selector, bytes32 name) external {
vm.assume(selector != IAddressRegistryV1.get.selector);
vm.assume(selector != IAddressRegistryV1.register.selector);

(bool success,) = address(sRegistry).call(abi.encodeWithSelector(selector, name, address(this)));
assertFalse(success);
}
}
Loading
Loading