Skip to content
Merged
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
42 changes: 22 additions & 20 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ code in this repository.

## Project Overview

rain.factory is a Solidity **library** repo: the `ICloneable*` interface surface
for EIP1167 minimal proxy (clone) factories in the Rain ecosystem. It is the
library half of the library/deploy split (rainlanguage/rain.factory#46) and
holds interfaces only — no concrete contract, no deploy pins, no deploy script,
and no tests.
rain.factory is a Solidity **library** repo for EIP1167 minimal proxy (clone)
factories in the Rain ecosystem: the `ICloneable*` interface surface and the
`LibICloneableFactoryV4` salt-derivation library. It is the library half of the
library/deploy split (rainlanguage/rain.factory#46) — no concrete contract, no
deploy pins, no deploy script.

The concrete `CloneFactory` that implements these interfaces, its deployed
address + codehash pins (`LibCloneFactoryDeploy`), the frozen
Expand All @@ -26,9 +26,9 @@ SPDX headers.

Nix + Foundry. Enter the shell with `nix develop`, then run rainix tasks:
`rainix-sol-static` (Slither), `rainix-sol-legal` (REUSE), `rainix-sol-prelude`
(deps, run first), `rainix-sol-test`, and `forge build` directly. There is no
test suite here — the interfaces have no behaviour; the tests that exercise them
live against the concrete in rain.factory.deploy.
(deps, run first), `rainix-sol-test`, and `forge build` directly.
`rainix-sol-test` runs the derivation library's tests, which live here; the
interfaces have no behaviour to test.

## Architecture

Expand All @@ -51,24 +51,26 @@ live against the concrete in rain.factory.deploy.
derivations are pinned to exact bytes, each `keccak256`-ing a 96-byte preimage
led by a distinct string-derived domain tag, so the two images are disjoint by
construction. The full spec is the NatSpec on the interface.
- `src/lib/LibICloneableFactoryV4.sol` — Pure `internal` library, the executable
form of the two `ICloneableFactoryV4` salt derivations; imports the domain
tags from the interface. Tested under `test/src/lib/`.
- `src/interface/deprecated/` — Legacy interfaces (`ICloneableV1`,
`ICloneableFactoryV1`, `IFactory`). Do not use for new work.

`src/` holds nothing else. The interfaces import nothing from outside this repo
— intra-repo inheritance is allowed and `ICloneableFactoryV4` extends
`ICloneableFactoryV3` — which is what makes this half a standalone publish.
No `src/` file imports from outside this repo — intra-repo inheritance is
allowed (`ICloneableFactoryV4` extends `ICloneableFactoryV3`), which is what
makes this half a standalone publish.

## Solidity Conventions

- Solidity version: every file here is an interface and floats `^` (the
interfaces use `^0.8.18`) so downstream soldeer consumers on a different
`0.8.x` can still compile them. The `=0.8.25` exact-pin rule applies to
concrete contracts, scripts and tests, which live in rain.factory.deploy.
- Solidity version: the interfaces and the derivation library float `^`
(`^0.8.18`) so downstream soldeer consumers on a different `0.8.x` can still
compile them; the tests here pin `=0.8.25`, as do the concrete contracts and
scripts in rain.factory.deploy.
- Compiler (`foundry.toml`): Cancun EVM, optimizer at 100,000 runs, no CBOR
metadata (`cbor_metadata = false`, `bytecode_hash = "none"`).
- Dependencies are managed with Soldeer (`[dependencies]` in `foundry.toml` +
`soldeer.lock`, vendored under `dependencies/`). The interfaces import nothing
external, so the only entry is forge-std. `@openzeppelin-contracts`,
`rain-extrospection`, `rain-deploy` and `rain-sol-codegen` belong to the
deploy half and must not be added here: adding one means concrete code has
landed in a library repo.
`soldeer.lock`, vendored under `dependencies/`). The only external entry is
forge-std. `@openzeppelin-contracts`, `rain-extrospection`, `rain-deploy` and
`rain-sol-codegen` belong to the deploy half and must not be added here:
adding one means concrete code has landed in a library repo.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Docs at https://rainprotocol.github.io/rain.factory

This repo is the **library** half of the library/deploy split
([#46](https://github.com/rainlanguage/rain.factory/issues/46)): the
`ICloneable*` interface surface, and nothing else. It publishes to Soldeer as
`rain-factory`.
`ICloneable*` interface surface and the `LibICloneableFactoryV4` salt-derivation
library. It publishes to Soldeer as `rain-factory`.

## Concrete implementations

Expand All @@ -21,6 +21,17 @@ Depend on `rain-factory` if you need only the interfaces. Depend on
`rain-factory-deploy` if you need the deployed address or codehash of a live
`CloneFactory`.

## Derivation library

`src/lib/LibICloneableFactoryV4.sol` is the executable form of the two `CREATE2`
salt derivations `ICloneableFactoryV4` pins to exact bytes — the
`msg.sender`-namespaced one and the open-salt one — as pure `internal`
functions. It imports the domain tags from the interface, so the tags have a
single source of truth, and its tests recompute both formulas independently to
pin them to the interface's spec byte for byte. A factory, an indexer or a
consumer predicting a clone address computes the salt from one place. The tests
live here under `test/src/lib/`.

## Interfaces

Contains interfaces for working with Rain factories.
Expand Down
45 changes: 45 additions & 0 deletions src/lib/LibICloneableFactoryV4.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity ^0.8.18;

import {
ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN,
ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN
} from "../interface/ICloneableFactoryV4.sol";

/// @title LibICloneableFactoryV4
/// @notice The executable form of the two effective-`CREATE2`-salt derivations
/// that `ICloneableFactoryV4` pins to exact bytes, so a factory, an indexer or a
/// consumer predicting a clone address computes them from one place instead of
/// re-deriving the formulas inline. Each function reproduces its interface
/// formula byte for byte and reads its domain tag from the interface, so the
/// tags have a single source of truth and the derivation cannot drift from the
/// spec. See `ICloneableFactoryV4` for what each salt commits to and why the two
/// images are disjoint.
library LibICloneableFactoryV4 {
/// The effective `CREATE2` salt for the namespaced derivation
/// (`cloneDeterministic` / `predictDeterministicAddress`): the caller-chosen
/// `salt` behind the namespaced domain tag and `deployer`, so the salt — and
/// therefore the clone address — is namespaced to the account that deploys.
/// @param deployer The account whose namespace the salt belongs to (the
/// `msg.sender` of `cloneDeterministic`, or the `deployer` argument of
/// `predictDeterministicAddress`).
/// @param salt The caller-chosen salt, before namespacing.
/// @return The effective salt `CREATE2` hashes.
function effectiveSalt(address deployer, bytes32 salt) internal pure returns (bytes32) {
return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt));
}

/// The effective `CREATE2` salt for the open-salt derivation
/// (`cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt`):
/// the caller-chosen `salt` and the hash of the initialization `data` behind
/// the open-salt domain tag, and nothing about the caller, so the clone
/// address commits to `salt` and `data` and every account reaches the same
/// one. `data` MAY be empty.
/// @param salt The caller-chosen salt.
/// @param data The initialization data passed to `ICloneableV2.initialize`.
/// @return The effective salt `CREATE2` hashes.
function effectiveOpenSalt(bytes32 salt, bytes memory data) internal pure returns (bytes32) {
return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)));
}
}
70 changes: 70 additions & 0 deletions test/src/lib/LibICloneableFactoryV4.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol";
import {
ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN,
ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN
} from "src/interface/ICloneableFactoryV4.sol";

/// @title LibICloneableFactoryV4Test
/// Every expectation is recomputed inline from the pinned formula, never read
/// back from the library, so a change to the library's derivation diverges from
/// the oracle here and the test fails.
contract LibICloneableFactoryV4Test is Test {
/// The domain tags the library derives against are exactly the two pinned
/// string hashes. Recomputed from the literal strings so this is independent
/// of the imported constants.
function testDomainTagsPinned() external pure {
assertEq(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, keccak256("rain.factory.clone.namespaced"));
assertEq(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, keccak256("rain.factory.clone.opensalt"));
}

/// `effectiveSalt` is the namespaced formula byte for byte.
function testEffectiveSaltMatchesFormula(address deployer, bytes32 salt) external pure {
assertEq(
LibICloneableFactoryV4.effectiveSalt(deployer, salt),
keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt))
);
}

/// `effectiveOpenSalt` is the open-salt formula byte for byte, across data
/// lengths.
function testEffectiveOpenSaltMatchesFormula(bytes32 salt, bytes memory data) external pure {
assertEq(
LibICloneableFactoryV4.effectiveOpenSalt(salt, data),
keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))
);
}

/// Empty `data` is a supported, distinct case: the derivation hashes
/// `keccak256("")`, not the empty bytes inline.
function testEffectiveOpenSaltEmptyData(bytes32 salt) external pure {
assertEq(
LibICloneableFactoryV4.effectiveOpenSalt(salt, ""),
keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256("")))
);
}

/// The two derivations use distinct fixed tags in word 0 — the whole of the
/// disjointness-by-construction guarantee.
function testDomainTagsDistinct() external pure {
assertTrue(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN != ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN);
}

/// No namespaced `(deployer, salt)` input collides with any open-salt
/// `(salt, data)` input: the distinct word-0 tags make the preimages
/// disjoint, so the images are too. Fuzzed over both input spaces at once,
/// including empty and non-empty data.
function testDerivationsDisjoint(address deployer, bytes32 namespacedSalt, bytes32 openSalt, bytes memory data)
external
pure
{
assertTrue(
LibICloneableFactoryV4.effectiveSalt(deployer, namespacedSalt)
!= LibICloneableFactoryV4.effectiveOpenSalt(openSalt, data)
);
}
}
Loading