diff --git a/rust/contracts/README.md b/rust/contracts/README.md
index 079f9932..40d38974 100644
--- a/rust/contracts/README.md
+++ b/rust/contracts/README.md
@@ -146,7 +146,7 @@ Other entry points:
- `deploy::upgrade_uups` — deploy a fresh implementation and
`upgradeToAndCall` a UUPS proxy onto it.
-- `factory::ensure_factory` / `factory::factory_deploy` — install the
+- `factory::FactoryGenesis::ensure` / `factory::factory_deploy` — install the
canonical cross-network factory where missing and deploy protocol proxies
through it at name-derived CREATE3 addresses.
- `circuits::version` — the `libid-circuits` release the vendored verifiers
diff --git a/rust/contracts/src/factory.rs b/rust/contracts/src/factory.rs
index 48ff49db..0bd45191 100644
--- a/rust/contracts/src/factory.rs
+++ b/rust/contracts/src/factory.rs
@@ -1,18 +1,18 @@
//! Bootstrap and use of the deterministic deployment factory.
//!
-//! The [`LibidFactory`] proxy lives at the same address on every EVM network
-//! because every byte that feeds its address is frozen: it is deployed
-//! through the canonical keyless CREATE2 deployer (Arachnid's
+//! An environment's [`LibidFactory`] proxy lives at the same address on every
+//! EVM network because every byte that feeds its address is frozen but one:
+//! it is deployed through the canonical keyless CREATE2 deployer (Arachnid's
//! deterministic-deployment proxy at [`CREATE2_DEPLOYER`]) with fixed salts
-//! and init codes that carry no per-network data — the admin is the baked
-//! [`FACTORY_GENESIS_ADMIN`] constant, so initialization happens atomically
-//! inside the deployment. Protocol proxies are then deployed *through* the
-//! factory via CREATE3, which makes their addresses a function of
-//! `(factory, name)` only — see `solidity/contracts/factory/README.md`.
+//! and init codes whose only input is the genesis admin, the environment's
+//! deployer key ([`FactoryGenesis`]), baked in so that initialization happens
+//! atomically inside the deployment. Protocol proxies are then deployed
+//! *through* the factory via CREATE3, which makes their addresses a function
+//! of `(factory, name)` only — see `solidity/contracts/factory/README.md`.
//!
-//! [`ensure_factory`] is the whole bootstrap: check → install the CREATE2
-//! deployer if missing (via its well-known presigned transaction) → deploy
-//! the factory impl and proxy at their canonical addresses. There is
+//! [`FactoryGenesis::ensure`] is the whole bootstrap: check → install the
+//! CREATE2 deployer if missing (via its well-known presigned transaction) →
+//! deploy the factory impl and proxy at their canonical addresses. There is
//! deliberately no fallback deployment path: anything else would change the
//! factory address and defeat the cross-network guarantee, so a chain that
//! cannot take the presigned install transaction is a hard error.
@@ -74,12 +74,6 @@ pub const CREATE2_DEPLOYER_FUNDING_WEI: u128 = 10_000_000_000_000_000;
/// alternate deployment path.
pub const CREATE2_DEPLOYER_INSTALL_TX: &str = "0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222";
-/// The genesis admin baked into the frozen factory-proxy init code.
-/// PLACEHOLDER until the owner substitutes the protocol-admin KMS address.
-/// Keep in sync with `solidity/contracts/factory/FactoryGenesis.sol`.
-pub const FACTORY_GENESIS_ADMIN: Address =
- address!("5bb76B0f81F028de363150602cC6d0Ca929E3C31");
-
/// The 16-byte CREATE3 proxy init code (`Create3.PROXY_INITCODE`). Constant
/// forever — its hash feeds every predicted address.
pub const CREATE3_PROXY_INITCODE: [u8; 16] = [
@@ -109,25 +103,93 @@ pub fn predict_factory_impl_address(artifacts: &Artifacts) -> Result
{
Ok(CREATE2_DEPLOYER.create2(factory_impl_salt(), keccak256(&init_code)))
}
-/// The frozen proxy init code: ERC1967Proxy creation code ++
-/// abi.encode(implAddress, initialize(FACTORY_GENESIS_ADMIN)). Every byte is
-/// network-invariant; `FactoryDeployer.proxyInitCode()` produces the same
-/// bytes (asserted against the vendored artifacts by the Solidity tests).
-pub fn factory_proxy_init_code(artifacts: &Artifacts) -> Result {
- let impl_addr = predict_factory_impl_address(artifacts)?;
- let init_data = LibidFactory::initializeCall {
- owner_: FACTORY_GENESIS_ADMIN,
- }
- .abi_encode();
- let mut code = artifacts.bytecode("ERC1967Proxy")?.to_vec();
- code.extend_from_slice(&(impl_addr, Bytes::from(init_data)).abi_encode_params());
- Ok(code.into())
+/// The factory of one environment, pinned by its genesis admin: the deployer
+/// key that owns the factory from its first block. The admin is the only
+/// varying input of the frozen proxy init code, so the factory's address, and
+/// every address deployed through it, follow from the admin and nothing
+/// else. Testnet and mainnet have their own.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct FactoryGenesis {
+ /// The deployer key: factory owner from genesis, `apply`'s signer.
+ pub admin: Address,
}
-/// The canonical factory address — the same on every EVM network.
-pub fn predict_factory_address(artifacts: &Artifacts) -> Result {
- let init_code = factory_proxy_init_code(artifacts)?;
- Ok(CREATE2_DEPLOYER.create2(factory_proxy_salt(), keccak256(&init_code)))
+impl FactoryGenesis {
+ /// The frozen proxy init code: ERC1967Proxy creation code ++
+ /// abi.encode(implAddress, initialize(admin)). `FactoryDeployer.proxyInitCode(admin)`
+ /// produces the same bytes (asserted against the vendored artifacts by
+ /// the Solidity tests).
+ pub fn proxy_init_code(&self, artifacts: &Artifacts) -> Result {
+ let impl_addr = predict_factory_impl_address(artifacts)?;
+ let init_data = LibidFactory::initializeCall { owner_: self.admin }.abi_encode();
+ let mut code = artifacts.bytecode("ERC1967Proxy")?.to_vec();
+ code.extend_from_slice(&(impl_addr, Bytes::from(init_data)).abi_encode_params());
+ Ok(code.into())
+ }
+
+ /// The factory's address — the same on every EVM network the admin
+ /// deploys to.
+ pub fn address(&self, artifacts: &Artifacts) -> Result {
+ let init_code = self.proxy_init_code(artifacts)?;
+ Ok(CREATE2_DEPLOYER.create2(factory_proxy_salt(), keccak256(&init_code)))
+ }
+
+ /// Make sure the factory exists at [`Self::address`], bootstrapping
+ /// whatever is missing: the CREATE2 deployer (via the keyless presigned
+ /// transaction), the factory implementation, and the factory proxy —
+ /// each at its deterministic address. Idempotent: reruns are read-only
+ /// no-ops once the factory is up.
+ pub async fn ensure(
+ &self,
+ provider: &P,
+ artifacts: &Artifacts,
+ ) -> Result {
+ let factory = self.address(artifacts)?;
+ let code = provider
+ .get_code_at(factory)
+ .await
+ .map_err(|e| Error::Rpc {
+ detail: format!("failed to read code at the factory address: {e}"),
+ })?;
+ if !code.is_empty() {
+ return Ok(factory);
+ }
+
+ ensure_create2_deployer(provider).await?;
+
+ let impl_addr = predict_factory_impl_address(artifacts)?;
+ let impl_code =
+ provider
+ .get_code_at(impl_addr)
+ .await
+ .map_err(|e| Error::Rpc {
+ detail: format!(
+ "failed to read code at the factory impl address: {e}"
+ ),
+ })?;
+ if impl_code.is_empty() {
+ deploy_via_create2(
+ provider,
+ factory_impl_salt(),
+ &factory_impl_init_code(artifacts)?,
+ impl_addr,
+ "LibidFactory (impl)",
+ None,
+ )
+ .await?;
+ }
+
+ deploy_via_create2(
+ provider,
+ factory_proxy_salt(),
+ &self.proxy_init_code(artifacts)?,
+ factory,
+ "LibidFactory (proxy)",
+ None,
+ )
+ .await?;
+ Ok(factory)
+ }
}
/// The CREATE3 address `factory.deploy(name, ·)` lands on: the factory
@@ -216,59 +278,6 @@ pub async fn ensure_create2_deployer(provider: &P) -> Result<()> {
Ok(())
}
-/// Make sure the canonical factory exists at [`predict_factory_address`],
-/// bootstrapping whatever is missing: the CREATE2 deployer (via the keyless
-/// presigned transaction), the factory implementation, and the factory
-/// proxy — each at its deterministic address. Idempotent: reruns are
-/// read-only no-ops once the factory is up.
-pub async fn ensure_factory(
- provider: &P,
- artifacts: &Artifacts,
-) -> Result {
- let factory = predict_factory_address(artifacts)?;
- let code = provider
- .get_code_at(factory)
- .await
- .map_err(|e| Error::Rpc {
- detail: format!("failed to read code at the factory address: {e}"),
- })?;
- if !code.is_empty() {
- return Ok(factory);
- }
-
- ensure_create2_deployer(provider).await?;
-
- let impl_addr = predict_factory_impl_address(artifacts)?;
- let impl_code = provider
- .get_code_at(impl_addr)
- .await
- .map_err(|e| Error::Rpc {
- detail: format!("failed to read code at the factory impl address: {e}"),
- })?;
- if impl_code.is_empty() {
- deploy_via_create2(
- provider,
- factory_impl_salt(),
- &factory_impl_init_code(artifacts)?,
- impl_addr,
- "LibidFactory (impl)",
- None,
- )
- .await?;
- }
-
- deploy_via_create2(
- provider,
- factory_proxy_salt(),
- &factory_proxy_init_code(artifacts)?,
- factory,
- "LibidFactory (proxy)",
- None,
- )
- .await?;
- Ok(factory)
-}
-
/// Deploy `creation_code` under `name` through the factory (the provider's
/// wallet must be the factory owner). Returns the deployed address, which
/// always equals [`predict_address`]`(factory, name)`.
diff --git a/rust/contracts/tests/anvil.rs b/rust/contracts/tests/anvil.rs
index 9c7f5c1b..ceb9449c 100644
--- a/rust/contracts/tests/anvil.rs
+++ b/rust/contracts/tests/anvil.rs
@@ -250,11 +250,12 @@ async fn rotates_and_upgrades_the_notary_service() {
}
/// (c) The deterministic factory, from truly nothing: anvil is started
-/// WITHOUT its predeployed CREATE2 deployer, so `ensure_factory` must
-/// install it via the keyless presigned transaction (funding the one-time
-/// signer first), then deploy the factory impl + proxy at their canonical
-/// addresses. Then a Notary Service proxy goes through the factory at its
-/// name-derived CREATE3 address.
+/// WITHOUT its predeployed CREATE2 deployer, so `FactoryGenesis::ensure`
+/// must install it via the keyless presigned transaction (funding the
+/// one-time signer first), then deploy the factory impl + proxy at their
+/// canonical addresses, owned by the genesis admin from the first block.
+/// Then a Notary Service proxy goes through the factory at its name-derived
+/// CREATE3 address.
#[tokio::test]
async fn bootstraps_the_deterministic_factory_and_deploys_through_it() {
use alloy::{
@@ -267,12 +268,10 @@ async fn bootstraps_the_deterministic_factory_and_deploys_through_it() {
use libid_contracts::{
bindings::factory::LibidFactory,
factory::{
- ensure_factory,
factory_deploy,
predict_address,
- predict_factory_address,
+ FactoryGenesis,
CREATE2_DEPLOYER,
- FACTORY_GENESIS_ADMIN,
},
};
@@ -283,6 +282,7 @@ async fn bootstraps_the_deterministic_factory_and_deploys_through_it() {
.expect("anvil spawns");
let artifacts = Artifacts::embedded();
let deployer0 = default_signer(&provider).await;
+ let genesis = FactoryGenesis { admin: deployer0 };
// Truly bare chain: no CREATE2 deployer.
assert!(provider
@@ -291,64 +291,26 @@ async fn bootstraps_the_deterministic_factory_and_deploys_through_it() {
.unwrap()
.is_empty());
- let factory = ensure_factory(&provider, &artifacts).await.unwrap();
- assert_eq!(factory, predict_factory_address(&artifacts).unwrap());
+ let factory = genesis.ensure(&provider, &artifacts).await.unwrap();
+ assert_eq!(factory, genesis.address(&artifacts).unwrap());
assert!(!provider.get_code_at(factory).await.unwrap().is_empty());
- // The instant the proxy exists it is owned by the baked genesis admin —
+ // The instant the proxy exists it is owned by its genesis admin —
// initialization was atomic with deployment.
let factory_contract = LibidFactory::new(factory, &provider);
- assert_eq!(
- factory_contract.owner().call().await.unwrap(),
- FACTORY_GENESIS_ADMIN
- );
+ assert_eq!(factory_contract.owner().call().await.unwrap(), deployer0);
// Rerun = read-only no-op.
assert_eq!(
- ensure_factory(&provider, &artifacts).await.unwrap(),
+ genesis.ensure(&provider, &artifacts).await.unwrap(),
factory
);
- // Hand ownership to the test signer: impersonate the genesis admin
- // (a placeholder address nobody holds a key for) through anvil.
- provider
- .raw_request::<_, serde_json::Value>(
- "anvil_setBalance".into(),
- (FACTORY_GENESIS_ADMIN, "0xde0b6b3a7640000"),
- )
- .await
- .unwrap();
- provider
- .raw_request::<_, serde_json::Value>(
- "anvil_impersonateAccount".into(),
- (FACTORY_GENESIS_ADMIN,),
- )
- .await
- .unwrap();
- let transfer = LibidFactory::transferOwnershipCall {
- newOwner: deployer0,
- }
- .abi_encode();
- provider
- .raw_request::<_, serde_json::Value>(
- "eth_sendTransaction".into(),
- (serde_json::json!({
- "from": FACTORY_GENESIS_ADMIN,
- "to": factory,
- "data": Bytes::from(transfer),
- }),),
- )
- .await
- .unwrap();
- factory_contract
- .acceptOwnership()
- .send()
- .await
- .unwrap()
- .get_receipt()
- .await
- .unwrap();
- assert_eq!(factory_contract.owner().call().await.unwrap(), deployer0);
+ // Another admin is another factory, at another address.
+ let other = FactoryGenesis {
+ admin: Address::repeat_byte(0x22),
+ };
+ assert_ne!(other.address(&artifacts).unwrap(), factory);
// A Notary Service PROXY through the factory: impl via plain CREATE (its
// address doesn't matter), proxy creation code = ERC1967Proxy ++ (impl,
diff --git a/solidity/contracts/factory/FactoryDeployer.sol b/solidity/contracts/factory/FactoryDeployer.sol
index 02f49c85..6580ca96 100644
--- a/solidity/contracts/factory/FactoryDeployer.sol
+++ b/solidity/contracts/factory/FactoryDeployer.sol
@@ -3,11 +3,10 @@ pragma solidity ^0.8.20;
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
-import {FACTORY_GENESIS_ADMIN} from "./FactoryGenesis.sol";
import {LibidFactory} from "./LibidFactory.sol";
-/// @title FactoryDeployer — the frozen bootstrap material for the canonical
-/// factory, and the math that predicts its address.
+/// @title FactoryDeployer — the frozen bootstrap material for a factory, and
+/// the math that predicts its address from its genesis admin.
///
/// @notice The factory (impl AND proxy) is deployed through the canonical
/// keyless CREATE2 deployer (Arachnid's deterministic-deployment
@@ -15,31 +14,33 @@ import {LibidFactory} from "./LibidFactory.sol";
/// standard EVM chain), with fixed salts and FROZEN init codes:
///
/// implAddress = CREATE2(CREATE2_DEPLOYER, IMPL_SALT, implInitCode)
-/// factoryAddr = CREATE2(CREATE2_DEPLOYER, PROXY_SALT, proxyInitCode)
+/// factoryAddr = CREATE2(CREATE2_DEPLOYER, PROXY_SALT, proxyInitCode(admin))
///
/// `implInitCode` is the LibidFactory creation code (no constructor
-/// args). `proxyInitCode` is the ERC1967Proxy creation code with
-/// `(implAddress, abi.encodeCall(initialize, (FACTORY_GENESIS_ADMIN)))`
-/// appended — the impl address is itself deterministic, and the admin
-/// is a baked constant, so the proxy's constructor args are
-/// network-invariant. Result: `predictFactoryAddress()` is the same
-/// on every EVM network, and the proxy initializes atomically inside
-/// its own deployment (no front-run window).
+/// args). `proxyInitCode(admin)` is the ERC1967Proxy creation code
+/// with `(implAddress, abi.encodeCall(initialize, (admin)))` appended
+/// — the impl address is itself deterministic, so the only input is
+/// the genesis admin: the deployer key that owns the factory from its
+/// first block. Result: `predictFactoryAddress(admin)` is the same on
+/// every EVM network that admin deploys to, and the proxy initializes
+/// atomically inside its own deployment (no front-run window). One
+/// admin per environment (testnet, mainnet) means one factory address
+/// and one address table per environment.
///
/// @dev THE INIT CODES ARE FROZEN. They are pinned by the compiler settings
/// (solc 0.8.33, via_ir, 200 runs, no CBOR metadata) and by the sources;
/// `test/FactoryDeployer.t.sol` asserts the built artifacts match
/// `type(·).creationCode`, and the vendored artifacts in
/// `rust/contracts/artifacts` carry the same bytes. Changing
-/// LibidFactory's source, the admin constant, or the compiler settings
-/// changes the init code and therefore the canonical address — that is a
-/// v2 factory and MUST use new salts (`libid.factory{,.impl}.v2`), never
-/// a silent replacement of v1. Factory-behavior changes that should NOT
-/// move the address go through the normal UUPS upgrade instead.
+/// LibidFactory's source or the compiler settings changes the init code
+/// and therefore every factory address — that is a v2 factory and MUST
+/// use new salts (`libid.factory{,.impl}.v2`), never a silent
+/// replacement of v1. Factory-behavior changes that should NOT move the
+/// address go through the normal UUPS upgrade instead.
///
/// To deploy on a new network, send two transactions to
/// `CREATE2_DEPLOYER` (its calldata format is `salt ++ initCode`):
-/// first `implDeployCalldata()`, then `proxyDeployCalldata()`.
+/// first `implDeployCalldata()`, then `proxyDeployCalldata(admin)`.
/// `rust/contracts/src/factory.rs::ensure_factory` automates this,
/// including installing the CREATE2 deployer itself via its well-known
/// presigned transaction where it is missing.
@@ -67,19 +68,19 @@ library FactoryDeployer {
return _create2Address(CREATE2_DEPLOYER, IMPL_SALT, keccak256(implInitCode()));
}
- /// The frozen proxy init code: ERC1967Proxy creation code ++
- /// abi.encode(implAddress, initialize(FACTORY_GENESIS_ADMIN)). Every byte
- /// of it is network-invariant.
- function proxyInitCode() internal pure returns (bytes memory) {
+ /// The frozen proxy init code of the factory `admin` owns from genesis:
+ /// ERC1967Proxy creation code ++ abi.encode(implAddress, initialize(admin)).
+ /// The admin is its only input that varies.
+ function proxyInitCode(address admin) internal pure returns (bytes memory) {
return abi.encodePacked(
type(ERC1967Proxy).creationCode,
- abi.encode(predictImplAddress(), abi.encodeCall(LibidFactory.initialize, (FACTORY_GENESIS_ADMIN)))
+ abi.encode(predictImplAddress(), abi.encodeCall(LibidFactory.initialize, (admin)))
);
}
- /// The canonical factory address — the same on every EVM network.
- function predictFactoryAddress() internal pure returns (address) {
- return _create2Address(CREATE2_DEPLOYER, PROXY_SALT, keccak256(proxyInitCode()));
+ /// The address of `admin`'s factory — the same on every EVM network.
+ function predictFactoryAddress(address admin) internal pure returns (address) {
+ return _create2Address(CREATE2_DEPLOYER, PROXY_SALT, keccak256(proxyInitCode(admin)));
}
/// Calldata for `CREATE2_DEPLOYER` that deploys the implementation.
@@ -88,11 +89,11 @@ library FactoryDeployer {
}
/// Calldata for `CREATE2_DEPLOYER` that deploys (and atomically
- /// initializes) the factory proxy. Requires the implementation to exist
- /// already: ERC1967Proxy's constructor refuses an implementation with no
- /// code.
- function proxyDeployCalldata() internal pure returns (bytes memory) {
- return abi.encodePacked(PROXY_SALT, proxyInitCode());
+ /// initializes) `admin`'s factory proxy. Requires the implementation to
+ /// exist already: ERC1967Proxy's constructor refuses an implementation
+ /// with no code.
+ function proxyDeployCalldata(address admin) internal pure returns (bytes memory) {
+ return abi.encodePacked(PROXY_SALT, proxyInitCode(admin));
}
function _create2Address(address deployer, bytes32 salt, bytes32 initCodeHash) private pure returns (address) {
diff --git a/solidity/contracts/factory/FactoryGenesis.sol b/solidity/contracts/factory/FactoryGenesis.sol
deleted file mode 100644
index c8f6b1c5..00000000
--- a/solidity/contracts/factory/FactoryGenesis.sol
+++ /dev/null
@@ -1,25 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.20;
-
-// The protocol-admin address baked into the FROZEN factory-proxy init code.
-//
-// The canonical factory proxy is deployed with init code that ends in
-// `abi.encode(implAddress, abi.encodeCall(LibidFactory.initialize, (FACTORY_GENESIS_ADMIN)))`
-// (see `FactoryDeployer.proxyInitCode()`). Baking the admin into the init
-// code — instead of passing it per network — is what makes the init code
-// identical everywhere (same CREATE2 address on every chain) AND makes
-// initialization atomic with deployment: there is no window in which an
-// uninitialized proxy exists for someone to front-run `initialize`.
-//
-// ─────────────────────────────────────────────────────────────────────────
-// PLACEHOLDER — DO NOT DEPLOY TO ANY MAINNET-FAMILY NETWORK AS-IS.
-//
-// The owner substitutes the real protocol-admin KMS address here BEFORE the
-// first mainnet-family deployment. Changing this constant changes the frozen
-// proxy init code and therefore the canonical factory address, so it must be
-// set once, before anything ships, and then NEVER touched again (a later
-// change is a v2 factory: new salts, new address — see the README).
-//
-// Keep in sync with `FACTORY_GENESIS_ADMIN` in `rust/contracts/src/factory.rs`.
-// ─────────────────────────────────────────────────────────────────────────
-address constant FACTORY_GENESIS_ADMIN = 0x5bb76B0f81F028de363150602cC6d0Ca929E3C31;
diff --git a/solidity/contracts/factory/LibidFactory.sol b/solidity/contracts/factory/LibidFactory.sol
index 751bb208..8abdfcd4 100644
--- a/solidity/contracts/factory/LibidFactory.sol
+++ b/solidity/contracts/factory/LibidFactory.sol
@@ -24,13 +24,14 @@ import {Create3} from "./Create3.sol";
/// @dev UUPS + Ownable2Step behind an ERC1967 proxy, like every other
/// upgradeable contract in the stack. Two things are special here:
///
-/// - The canonical deployment path does NOT choose an owner per network.
-/// The proxy init code is FROZEN with `initialize(FACTORY_GENESIS_ADMIN)`
-/// baked in (see `FactoryGenesis.sol` / `FactoryDeployer.proxyInitCode()`),
+/// - The canonical deployment path chooses the owner once per
+/// environment, not per network. The proxy init code is FROZEN with
+/// `initialize(admin)` baked in (see `FactoryDeployer.proxyInitCode`),
/// so deployment and initialization are one atomic CREATE2 — no
/// front-run window — and the init code (hence the address) is
-/// identical on every chain. `initialize` exists per UUPS convention
-/// and for tests; on the canonical path nobody ever calls it manually.
+/// identical on every chain that admin deploys to. `initialize`
+/// exists per UUPS convention and for tests; on the canonical path
+/// nobody ever calls it manually.
/// - Upgrading this factory keeps its address and its deployment
/// records; everything deployed through it stays where it is.
contract LibidFactory is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable {
@@ -51,8 +52,8 @@ contract LibidFactory is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable
/// @param owner_ Address that will own the factory (deploy + upgrade
/// authority; should be a multisig / KMS key). The
- /// canonical path bakes `FACTORY_GENESIS_ADMIN` here via
- /// the frozen proxy init code.
+ /// canonical path bakes the environment's deployer key
+ /// here via the frozen proxy init code.
function initialize(address owner_) external initializer {
__Ownable_init(owner_);
__Ownable2Step_init();
diff --git a/solidity/contracts/factory/README.md b/solidity/contracts/factory/README.md
index ecb15108..a4663243 100644
--- a/solidity/contracts/factory/README.md
+++ b/solidity/contracts/factory/README.md
@@ -1,18 +1,20 @@
# Deterministic deployment factory
One guarantee: **a protocol contract's address is a function of its name
-alone** — `address = f(name)` — the same on every EVM network, before anything
-is deployed there, forever. Not a function of transaction order, deployer
-nonces, bytecode versions, or constructor arguments.
+and its environment's deployer key alone** — `address = f(admin, name)` —
+the same on every EVM network that deployer deploys to, before anything is
+deployed there, forever. Not a function of transaction order, deployer
+nonces, bytecode versions, or constructor arguments. One deployer key per
+environment (testnet, mainnet) gives one address table per environment.
```
Arachnid CREATE2 deployer (0x4e59b4…956C, keyless, same address everywhere)
│ fixed salt keccak("libid.factory.impl.v1") + frozen impl init code
├──▶ LibidFactory implementation
- │ fixed salt keccak("libid.factory.v1") + frozen proxy init code
- └──▶ LibidFactory proxy ← THE canonical factory, same address on every chain
+ │ fixed salt keccak("libid.factory.v1") + frozen proxy init code (admin)
+ └──▶ LibidFactory proxy ← the environment's factory, same address on every chain
│ CREATE3, salt = keccak(bytes(name)), owner-gated
- └──▶ every protocol proxy: address = f(factory, name) = f(name)
+ └──▶ every protocol proxy: address = f(factory, name) = f(admin, name)
```
## Why CREATE3, not raw CREATE2
@@ -35,18 +37,17 @@ their proxy and don't need to be deterministic.
## Why the admin is baked in (atomic initialization)
The factory's own proxy init code is **frozen**
-([`FactoryDeployer.proxyInitCode()`](./FactoryDeployer.sol)): ERC1967Proxy
-creation code ++ `abi.encode(implAddress, abi.encodeCall(initialize,
-(FACTORY_GENESIS_ADMIN)))`. No per-network constructor args means the same
-CREATE2 address everywhere, and it means the proxy initializes *inside its
-own deployment transaction*: at no block height does an uninitialized factory
+([`FactoryDeployer.proxyInitCode(admin)`](./FactoryDeployer.sol)):
+ERC1967Proxy creation code ++ `abi.encode(implAddress,
+abi.encodeCall(initialize, (admin)))`. The admin is the environment's
+deployer key, the only input that varies, so the same key gets the same
+CREATE2 address everywhere, and the proxy initializes *inside its own
+deployment transaction*: at no block height does an uninitialized factory
exist, so there is nothing to front-run. The raw implementation is bricked
with `_disableInitializers()`.
-`FACTORY_GENESIS_ADMIN` ([`FactoryGenesis.sol`](./FactoryGenesis.sol)) is a
-**placeholder** until the owner substitutes the real protocol-admin KMS
-address — that must happen *before* the first mainnet-family deployment,
-because changing it afterwards changes the canonical address.
+The admin is chosen once per environment: changing it afterwards is a new
+factory at a new address, with a new address table behind it.
## Bootstrap: the keyless CREATE2 deployer is the only path
@@ -58,7 +59,7 @@ signature was fixed before any key existed), so it has the same address on
every chain. On a chain where it is missing, it is installable by anyone:
fund `0x3fab184622dc19b6109349b94811493bf2a45362` with exactly 0.01 ETH
(100 gwei × 100 000 gas) and broadcast the well-known raw transaction —
-`ensure_factory` in `rust/contracts/src/factory.rs` automates the whole
+`FactoryGenesis::ensure` in `rust/contracts/src/factory.rs` automates the whole
sequence (install deployer if absent → deploy impl → deploy proxy).
**There is deliberately no fallback path.** Any other deployment route (e.g.
@@ -67,7 +68,7 @@ silently defeat the cross-network guarantee. Network onboarding requirement:
the canonical CREATE2 deployer must be present or installable. A chain that
enforces EIP-155 on all transactions (cannot accept the presigned install tx)
and doesn't ship the deployer in genesis **cannot host the deterministic
-factory** — `ensure_factory` fails hard, and the network must be
+factory** — `FactoryGenesis::ensure` fails hard, and the network must be
reconsidered. Chains that changed CREATE2/CREATE address derivation
(zkSync-Era-style) are out of scope for address parity. First supported
network: **Eden testnet** (chain 3735928814), where the deployer is already
@@ -75,13 +76,12 @@ present.
## The frozen-init-code invariant
-`implInitCode()` and `proxyInitCode()` must **never change**: they are pinned
-by the LibidFactory/ERC1967Proxy sources, the `FACTORY_GENESIS_ADMIN`
-constant, and the compiler settings (solc 0.8.33, via_ir, 200 runs, no CBOR
-metadata). `test/FactoryDeployer.t.sol` asserts they match the built
+`implInitCode()` and `proxyInitCode(admin)` must **never change** for a
+given admin: they are pinned by the LibidFactory/ERC1967Proxy sources and
+the compiler settings (solc 0.8.33, via_ir, 200 runs, no CBOR metadata). `test/FactoryDeployer.t.sol` asserts they match the built
artifacts byte-for-byte, which are in turn vendored for the Rust bootstrap —
one set of bytes everywhere. Behavior changes to the live factory go through
its UUPS upgrade (address and records are kept). A change that *does* alter
-the init code — new admin, new sources you want deployed as the genesis
-bytes — is a **v2 factory**: new salts (`libid.factory{,.impl}.v2`), a new
+the init code — new sources you want deployed as the genesis bytes — is a
+**v2 factory**: new salts (`libid.factory{,.impl}.v2`), a new
canonical address, and a migration story; never a silent replacement of v1.
diff --git a/solidity/contracts/factory/test/FactoryDeployer.t.sol b/solidity/contracts/factory/test/FactoryDeployer.t.sol
index 6499bf68..806c08c8 100644
--- a/solidity/contracts/factory/test/FactoryDeployer.t.sol
+++ b/solidity/contracts/factory/test/FactoryDeployer.t.sol
@@ -3,7 +3,6 @@ pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
-import {FACTORY_GENESIS_ADMIN} from "../FactoryGenesis.sol";
import {FactoryDeployer} from "../FactoryDeployer.sol";
import {LibidFactory} from "../LibidFactory.sol";
import {Ping} from "./Create3.t.sol";
@@ -14,6 +13,10 @@ import {Ping} from "./Create3.t.sol";
/// impl and proxy init codes are deployed through it exactly as
/// `ensure_factory` does on a real network.
contract FactoryDeployerTest is Test {
+ /// The deployer key of this test's environment: the factory's owner from
+ /// its first block.
+ address internal constant ADMIN = address(0xAD1);
+
/// The runtime bytecode of the deterministic-deployment proxy — the code
/// that lives at 0x4e59b4…956C on every chain it was installed on.
/// Calldata format: 32-byte salt ++ init code; returns the 20-byte
@@ -39,29 +42,32 @@ contract FactoryDeployerTest is Test {
bool ok;
(ok, impl) = _deployVia(FactoryDeployer.implDeployCalldata());
assertTrue(ok, "impl deploy failed");
- (ok, factoryAddr) = _deployVia(FactoryDeployer.proxyDeployCalldata());
+ (ok, factoryAddr) = _deployVia(FactoryDeployer.proxyDeployCalldata(ADMIN));
assertTrue(ok, "proxy deploy failed");
}
function test_bootstrap_landsOnThePredictedAddresses() public {
(address impl, address factoryAddr) = _bootstrap();
assertEq(impl, FactoryDeployer.predictImplAddress());
- assertEq(factoryAddr, FactoryDeployer.predictFactoryAddress());
+ assertEq(factoryAddr, FactoryDeployer.predictFactoryAddress(ADMIN));
assertGt(impl.code.length, 0);
assertGt(factoryAddr.code.length, 0);
}
/// The anti-front-running property: the instant the proxy exists, it is
- /// already initialized with the baked genesis admin. There is no
+ /// already initialized with its genesis admin. There is no
/// deploy-then-initialize gap.
+ /// A different admin is a different factory, at a different address.
function test_bootstrap_ownerIsTheGenesisAdminAtomically() public {
(, address factoryAddr) = _bootstrap();
LibidFactory factory = LibidFactory(factoryAddr);
- assertEq(factory.owner(), FACTORY_GENESIS_ADMIN);
+ assertEq(factory.owner(), ADMIN);
// Nobody can re-initialize the proxy…
vm.expectRevert();
factory.initialize(address(this));
+
+ assertTrue(FactoryDeployer.predictFactoryAddress(address(0xAD2)) != factoryAddr);
}
/// …and the raw implementation is bricked by _disableInitializers.
@@ -75,7 +81,7 @@ contract FactoryDeployerTest is Test {
/// the proxy cannot be deployed ahead of the impl (wrong order fails
/// loudly instead of minting a broken factory).
function test_bootstrap_proxyBeforeImplFails() public {
- (bool ok,) = _deployVia(FactoryDeployer.proxyDeployCalldata());
+ (bool ok,) = _deployVia(FactoryDeployer.proxyDeployCalldata(ADMIN));
assertFalse(ok);
}
@@ -84,7 +90,7 @@ contract FactoryDeployerTest is Test {
LibidFactory factory = LibidFactory(factoryAddr);
address predicted = factory.predict("libid.thing");
- vm.prank(FACTORY_GENESIS_ADMIN);
+ vm.prank(ADMIN);
assertEq(factory.deploy("libid.thing", type(Ping).creationCode), predicted);
vm.prank(address(this)); // not the admin
@@ -105,10 +111,8 @@ contract FactoryDeployerTest is Test {
vm.parseJsonBytes(vm.readFile("out/ERC1967Proxy.sol/ERC1967Proxy.json"), ".bytecode.object");
bytes memory expectedProxyInitCode = abi.encodePacked(
proxyArtifact,
- abi.encode(
- FactoryDeployer.predictImplAddress(), abi.encodeCall(LibidFactory.initialize, (FACTORY_GENESIS_ADMIN))
- )
+ abi.encode(FactoryDeployer.predictImplAddress(), abi.encodeCall(LibidFactory.initialize, (ADMIN)))
);
- assertEq(keccak256(expectedProxyInitCode), keccak256(FactoryDeployer.proxyInitCode()));
+ assertEq(keccak256(expectedProxyInitCode), keccak256(FactoryDeployer.proxyInitCode(ADMIN)));
}
}