Skip to content

Commit 7b1981f

Browse files
committed
feat(deploy): deterministic proxy deploy script + salt-mining docs
Adds `script/Deploy.s.sol` wrapping the three deployment steps into a single atomic broadcast: the canonical solady `ERC1967Factory` is bootstrapped via `ImmutableCreate2Factory` when cold, the `RequestWhitelist` implementation is CREATE2'd at a configurable salt (default `0x…003f`), and the proxy is deployed + initialised in one tx via `deployDeterministicAndCall`. Every artefact ends up at the same address on every chain. README gains a "Deploying with `script/Deploy.s.sol`" section and a "Mining a vanity proxy salt" walkthrough, including the pinned canonical `initCodeHash` and the `--no-random` caveat for `cast create2` (without it, the factory reverts `SaltDoesNotStartWithCaller`).
1 parent 917b4a7 commit 7b1981f

2 files changed

Lines changed: 270 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,132 @@ Deployment is proxy-based and **must** be atomic to close the one-shot
300300
3. Consumers import `IWhitelist`, call `isWhitelisted(address)` on the
301301
proxy address, and gate strictly on
302302
`status == IWhitelist.WhitelistStatus.Whitelisted`.
303+
304+
### Deploying with `script/Deploy.s.sol`
305+
306+
`script/Deploy.s.sol` wraps the three steps above into one atomic
307+
broadcast. Every artefact lives at a cross-chain-deterministic address:
308+
309+
- **`ERC1967Factory`** is always the canonical solady address
310+
`0x0000000000006396FF2a80c067f99B3d2Ab4Df24` — not a config knob the
311+
caller can get wrong. If that factory is not yet deployed on the
312+
target chain, the script bootstraps it via 0age's
313+
`ImmutableCreate2Factory` (`0x0000000000FFe8B47B3e2130213B802212439497`)
314+
with the pinned SALT/INITCODE from `ERC1967FactoryConstants`.
315+
- **`RequestWhitelist` implementation** is CREATE2'd through
316+
`ImmutableCreate2Factory` at `IMPLEMENTATION_SALT` (default
317+
`bytes32(uint256(0x3f))`). Its address is a pure function of
318+
`(create2-factory, salt, creationCode)` and is therefore identical
319+
on every chain that runs this script. Override the salt if you want
320+
a different vanity prefix, or pass `IMPLEMENTATION` directly to
321+
reuse an existing deployment.
322+
- **Proxy** is CREATE2'd through `ERC1967Factory` at the user-provided
323+
`SALT`, initialised atomically in the same tx.
324+
325+
The script reverts with guidance if `ImmutableCreate2Factory` itself is
326+
missing — a fully cold chain needs one manual bootstrap the script
327+
cannot do for you. It is idempotent at the artefact level: if the
328+
factory or the implementation already has code at its predicted
329+
address, that step is skipped.
330+
331+
Required env:
332+
333+
| Var | Type | Purpose |
334+
|---|---|---|
335+
| `SALT` | `bytes32` | CREATE2 salt for the proxy (see [Mining a vanity proxy salt](#mining-a-vanity-proxy-salt)). |
336+
| `PROXY_ADMIN` | `address` | ERC-1967 admin — controls upgrades, distinct from `owner()`. |
337+
| `OWNER` | `address` | Phase-1 application owner. |
338+
| `VALIDATORS` | `address[]` (comma-sep) | Initial validator set. |
339+
| `QUORUM` | `uint` | `1 <= QUORUM < VALIDATORS.length`. |
340+
341+
Optional env:
342+
343+
| Var | Type | Purpose |
344+
|---|---|---|
345+
| `IMPLEMENTATION` | `address` | Explicit impl override. Skips CREATE2 deployment entirely. |
346+
| `IMPLEMENTATION_SALT` | `bytes32` | CREATE2 salt for the impl. Default `0x000...003f`. Ignored if `IMPLEMENTATION` is set. |
347+
| `COMPLIANCE_OFFICERS` | `address[]` (comma-sep) | Empty if unset. |
348+
349+
```shell
350+
SALT=0x0000000000000000000000000000000000000000....cafe \
351+
PROXY_ADMIN=0x... \
352+
OWNER=0x... \
353+
VALIDATORS=0xaaa,0xbbb,0xccc \
354+
QUORUM=2 \
355+
COMPLIANCE_OFFICERS=0xddd \
356+
forge script script/Deploy.s.sol:DeployRequestWhitelist --rpc-url <RPC> --broadcast
357+
```
358+
359+
### Mining a vanity proxy salt
360+
361+
The proxy is deployed via `factory.deployDeterministicAndCall`, which is
362+
a CREATE2 call keyed on `(factory, salt, initCodeHash)`. Pick a salt
363+
whose resulting proxy address starts with a chosen hex prefix (e.g.
364+
`0x3f...`) by grinding candidates offline:
365+
366+
1. **Use the canonical factory address.** Mining is always against
367+
`ERC1967FactoryConstants.ADDRESS = 0x0000000000006396FF2a80c067f99B3d2Ab4Df24`
368+
— the same CREATE2 deployer on every chain. No per-chain
369+
bookkeeping, no config knob.
370+
2. **Use the pinned `initCodeHash`.** The proxy init code embeds the
371+
factory address, so the hash is fixed for the canonical factory on
372+
every chain:
373+
```
374+
0x4435a5963c29bc1c221d8cfc9c546a167425b5e1f4b5017cc0a0dd4ccaac27d1
375+
```
376+
(Sanity-check from any chain that has the factory deployed:
377+
`cast call 0x0000000000006396FF2a80c067f99B3d2Ab4Df24 "initCodeHash()(bytes32)"`.)
378+
3. **Mine the salt** with foundry's `cast create2` (sufficient up to
379+
~4–5 hex chars on a laptop):
380+
```shell
381+
cast create2 \
382+
--deployer 0x0000000000006396FF2a80c067f99B3d2Ab4Df24 \
383+
--init-code-hash 0x4435a5963c29bc1c221d8cfc9c546a167425b5e1f4b5017cc0a0dd4ccaac27d1 \
384+
--starts-with 3f \
385+
--no-random
386+
```
387+
Output includes both the mined address and the salt. The factory
388+
requires the top 20 bytes of the salt to be either `address(0)` or
389+
the caller of `deployDeterministic*`. **`--no-random` is
390+
load-bearing**: without it, `cast create2` seeds the search with a
391+
random 32-byte salt, leaving random bytes in the top 20 and causing
392+
the factory to revert `SaltDoesNotStartWithCaller`. With
393+
`--no-random` the search starts at `0x0…0` and increments, so the
394+
top 20 bytes stay zero (permissionless claim) and the factory
395+
accepts from any caller.
396+
397+
To bind the salt to a specific deployer EOA, replace `--no-random`
398+
with `--caller <EOA>`: `cast` pins the top 20 bytes to that address
399+
and grinds only the trailing 12. In that case the EOA must be the
400+
actual transaction sender during both simulation (`--sender <EOA>`
401+
or matching `--private-key`) and broadcast, or the factory reverts
402+
with the same error.
403+
4. **Verify the mined salt on-chain before deploying:**
404+
```shell
405+
cast call 0x0000000000006396FF2a80c067f99B3d2Ab4Df24 \
406+
"predictDeterministicAddress(bytes32)(address)" <SALT> --rpc-url <RPC>
407+
```
408+
must equal the address `cast create2` printed. The deploy script
409+
also asserts `proxy == predictDeterministicAddress(salt)` post-deploy.
410+
5. **Deploy** with the mined salt:
411+
```shell
412+
SALT=<mined-salt> \
413+
PROXY_ADMIN=0x... \
414+
OWNER=0x... \
415+
VALIDATORS=0xaaa,0xbbb,0xccc \
416+
QUORUM=2 \
417+
COMPLIANCE_OFFICERS=0xddd \
418+
IMPLEMENTATION_SALT=0x000000000000000000000000000000000000000000000000000000000000003f \
419+
forge script script/Deploy.s.sol:DeployRequestWhitelist \
420+
--rpc-url <RPC> \
421+
--broadcast
422+
```
423+
`IMPLEMENTATION_SALT` and `COMPLIANCE_OFFICERS` are optional — omit
424+
them to take the defaults (`0x000...003f` and empty, respectively).
425+
`IMPLEMENTATION` can be set instead to reuse an existing impl and
426+
skip the CREATE2 deploy for it.
427+
428+
For deeper prefixes (6+ hex chars) or leading-zero vanity, use a GPU
429+
miner such as [`create2crunch`](https://github.com/0age/create2crunch);
430+
it consumes the same `(deployer, initCodeHash)` inputs. Mining
431+
difficulty scales by `16^k` per extra hex char, so plan accordingly.

‎script/Deploy.s.sol‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {Script} from "forge-std/Script.sol";
5+
import {console2} from "forge-std/console2.sol";
6+
7+
import {ERC1967Factory} from "solady/utils/ERC1967Factory.sol";
8+
import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol";
9+
10+
import {RequestWhitelist} from "../src/RequestWhitelist.sol";
11+
12+
/// @dev 0age's canonical `ImmutableCreate2Factory`, used to bootstrap the
13+
/// solady `ERC1967Factory` at its canonical cross-chain address and
14+
/// to CREATE2-deploy the `RequestWhitelist` implementation if the
15+
/// target chain is cold.
16+
interface IImmutableCreate2Factory {
17+
function safeCreate2(bytes32 salt, bytes calldata initializationCode) external payable returns (address);
18+
}
19+
20+
/// @title DeployRequestWhitelist
21+
/// @notice Deterministic proxy deployment of `RequestWhitelist`.
22+
///
23+
/// Every deployed artefact lives at a cross-chain-deterministic
24+
/// address:
25+
/// 1. `ERC1967Factory` — always the canonical solady address
26+
/// (`ERC1967FactoryConstants.ADDRESS`); bootstrapped via
27+
/// `ImmutableCreate2Factory` if cold.
28+
/// 2. `RequestWhitelist` implementation — CREATE2'd via
29+
/// `ImmutableCreate2Factory` at `IMPLEMENTATION_SALT`
30+
/// (default `bytes32(uint256(0x3f))`). The address is a
31+
/// function of `(create2-factory, salt, creationCode)` and
32+
/// is identical on every chain that runs this script.
33+
/// 3. Proxy — CREATE2'd via the `ERC1967Factory` at the
34+
/// user-provided `SALT`, initialised atomically in the
35+
/// same tx (closing the `initialize` front-run window).
36+
///
37+
/// The script is idempotent at the artefact level: if an
38+
/// artefact already has code at its predicted address, the
39+
/// deploy step for that artefact is skipped.
40+
///
41+
/// @dev Required env:
42+
/// - `SALT` bytes32 CREATE2 salt for the proxy.
43+
/// - `PROXY_ADMIN` address ERC-1967 admin (upgrades).
44+
/// - `OWNER` address Phase-1 application owner.
45+
/// - `VALIDATORS` address[],comma-delimited
46+
/// - `QUORUM` uint 1 <= QUORUM < validators.length
47+
///
48+
/// Optional env:
49+
/// - `IMPLEMENTATION` address Explicit impl override.
50+
/// Skips CREATE2 deployment
51+
/// entirely.
52+
/// - `IMPLEMENTATION_SALT` bytes32 CREATE2 salt for the impl.
53+
/// Default `bytes32(uint256(0x3f))`.
54+
/// Ignored if `IMPLEMENTATION`
55+
/// is set.
56+
/// - `COMPLIANCE_OFFICERS` address[],comma-delimited; empty if unset.
57+
contract DeployRequestWhitelist is Script {
58+
/// @dev Canonical address of 0age's `ImmutableCreate2Factory` on every
59+
/// EVM chain where it has been deployed.
60+
address internal constant IMMUTABLE_CREATE2_FACTORY = 0x0000000000FFe8B47B3e2130213B802212439497;
61+
62+
/// @dev Default CREATE2 salt for the implementation. The top 20 bytes
63+
/// are zero, so the salt satisfies
64+
/// `ImmutableCreate2Factory`'s "zero-prefix or caller-prefix"
65+
/// guard from any deployer.
66+
bytes32 internal constant DEFAULT_IMPLEMENTATION_SALT = bytes32(uint256(0x3f));
67+
68+
function run() external returns (ERC1967Factory factory, address implementation, address proxy) {
69+
bytes32 salt = vm.envBytes32("SALT");
70+
address proxyAdmin = vm.envAddress("PROXY_ADMIN");
71+
address owner = vm.envAddress("OWNER");
72+
uint16 quorumValue = uint16(vm.envUint("QUORUM"));
73+
address[] memory validatorSet = vm.envAddress("VALIDATORS", ",");
74+
75+
address[] memory complianceOfficers;
76+
if (vm.envExists("COMPLIANCE_OFFICERS")) {
77+
complianceOfficers = vm.envAddress("COMPLIANCE_OFFICERS", ",");
78+
} else {
79+
complianceOfficers = new address[](0);
80+
}
81+
82+
address implEnv = vm.envOr("IMPLEMENTATION", address(0));
83+
bytes32 implSalt = vm.envOr("IMPLEMENTATION_SALT", DEFAULT_IMPLEMENTATION_SALT);
84+
85+
bytes memory initData =
86+
abi.encodeCall(RequestWhitelist.initialize, (owner, validatorSet, quorumValue, complianceOfficers));
87+
88+
vm.startBroadcast();
89+
90+
factory = _canonicalFactory();
91+
implementation = implEnv != address(0) ? implEnv : _deterministicImplementation(implSalt);
92+
93+
address predicted = factory.predictDeterministicAddress(salt);
94+
proxy = factory.deployDeterministicAndCall(implementation, proxyAdmin, salt, initData);
95+
96+
vm.stopBroadcast();
97+
98+
require(proxy == predicted, "deterministic address mismatch");
99+
100+
console2.log("ERC1967Factory:", address(factory));
101+
console2.log("Implementation:", implementation);
102+
console2.log("Proxy: ", proxy);
103+
}
104+
105+
/// @dev Returns the canonical `ERC1967Factory`, deploying it via
106+
/// `ImmutableCreate2Factory` if the target chain hasn't seen it
107+
/// yet. Reverts with a clear message if even the underlying
108+
/// create2 factory is missing — a fully cold chain needs one
109+
/// bootstrap step the script cannot do for you.
110+
function _canonicalFactory() internal returns (ERC1967Factory) {
111+
address canonical = ERC1967FactoryConstants.ADDRESS;
112+
if (canonical.code.length != 0) return ERC1967Factory(canonical);
113+
114+
_requireCreate2Factory();
115+
address deployed = IImmutableCreate2Factory(IMMUTABLE_CREATE2_FACTORY)
116+
.safeCreate2(ERC1967FactoryConstants.SALT, ERC1967FactoryConstants.INITCODE);
117+
require(deployed == canonical, "canonical factory address mismatch");
118+
return ERC1967Factory(canonical);
119+
}
120+
121+
/// @dev Returns the CREATE2-deployed `RequestWhitelist` implementation
122+
/// at `salt`, deploying it via `ImmutableCreate2Factory` if the
123+
/// target chain hasn't seen this salt before. Idempotent: a
124+
/// previously-deployed impl at the predicted address is reused.
125+
function _deterministicImplementation(bytes32 salt) internal returns (address impl) {
126+
bytes memory initcode = type(RequestWhitelist).creationCode;
127+
address predicted = vm.computeCreate2Address(salt, keccak256(initcode), IMMUTABLE_CREATE2_FACTORY);
128+
if (predicted.code.length != 0) return predicted;
129+
130+
_requireCreate2Factory();
131+
impl = IImmutableCreate2Factory(IMMUTABLE_CREATE2_FACTORY).safeCreate2(salt, initcode);
132+
require(impl == predicted, "implementation address mismatch");
133+
}
134+
135+
function _requireCreate2Factory() internal view {
136+
require(
137+
IMMUTABLE_CREATE2_FACTORY.code.length != 0,
138+
"ImmutableCreate2Factory not deployed; bootstrap it first (see 0age/ImmutableCreate2Factory)"
139+
);
140+
}
141+
}

0 commit comments

Comments
 (0)