Skip to content

Commit a9c6fcd

Browse files
committed
Initial commit
0 parents  commit a9c6fcd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+34292
-0
lines changed

.github/workflows/test.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: CI
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
push:
8+
pull_request:
9+
workflow_dispatch:
10+
11+
env:
12+
FOUNDRY_PROFILE: ci
13+
14+
jobs:
15+
foundry:
16+
name: Foundry project
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v5
20+
with:
21+
persist-credentials: false
22+
submodules: recursive
23+
24+
- name: Install Foundry
25+
uses: foundry-rs/foundry-toolchain@v1
26+
27+
- name: Show Forge version
28+
run: forge --version
29+
30+
- name: Run Forge fmt
31+
run: forge fmt --check
32+
33+
- name: Run Forge build
34+
run: forge build --sizes
35+
36+
- name: Run Forge tests
37+
run: forge test -vvv
38+
39+
node-cli:
40+
name: Node CLI
41+
runs-on: ubuntu-latest
42+
defaults:
43+
run:
44+
working-directory: tools/CREATE4-cli
45+
steps:
46+
- uses: actions/checkout@v5
47+
with:
48+
persist-credentials: false
49+
submodules: recursive
50+
51+
- name: Setup Node.js
52+
uses: actions/setup-node@v4
53+
with:
54+
node-version: 20
55+
cache: npm
56+
cache-dependency-path: tools/CREATE4-cli/package-lock.json
57+
58+
- name: Install dependencies
59+
run: npm ci
60+
61+
- name: Run lint
62+
run: npm run lint
63+
64+
- name: Run build
65+
run: npm run build
66+
67+
- name: Run tests
68+
run: npm test

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Compiler files
2+
cache/
3+
out/
4+
node_modules/
5+
**/node_modules/
6+
7+
# Ignores development broadcast logs
8+
!/broadcast
9+
/broadcast/*/31337/
10+
/broadcast/**/dry-run/
11+
12+
# Docs
13+
docs/
14+
15+
# Dotenv file
16+
.env

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "lib/forge-std"]
2+
path = lib/forge-std
3+
url = https://github.com/foundry-rs/forge-std

README.md

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# CREATE4 - EVM Universal Binary Deployer
2+
3+
CREATE4 is an ethereum universal deployer that lets you trustlessly deploy, to the same address, contracts that have different binaries on each chain. It lets you keep one canonical address while customizing the bytecode per network.
4+
5+
## Abstract
6+
7+
There are hundreds of EVM-compatible chains. CREATE2 and CREATE3-style universal deployers are commonly used to deploy the same bytecode to a fixed address across networks, which is great when you want identical behavior everywhere.
8+
9+
The catch is that existing patterns require all deployments to share the exact same bytecode, so you end up targeting the lowest common denominator of the EVM versions in use.
10+
11+
CREATE4 drops that constraint. It uses CREATE3 to decouple the address from the bytecode, and a factory that re-couples the address to a “deployment plan”: a Merkle tree of per-chain init codes plus a global fallback. The plan is committed to on-chain, and the same plan can be deployed consistently from any chain.
12+
13+
### Deployment address
14+
15+
The deployment plan root is computed as a Merkle tree where:
16+
17+
- `leaf = keccak256(pack(chainId, nextChainId, isFallback) ++ keccak256(initCode))`
18+
- `parent(a,b) = keccak256(min(a,b) ++ max(a,b))`
19+
20+
Given `root` and `userSalt`, the deployed address is derived as:
21+
22+
- `createSalt = keccak256(root ++ userSalt)`
23+
- `proxy = keccak256(0xff ++ factory ++ createSalt ++ KECCAK256_PROXY_CHILD_BYTECODE)[12:]`
24+
- `addr = keccak256(0xd694 ++ proxy ++ 0x01)[12:]`
25+
26+
Where:
27+
28+
- `KECCAK256_PROXY_CHILD_BYTECODE = keccak256(0x67363d3d37363d34f03d5260086018f3) = 0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f`.
29+
30+
### Gap semantics
31+
32+
Every non-fallback leaf defines a “gap” that determines whether the fallback bytecode can be deployed on the current chain. When `chainId < nextChainId`, the gap is the open interval `(chainId, nextChainId)` (exclusive on both ends). The final entry in the sorted plan wraps back to the smallest chain id, so when `chainId > nextChainId` the gap covers _two_ segments: any id strictly greater than `chainId` or strictly smaller than `nextChainId`. This wrap-around behavior makes sure all undefined chains can still deploy the fallback, even when they sit “past” the highest listed chain id. Plans with only one chain entry have `chainId == nextChainId`; their gap is interpreted as “any chain id other than this one”, so the fallback remains deployable everywhere else.
33+
34+
The CLI `view` command prints these gaps, and the JavaScript library exposes helpers (`isChainIdInGap` / `describeGapRange`) so downstream tooling can reason about wrap-around intervals without re-implementing the logic.
35+
36+
#### Features
37+
38+
- Deployment plan specifies what bytecode to deploy on each network
39+
- Fallback bytecode for any network not specifically defined
40+
- Lightweight and without clutter
41+
- Supports any EVM-compatible chain that implements `CREATE2` and `CHAINID`
42+
- Merkle proofs keep gas overhead low
43+
- Constructors fully supported
44+
- Standard contract init code (Etherscan-style verification supported)
45+
- Deterministic address across chains for a given factory + plan root + user salt
46+
- CLI to build plans, compute addresses, and export per-chain proofs
47+
- Agnostic to tooling: works with Solidity, Yul, Huff, or any source that produces init code
48+
49+
#### Limitations
50+
51+
- More expensive than `CREATE`, `CREATE2` and `CREATE3`
52+
- Requires off-chain tooling to construct the deployment plan and proofs
53+
- Plan semantics are “garbage in, garbage out”: the contract does not verify that the tree is well-formed or non-malleable
54+
- Changing the plan (e.g. new per-chain bytecode) requires a new plan root, and thus a new CREATE3 salt or a new factory/plan combo (address)
55+
56+
57+
## Use cases
58+
59+
### Multiple EVM version targeting
60+
61+
New EVM versions keep adding opcodes like `PUSH0` (Shanghai) or `MCOPY` (Cancun) that let you implement the same logic more cheaply. Chains adopt these hard forks at different times, and some never do.
62+
63+
If you need one shared address today, you typically compile everything against the oldest EVM you care about. With CREATE4, you can ship a “best EVM per chain” version while using the fallback only where nothing better is available.
64+
65+
### L1 / L2 / L3 specific optimizations
66+
67+
Different networks meter gas differently. Some L1s make calldata cheap, some L2s make compute cheap, and so on.
68+
69+
CREATE4 lets you deploy variants of the same contract that are tuned to each chain’s gas model (while preserving the address), instead of compromising on a single “okay everywhere, great nowhere” implementation.
70+
71+
### Non-symmetric contracts
72+
73+
Sometimes you do not want identical behavior on every chain. For example, on a “parent” chain you might have the canonical ERC20, and on other chains you want a bridge representation or a wrapper with extra logic.
74+
75+
CREATE4 lets you keep one shared address while deploying non-symmetric implementations: same address, different behavior per chain, defined and committed to by the deployment plan.
76+
77+
## Usage
78+
79+
### CLI
80+
81+
The CLI works over JSON specs of the form:
82+
83+
```json
84+
{
85+
"salt": "0x1111...1111",
86+
"chains": [
87+
{ "chainId": 1, "label": "alpha", "initCode": "0x..." },
88+
{ "chainId": 10, "label": "beta", "initCode": "0x..." }
89+
],
90+
"fallbackInitCode": "0x..."
91+
}
92+
```
93+
94+
Chain IDs in specs may be provided as numbers when they are within JavaScript’s safe integer range, but for the full
95+
uint64 space you should quote them (decimal or `0x` strings both work). CLI and library outputs always return chain IDs
96+
as decimal strings to avoid silent precision loss.
97+
98+
Build a plan (root + leaves + fallback):
99+
100+
```sh
101+
CREATE4-plan build --input ./spec.json --pretty > ./plan.json
102+
```
103+
104+
Compute the CREATE3 child address for a factory + plan:
105+
106+
```sh
107+
CREATE4-plan address \
108+
--input ./spec.json \
109+
--factory 0x1111111111111111111111111111111111111111
110+
```
111+
112+
Override the plan salt when computing the deployment:
113+
114+
```sh
115+
CREATE4-plan address \
116+
--input ./spec.json \
117+
--factory 0x1111111111111111111111111111111111111111 \
118+
--salt 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
119+
```
120+
121+
Get the inclusion proof and leaf data for a specific chain:
122+
123+
```sh
124+
CREATE4-plan proof \
125+
--input ./spec.json \
126+
--chain 10 \
127+
--pretty > ./proof-10.json
128+
```
129+
130+
Human-readable view of the plan:
131+
132+
```sh
133+
CREATE4-plan view --input ./spec.json
134+
CREATE4-plan view --input ./spec.json --proofs # also print Merkle proofs
135+
```
136+
137+
Interactive editing workflow (no manual JSON editing needed):
138+
139+
```sh
140+
# Create a new editable spec file
141+
CREATE4-plan edit create --file deployment-plan.edit.json --name "My Plan"
142+
143+
# Add chains and fallback from build artifacts or inline bytecode
144+
CREATE4-plan edit add --file deployment-plan.edit.json --chain 1 --code 0x...
145+
CREATE4-plan edit add --file deployment-plan.edit.json --chain 10 --code-file ./MyContract.json
146+
CREATE4-plan edit add --file deployment-plan.edit.json --fallback --code 0x...
147+
148+
# Inspect the editable plan
149+
CREATE4-plan edit view --file deployment-plan.edit.json
150+
151+
# Build a finalized plan from the editable spec
152+
CREATE4-plan build --input deployment-plan.edit.json --pretty > plan.json
153+
```
154+
155+
### Library (Node.js)
156+
157+
Install in your project:
158+
159+
```sh
160+
npm install @0xsequence/CREATE4
161+
```
162+
163+
Basic plan build + deployment address:
164+
165+
```js
166+
const {
167+
buildPlanFromSpec,
168+
computePlanDeployment,
169+
getChainProof,
170+
computeCreate3Address,
171+
deriveDeploymentSalt,
172+
isChainIdInGap,
173+
describeGapRange,
174+
} = require('@0xsequence/CREATE4');
175+
176+
const spec = {
177+
salt: '0x1111111111111111111111111111111111111111111111111111111111111111',
178+
chains: [
179+
{ chainId: 1, label: 'mainnet', initCode: '0x...' },
180+
{ chainId: 10, label: 'optimism', initCode: '0x...' },
181+
],
182+
fallbackInitCode: '0x...',
183+
};
184+
// chainId values can also be decimal/hex strings or BigInts; plan outputs always use decimal strings.
185+
186+
// Build the plan (same shape as CLI build output)
187+
const plan = buildPlanFromSpec(spec);
188+
// plan.root, plan.leaves[], plan.fallback, plan.salt
189+
190+
// Compute CREATE3 deployment details for a factory
191+
const factory = '0x1111111111111111111111111111111111111111';
192+
const deployment = computePlanDeployment(spec, factory);
193+
// deployment.address -> CREATE3 child address
194+
// deployment.planRoot -> plan.root
195+
// deployment.salt -> effective salt
196+
// deployment.deploymentSalt-> keccak256(planRoot, salt)
197+
198+
// Derive the CREATE3 deployment salt directly (if needed)
199+
const deploymentSalt = deriveDeploymentSalt(plan.root, plan.salt);
200+
const sameAddress = computeCreate3Address(factory, deploymentSalt);
201+
```
202+
203+
Get the proof and leaf data for a specific chain (to send on-chain to `CREATE4.deploy` or `deployFallback`):
204+
205+
```js
206+
const proof = getChainProof(spec, 10);
207+
/*
208+
proof = {
209+
root,
210+
chainId,
211+
nextChainId,
212+
prefix,
213+
initCode,
214+
initCodeHash,
215+
leafHash,
216+
proof, // bytes32[] as 0x-prefixed strings
217+
salt,
218+
}
219+
*/
220+
221+
// Inspecting gap coverage for the fallback
222+
const canFallbackOn120 = isChainIdInGap(proof.chainId, proof.nextChainId, 120);
223+
const gapSummary = describeGapRange(proof.chainId, proof.nextChainId);
224+
console.log({ canFallbackOn120, gapSummary });
225+
```
226+
227+
# License - MIT
228+
229+
```
230+
MIT License
231+
232+
Copyright (c) 2025 Sequence Platforms Inc.
233+
234+
Permission is hereby granted, free of charge, to any person obtaining a copy
235+
of this software and associated documentation files (the "Software"), to deal
236+
in the Software without restriction, including without limitation the rights
237+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
238+
copies of the Software, and to permit persons to whom the Software is
239+
furnished to do so, subject to the following conditions:
240+
241+
The above copyright notice and this permission notice shall be included in all
242+
copies or substantial portions of the Software.
243+
244+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
245+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
246+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
247+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
248+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
249+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
250+
SOFTWARE.
251+
```

foundry.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"lib/forge-std": {
3+
"tag": {
4+
"name": "v1.11.0",
5+
"rev": "8e40513d678f392f398620b3ef2b418648b33e89"
6+
}
7+
}
8+
}

foundry.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[profile.default]
2+
src = "src"
3+
out = "out"
4+
libs = ["lib"]
5+
evm_version = "istanbul"
6+
ffi = true
7+
fs_permissions = [
8+
{ access = "read", path = "./test/fixtures" },
9+
{ access = "read-write", path = "./cache" }
10+
]
11+
12+
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

lib/forge-std/.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/Vm.sol linguist-generated

lib/forge-std/.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @danipopes @klkvr @mattsse @grandizzy @yash-atreya @zerosnacks @onbjerg @0xrusowsky
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "github-actions"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"

0 commit comments

Comments
 (0)