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
2 changes: 1 addition & 1 deletion rust/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
183 changes: 96 additions & 87 deletions rust/contracts/src/factory.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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] = [
Expand Down Expand Up @@ -109,25 +103,93 @@ pub fn predict_factory_impl_address(artifacts: &Artifacts) -> Result<Address> {
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<Bytes> {
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<Address> {
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<Bytes> {
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<Address> {
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<P: Provider>(
&self,
provider: &P,
artifacts: &Artifacts,
) -> Result<Address> {
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
Expand Down Expand Up @@ -216,59 +278,6 @@ pub async fn ensure_create2_deployer<P: Provider>(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<P: Provider>(
provider: &P,
artifacts: &Artifacts,
) -> Result<Address> {
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)`.
Expand Down
74 changes: 18 additions & 56 deletions rust/contracts/tests/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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,
},
};

Expand All @@ -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
Expand All @@ -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,
Expand Down
Loading
Loading