@bananapus/omnichain-deployers-v6 0.0.19 → 0.0.20

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.
package/ADMINISTRATION.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  Admin privileges and their scope in nana-omnichain-deployers-v6.
4
4
 
5
+ ## At A Glance
6
+
7
+ | Item | Details |
8
+ |------|---------|
9
+ | Scope | Omnichain project launch, hook composition, sucker deployment, and the deployer's data-hook proxy behavior. |
10
+ | Operators | Project owners and delegates, the `JBOmnichainDeployer`, and the configured `JBSuckerRegistry` with its wildcard token-mapping grant. |
11
+ | Highest-risk actions | Launching a project with the wrong hook composition, terminal configuration, or cross-chain setup, then assuming it can be rewritten later. |
12
+ | Recovery posture | The deployer's immutable dependencies cannot be edited in place. Project-level recovery usually means launching corrected rulesets or redeploying the broader project path. |
13
+
14
+ ## Routine Operations
15
+
16
+ - Validate all deploy-time hook choices, 721 settings, and sucker configuration before using `launchProjectFor` or `launchRulesetsFor`.
17
+ - Keep the distinction clear between per-ruleset composed hooks and the deployer's permanent proxy role.
18
+ - Use the deployer when you want its tax-free sucker and mint-permission behavior; otherwise, do not assume it is a drop-in replacement for arbitrary hook wiring.
19
+
20
+ ## One-Way Or High-Risk Actions
21
+
22
+ - Constructor-time wildcard permissions and immutable references on the deployer cannot be changed afterward.
23
+ - Launch-time hook composition choices determine how future pay and cash-out flows are merged for that ruleset.
24
+ - A bad omnichain deployment can leave a project with a cross-chain shape that is expensive to unwind operationally.
25
+
26
+ ## Recovery Notes
27
+
28
+ - If the project is still administratively flexible, queue new rulesets or use project-level migration paths to move to corrected hook composition.
29
+ - If the deployer's own immutable assumptions are wrong, recovery means deploying a new deployer path rather than trying to hot-fix the existing one.
30
+
5
31
  ## Roles
6
32
 
7
33
  | Role | How Assigned | Scope |
package/ARCHITECTURE.md CHANGED
@@ -1,107 +1,70 @@
1
- # nana-omnichain-deployers-v6 — Architecture
1
+ # Architecture
2
2
 
3
3
  ## Purpose
4
4
 
5
- Omnichain project deployer for Juicebox V6. Wraps the project deployment flow to automatically configure cross-chain suckers and a 721 tiers hook, acting as a data hook that gives suckers 0% cash-out tax (bridging privilege) and mint permission. Every project gets a 721 hook (even with 0 initial tiers).
5
+ `nana-omnichain-deployers-v6` launches Juicebox projects that are ready for both tiered NFTs and cross-chain suckers from day one. It also acts as a wrapper data hook so it can compose a 721 hook with an optional extra data hook while granting suckers tax-free cash outs and mint permission.
6
6
 
7
- ## Contract Map
7
+ ## Boundaries
8
8
 
9
- ```
10
- src/
11
- ├── JBOmnichainDeployer.sol — Deploys projects with sucker + 721 hook integration, acts as data hook wrapper
12
- ├── interfaces/
13
- │ └── IJBOmnichainDeployer.sol — Interface
14
- └── structs/
15
- ├── JBDeployerHookConfig.sol — Custom hook configuration for deployment
16
- ├── JBOmnichain721Config.sol — 721 hook deployment config (tiers + cashout flag + salt)
17
- ├── JBTiered721HookConfig.sol — Per-ruleset 721 hook configuration
18
- └── JBSuckerDeploymentConfig.sol — Sucker deployment parameters
19
- ```
20
-
21
- ## Hook Storage Mappings
9
+ - `JBOmnichainDeployer` is both a deployer and a live hook wrapper. Those two roles are inseparable.
10
+ - The repo composes `nana-721-hook-v6` and `nana-suckers-v6`; it should not duplicate their internal logic.
11
+ - Project accounting still happens in the core protocol.
22
12
 
23
- The deployer maintains two internal mappings, both keyed by `(projectId, rulesetId)`:
13
+ ## Main Components
24
14
 
25
- - **`_tiered721HookOf`** Stores the project's `IJB721TiersHook` reference and a `useDataHookForCashOut` flag. Always populated for every ruleset (every project gets a 721 hook). The hook is always consulted for payments (it controls NFT tier minting), and optionally consulted for cash outs based on the flag.
15
+ | Component | Responsibility |
16
+ | --- | --- |
17
+ | `JBOmnichainDeployer` | Project launch, ruleset queueing, hook composition, and sucker-safe cash-out policy |
18
+ | config structs | 721 hook config, extra hook config, and sucker deployment config |
19
+ | `IJBOmnichainDeployer` | Public deployer and inspection interface |
26
20
 
27
- - **`_extraDataHookOf`** — Stores an optional secondary data hook (e.g., a buyback hook) extracted from the ruleset's original `metadata.dataHook` field before the deployer overwrites it with itself. Includes separate `useDataHookForPay` and `useDataHookForCashOut` flags, preserved from the original ruleset metadata. Only populated when the caller's ruleset config specifies a non-zero `dataHook`.
21
+ ## Runtime Model
28
22
 
29
- During `_setup721`, the deployer extracts any user-specified data hook into `_extraDataHookOf`, then replaces `metadata.dataHook` with itself and forces both pay/cashout flags to `true`. At runtime, the deployer delegates to the 721 hook first, then the extra hook (if present), and merges their results.
23
+ ### Launch
30
24
 
31
- ## Key Data Flows
32
-
33
- ### Omnichain Project Deployment
34
- ```
35
- Caller JBOmnichainDeployer.launchProjectFor()
36
- Deploy 721 hook via HOOK_DEPLOYER (always, even with 0 tiers)
37
- _setup721(): store hooks, insert deployer as data hook
38
- → Launch JB project via controller.launchProjectFor
39
- → Transfer 721 hook ownership to project (after project NFT exists)
40
- → Deploy suckers via JBSuckerRegistry
41
- → Transfer project NFT to owner
25
+ ```text
26
+ caller
27
+ -> launch project or queue rulesets through the deployer
28
+ -> deployer installs itself as the ruleset data hook
29
+ -> deployer deploys or carries forward the 721 hook
30
+ -> deployer optionally deploys sucker pairs with deterministic salts
31
+ -> project ownership is transferred to the intended owner
42
32
  ```
43
33
 
44
- ### Data Hook Behavior
45
- ```
46
- Payment → JBOmnichainDeployer.beforePayRecordedWith()
47
- → Calls 721 hook first (from _tiered721HookOf) for specs/split amounts
48
- → If 721 hook returned specs: include in merged output
49
- → Calls custom hook from _extraDataHookOf (if useDataHookForPay=true)
50
- → Custom hook receives reduced amount (payment - splitAmount)
51
- → Uses 721 hook's split-adjusted weight directly
52
- → Merges both hook specs (721 first if any, then custom)
53
-
54
- Cash Out → JBOmnichainDeployer.beforeCashOutRecordedWith()
55
- → If caller is a registered sucker: return 0% cash-out tax (early return)
56
- → Calls 721 hook (from _tiered721HookOf, if useDataHookForCashOut=true)
57
- → Updates cashOutTaxRate, cashOutCount, totalSupply from 721 hook response
58
- → Calls custom hook (from _extraDataHookOf, if useDataHookForCashOut=true)
59
- → Receives already-updated values from 721 hook
60
- → Further updates cashOutTaxRate, cashOutCount, totalSupply
61
- → Merges both hooks' specifications (721 specs first, then custom hook specs)
62
- → If 721 hook has flag=true and reverts (fungible cashout): revert propagates
63
- → If neither hook has the flag set: return original values
64
- ```
34
+ ### Pay And Cash-Out Wrapping
65
35
 
66
- ### Ruleset Management
67
- ```
68
- Owner JBOmnichainDeployer.queueRulesetsOf()
69
- If new tiers provided: deploy new 721 hook
70
- If no new tiers: carry forward 721 hook from latest ruleset
71
- _setup721(): store hooks, insert deployer as data hook
72
- → Queue new rulesets via JBController
73
-
74
- Owner → JBOmnichainDeployer.launchRulesetsFor()
75
- → Deploy new 721 hook
76
- → Launch rulesets for an existing project
77
- → Configure terminal integration
36
+ ```text
37
+ runtime callback
38
+ -> if the actor is a registered sucker, return the special tax-free / mint-enabled path
39
+ -> otherwise call the 721 hook first when configured
40
+ -> then call the extra data hook when configured
41
+ -> merge hook specs in order and return the combined result
78
42
  ```
79
43
 
80
- ## Extension Points
81
-
82
- | Point | Interface | Purpose |
83
- |-------|-----------|---------|
84
- | Data hook (pay) | `IJBRulesetDataHook.beforePayRecordedWith` | Compose 721 + custom hook for payments |
85
- | Data hook (cashout) | `IJBRulesetDataHook.beforeCashOutRecordedWith` | 0% tax for suckers, forward to hooks |
86
- | Sucker registry | `IJBSuckerRegistry` | Sucker deployment and discovery |
87
- | 721 hook deployer | `IJB721TiersHookDeployer` | 721 tiers hook deployment (always used) |
44
+ ## Critical Invariants
88
45
 
89
- ## Design Decisions
46
+ - Suckers must be able to bridge without getting trapped behind custom cash-out policies.
47
+ - Hook order matters: the 721 hook runs first, and the extra hook receives the updated context.
48
+ - The deployer's predicted ruleset IDs must stay aligned with `JBRulesets` behavior; the storage keys depend on it.
49
+ - Every project launched through this repo gets a 721 hook surface, even if it starts with zero tiers.
90
50
 
91
- 1. **Always deploy a 721 hook, even with 0 tiers.** Every project gets a 721 hook instance so that NFT tiers can be added later via `queueRulesetsOf` without changing the data hook architecture. The hook is also needed as the pay hook target for tier minting. Deploying with 0 tiers is a no-op at runtime (no specs returned) but keeps the infrastructure in place.
51
+ ## Where Complexity Lives
92
52
 
93
- 2. **Deployer acts as a data hook wrapper instead of direct hook assignment.** The core protocol only supports a single `dataHook` per ruleset. The deployer inserts itself as that hook so it can compose two hooks (721 + custom) behind a single interface, while also injecting sucker-specific logic (0% cash-out tax for suckers, mint permission for suckers). Without this wrapper, projects would have to choose between NFT tiers, a buyback hook, and sucker privileges.
53
+ - This repo hides composition complexity behind a simple launch surface, which makes stale assumptions dangerous.
54
+ - Ruleset ID prediction is a subtle but central storage keying mechanism.
55
+ - The sucker exception path intentionally short-circuits normal hook composition and must stay easy to reason about.
94
56
 
95
- 3. **721 hook specs are merged first, custom hook specs second.** During payments, the 721 hook's split amount is subtracted from the payment before the custom hook sees it. This ordering ensures the 721 hook claims funds for tier mints at full price, and the custom hook (e.g., buyback) operates on the remaining amount. For cash outs, the 721 hook adjusts `cashOutTaxRate`/`cashOutCount`/`totalSupply` first, and the custom hook receives those already-updated values, allowing each hook to build on the previous hook's adjustments.
57
+ ## Dependencies
96
58
 
97
- 4. **721 hook's weight is used directly after tier splits.** The 721 hook's `beforePayRecordedWith` returns a weight that is already adjusted for tier-split deductions (via `JB721TiersHookLib.calculateWeight`). The deployer uses this weight directly instead of re-scaling with `mulDiv`. This prevents double-counting: the 721 hook mints its own NFTs for the split amount, and the terminal mints fungible tokens only for the remainder at the hook's pre-adjusted weight.
59
+ - `nana-core-v6` for project launch and hook interfaces
60
+ - `nana-721-hook-v6` for tiered NFT behavior
61
+ - `nana-suckers-v6` for cross-chain transport
62
+ - `nana-ownable-v6` for project-following hook ownership
98
63
 
99
- 5. **Ruleset IDs are predicted as `block.timestamp + i`.** The deployer must store hook configs keyed by ruleset ID before the rulesets are actually created. It predicts IDs using the core protocol's convention (`block.timestamp` for the first, incrementing for subsequent rulesets in the same transaction). `queueRulesetsOf` explicitly reverts if `latestRulesetId >= block.timestamp`, which would mean rulesets were already queued in the same block and the prediction would be wrong.
64
+ ## Safe Change Guide
100
65
 
101
- ## Dependencies
102
- - `@bananapus/core-v6` Core protocol (controller, directory, permissions)
103
- - `@bananapus/721-hook-v6` NFT tier deployment
104
- - `@bananapus/ownable-v6` JB-aware ownership
105
- - `@bananapus/permission-ids-v6` Permission constants
106
- - `@bananapus/suckers-v6` — Cross-chain sucker registry
107
- - `@openzeppelin/contracts` — ERC2771, ERC721Receiver
66
+ - Review launch-time logic and runtime-hook logic together. This repo is easy to break by fixing only one side.
67
+ - When changing hook composition, verify both payment and cash-out ordering.
68
+ - If you touch ruleset ID prediction, test same-block and queued-ruleset edge cases explicitly.
69
+ - Keep deterministic salt handling stable across chains; address predictability is part of the feature.
70
+ - Treat "transparent wrapper" claims as something to prove continuously, not assume.
@@ -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.