diff --git a/.gas-snapshot b/.gas-snapshot index 59833c5..2472f50 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -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) diff --git a/CLAUDE.md b/CLAUDE.md index 4c277ed..4984a64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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//` 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. @@ -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//` snapshot). @@ -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//` 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//` 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: diff --git a/README.md b/README.md index 9e5cdbc..e8512b9 100644 --- a/README.md +++ b/README.md @@ -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//`, 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//`, 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 diff --git a/foundry.toml b/foundry.toml index ab1ebb2..48b6053 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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" "rain-sol-codegen" = "0.1.0" "rain-factory" = "0.1.5" diff --git a/script/BuildPointers.sol b/script/BuildPointers.sol index e108599..f9a0cd4 100644 --- a/script/BuildPointers.sol +++ b/script/BuildPointers.sol @@ -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 diff --git a/script/Deploy.sol b/script/Deploy.sol index eb7a3d4..9554f35 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -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. @@ -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"); @@ -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( diff --git a/src/concrete/AddressRegistry.sol b/src/concrete/AddressRegistry.sol new file mode 100644 index 0000000..624e628 --- /dev/null +++ b/src/concrete/AddressRegistry.sol @@ -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); + } + } +} diff --git a/test/src/concrete/AddressRegistryDeployPins.t.sol b/test/src/concrete/AddressRegistryDeployPins.t.sol new file mode 100644 index 0000000..e1db2e3 --- /dev/null +++ b/test/src/concrete/AddressRegistryDeployPins.t.sol @@ -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); + } +} diff --git a/test/src/concrete/AddressRegistryGet.t.sol b/test/src/concrete/AddressRegistryGet.t.sol new file mode 100644 index 0000000..73848aa --- /dev/null +++ b/test/src/concrete/AddressRegistryGet.t.sol @@ -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); + } +} diff --git a/test/src/concrete/AddressRegistryRegister.t.sol b/test/src/concrete/AddressRegistryRegister.t.sol new file mode 100644 index 0000000..7ecdd1d --- /dev/null +++ b/test/src/concrete/AddressRegistryRegister.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test, Vm} 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 AddressRegistryRegisterTest +/// @notice A test suite for `AddressRegistry.register`: who may bind a name, +/// and the write-once property that makes every binding a constant. +contract AddressRegistryRegisterTest is Test { + /// The registry under test. Stateful, so a fresh one per test. + AddressRegistry internal sRegistry; + + function setUp() external { + sRegistry = new AddressRegistry(); + } + + /// Only root may bind a name. Checked before the zero-address check, so a + /// non-root caller is rejected as `NotRoot` whatever it passes. + function testRegisterOnlyRoot(address sender, bytes32 name, address account) external { + vm.assume(sender != ADDRESS_REGISTRY_ROOT); + + vm.expectRevert(abi.encodeWithSelector(IAddressRegistryV1.NotRoot.selector, sender)); + vm.prank(sender); + sRegistry.register(name, account); + } + + /// A non-root caller cannot bind a name that is already bound either: the + /// authority check precedes the write-once check, and neither path lets a + /// binding move. + function testRegisterOnlyRootWhenAlreadyBound(address sender, bytes32 name, address bound, address account) + external + { + vm.assume(sender != ADDRESS_REGISTRY_ROOT); + vm.assume(bound != address(0)); + + vm.prank(ADDRESS_REGISTRY_ROOT); + sRegistry.register(name, bound); + + vm.expectRevert(abi.encodeWithSelector(IAddressRegistryV1.NotRoot.selector, sender)); + vm.prank(sender); + sRegistry.register(name, account); + + assertEq(sRegistry.get(name), bound); + } + + /// A bound name can never be rebound, including by root. The binding that + /// is already there survives. + function testRegisterWriteOnce(bytes32 name, address bound, address account) external { + vm.assume(bound != address(0)); + vm.assume(account != address(0)); + + vm.prank(ADDRESS_REGISTRY_ROOT); + sRegistry.register(name, bound); + + vm.expectRevert(abi.encodeWithSelector(IAddressRegistryV1.NameAlreadyRegistered.selector, name, bound)); + vm.prank(ADDRESS_REGISTRY_ROOT); + sRegistry.register(name, account); + + assertEq(sRegistry.get(name), bound); + } + + /// Rebinding a name to the address it is already bound to is rejected too. + /// Write-once is a property of the name, not of the value changing, so a + /// no-op rebind is not a special case that slips through. + function testRegisterWriteOnceSameAccount(bytes32 name, address account) external { + vm.assume(account != address(0)); + + vm.prank(ADDRESS_REGISTRY_ROOT); + sRegistry.register(name, account); + + vm.expectRevert(abi.encodeWithSelector(IAddressRegistryV1.NameAlreadyRegistered.selector, name, account)); + vm.prank(ADDRESS_REGISTRY_ROOT); + sRegistry.register(name, account); + + assertEq(sRegistry.get(name), account); + } + + /// The zero address is rejected. An unbound name reads as the zero address + /// internally, so binding it would produce a name that is bound but + /// unreadable — and that `register` would happily accept a second time, + /// destroying write-once. + function testRegisterZeroAccount(bytes32 name) external { + vm.expectRevert(abi.encodeWithSelector(IAddressRegistryV1.ZeroAccount.selector, name)); + vm.prank(ADDRESS_REGISTRY_ROOT); + sRegistry.register(name, address(0)); + + vm.expectRevert(abi.encodeWithSelector(IAddressRegistryV1.NameNotRegistered.selector, name)); + sRegistry.get(name); + } + + /// Names are independent: binding one says nothing about any other, and + /// each remains bindable exactly once. + function testRegisterDistinctNames(bytes32 nameA, bytes32 nameB, address accountA, address accountB) external { + vm.assume(nameA != nameB); + vm.assume(accountA != address(0)); + vm.assume(accountB != address(0)); + + vm.prank(ADDRESS_REGISTRY_ROOT); + sRegistry.register(nameA, accountA); + + // Binding `nameA` did not bind `nameB`. + vm.expectRevert(abi.encodeWithSelector(IAddressRegistryV1.NameNotRegistered.selector, nameB)); + sRegistry.get(nameB); + + vm.prank(ADDRESS_REGISTRY_ROOT); + sRegistry.register(nameB, accountB); + + assertEq(sRegistry.get(nameA), accountA); + assertEq(sRegistry.get(nameB), accountB); + } + + /// `Register` is emitted exactly once, with the name and account both + /// indexed so the log can be filtered by either. The log is the only + /// enumeration of the registry, so a binding that does not emit is a + /// binding nobody can find. + function testRegisterEvent(bytes32 name, address account) external { + vm.assume(account != address(0)); + + vm.recordLogs(); + vm.prank(ADDRESS_REGISTRY_ROOT); + sRegistry.register(name, account); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].emitter, address(sRegistry)); + assertEq(entries[0].topics.length, 3); + assertEq(entries[0].topics[0], keccak256("Register(bytes32,address)")); + assertEq(entries[0].topics[1], name); + assertEq(entries[0].topics[2], bytes32(uint256(uint160(account)))); + assertEq(entries[0].data.length, 0); + } + + /// A rejected `register` emits nothing, so a failed bind can never be + /// mistaken for a binding by anything reading the logs. + function testRegisterNoEventOnRevert(address sender, bytes32 name, address account) external { + vm.assume(sender != ADDRESS_REGISTRY_ROOT); + + vm.recordLogs(); + vm.expectRevert(abi.encodeWithSelector(IAddressRegistryV1.NotRoot.selector, sender)); + vm.prank(sender); + sRegistry.register(name, account); + assertEq(vm.getRecordedLogs().length, 0); + } +} diff --git a/test/src/lib/LibCloneFactoryDeploy.t.sol b/test/src/lib/LibCloneFactoryDeploy.t.sol index c319fec..40444ca 100644 --- a/test/src/lib/LibCloneFactoryDeploy.t.sol +++ b/test/src/lib/LibCloneFactoryDeploy.t.sol @@ -3,7 +3,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.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"; import {CloneFactory} from "../../../src/concrete/CloneFactory.sol"; diff --git a/test/src/lib/LibCloneFactoryDeployProd.t.sol b/test/src/lib/LibCloneFactoryDeployProd.t.sol index ac4c5ba..88daa95 100644 --- a/test/src/lib/LibCloneFactoryDeployProd.t.sol +++ b/test/src/lib/LibCloneFactoryDeployProd.t.sol @@ -3,7 +3,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.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"; /// @title LibCloneFactoryDeployProdTest diff --git a/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol b/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol index a13cb31..f1f4d15 100644 --- a/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol +++ b/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol @@ -3,7 +3,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.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 { BYTECODE_HASH as CLONE_FACTORY_BYTECODE_HASH_0_1_3, DEPLOYED_ADDRESS as CLONE_FACTORY_DEPLOYED_ADDRESS_0_1_3,