ST0xOrchestratorBeaconSetDeployer.deploy() creates the orchestrator instance with a plain new BeaconProxy(...) — a CREATE whose address derives from the set-deployer's account nonce — and the function is explicitly permissionless ("Callable by anyone — no auth on the deployer", src/concrete/deploy/ST0xOrchestratorBeaconSetDeployer.sol:44-45).
PR #316 pins the resulting instance as LibOrchestratorInvariants.ST0X_ORCHESTRATOR_INSTANCE = 0x3A7387a4…, computed as computeCreateAddress(setDeployer, 2). That address is ours only if the ops broadcast wins a race to be the first deploy() call on each chain. The set-deployer is a Zoltu CREATE2 deploy at a chain-invariant address and is already live on Base, and this PR publishes the target address in a public repo before claiming it.
Consequences of losing that race, for one transaction's gas:
- The attacker permanently holds
0x3A7387a4… with an attacker-admin BeaconProxy.
- The script is permanently bricked —
_assertNoInstanceYet reverts OrchestratorAlreadyDeployed forever, with no override path.
- Recovery is worse than a redeploy: CLAUDE.md forbids updating an address constant in
LibProdDeploy* in place, and the same set-deployer can never produce a nonce-2 CREATE again, so recovery needs a whole new orchestrator beacon-set deployer — new creation bytecode, new Zoltu address, new audit.
The fix
Deterministic cloning, which this repo already does elsewhere and already depends on.
script/20260619-deploy-v4-authoriser-clone.s.sol deploys its clone through ICloneableFactoryV2 + LibCloneFactoryDeploy from rain-factory-0.1.1, pinned in foundry.toml:65. The pattern, the dependency and a working in-repo precedent all exist; the orchestrator instance is the one that does not use them.
A clone factory derives the address from f(factory, implementation, initData) via CREATE2 rather than from a deployer nonce, which removes the race rather than guarding it. An attacker calling first has two options and neither takes the pin: deploy our exact config, which lands at the address we wanted with the owner we wanted and is the outcome we were going to produce anyway; or deploy different config, which lands at a different address and leaves the pin untouched.
It also dissolves two adjacent findings from the #316 review rather than patching them. _assertNoInstanceYet's nonce assumption disappears, because the address stops depending on how many times deploy() has been called. And the simulation-only post-condition gap (assertDeployLanded is an internal call, so it never lands in the broadcast transaction) stops being able to produce a stray unpinned instance, since a lost race cannot mint at a config-derived address.
This is also the org's stated rule: instances and per-token proxies use deterministic cloning (CREATE2 clones or a fixed-salt clone factory), never plain new / CREATE.
Cost
deploy() lives in ST0xOrchestratorBeaconSetDeployer, which is part of the audited 0.1.30 bytecode frozen by #314. Changing it is a new audited version, not a script edit — so this is a decision about the next audit round rather than something to patch in flight.
Out of scope here: the orchestrator beacon's owner being a hot deploy EOA with no migration window (LibOrchestratorInvariants.assertBeaconSet pins BEACON_INITIAL_OWNER permanently, and the governance-timelock bundle's fixed address[3] cannot reach a fourth beacon). That is about who holds upgrade authority, not how the instance address is derived, and wants its own issue.
Found by an adversarial audit of #316 at pr:316 scope.
ST0xOrchestratorBeaconSetDeployer.deploy()creates the orchestrator instance with a plainnew BeaconProxy(...)— a CREATE whose address derives from the set-deployer's account nonce — and the function is explicitly permissionless ("Callable by anyone — no auth on the deployer", src/concrete/deploy/ST0xOrchestratorBeaconSetDeployer.sol:44-45).PR #316 pins the resulting instance as
LibOrchestratorInvariants.ST0X_ORCHESTRATOR_INSTANCE = 0x3A7387a4…, computed ascomputeCreateAddress(setDeployer, 2). That address is ours only if the ops broadcast wins a race to be the firstdeploy()call on each chain. The set-deployer is a Zoltu CREATE2 deploy at a chain-invariant address and is already live on Base, and this PR publishes the target address in a public repo before claiming it.Consequences of losing that race, for one transaction's gas:
0x3A7387a4…with an attacker-adminBeaconProxy._assertNoInstanceYetrevertsOrchestratorAlreadyDeployedforever, with no override path.LibProdDeploy*in place, and the same set-deployer can never produce a nonce-2 CREATE again, so recovery needs a whole new orchestrator beacon-set deployer — new creation bytecode, new Zoltu address, new audit.The fix
Deterministic cloning, which this repo already does elsewhere and already depends on.
script/20260619-deploy-v4-authoriser-clone.s.soldeploys its clone throughICloneableFactoryV2+LibCloneFactoryDeployfromrain-factory-0.1.1, pinned infoundry.toml:65. The pattern, the dependency and a working in-repo precedent all exist; the orchestrator instance is the one that does not use them.A clone factory derives the address from
f(factory, implementation, initData)via CREATE2 rather than from a deployer nonce, which removes the race rather than guarding it. An attacker calling first has two options and neither takes the pin: deploy our exact config, which lands at the address we wanted with the owner we wanted and is the outcome we were going to produce anyway; or deploy different config, which lands at a different address and leaves the pin untouched.It also dissolves two adjacent findings from the #316 review rather than patching them.
_assertNoInstanceYet's nonce assumption disappears, because the address stops depending on how many timesdeploy()has been called. And the simulation-only post-condition gap (assertDeployLandedis an internal call, so it never lands in the broadcast transaction) stops being able to produce a stray unpinned instance, since a lost race cannot mint at a config-derived address.This is also the org's stated rule: instances and per-token proxies use deterministic cloning (CREATE2 clones or a fixed-salt clone factory), never plain
new/ CREATE.Cost
deploy()lives inST0xOrchestratorBeaconSetDeployer, which is part of the audited 0.1.30 bytecode frozen by #314. Changing it is a new audited version, not a script edit — so this is a decision about the next audit round rather than something to patch in flight.Out of scope here: the orchestrator beacon's owner being a hot deploy EOA with no migration window (
LibOrchestratorInvariants.assertBeaconSetpinsBEACON_INITIAL_OWNERpermanently, and the governance-timelock bundle's fixedaddress[3]cannot reach a fourth beacon). That is about who holds upgrade authority, not how the instance address is derived, and wants its own issue.Found by an adversarial audit of #316 at
pr:316scope.