@bananapus/omnichain-deployers-v6 0.0.19 → 0.0.21

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.
@@ -1,386 +1,72 @@
1
- # nana-omnichain-deployers-v6 -- Audit Instructions
1
+ # Audit Instructions
2
2
 
3
- ## Previous Audit Findings
3
+ This repo launches projects that are immediately composed with 721 hooks and sucker deployments. Audit it as a privileged deployer and runtime data-hook participant.
4
4
 
5
- No prior formal audit with finding IDs has been conducted on this repository. Known risks and design trade-offs are documented in [`RISKS.md`](./RISKS.md).
5
+ ## Objective
6
6
 
7
- ## Compiler and Version Info
8
-
9
- Settings from `foundry.toml`:
10
-
11
- | Setting | Value |
12
- |---------|-------|
13
- | Solidity version | `0.8.28` |
14
- | EVM target | `cancun` |
15
- | Intermediate representation | `via_ir = true` |
16
- | Optimizer runs | `200` |
17
- | Dependency paths | `node_modules`, `lib` |
18
- | Fuzz runs | `4,096` |
19
- | Invariant runs | `1,024` (depth: `100`, `fail_on_revert: false`) |
7
+ Find issues that:
8
+ - launch projects with incorrect rulesets, terminals, or hook ownership
9
+ - grant cash-out or mint privileges to non-suckers
10
+ - mis-scale weight or tax behavior during omnichain-specific data-hook flows
11
+ - leave deployed hook or sucker ownership in the wrong hands
12
+ - create inconsistent behavior between local-only and omnichain project launches
20
13
 
21
14
  ## Scope
22
15
 
23
- One contract, 872 lines, four structs. This repo wraps Juicebox V6 project deployment to automatically configure cross-chain suckers and a 721 tiers hook. The deployer itself acts as a data hook proxy, composing a 721 hook with an optional custom hook (e.g. buyback) while granting registered suckers 0% cash-out tax and mint permission.
24
-
25
- ## Architecture
26
-
27
- | File | Lines | Role |
28
- |------|------:|------|
29
- | `src/JBOmnichainDeployer.sol` | 872 | Main contract. Deploys projects, queues rulesets, proxies data hook calls. Implements `IJBRulesetDataHook`, `IERC721Receiver`, `JBPermissioned`, `ERC2771Context`. |
30
- | `src/interfaces/IJBOmnichainDeployer.sol` | 171 | Public interface. |
31
- | `src/structs/JBDeployerHookConfig.sol` | 11 | Stores custom data hook address + pay/cashout flags per ruleset. |
32
- | `src/structs/JBOmnichain721Config.sol` | 16 | 721 hook deployment config: tiers config + cashout flag + salt. |
33
- | `src/structs/JBSuckerDeploymentConfig.sol` | 12 | Sucker deployer configs + salt for deterministic addresses. |
34
- | `src/structs/JBTiered721HookConfig.sol` | 10 | Stores 721 hook address + `useDataHookForCashOut` flag per ruleset. |
35
-
36
- **Total source**: ~1,092 lines.
37
-
38
- ## External Dependencies
39
-
40
- | Dependency | What the deployer calls |
41
- |------------|------------------------|
42
- | `IJBController` | `launchProjectFor`, `launchRulesetsFor`, `queueRulesetsOf`, `DIRECTORY()`, `RULESETS()` |
43
- | `IJBProjects` | `count()`, `ownerOf()`, `transferFrom()` |
44
- | `IJBPermissions` | `setPermissionsFor()` (constructor), `hasPermission()` (via `_requirePermissionFrom`) |
45
- | `IJB721TiersHookDeployer` | `deployHookFor()` |
46
- | `IJBSuckerRegistry` | `isSuckerOf()`, `deploySuckersFor()` |
47
- | `JBOwnable` | `transferOwnershipToProject()` on deployed 721 hooks |
48
- | `IJBRulesetDataHook` | `beforePayRecordedWith()`, `beforeCashOutRecordedWith()`, `hasMintPermissionFor()` on stored hooks |
49
- | `@prb/math` | `mulDiv()` for weight scaling |
50
-
51
- ## Storage Layout
52
-
53
- Two mappings, both `internal`:
54
-
55
- ```solidity
56
- // Slot 0 (after inherited storage)
57
- mapping(uint256 projectId => mapping(uint256 rulesetId => JBDeployerHookConfig)) internal _extraDataHookOf;
58
-
59
- // Slot 1
60
- mapping(uint256 projectId => mapping(uint256 rulesetId => JBTiered721HookConfig)) internal _tiered721HookOf;
61
- ```
62
-
63
- Both are keyed by `(projectId, rulesetId)` where `rulesetId` is predicted as `block.timestamp + i` during setup.
64
-
65
- ## Key Constants
66
-
67
- - Constructor grants `MAP_SUCKER_TOKEN` permission to `SUCKER_REGISTRY` for `projectId = 0` (wildcard -- all projects).
68
- - Salt for 721 hook deployment: `keccak256(abi.encode(_msgSender(), config.salt))` -- includes sender for cross-chain replay protection. `bytes32(0)` salt bypasses determinism.
69
- - Salt for sucker deployment: `keccak256(abi.encode(suckerDeploymentConfiguration.salt, _msgSender()))`.
70
- - Sucker deployment skipped when `suckerDeploymentConfiguration.salt == bytes32(0)`.
71
-
72
- ## Key Flows
73
-
74
- ### 1. `launchProjectFor` (two overloads)
75
-
76
- ```
77
- launchProjectFor(owner, projectUri, [deploy721Config], rulesetConfigurations, terminalConfigurations, memo, suckerDeploymentConfiguration, controller)
78
- ```
79
-
80
- **Full signature (with explicit 721 config)**:
81
- ```solidity
82
- function launchProjectFor(
83
- address owner,
84
- string calldata projectUri,
85
- JBOmnichain721Config calldata deploy721Config,
86
- JBRulesetConfig[] memory rulesetConfigurations,
87
- JBTerminalConfig[] calldata terminalConfigurations,
88
- string calldata memo,
89
- JBSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
90
- IJBController controller
91
- ) external returns (uint256 projectId, IJB721TiersHook hook, address[] memory suckers)
92
- ```
93
-
94
- **Simplified overload** (omits `deploy721Config`, derives default from first ruleset's `baseCurrency`):
95
- ```solidity
96
- function launchProjectFor(
97
- address owner,
98
- string calldata projectUri,
99
- JBRulesetConfig[] memory rulesetConfigurations,
100
- JBTerminalConfig[] calldata terminalConfigurations,
101
- string calldata memo,
102
- JBSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
103
- IJBController controller
104
- ) external returns (uint256 projectId, IJB721TiersHook hook, address[] memory suckers)
105
- ```
106
-
107
- **Execution order**:
108
- 1. `projectId = PROJECTS.count() + 1` -- predicted before creation.
109
- 2. `_deploy721Hook(projectId, config)` -- deploys 721 hook via `HOOK_DEPLOYER.deployHookFor()`.
110
- 3. `_setup721(projectId, rulesetConfigurations, hook, use721ForCashOut)` -- stores hook mappings, replaces `metadata.dataHook` with `address(this)`.
111
- 4. `controller.launchProjectFor(address(this), ...)` -- project NFT minted to deployer.
112
- 5. Reverts with `JBOmnichainDeployer_ProjectIdMismatch` if returned ID does not match prediction.
113
- 6. `JBOwnable(hook).transferOwnershipToProject(projectId)` -- transfers hook ownership.
114
- 7. `SUCKER_REGISTRY.deploySuckersFor(...)` -- if salt is non-zero.
115
- 8. `PROJECTS.transferFrom(address(this), owner, projectId)` -- transfers project NFT to intended owner.
116
-
117
- **No permission checks**: Anyone can call `launchProjectFor`. The caller-supplied `controller` is trusted because the project does not exist yet (no controller to validate against).
118
-
119
- ### 2. `launchRulesetsFor` (two overloads)
120
-
121
- ```solidity
122
- function launchRulesetsFor(
123
- uint256 projectId,
124
- JBOmnichain721Config memory deploy721Config,
125
- JBRulesetConfig[] memory rulesetConfigurations,
126
- JBTerminalConfig[] calldata terminalConfigurations,
127
- string calldata memo,
128
- IJBController controller
129
- ) external returns (uint256 rulesetId, IJB721TiersHook hook)
130
- ```
131
-
132
- **Permission checks**: Requires both `LAUNCH_RULESETS` and `SET_TERMINALS` from project owner.
133
-
134
- **Controller validation**: `_validateController(projectId, controller)` checks `controller.DIRECTORY().controllerOf(projectId) == controller`.
135
-
136
- **Execution**: Always deploys a new 721 hook, transfers ownership, then calls `controller.launchRulesetsFor()`.
137
-
138
- ### 3. `queueRulesetsOf` (two overloads)
139
-
140
- ```solidity
141
- function queueRulesetsOf(
142
- uint256 projectId,
143
- JBOmnichain721Config memory deploy721Config,
144
- JBRulesetConfig[] memory rulesetConfigurations,
145
- string calldata memo,
146
- IJBController controller
147
- ) external returns (uint256 rulesetId, IJB721TiersHook hook)
148
- ```
149
-
150
- **Permission checks**: Requires `QUEUE_RULESETS` from project owner.
151
-
152
- **Controller validation**: Same as `launchRulesetsFor`.
153
-
154
- **Ruleset ID prediction guard**:
155
- ```solidity
156
- uint256 latestRulesetId = controller.RULESETS().latestRulesetIdOf(projectId);
157
- if (latestRulesetId >= block.timestamp) {
158
- revert JBOmnichainDeployer_RulesetIdsUnpredictable();
159
- }
160
- ```
161
-
162
- **721 hook handling**:
163
- - If `deploy721Config.deployTiersHookConfig.tiersConfig.tiers.length > 0`: deploy new hook, transfer ownership.
164
- - Otherwise: carry forward `_tiered721HookOf[projectId][latestRulesetId].hook`.
165
-
166
- ### 4. `deploySuckersFor`
167
-
168
- ```solidity
169
- function deploySuckersFor(
170
- uint256 projectId,
171
- JBSuckerDeploymentConfig calldata suckerDeploymentConfiguration
172
- ) external returns (address[] memory suckers)
173
- ```
174
-
175
- **Permission checks**: Requires `DEPLOY_SUCKERS` from project owner.
176
-
177
- ### 5. `beforePayRecordedWith` (data hook proxy -- view)
178
-
179
- ```solidity
180
- function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
181
- external view returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
182
- ```
183
-
184
- **Composition logic**:
185
- 1. Call 721 hook's `beforePayRecordedWith` (always, if hook exists). Extract `tiered721HookSpec` and `totalSplitAmount`.
186
- 2. Compute `projectAmount = context.amount.value - totalSplitAmount` (clamped to 0).
187
- 3. Call custom hook's `beforePayRecordedWith` with `hookContext.amount.value = projectAmount` (if `useDataHookForPay == true`).
188
- 4. If custom hook not called, `weight = context.weight`.
189
- 5. Scale weight: if `projectAmount == 0`, `weight = 0`. If `projectAmount < context.amount.value`, `weight = mulDiv(weight, projectAmount, context.amount.value)`.
190
- 6. Merge specs: 721 spec first, then custom hook specs.
191
-
192
- ### 6. `beforeCashOutRecordedWith` (data hook proxy -- view)
193
-
194
- ```solidity
195
- function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
196
- external view returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
197
- ```
198
-
199
- **Sequential composition**:
200
- 1. **Sucker check** (early return): If `SUCKER_REGISTRY.isSuckerOf(projectId, holder)` returns true, return `(0, cashOutCount, totalSupply, empty)`.
201
- 2. **Initialize**: Set `cashOutTaxRate`, `cashOutCount`, `totalSupply` from context.
202
- 3. **721 hook**: If stored and `useDataHookForCashOut == true`, call its `beforeCashOutRecordedWith` with current values. Updates `cashOutTaxRate`, `cashOutCount`, `totalSupply`, and stores returned specs (always 0 or 1).
203
- 4. **Custom hook**: If stored and `useDataHookForCashOut == true`, call its `beforeCashOutRecordedWith` with the already-updated values from the 721 hook. Further updates values and stores returned specs.
204
- 5. **Merge specs**: If either hook returned specs, merge them (721 first, then custom) into a single array.
205
- 6. **Fallback**: If neither returned specs, return adjusted `(cashOutTaxRate, cashOutCount, totalSupply, empty)`.
206
-
207
- Note: Both hooks are called if both have the flag set. The 721 hook's output feeds into the custom hook's input. The sucker check always takes priority.
208
-
209
- ### 7. `hasMintPermissionFor` (data hook proxy -- view)
210
-
211
- ```solidity
212
- function hasMintPermissionFor(uint256 projectId, JBRuleset memory ruleset, address addr)
213
- external view returns (bool)
214
- ```
215
-
216
- 1. If `SUCKER_REGISTRY.isSuckerOf(projectId, addr)` returns true, return `true`.
217
- 2. If custom hook exists and its `hasMintPermissionFor` returns true, return `true`.
218
- 3. The 721 hook is NOT checked for mint permission.
219
- 4. Otherwise return `false`.
220
-
221
- ## The `_setup721` Pattern (Critical)
222
-
223
- ```solidity
224
- function _setup721(
225
- uint256 projectId,
226
- JBRulesetConfig[] memory rulesetConfigurations,
227
- IJB721TiersHook hook721,
228
- bool use721ForCashOut
229
- ) internal returns (JBRulesetConfig[] memory)
230
- ```
231
-
232
- For each ruleset config at index `i`:
233
- 1. **Self-reference guard**: Reverts with `JBOmnichainDeployer_InvalidHook` if `metadata.dataHook == address(this)`.
234
- 2. **Store 721 hook**: `_tiered721HookOf[projectId][block.timestamp + i] = JBTiered721HookConfig(hook721, use721ForCashOut)`.
235
- 3. **Store custom hook**: If `metadata.dataHook != address(0)`, stores as `_extraDataHookOf[projectId][block.timestamp + i]`.
236
- 4. **Replace metadata**: Sets `metadata.dataHook = address(this)`, forces `useDataHookForPay = true`, `useDataHookForCashOut = true`.
237
-
238
- **Ruleset ID prediction**: Keys are `block.timestamp + i`. This MUST match the IDs assigned by `JBRulesets` when the controller processes the configs. This is validated by:
239
- - `launchProjectFor`: The project ID prediction (`PROJECTS.count() + 1`) is validated post-hoc via the `ProjectIdMismatch` revert.
240
- - `queueRulesetsOf`: The `latestRulesetId >= block.timestamp` guard prevents same-block conflicts.
241
- - `launchRulesetsFor`: No explicit guard on the ruleset ID prediction. This is safe because `launchRulesetsFor` is called on the controller which assigns IDs starting from `block.timestamp`.
242
-
243
- ## Gotchas for Auditors
244
-
245
- 1. **Ruleset ID prediction is fragile**: The deployer predicts ruleset IDs as `block.timestamp + i`. If the core protocol changes how IDs are assigned (e.g. incrementing differently), stored hooks will be keyed to the wrong rulesets. The `queueRulesetsOf` guard (`latestRulesetId >= block.timestamp`) catches same-block conflicts but does NOT protect against multi-tx-in-block race conditions on `launchRulesetsFor`.
246
-
247
- 2. **No reentrancy guard**: The contract does not use `ReentrancyGuard`. The `launchProjectFor` flow holds the project NFT temporarily. If `controller.launchProjectFor` calls back into the deployer, the project NFT is still held by the deployer. However, the entire tx would revert if the returned project ID doesn't match, so exploitation would require a cooperating controller.
248
-
249
- 3. **721 hook always deployed**: Even with 0 tiers, a 721 hook is deployed for every project/ruleset launch. This is intentional -- it ensures the hook exists for future tier additions.
250
-
251
- 4. **Cash-out hooks are now composed**: Like pay hooks, cash-out handling calls both hooks sequentially (721 hook first, then custom hook) and merges their specifications. The 721 hook's output values feed into the custom hook's input context.
252
-
253
- 5. **Carry-forward can yield zero-address hook**: In `queueRulesetsOf` with 0 tiers, the hook is carried from `_tiered721HookOf[projectId][latestRulesetId]`. If no hook was stored for that ruleset (e.g. the project was created outside the deployer), this returns `address(0)`.
254
-
255
- 6. **Custom hook receives reduced amount**: In `beforePayRecordedWith`, the custom data hook sees `amount.value = projectAmount` (original minus 721 splits). This is a modified `memory` copy of the original calldata context. The custom hook cannot see the original payment amount.
256
-
257
- 7. **Weight can overflow from custom hooks**: The deployer passes whatever weight the custom hook returns through `mulDiv`. If a custom hook returns `type(uint256).max`, the `mulDiv` still works correctly (PRB math handles this), but the resulting token mint amount could be extremely large.
258
-
259
- 8. **Constructor grants wildcard permission**: The deployer grants `MAP_SUCKER_TOKEN` to the `SUCKER_REGISTRY` for `projectId = 0` (wildcard). This means the sucker registry can map tokens for ANY project the deployer is associated with.
260
-
261
- 9. **`launchProjectFor` has no permission checks**: Anyone can call it. The project is created with the deployer as owner, then transferred. The caller-supplied controller is not validated (there's no existing project to validate against).
262
-
263
- 10. **`launchRulesetsFor` has no ruleset ID prediction guard**: Unlike `queueRulesetsOf`, `launchRulesetsFor` does not check `latestRulesetId >= block.timestamp`. This is acceptable because `launchRulesetsFor` is the first ruleset launch for an existing project, but could fail if called in the same block as another ruleset operation.
264
-
265
- ## Error Conditions
266
-
267
- | Error | Trigger | Function |
268
- |-------|---------|----------|
269
- | `JBOmnichainDeployer_ControllerMismatch` | Provided controller does not match `directory.controllerOf(projectId)` | `queueRulesetsOf`, `launchRulesetsFor` |
270
- | `JBOmnichainDeployer_InvalidHook` | Ruleset's `metadata.dataHook == address(this)` | `_setup721` (called by all launch/queue functions) |
271
- | `JBOmnichainDeployer_ProjectIdMismatch` | `controller.launchProjectFor` returns unexpected project ID | `_launchProjectFor` |
272
- | `JBOmnichainDeployer_RulesetIdsUnpredictable` | `latestRulesetIdOf(projectId) >= block.timestamp` | `_queueRulesetsOf` |
273
- | `JBOmnichainDeployer_UnexpectedNFTReceived` | `onERC721Received` called by non-`PROJECTS` contract | `onERC721Received` |
274
-
275
- ## Priority Audit Areas
276
-
277
- ### P0 -- Critical
278
-
279
- 1. **Sucker privilege escalation**: Can a non-sucker address obtain 0% cash-out tax? The only gate is `SUCKER_REGISTRY.isSuckerOf()`. If the registry is compromised or returns incorrect values, all omnichain projects are affected. Verify the sucker registry's access control for `deploySuckersFor`.
280
-
281
- 2. **Ruleset ID prediction correctness**: Verify that `block.timestamp + i` matches the IDs assigned by `JBRulesets` in all scenarios. If the prediction is wrong, the deployer's stored hooks will never be consulted, and `beforePayRecordedWith` / `beforeCashOutRecordedWith` will return default values (no 721 integration, no sucker bypass).
282
-
283
- 3. **Ownership transfer ordering**: In `launchProjectFor`, the 721 hook is deployed BEFORE the project exists. Hook ownership is transferred after `controller.launchProjectFor` returns. If the controller reverts, the hook exists but is owned by the deployer with no project to transfer to. Verify this is a safe failure mode (entire tx reverts atomically).
284
-
285
- ### P1 -- High
286
-
287
- 4. **Data hook proxy composition**: Verify that the weight scaling in `beforePayRecordedWith` is correct when 721 splits consume part of the payment. Specifically verify: `mulDiv(weight, projectAmount, context.amount.value)` when `totalSplitAmount > 0`.
288
-
289
- 5. **Controller validation bypass**: `launchProjectFor` does not validate the controller because the project doesn't exist yet. Verify a malicious controller cannot exploit this (e.g. by returning a different project ID and tricking the deployer into configuring the wrong project).
290
-
291
- 6. **Permission escalation via deploySuckersFor**: The deployer grants `MAP_SUCKER_TOKEN` with `projectId = 0` (wildcard). Verify this cannot be abused to map tokens for projects not created through the deployer.
292
-
293
- ### P2 -- Medium
294
-
295
- 7. **Carry-forward stale hook**: When `queueRulesetsOf` carries forward a 721 hook from `latestRulesetId`, verify the carried hook is correct even after multiple queue operations.
296
-
297
- 8. **Custom hook isolation**: The custom hook receives a modified context (`amount.value = projectAmount`). Verify the hook cannot observe or manipulate the original amount. Verify the memory copy does not alias the original calldata.
298
-
299
- 9. **ERC2771 interaction**: The deployer uses `_msgSender()` for salt computation and permission checks. Verify the trusted forwarder cannot be used to spoof senders in permission-gated functions.
300
-
301
- ### P3 -- Low
302
-
303
- 10. **onERC721Received guard**: Only accepts NFTs from `PROJECTS`. Verify no other ERC721 can be sent to the deployer.
304
-
305
- 11. **supportsInterface completeness**: Verify the reported interfaces match actual implementations.
306
-
307
- ## Invariants to Verify
308
-
309
- 1. **Sucker always gets 0% tax**: For any `context` where `SUCKER_REGISTRY.isSuckerOf(projectId, holder)` returns true, `beforeCashOutRecordedWith` returns `cashOutTaxRate == 0`.
310
-
311
- 2. **Sucker always gets mint permission**: For any `(projectId, ruleset, addr)` where `SUCKER_REGISTRY.isSuckerOf(projectId, addr)` returns true, `hasMintPermissionFor` returns `true`.
312
-
313
- 3. **721 spec ordering**: In `beforePayRecordedWith`, if the 721 hook returns a spec, it is always the first element in the returned `hookSpecifications` array.
314
-
315
- 4. **Data hook replacement**: After `_setup721`, every ruleset config has `metadata.dataHook == address(this)`, `useDataHookForPay == true`, `useDataHookForCashOut == true`.
316
-
317
- 5. **Self-reference prevention**: `_setup721` reverts if any ruleset's `metadata.dataHook == address(this)`.
318
-
319
- 6. **Weight scaling correctness**: `weight = mulDiv(hookWeight, projectAmount, totalAmount)` where `projectAmount = totalAmount - splitAmount`. When `splitAmount >= totalAmount`, `weight == 0`.
320
-
321
- 7. **Controller validation**: `queueRulesetsOf` and `launchRulesetsFor` revert if the provided controller does not match `directory.controllerOf(projectId)`.
322
-
323
- 8. **Deployer never holds ETH**: The deployer has no `receive()` or `fallback()`, so it should never hold ETH.
324
-
325
- 9. **Hook storage consistency**: After `launchProjectFor` or `queueRulesetsOf`, `_tiered721HookOf[projectId][predictedRulesetId]` is non-zero for every queued ruleset.
326
-
327
- 10. **Ownership transfer completeness**: After `launchProjectFor`, the project NFT is owned by `owner` (not the deployer). After any 721 hook deployment, the hook's JBOwnable ownership is transferred to the project.
328
-
329
- ## Test Suite Overview
16
+ In scope:
17
+ - `src/JBOmnichainDeployer.sol`
18
+ - `src/interfaces/`
19
+ - `src/structs/`
20
+ - deployment scripts in `script/`
330
21
 
331
- 14 test files, ~5,000 lines of test code:
22
+ Key dependencies:
23
+ - `nana-core-v6`
24
+ - `nana-721-hook-v6`
25
+ - `nana-suckers-v6`
332
26
 
333
- | Category | Files | Coverage |
334
- |----------|-------|----------|
335
- | Unit tests | `JBOmnichainDeployer.t.sol` | Constructor, `supportsInterface`, `onERC721Received`, `beforePayRecordedWith`, `beforeCashOutRecordedWith`, `hasMintPermissionFor`, `deploySuckersFor`, simplified overloads |
336
- | Guard tests | `JBOmnichainDeployerGuard.t.sol` | Ruleset ID prediction guard, same-block queue revert, multi-ruleset conflict |
337
- | Attack tests | `OmnichainDeployerAttacks.t.sol` | Fake sucker bypass, reverting hook propagation, inflating hook weight, sucker bypass of reverting hooks |
338
- | Edge cases | `OmnichainDeployerEdgeCases.t.sol` | `InvalidHook` self-reference, `ProjectIdMismatch`, weight=0 on full splits, `mulDiv` safety with `type(uint256).max`, `useDataHookForCashOut` flag routing, mint permission delegation |
339
- | Composition | `Tiered721HookComposition.t.sol` | 721+buyback hook composition, split amount forwarding, weight adjustment, spec merging, cashout routing (721 vs custom vs fallback) |
340
- | Reentrancy | `OmnichainDeployerReentrancy.t.sol` | Pay hook re-entering pay, cashout hook re-entering cashout, pay hook re-entering cashout (fork tests) |
341
- | Invariants | `invariants/OmnichainDeployerInvariant.t.sol` + handler | Sucker 0% tax, 721 spec ordering, fund conservation, token supply consistency, deployer ETH balance, hook storage consistency |
342
- | Regression | `regression/HookOwnershipTransfer.t.sol` | Hook ownership transfer in `queueRulesetsOf` |
343
- | Regression | `regression/ValidateController.t.sol` | Controller validation rejects fake controllers |
344
- | Fork | `fork/TestOmnichain*.t.sol` (5 files) | Real V4 PoolManager + buyback hook integration, 721 queue-and-adjust, cashout fork, stress, weight fork, sucker deployment fork |
27
+ ## System Model
345
28
 
346
- ## Testing Setup
29
+ `JBOmnichainDeployer` is a launch surface that can:
30
+ - create a new Juicebox project
31
+ - deploy and configure a 721 hook
32
+ - configure rulesets and terminals
33
+ - deploy suckers and register them for the project
34
+ - participate in pay or cash-out accounting as a data hook where needed
347
35
 
348
- ```bash
349
- # Install dependencies
350
- npm install
36
+ ## Critical Invariants
351
37
 
352
- # Run unit tests
353
- forge test --match-path 'test/*.t.sol' -vvv
38
+ 1. Launch configuration is faithful
39
+ The deployed project must end up with the exact hook, ruleset, and ownership configuration the caller requested.
354
40
 
355
- # Run fork tests (requires RPC)
356
- RPC_ETHEREUM_MAINNET=<your_rpc> forge test --match-path 'test/fork/*.t.sol' -vvv
41
+ 2. Sucker privileges stay restricted
42
+ Zero-tax or mint-permission behavior intended for legitimate suckers must not be reachable by arbitrary contracts or stale registry entries.
357
43
 
358
- # Run invariant tests (requires RPC)
359
- RPC_ETHEREUM_MAINNET=<your_rpc> forge test --match-contract OmnichainDeployerInvariant -vvv
44
+ 3. Weight and accounting scaling are correct
45
+ If the deployer proxies or modifies hook outputs, the resulting project token issuance and reclaim math must still match intended economics.
360
46
 
361
- # Compiler settings
362
- # Solidity 0.8.28, EVM version: cancun, via_ir: true, optimizer: 200 runs
363
- ```
47
+ 4. Ownership transfer is complete
48
+ Deployer-created hooks and helper contracts must not retain silent control after initialization.
364
49
 
365
- Foundry config is at `foundry.toml`. Fuzz runs: 4096. Invariant runs: 1024, depth: 100, `fail_on_revert: false`.
50
+ ## Threat Model
366
51
 
367
- ## How to Report Findings
52
+ Prioritize:
53
+ - empty or malformed ruleset configurations
54
+ - hook ownership transfer races
55
+ - registry-based privilege spoofing
56
+ - reentrancy around project launch and initialization
57
+ - stale assumptions when optional sucker deployment is skipped
368
58
 
369
- Each finding should follow this 7-point structure:
59
+ ## Build And Verification
370
60
 
371
- 1. **Title** -- A short, descriptive name (e.g. "Ruleset ID prediction fails on same-block queue").
372
- 2. **Affected contract(s)** -- File path(s) and line number(s).
373
- 3. **Description** -- What the issue is and why it matters. Include relevant code snippets.
374
- 4. **Trigger sequence** -- Step-by-step instructions to reproduce the issue (transactions, parameters, state preconditions).
375
- 5. **Impact** -- What can go wrong: fund loss, privilege escalation, denial of service, incorrect accounting, etc.
376
- 6. **Proof** -- A Foundry test, call trace, or formal argument demonstrating the issue. Runnable PoC strongly preferred.
377
- 7. **Fix** -- A concrete recommendation. Code diff preferred; otherwise a description of the required change.
61
+ Standard workflow:
62
+ - `npm install`
63
+ - `forge build`
64
+ - `forge test`
378
65
 
379
- ### Severity Guide
66
+ Existing tests emphasize:
67
+ - reentrancy and attack paths
68
+ - hook composition
69
+ - weight scaling
70
+ - omnichain fork and stress scenarios
380
71
 
381
- | Severity | Criteria |
382
- |----------|----------|
383
- | **CRITICAL** | Direct loss or theft of funds, permanent freezing of funds, or unauthorized minting/burning of tokens. Exploitable without unusual preconditions. |
384
- | **HIGH** | Significant fund loss under specific but realistic conditions, privilege escalation that bypasses access control, or corruption of core protocol state (e.g. wrong hook mappings that silently disable sucker bypass). |
385
- | **MEDIUM** | Conditional issues requiring atypical state or timing (e.g. same-block race conditions), griefing attacks with bounded cost, or incorrect accounting that does not directly lose funds but violates documented invariants. |
386
- | **LOW** | Code quality issues, gas inefficiencies, dead code, missing events, deviations from best practices, or edge cases with negligible economic impact. |
72
+ High-value findings typically show either a bad project launch state or a non-sucker actor receiving omnichain-only privileges.
package/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ ## Scope
4
+
5
+ This file describes the verified change from `nana-omnichain-deployers-v5` to the current `nana-omnichain-deployers-v6` repo.
6
+
7
+ ## Current v6 surface
8
+
9
+ - `JBOmnichainDeployer`
10
+ - `IJBOmnichainDeployer`
11
+ - `JBDeployerHookConfig`
12
+ - `JBOmnichain721Config`
13
+ - `JBTiered721HookConfig`
14
+
15
+ ## Summary
16
+
17
+ - The deployer now assumes a 721 hook is part of the standard deployment path instead of a special-case path.
18
+ - Hook composition is more explicit. The current repo separates 721-hook behavior from extra data-hook behavior and combines them deliberately.
19
+ - The v6 test suite includes dedicated coverage for ownership transfer, controller validation, empty ruleset edge cases, hook composition, and invariants that were not present in the small v5 tree.
20
+ - The repo moved to the v6 Solidity and dependency baseline.
21
+
22
+ ## Verified deltas
23
+
24
+ - `launch721ProjectFor(...)`, `launch721RulesetsFor(...)`, and `queue721RulesetsOf(...)` no longer define the public API shape.
25
+ - Their role is covered by overloaded `launchProjectFor(...)`, `launchRulesetsFor(...)`, and `queueRulesetsOf(...)` entry points that accept `JBOmnichain721Config`.
26
+ - `extraDataHookOf(...)` and `tiered721HookOf(...)` replace the older single `dataHookOf(...)` view model.
27
+ - The overloaded launch and queue functions now return the `IJB721TiersHook` they deploy or carry forward.
28
+
29
+ ## Breaking ABI changes
30
+
31
+ - The 721-specific launch and queue entry points were removed from the public API shape.
32
+ - `dataHookOf(...)` was replaced by `extraDataHookOf(...)` plus `tiered721HookOf(...)`.
33
+ - `launchProjectFor(...)`, `launchRulesetsFor(...)`, and `queueRulesetsOf(...)` now have overloads that return the hook.
34
+ - `JBOmnichain721Config` replaces the old direct 721 deploy config entrypoint model.
35
+
36
+ ## Indexer impact
37
+
38
+ - Hook composition is now split across two tracked hook sources instead of one.
39
+ - Launch and queue flows should expect a 721 hook in the returned state and in the deployer's stored per-ruleset data.
40
+
41
+ ## Migration notes
42
+
43
+ - Update any code that expected separate "with 721" and "without 721" deployment paths to behave like v5.
44
+ - Re-check ownership assumptions after hook deployment. The current repo is stricter and more explicit about that flow.
45
+ - If you decode launch or queue inputs, use the current v6 structs instead of v5 layouts.
46
+
47
+ ## ABI appendix
48
+
49
+ - Removed public API shape
50
+ - `launch721ProjectFor(...)`
51
+ - `launch721RulesetsFor(...)`
52
+ - `queue721RulesetsOf(...)`
53
+ - Replaced with overload families
54
+ - `launchProjectFor(...)`
55
+ - `launchRulesetsFor(...)`
56
+ - `queueRulesetsOf(...)`
57
+ - Replaced hook lookup model
58
+ - `dataHookOf(...)` -> `extraDataHookOf(...)` + `tiered721HookOf(...)`
59
+ - New migration-sensitive structs
60
+ - `JBOmnichain721Config`
61
+ - `JBTiered721HookConfig`