@warptoad/skinny-fat-imt-js 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CREATE2.md +218 -0
  2. package/README.md +47 -3
  3. package/create2-artifacts/FatIMTPoseidon2Read.create2.json +268 -0
  4. package/create2-artifacts/FatIMTPoseidon2WriteEvent.create2.json +320 -0
  5. package/create2-artifacts/FatIMTPoseidon2WriteStorage.create2.json +320 -0
  6. package/create2-artifacts/SkinnyIMTPoseidon2Read.create2.json +258 -0
  7. package/create2-artifacts/SkinnyIMTPoseidon2WriteEvent.create2.json +301 -0
  8. package/create2-artifacts/SkinnyIMTPoseidon2WriteStorage.create2.json +301 -0
  9. package/create2-artifacts/create2-salts.json +26 -0
  10. package/dist/Trees.d.ts +5 -68
  11. package/dist/Trees.d.ts.map +1 -1
  12. package/dist/Trees.js +78 -41
  13. package/dist/Trees.js.map +1 -1
  14. package/dist/abis.js +1 -1
  15. package/dist/abis.js.map +1 -1
  16. package/dist/create2/create2.d.ts +144 -0
  17. package/dist/create2/create2.d.ts.map +1 -0
  18. package/dist/create2/create2.js +436 -0
  19. package/dist/create2/create2.js.map +1 -0
  20. package/dist/create2/index.d.ts +4 -0
  21. package/dist/create2/index.d.ts.map +1 -0
  22. package/dist/create2/index.js +4 -0
  23. package/dist/create2/index.js.map +1 -0
  24. package/dist/create2/interfaces.d.ts +79 -0
  25. package/dist/create2/interfaces.d.ts.map +1 -0
  26. package/dist/create2/interfaces.js +6 -0
  27. package/dist/create2/interfaces.js.map +1 -0
  28. package/dist/create2/types.d.ts +73 -0
  29. package/dist/create2/types.d.ts.map +1 -0
  30. package/dist/create2/types.js +4 -0
  31. package/dist/create2/types.js.map +1 -0
  32. package/dist/eventScanning.d.ts.map +1 -1
  33. package/dist/eventScanning.js +2 -2
  34. package/dist/eventScanning.js.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/types.d.ts +71 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +12 -0
  42. package/dist/types.js.map +1 -0
  43. package/package.json +9 -2
  44. package/src/Trees.ts +101 -99
  45. package/src/abis.ts +1 -1
  46. package/src/create2/create2.ts +643 -0
  47. package/src/create2/index.ts +3 -0
  48. package/src/create2/interfaces.ts +69 -0
  49. package/src/create2/types.ts +88 -0
  50. package/src/eventScanning.ts +31 -28
  51. package/src/index.ts +1 -0
  52. package/src/types.ts +71 -0
package/CREATE2.md ADDED
@@ -0,0 +1,218 @@
1
+ # `@warptoad/skinny-fat-imt-js/create2`
2
+ TODO way too much text. Re write this claude slop
3
+
4
+
5
+ Deterministic CREATE2 deployment for **any** Hardhat 3 contract — not just the IMT libraries.
6
+
7
+ Freeze a contract once into a self-contained JSON artifact, then deploy it to the same address on
8
+ every chain, mine a vanity salt for it, and verify it on Etherscan and Sourcify. Nothing downstream
9
+ ever recompiles.
10
+
11
+ ## Why freezing is necessary
12
+
13
+ A CREATE2 address is a function of the exact init code, and init code is **not** reproducible by
14
+ asking someone to compile the same source. It depends on the solc version, every optimizer setting,
15
+ the evm target, and — because solc's metadata hash covers the source paths — on the exact paths the
16
+ build assigned. Hardhat puts npm package versions in those paths, so bumping a dependency with
17
+ byte-identical Solidity still moves the address.
18
+
19
+ So `makeCreate2()` captures the bytes *and* the solc input that produced them. Everything after that
20
+ works off the frozen object.
21
+
22
+ ```
23
+ makeCreate2() build time, needs Hardhat → Create2Artifact (plain JSON)
24
+ predictCreate2Address() pure
25
+ deployCreate2() runtime, viem only
26
+ verifyOnEtherscan() runtime, fetch only
27
+ verifyOnSourcify() runtime, fetch only
28
+ ```
29
+
30
+ Only `makeCreate2` touches Hardhat, through a dynamic import — consumers who just deploy a published
31
+ artifact never need Hardhat installed.
32
+
33
+ ## 1. Freeze
34
+
35
+ Compile with the build profile you intend to publish, then:
36
+
37
+ ```ts
38
+ import { makeCreate2 } from "@warptoad/skinny-fat-imt-js/create2";
39
+ import { writeFile } from "node:fs/promises";
40
+
41
+ const artifact = await makeCreate2("MyContract");
42
+ await writeFile("MyContract.create2.json", JSON.stringify(artifact, null, 2));
43
+ ```
44
+
45
+ Run it inside your Hardhat project (`npx hardhat run scripts/freeze.ts`). Hardhat keeps a single
46
+ `artifacts/` tree rather than one per profile, so **whichever profile you compiled last is the one
47
+ that gets frozen**. Pin every setting in that profile — anything left to a solc default will move the
48
+ address on the next toolchain bump.
49
+
50
+ Constructor arguments and libraries are part of the init code, so they are frozen too:
51
+
52
+ ```ts
53
+ await makeCreate2("MyToken", { constructorArgs: ["Token", "TKN", 18n] });
54
+
55
+ await makeCreate2("SkinnyFat", {
56
+ libraries: { FatIMTPoseidon2WriteStorage: "0x…", /* … */ },
57
+ });
58
+ ```
59
+
60
+ Unlinked bytecode still contains `__$…$__` placeholders and is not deployable, so a contract with
61
+ libraries throws until you supply them. Deploy and mine the libraries first — their addresses become
62
+ part of this contract's init code, and therefore of its address.
63
+
64
+ Without a Hardhat project, pass the JSON yourself:
65
+
66
+ ```ts
67
+ await makeCreate2({ artifact, buildInfo, buildInfoOutput });
68
+ ```
69
+
70
+ ## 2. Mine a vanity salt (optional)
71
+
72
+ ```ts
73
+ import { create2MiningTarget } from "@warptoad/skinny-fat-imt-js/create2";
74
+
75
+ console.log(create2MiningTarget(artifact));
76
+ // { factory: "0x4e59…", initCodeHash: "0x…" }
77
+ ```
78
+
79
+ Feed those to `create2crunch` / `createXcrunch`. Any salt works if you don't care about the address —
80
+ `keccak256(toBytes("v1"))` is fine.
81
+
82
+ The default factory doesn't bind the salt to a sender, so anyone can front-run the deployment. That's
83
+ harmless: identical init code at the same address is the outcome you wanted anyway.
84
+
85
+ > Leading zeros in a **library** address buy essentially nothing in gas — a linked library address is
86
+ > embedded in code as `PUSH20`, not passed in calldata, and code deposit has no zero-byte discount.
87
+ > Vanity here is branding.
88
+
89
+ ## 3. Deploy
90
+
91
+ ```ts
92
+ import { deployCreate2 } from "@warptoad/skinny-fat-imt-js/create2";
93
+
94
+ const { address, alreadyDeployed } = await deployCreate2({
95
+ artifact, salt, walletClient, publicClient, // factory defaults to 0x4e59…
96
+ });
97
+ ```
98
+
99
+ Idempotent — if the address already holds code it returns without sending anything, which is the
100
+ normal case on a chain where someone deployed before you.
101
+
102
+ **On gas:** a failed CREATE2 *does not revert the caller*. The factory's inner create returns the zero
103
+ address, the factory returns that, and your transaction succeeds — burning gas and deploying nothing.
104
+ EIP-150 forwards only 63/64 of remaining gas to the inner call, so a limit that looks sufficient can
105
+ leave the create just short. This bit us on a real 20KB library, where Hardhat's automatic gas came in
106
+ below the estimate and produced a "successful" transaction with no contract. `deployCreate2` therefore
107
+ estimates with headroom and re-reads the code afterwards, turning the silent failure into a thrown
108
+ error. Pass `gas` to override.
109
+
110
+ ## 4. Verify
111
+
112
+ Both clients handle submission and polling themselves, from the frozen `solcInput` — so they work on
113
+ a machine with no sources and no compiler.
114
+
115
+ ```ts
116
+ import { verifyOnEtherscan, verifyOnSourcify } from "@warptoad/skinny-fat-imt-js/create2";
117
+
118
+ await verifyOnEtherscan({ artifact, address, chainId, apiKey });
119
+ await verifyOnSourcify({ artifact, address, chainId });
120
+ ```
121
+
122
+ Both return `{ outcome: "verified" | "already-verified" | "failed", message }`. Sourcify also returns
123
+ `match`, which is `"exact_match"` when the metadata hash matched too — that only happens if your
124
+ profile kept it (`bytecodeHash` other than `"none"`). Etherscan uses the v2 multichain API, so one key
125
+ covers every supported chain.
126
+
127
+ If you're verifying a contract in the same repo that compiled it, `npx hardhat verify` works too and
128
+ Nomic maintains the explorer clients. These functions exist for everyone else: `hardhat-verify`
129
+ resolves the solc input from local build-info, so it can't help someone deploying your published
130
+ artifact from a repo that doesn't contain your sources.
131
+
132
+ ## 5. Check a deployment independently
133
+
134
+ ```ts
135
+ import { matchesOnchainBytecode } from "@warptoad/skinny-fat-imt-js/create2";
136
+
137
+ const code = await publicClient.getCode({ address });
138
+ matchesOnchainBytecode(artifact, code, address);
139
+ ```
140
+
141
+ Don't compare `deployedBytecode` directly — it never equals what's on chain, for two reasons that
142
+ aren't defects. solc protects public library functions from direct `CALL`s by opening the runtime code
143
+ with `PUSH20 <own address>; ADDRESS; EQ` and patching the real address in at construction; and
144
+ immutables are written the same way. Both are zeros in the compiler's output. `matchesOnchainBytecode`
145
+ splices the address in and masks the immutables before comparing.
146
+
147
+ ## This package's own frozen artifacts
148
+
149
+ The IMT libraries are published pre-frozen, one JSON per contract, under
150
+ `@warptoad/skinny-fat-imt-js/create2/evm-artifacts/<Contract>`:
151
+
152
+ ```ts
153
+ import type { Create2Artifact } from "@warptoad/skinny-fat-imt-js/create2";
154
+ import artifact from "@warptoad/skinny-fat-imt-js/create2/evm-artifacts/SkinnyIMTPoseidon2Read"
155
+ with { type: "json" };
156
+
157
+ await deployCreate2({ artifact: artifact as Create2Artifact, salt, walletClient, publicClient });
158
+ ```
159
+
160
+ Available: `SkinnyIMTPoseidon2Read`, `SkinnyIMTPoseidon2WriteEvent`, `SkinnyIMTPoseidon2WriteStorage`,
161
+ `FatIMTPoseidon2Read`, `FatIMTPoseidon2WriteEvent`, `FatIMTPoseidon2WriteStorage` — the same files
162
+ `npx hardhat gen-artifact-create2` writes to `create2-artifacts/`. A JSON import widens `0x…` strings
163
+ to `string`, hence the `as Create2Artifact`. Deploying one on a new chain reproduces the addresses
164
+ these were mined for, as long as the salt matches — the salts are published alongside them, in
165
+ `create2-artifacts/create2-salts.json`.
166
+
167
+ ## The tasks in this repo
168
+
169
+ Four tasks, in the order you'd run them. Each defaults to the `CONTRACTS` list in
170
+ `tasks/create2Config.ts`, and each takes names to narrow it down.
171
+
172
+ ```sh
173
+ npx hardhat mine-create2 --zeros 6 # salts -> create2-artifacts/create2-salts.json
174
+ npx hardhat gen-artifact-create2 # artifacts -> create2-artifacts/
175
+ npx hardhat deploy-create2 --network sepolia
176
+ npx hardhat verify-create2 --network sepolia
177
+ ```
178
+
179
+ Only the last two touch a network. `mine-create2` searches on every core and skips any contract
180
+ whose stored salt still lands on its stored address with enough zeros — ask for more zeros to redo
181
+ one. `deploy-create2` reads the frozen JSON off disk rather than recompiling, so what lands on chain
182
+ is what was frozen, published and verified. Salts come from `create2-salts.json`; `--salt` overrides
183
+ for every contract in the run.
184
+
185
+ ## What invalidates an address
186
+
187
+ All of these change the init code, none of them break a build, and each needs the salt re-mined:
188
+
189
+ - a dependency bump — **even to a version with byte-identical Solidity**, since the version is in the
190
+ source path the metadata hash covers
191
+ - a comment or whitespace edit anywhere in the compilation unit
192
+ - any change to the build profile, or a solc upgrade
193
+ - different constructor arguments, or different library addresses
194
+
195
+ Worth a CI check that recompiles and asserts the frozen `initCodeHash` still matches, so a change
196
+ that moves an address can't land unnoticed.
197
+
198
+ ## Chain caveats
199
+
200
+ - The default factory must exist on the target chain. `deployCreate2` throws with a clear message if
201
+ it doesn't. A different factory yields a different address.
202
+ - zkSync-family chains derive CREATE2 differently and cannot match these addresses at all.
203
+ - If your profile targets `cancun`, the bytecode may use `PUSH0`, which reverts on pre-Shanghai
204
+ chains. Pin `evmVersion: "paris"` for maximum reach — and decide before you mine, since changing it
205
+ moves every address.
206
+
207
+ ## API
208
+
209
+ | Export | |
210
+ | --- | --- |
211
+ | `makeCreate2(target, options?)` | Freeze a contract into a `Create2Artifact` |
212
+ | `predictCreate2Address({ initCodeHash, salt, factory? })` | Pure address derivation |
213
+ | `create2MiningTarget(artifact, factory?)` | Inputs for a vanity miner |
214
+ | `deployCreate2({ artifact, salt, walletClient, publicClient, factory?, gas? })` | Idempotent deploy |
215
+ | `matchesOnchainBytecode(artifact, code, address)` | Compare against chain state |
216
+ | `verifyOnEtherscan({ artifact, address, chainId, apiKey, … })` | Submit + poll |
217
+ | `verifyOnSourcify({ artifact, address, chainId, … })` | Submit + poll |
218
+ | `DEFAULT_CREATE2_FACTORY` | `0x4e59b44847b379578588920cA78FbF26c0B4956C` |
package/README.md CHANGED
@@ -4,6 +4,9 @@ JS library counterpart of the [skinny-fat-imt](https://github.com/warptoad/skinn
4
4
 
5
5
  Reads a contract's [LeanIMT](https://github.com/privacy-scaling-explorations/zk-kit) state over viem — from storage or from events, whichever the contract exposes — and keeps a local mirror in sync.
6
6
 
7
+ ## TODO
8
+ Decide where to keep the create2 artifacts and all that byte code. Using this package in frontend also downloads all that? Probably less of an issues when using bundlers?
9
+
7
10
  ## Install
8
11
 
9
12
  ```sh
@@ -46,6 +49,26 @@ import { queryEventInChunks, queryMultiEventsInChunks, minBigInt } from "@warpto
46
49
  | --- | --- |
47
50
  | `@warptoad/skinny-fat-imt-js` | `Trees`, `identifyTree`, `getEventFilter`, `copyTree`, `ERC165_IDS`, the ABIs, `getInterfaceId`, `DEPLOY_BLOCK`, … |
48
51
  | `@warptoad/skinny-fat-imt-js/event-scanning` | `queryEventInChunks`, `queryMultiEventsInChunks`, `minBigInt`, `EventLog`, `PostQueryEventFilter` |
52
+ | `@warptoad/skinny-fat-imt-js/create2` | `makeCreate2`, `deployCreate2`, `predictCreate2Address`, `create2MiningTarget`, `verifyOnEtherscan`, `verifyOnSourcify`, `matchesOnchainBytecode` |
53
+ | `@warptoad/skinny-fat-imt-js/create2/evm-artifacts/<Contract>` | The frozen `Create2Artifact` JSON for each shipped IMT library |
54
+
55
+ ## Deterministic CREATE2 for any contract
56
+
57
+ `/create2` is a general toolkit, independent of the IMT libraries — point it at any Hardhat 3 contract:
58
+
59
+ ```ts
60
+ const artifact = await makeCreate2("MyContract", { constructorArgs: [...] });
61
+ const { address } = await deployCreate2({ artifact, salt, walletClient, publicClient });
62
+ await verifyOnEtherscan({ artifact, address, chainId, apiKey });
63
+ ```
64
+
65
+ `makeCreate2` freezes the init code together with the solc input that produced it, so nothing downstream recompiles and the address holds on every chain. The IMT libraries ship pre-frozen, so bringing them to a new chain needs no compiler:
66
+
67
+ ```ts
68
+ import artifact from "@warptoad/skinny-fat-imt-js/create2/evm-artifacts/SkinnyIMTPoseidon2Read" with { type: "json" };
69
+ ```
70
+
71
+ See **[CREATE2.md](CREATE2.md)**.
49
72
 
50
73
  ## Development
51
74
 
@@ -53,13 +76,13 @@ import { queryEventInChunks, queryMultiEventsInChunks, minBigInt } from "@warpto
53
76
  pnpm install
54
77
  pnpm compile # hardhat compile, then regenerate src/abis.ts
55
78
  pnpm test # hardhat test
56
- pnpm typecheck # tsc over src + test + hardhat.config.ts
79
+ pnpm typecheck # tsc over src + test + scripts + hardhat.config.ts
57
80
  pnpm build # emit dist/ from src/ via tsconfig.build.json
58
81
  ```
59
82
 
60
83
  ### About `src/abis.ts`
61
84
 
62
- `src/abis.ts` is **generated and committed**. `scripts/genAbis.mjs` copies nine ABIs out of hardhat's `artifacts/` into `as const` tuples.
85
+ `src/abis.ts` is **generated and committed**. `scripts/genAbis.ts` copies nine ABIs out of hardhat's `artifacts/` into `as const` tuples.
63
86
 
64
87
  The library deliberately does not import `artifacts/` directly, even though that's the obvious thing to do. `artifacts/` is gitignored, ~13MB, and hardhat's generated `artifacts.d.ts` files end with:
65
88
 
@@ -71,6 +94,14 @@ Shipping that would make the published typings require `hardhat` to be resolvabl
71
94
 
72
95
  **Whenever the contracts change, run `pnpm compile` and commit the resulting `src/abis.ts` diff.** Nothing regenerates it automatically at publish time — that's on purpose, so `npm publish` can never silently rewrite source.
73
96
 
97
+ ## deployment
98
+ ```
99
+ pnpm hardhat gen-artifact-create2 --network sepolia;
100
+ pnpm hardhat mine-create2 --network sepolia --zeros 6;
101
+ pnpm hardhat deploy-create2 --network sepolia;
102
+ pnpm hardhat verify-create2 --network sepolia;
103
+ ```
104
+
74
105
  ## Publishing to npm
75
106
 
76
107
  ### One-time setup
@@ -110,7 +141,7 @@ npm whoami # confirm the right account
110
141
  npm pack --dry-run
111
142
  ```
112
143
 
113
- Expect `dist/**`, `src/**`, `README.md`, `LICENSE`, `package.json` — about 50KB. If you see `artifacts/` or a multi-MB tarball, something is wrong with `files` in package.json.
144
+ Expect `dist/**`, `src/**`, `README.md`, `CREATE2.md`, `LICENSE`, `package.json` — about 50KB. If you see `artifacts/` or a multi-MB tarball, something is wrong with `files` in package.json.
114
145
 
115
146
  4. Publish. `prepublishOnly` runs `pnpm build` (a clean `rm -rf dist` then `tsc -p tsconfig.build.json`), so `dist/` is always rebuilt from current source:
116
147
 
@@ -143,9 +174,22 @@ pnpm add @warptoad/skinny-fat-imt-js viem
143
174
  node -e "import('@warptoad/skinny-fat-imt-js').then(m => console.log(Object.keys(m)))"
144
175
  ```
145
176
 
177
+
178
+
179
+
146
180
  ### Notes
147
181
 
148
182
  - **You cannot republish a version.** Once `0.0.1` is out, `0.0.1` is taken forever, even if you unpublish. Bump and move on.
149
183
  - `npm unpublish` is only allowed within 72 hours, and only if nothing depends on it. Use `npm deprecate` instead for anything older.
150
184
  - If publish fails with `402 Payment Required`, `publishConfig.access` got lost — scoped packages need `"access": "public"`.
151
185
  - If it fails with `404`, you're either not logged in or not a member of the `@warptoad` scope.
186
+
187
+ ## Deployed addresses sepolia
188
+ ```
189
+ SkinnyIMTPoseidon2WriteStorage 0x00000066C760D24272a9E8A424EF7233A0F0da83
190
+ SkinnyIMTPoseidon2WriteEvent 0x000000D74387530f88b9Cf1A23CDf16fdDC01bf6
191
+ SkinnyIMTPoseidon2Read 0x000000A3a94867DD396753dEFABF989246211329
192
+ FatIMTPoseidon2WriteStorage 0x000000F35EFB6F537d3663CF263c04f467f48c73
193
+ FatIMTPoseidon2WriteEvent 0x00000012de70B336818C26517A4B2d7c0B949C7b
194
+ FatIMTPoseidon2Read 0x000000FF3329863F1Eb55773aDEF01863791370
195
+ ```