@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.
package/README.md CHANGED
@@ -1,134 +1,46 @@
1
1
  # Juicebox Omnichain Deployers
2
2
 
3
- Deploy Juicebox projects with cross-chain suckers and 721 tiers hooks in a single transaction. Every project gets a 721 hook (even with 0 initial tiers), so projects can add NFT tiers later without reconfiguring. Acts as a transparent data hook wrapper that gives suckers tax-free cash outs and on-demand mint permission -- without interfering with any custom data hook the project uses. Supports composing a 721 tiers hook alongside a custom data hook (e.g., a buyback hook) so both run on every payment.
3
+ `@bananapus/omnichain-deployers-v6` launches Juicebox projects with cross-chain suckers and a 721 hook already wired in. It is the package you use when the default project shape should be omnichain from day one.
4
4
 
5
- [Docs](https://docs.juicebox.money) | [Discord](https://discord.gg/juicebox)
5
+ Docs: <https://docs.juicebox.money>
6
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
7
 
7
- ## Conceptual Overview
8
+ ## Overview
8
9
 
9
- Launching a cross-chain Juicebox project normally takes several steps: deploy the project, configure rulesets, set up terminals, deploy suckers, and wire up a data hook that exempts suckers from cash out taxes. `JBOmnichainDeployer` collapses all of this into one transaction.
10
+ The deployer wraps multiple launch concerns into one surface:
10
11
 
11
- It works by inserting itself as the data hook on every ruleset it touches, storing hooks in two separate mappings: the 721 tiers hook is stored per-ruleset in `_tiered721HookOf[projectId][rulesetId]` with its own `useDataHookForCashOut` flag, and an optional custom data hook (e.g., buyback hook) is stored per-ruleset in `_extraDataHookOf[projectId][rulesetId]` with `useDataHookForPay` and `useDataHookForCashOut` flags. When the protocol calls data hook functions during payments and cash outs, the deployer:
12
+ - deploy or carry forward a tiered 721 hook
13
+ - install itself as the project's data-hook wrapper
14
+ - compose the 721 hook with an optional extra custom hook
15
+ - grant tax-free and mint-safe behavior to project suckers
16
+ - deploy suckers deterministically across chains
12
17
 
13
- - **Checks if the holder is a sucker** -- if so, returns 0% cash out tax and grants mint permission. This early return means suckers can always bridge tokens without interference, even if the project's hooks would revert.
14
- - **Composes the 721 hook and custom data hook** for payments -- the 721 hook is called first (via `tiered721HookOf`) to get its specs (including split fund amounts), then the custom hook from `_extraDataHookOf` (if `useDataHookForPay: true`) is called with a reduced amount context (payment minus split amount) so it only considers the available funds. The 721 hook's weight (already split-adjusted by `JB721TiersHookLib.calculateWeight`) is used directly, ensuring the terminal only mints tokens for the amount that actually enters the project treasury. If the 721 hook returns no specs (0 tiers), it is skipped in the merged output.
15
- - **Composes hooks for cash outs** -- the 721 hook is called first (if `useDataHookForCashOut: true`), updating the cash out parameters (tax rate, count, supply). Then the custom hook is called (if `useDataHookForCashOut: true`) with the already-updated values from the 721 hook. Both hooks' specifications are merged into a single array (721 specs first, then custom hook specs). If the 721 hook has `useDataHookForCashOut: true` and reverts (e.g., for fungible-only cashouts), that revert propagates. Set `useDataHookForCashOut: false` on the 721 config to skip it.
16
- - **Returns default values** if neither hook has the relevant flag set.
18
+ The wrapper exists so suckers can bridge without being blocked by project-specific cash-out tax logic while the project still keeps its own data hooks.
17
19
 
18
- This wrapping is invisible to the project and its users. The project's hooks (buyback hook, 721 hook, etc.) work exactly as configured, and can be composed together.
20
+ Use this repo when the default project shape is "Juicebox project plus 721 hook plus cross-chain bridge." Do not use it when a project is single-chain or does not need the wrapper semantics around suckers.
19
21
 
20
- ### How It Works
22
+ If the question is "how do suckers bridge?" start in `nana-suckers-v6`. If the question is "how does a 721 hook behave?" start in `nana-721-hook-v6`. This repo is where those components are packaged together and wrapped.
21
23
 
22
- ```mermaid
23
- sequenceDiagram
24
- participant Caller
25
- participant Deployer as JBOmnichainDeployer
26
- participant HookDeployer as IJB721TiersHookDeployer
27
- participant Controller as JBController
28
- participant Registry as JBSuckerRegistry
29
- participant Owner
24
+ ## Key Contract
30
25
 
31
- Caller->>Deployer: launchProjectFor(owner, rulesets, suckers, ...)
32
- Deployer->>HookDeployer: deployHookFor(projectId, ...) deploy 721 hook
33
- Deployer->>Deployer: _setup721() store hooks, insert self as data hook
34
- Deployer->>Controller: launchProjectFor(owner=deployer, ...)
35
- Controller-->>Deployer: projectId + project NFT
36
- Deployer->>Deployer: transferOwnershipToProject(projectId) — 721 hook ownership
37
- Deployer->>Registry: deploySuckersFor(projectId, salt)
38
- Registry-->>Deployer: sucker addresses
39
- Deployer->>Owner: transfer project NFT
40
- ```
41
-
42
- During operation:
43
-
44
- ```mermaid
45
- sequenceDiagram
46
- participant Terminal
47
- participant Deployer as JBOmnichainDeployer
48
- participant Registry as JBSuckerRegistry
49
- participant Hook721 as 721 Hook
50
- participant HookExtra as Custom Hook
51
-
52
- Terminal->>Deployer: beforeCashOutRecordedWith(context)
53
- Deployer->>Registry: isSuckerOf(projectId, holder)?
54
- alt Holder is a sucker
55
- Deployer-->>Terminal: 0% tax (early return)
56
- else Hooks configured
57
- opt 721 hook with useDataHookForCashOut=true
58
- Deployer->>Hook721: beforeCashOutRecordedWith(context)
59
- Hook721-->>Deployer: updated taxRate, count, supply, specs
60
- end
61
- opt Custom hook with useDataHookForCashOut=true
62
- Deployer->>HookExtra: beforeCashOutRecordedWith(updated context)
63
- HookExtra-->>Deployer: further updated values + specs
64
- end
65
- Deployer-->>Terminal: merged specs from both hooks
66
- else Neither hook has useDataHookForCashOut=true
67
- Deployer-->>Terminal: original values (default)
68
- end
69
- ```
70
-
71
- ### 721 Tiers Hook Integration
72
-
73
- Every project deployed through `JBOmnichainDeployer` gets a 721 tiers hook, even with 0 initial tiers. This lets projects add NFT tiers later without needing to reconfigure the data hook. The deployer:
74
-
75
- 1. Deploys the 721 hook via `HOOK_DEPLOYER`
76
- 2. Transfers hook ownership to the project via `JBOwnable.transferOwnershipToProject(projectId)` (after the project NFT exists)
77
- 3. Stores the 721 hook per-ruleset in `_tiered721HookOf[projectId][rulesetId]` with its `useDataHookForCashOut` flag
78
- 4. Sets itself as the data hook on each ruleset, enforcing `useDataHookForPay = true` and `useDataHookForCashOut = true`
79
- 5. Stores the optional custom hook (e.g., buyback hook) separately in `_extraDataHookOf[projectId][rulesetId]` with its own per-hook flags
80
-
81
- For `queueRulesetsOf`, if no new tiers are provided, the 721 hook from the latest ruleset is carried forward instead of deploying a new one.
82
-
83
- This means a project can have both a 721 hook (for NFT minting on payments) and a custom data hook (for buyback, custom weight logic, etc.) running simultaneously. During both payments and cash outs, the hooks are called sequentially (721 hook first, then custom hook) and their specifications are merged.
84
-
85
- ### Simplified Overloads
86
-
87
- Each of `launchProjectFor`, `launchRulesetsFor`, and `queueRulesetsOf` has a simplified overload that omits the `deploy721Config` parameter. These use `_default721Config(rulesetConfigurations)`, which creates an empty-tier 721 config with `currency` from the first ruleset's `baseCurrency`, `decimals = 18`, `useDataHookForCashOut = false`, and no salt. At least one ruleset configuration is required. For `queueRulesetsOf`, since the default config has 0 tiers, the existing 721 hook is always carried forward.
88
-
89
- ### Deterministic Cross-Chain Addresses
90
-
91
- Sucker deployment salts are hashed with `_msgSender()` before use:
92
-
93
- ```
94
- salt = keccak256(abi.encode(userSalt, _msgSender()))
95
- ```
96
-
97
- This means:
98
- - **Same sender + same salt on each chain = same sucker addresses** (deterministic via CREATE2)
99
- - Different senders can't collide, even with the same salt
100
- - `salt = bytes32(0)` skips sucker deployment entirely
26
+ | Contract | Role |
27
+ | --- | --- |
28
+ | `JBOmnichainDeployer` | Launches projects and rulesets, manages per-ruleset hook composition, and deploys suckers with deterministic salts. |
101
29
 
102
- ### Supported Chains
30
+ ## Mental Model
103
31
 
104
- `JBOmnichainDeployer` supports the same chains as the sucker deployers it wraps. Currently supported:
32
+ This repo owns orchestration plus runtime wrapping:
105
33
 
106
- - **Mainnets**: Ethereum, Optimism, Base, Arbitrum
107
- - **Testnets**: Ethereum Sepolia, Optimism Sepolia, Base Sepolia, Arbitrum Sepolia
34
+ 1. launch a project with a known omnichain-capable shape
35
+ 2. remember which hook composition belongs to which ruleset
36
+ 3. special-case sucker behavior so bridge flows are not broken by project-specific logic
108
37
 
109
- To deploy a cross-chain project, call `launchProjectFor` on each chain with the same salt. The sucker deployers use CREATE2 so that matching salts from the same sender produce deterministic addresses across chains.
38
+ ## Read These Files First
110
39
 
111
- ### Ruleset ID Prediction
112
-
113
- The deployer stores hook configs keyed by predicted ruleset IDs (`block.timestamp + i`). This works because `JBRulesets` assigns IDs as `latestId >= block.timestamp ? latestId + 1 : block.timestamp`. For new projects, `latestId` starts at 0, so the first ID is always `block.timestamp`.
114
-
115
- The `queueRulesetsOf` function guards against prediction failures by reverting if `latestRulesetIdOf(projectId) >= block.timestamp` (i.e., rulesets were already queued in the same block).
116
-
117
- ## Architecture
118
-
119
- | Contract | Description |
120
- |----------|-------------|
121
- | `JBOmnichainDeployer` | Deploys projects, rulesets, and suckers. Wraps the project's real data hook to intercept cash outs from suckers (tax-free) and grant suckers mint permission. Implements `IJBRulesetDataHook`, `IERC721Receiver`, `ERC2771Context`, `JBPermissioned`. |
122
-
123
- ### Supporting Types
124
-
125
- | Type | Description |
126
- |------|-------------|
127
- | `JBOmnichain721Config` | 721 hook deployment config: `deployTiersHookConfig` (tier configuration), `useDataHookForCashOut` flag, and `salt` for deterministic deployment. Passed to all deploy/launch/queue functions. |
128
- | `JBDeployerHookConfig` | Per-hook config with `dataHook`, `useDataHookForPay`, and `useDataHookForCashOut` flags. Stored as a single value per `(projectId, rulesetId)` in `_extraDataHookOf` for the custom data hook. |
129
- | `JBTiered721HookConfig` | Per-ruleset 721 hook config with `hook` (the `IJB721TiersHook`) and `useDataHookForCashOut` flag. Stored per `(projectId, rulesetId)` in `_tiered721HookOf`. |
130
- | `JBSuckerDeploymentConfig` | Wraps an array of `JBSuckerDeployerConfig` with a `bytes32` salt for deterministic cross-chain addresses. |
131
- | `IJBOmnichainDeployer` | Interface for all deployer entry points and the `extraDataHookOf` view. |
40
+ 1. `src/JBOmnichainDeployer.sol`
41
+ 2. `nana-suckers-v6/src/JBSucker.sol`
42
+ 3. `nana-721-hook-v6/src/JB721TiersHook.sol`
43
+ 4. the extra hook repo, if the deployment composes one
132
44
 
133
45
  ## Install
134
46
 
@@ -136,96 +48,40 @@ The `queueRulesetsOf` function guards against prediction failures by reverting i
136
48
  npm install @bananapus/omnichain-deployers-v6
137
49
  ```
138
50
 
139
- If using Forge directly:
51
+ ## Development
140
52
 
141
53
  ```bash
142
- forge install Bananapus/nana-omnichain-deployers-v6
54
+ npm install
55
+ forge build
56
+ forge test
143
57
  ```
144
58
 
145
- Add to `remappings.txt`:
146
- ```
147
- @bananapus/omnichain-deployers-v6/=lib/nana-omnichain-deployers-v6/
148
- ```
59
+ Useful scripts:
149
60
 
150
- ## Develop
61
+ - `npm run deploy:mainnets`
62
+ - `npm run deploy:testnets`
151
63
 
152
- | Command | Description |
153
- |---------|-------------|
154
- | `forge build` | Compile contracts |
155
- | `forge test` | Run unit, integration, and attack tests |
156
- | `forge test -vvv` | Run tests with full stack traces |
157
- | `npm run deploy:mainnets` | Propose mainnet deployment via Sphinx |
158
- | `npm run deploy:testnets` | Propose testnet deployment via Sphinx |
64
+ ## Deployment Notes
159
65
 
160
- ### Settings
161
-
162
- ```toml
163
- # foundry.toml
164
- [profile.default]
165
- solc = '0.8.26'
166
- evm_version = 'cancun'
167
- via_ir = true
168
- optimizer_runs = 200
169
-
170
- [fuzz]
171
- runs = 4096
172
- ```
66
+ This repo assumes the 721 hook, address registry, buyback hook, suckers, ownable, and core packages are already available. Matching salts from the same sender keep sucker addresses deterministic across chains.
173
67
 
174
68
  ## Repository Layout
175
69
 
176
- ```
70
+ ```text
177
71
  src/
178
- JBOmnichainDeployer.sol # Main contract (~817 lines)
72
+ JBOmnichainDeployer.sol
179
73
  interfaces/
180
- IJBOmnichainDeployer.sol # Public interface
181
74
  structs/
182
- JBDeployerHookConfig.sol # Custom hook config (dataHook + flags)
183
- JBOmnichain721Config.sol # 721 hook deployment config
184
- JBSuckerDeploymentConfig.sol # Sucker deployment params
185
- JBTiered721HookConfig.sol # Per-ruleset 721 hook config
186
75
  test/
187
- JBOmnichainDeployer.t.sol # Unit tests
188
- JBOmnichainDeployerGuard.t.sol # Ruleset ID prediction tests
189
- OmnichainDeployerAttacks.t.sol # Adversarial security tests
190
- OmnichainDeployerEdgeCases.t.sol # Edge case tests (weight, cashout, mint)
191
- OmnichainDeployerReentrancy.t.sol # Reentrancy tests
192
- TestAuditGaps.sol # Audit gap coverage tests
193
- Tiered721HookComposition.t.sol # 721 hook + custom hook composition tests
194
- fork/
195
- OmnichainForkTestBase.sol # Shared fork test base
196
- TestOmnichain721QueueAndAdjust.t.sol # Fork: queue and adjust 721 tiers
197
- TestOmnichainCashOutFork.t.sol # Fork: cash out flows
198
- TestOmnichainStressFork.t.sol # Fork: stress / load tests
199
- TestOmnichainWeightFork.t.sol # Fork: weight decay tests
200
- TestSuckerDeploymentFork.t.sol # Fork: sucker deployment
201
- invariants/
202
- OmnichainDeployerInvariant.t.sol # Invariant tests
203
- handlers/
204
- OmnichainDeployerHandler.sol # Invariant handler
205
- regression/
206
- HookOwnershipTransfer.t.sol # Hook ownership transfer regression
207
- ValidateController.t.sol # Controller validation regression
76
+ unit, attack, invariant, fork, audit, and regression coverage
208
77
  script/
209
- Deploy.s.sol # Sphinx deployment script
78
+ Deploy.s.sol
210
79
  helpers/
211
- DeployersDeploymentLib.sol # Deployment address helper
212
80
  ```
213
81
 
214
- ## Permissions
215
-
216
- | Permission | ID | Required For |
217
- |------------|-----|-------------|
218
- | `DEPLOY_SUCKERS` | `JBPermissionIds.DEPLOY_SUCKERS` | `deploySuckersFor` |
219
- | `QUEUE_RULESETS` | `JBPermissionIds.QUEUE_RULESETS` | `launchRulesetsFor`, `queueRulesetsOf` |
220
- | `SET_TERMINALS` | `JBPermissionIds.SET_TERMINALS` | `launchRulesetsFor` |
221
- | `MAP_SUCKER_TOKEN` | `JBPermissionIds.MAP_SUCKER_TOKEN` | Granted to `SUCKER_REGISTRY` globally (projectId=0) at construction |
222
-
223
- Note: `launchProjectFor` requires no permissions -- anyone can launch a project to any owner.
224
-
225
- ## Risks
82
+ ## Risks And Notes
226
83
 
227
- - **Ruleset ID mismatch**: If `_setup721()` predictions are wrong (e.g., due to same-block queuing from another source), the stored hook configs will be keyed to the wrong rulesets. The `queueRulesetsOf` guard prevents this, but `launchProjectFor` relies on `PROJECTS.count()` being accurate at call time.
228
- - **Reverting real hook**: If any stored hook reverts on `beforePayRecordedWith`, payments are blocked. If the 721 hook has `useDataHookForCashOut: true`, its revert for fungible cashouts propagates. Suckers are immune to this for cash outs (early return), but not for payments.
229
- - **Hook forwarding is view-only**: The deployer's data hook functions are `view`, so any real hook that requires state changes in `beforePayRecordedWith` or `beforeCashOutRecordedWith` will fail.
230
- - **Meta-transaction trust**: ERC2771 `_msgSender()` is used for salt hashing. A compromised trusted forwarder could impersonate senders and create suckers at unexpected addresses.
231
- - **Ownership transfer timing**: The 721 hook's ownership is transferred to the project after the project NFT is minted. In `launchProjectFor`, the hook is deployed before the project exists, and ownership is transferred after `controller.launchProjectFor` returns. If the controller call reverts, the hook exists but is owned by the deployer (the whole transaction reverts, so this is safe).
84
+ - ruleset ID prediction is part of the implementation strategy and should be treated as a real assumption
85
+ - hook composition order matters because the 721 hook runs before any extra custom hook
86
+ - using the default empty-tier 721 config is convenient, but teams should still decide explicitly whether the hook participates in cash-out behavior
87
+ - deterministic salts help with cross-chain address alignment, but only when the sender and configuration match exactly
package/RISKS.md CHANGED
@@ -1,11 +1,28 @@
1
- # RISKS.md -- nana-omnichain-deployers-v6
1
+ # Omnichain Deployers Risk Register
2
+
3
+ This file focuses on the risks in the deployer layer that launches Juicebox projects across chains while composing 721 hooks, suckers, and optional custom data hooks.
4
+
5
+ ## How to use this file
6
+
7
+ - Read `Priority risks` first; they capture the highest-blast-radius deployment and cash-out assumptions.
8
+ - Use the detailed sections for access control, integration, and reentrancy analysis.
9
+ - Treat `Invariants to Verify` as required checks for any new omnichain deployment flow.
10
+
11
+ ## Priority risks
12
+
13
+ | Priority | Risk | Why it matters | Primary controls |
14
+ |----------|------|----------------|------------------|
15
+ | P0 | Registry-trusted sucker bypass | This deployer gives suckers privileged cash-out behavior based on registry answers. A bad registry entry can affect many projects. | Registry allowlists, deployment verification, and explicit registry scrutiny. |
16
+ | P1 | Cross-chain deployment drift | Omnichain assumptions fail if chain-specific wiring, peers, or composed hooks do not match. | Deterministic deploy ordering, parity checks, and post-deploy peer verification. |
17
+ | P1 | Data-hook composition mistakes | The deployer wraps or forwards custom data hooks; a bad composition can alter pay or cash-out semantics unexpectedly. | Integration tests for wrapped hooks and careful review of forwarding logic. |
18
+
2
19
 
3
20
  ## 1. Trust Assumptions
4
21
 
5
22
  - **Trusted forwarder.** ERC-2771 `_msgSender()` is trusted to append the real sender. A compromised forwarder can impersonate any address for `deploySuckersFor`, `launchProjectFor`, `queueRulesetsOf`, and `launchRulesetsFor`.
6
23
  - **Sucker registry.** `SUCKER_REGISTRY.isSuckerOf()` is the sole gatekeeper for 0% cashout tax and mint permission. A compromised or malicious registry lets any address bypass cashout taxes and mint tokens freely.
7
- - **Controller trust.** The deployer passes an arbitrary `IJBController controller` parameter. `_validateController` checks `controller.DIRECTORY().controllerOf(projectId)` (a reflexive lookup through the controller's own directory reference), but during `launchProjectFor` the project does not yet exist -- validation is skipped, relying on the controller returning the correct project ID.
8
- - **Extra data hooks.** Arbitrary `IJBRulesetDataHook` addresses from ruleset metadata are stored and delegated to with `staticcall`. A malicious hook can return arbitrary weight, cashout tax rate, or hook specifications.
24
+ - **Controller trust.** The deployer passes an arbitrary `IJBController controller` parameter. `_validateController` checks `controller.DIRECTORY().controllerOf(projectId)` (a reflexive lookup through the controller's own directory reference), but during `launchProjectFor` the project does not yet exist, so no pre-launch controller validation is possible. The post-launch safeguard is the explicit project-ID match check on `controller.launchProjectFor(...)`.
25
+ - **Extra data hooks.** Arbitrary `IJBRulesetDataHook` addresses from ruleset metadata are stored and called directly from the deployer's pay/cash-out wrapper paths. A malicious hook can return arbitrary weight, cashout tax rate, or hook specifications, and can also consume all available gas or revert.
9
26
 
10
27
  ## 2. Economic / Manipulation Risks
11
28
 
@@ -22,13 +39,13 @@
22
39
  ## 4. DoS Vectors
23
40
 
24
41
  - **Ruleset ID collision.** `_setup721` stores hook configs at `block.timestamp + i`. If `latestRulesetIdOf >= block.timestamp` (rulesets already queued this block), `queueRulesetsOf` reverts with `RulesetIdsUnpredictable`. An attacker who queues rulesets in the same block as the legitimate owner can front-run and block their queue attempt. Gas impact: `queueRulesetsOf` costs ~200-400k gas per ruleset queued. The collision only occurs when two transactions queue rulesets in the same block for the same project — race condition window is one block (~12 seconds on L1, 2 seconds on L2).
25
- - **External hook revert.** `beforePayRecordedWith` and `beforeCashOutRecordedWith` call external hooks without try-catch. A reverting hook blocks all payments or cashouts for that project/ruleset. For cash-outs, if the 721 hook reverts, the custom hook is never reached (the revert propagates before it). Gas impact: the `staticcall` to the extra data hook has no gas limit a gas-griefing hook can consume the entire transaction gas. The 721 hook call is similarly unbounded.
42
+ - **External hook revert.** `beforePayRecordedWith` and `beforeCashOutRecordedWith` call external hooks without try-catch. A reverting hook blocks all payments or cashouts for that project/ruleset. For cash-outs, if the 721 hook reverts, the custom hook is never reached (the revert propagates before it). Gas impact: the direct call into the extra data hook has no gas limit, so a gas-griefing hook can consume the entire transaction gas. The 721 hook call is similarly unbounded.
26
43
  - **721 hook deployment revert.** `HOOK_DEPLOYER.deployHookFor` is called without try-catch. A failing deployment blocks the entire project launch.
27
44
 
28
45
  ## 5. Reentrancy Surface
29
46
 
30
47
  - **`launchProjectFor` external call chain.** The function makes external calls to: (1) `_deploy721Hook()` via `HOOK_DEPLOYER.deployHookFor()` (deploys 721 hook clone), (2) `controller.launchProjectFor()` (creates project, deploys rulesets), (3) `JBOwnable(hook).transferOwnershipToProject()` (transfers hook ownership to the new project), (4) `SUCKER_REGISTRY.deploySuckersFor()` (deploys suckers if configured), (5) `PROJECTS.transferFrom()` (transfers the project NFT to the owner). None of these calls are try-catch wrapped — a revert in any of them fails the entire launch. Reentrancy from the controller callback during project creation could call back into `launchProjectFor`, but the new project would get a different ID (monotonically incrementing), so state corruption is not possible.
31
- - **`beforePayRecordedWith` delegates to external hooks.** Calls `IJBRulesetDataHook(tiered721Hook).beforePayRecordedWith(context)` (not try-caught) and optionally delegates to the extra data hook via `staticcall`. The 721 hook call can execute arbitrary code. At callback time, no deployer state has been modified (the deployer is stateless during payments — it only routes). Reentrancy through the pay path processes as an independent payment.
48
+ - **`beforePayRecordedWith` delegates to external hooks.** Calls `IJBRulesetDataHook(tiered721Hook).beforePayRecordedWith(context)` (not try-caught) and optionally calls the extra data hook directly. The 721 hook and extra hook can execute arbitrary code. At callback time, no deployer state has been modified (the deployer is stateless during payments — it only routes). Reentrancy through the pay path processes as an independent payment.
32
49
  - **`beforeCashOutRecordedWith` delegates to external hooks.** Same pattern as pay: calls the 721 hook (not try-caught), then optionally the extra data hook. Sucker check via `SUCKER_REGISTRY.isSuckerOf` is a view call. No deployer state is modified during cashouts.
33
50
  - **No `ReentrancyGuard`.** Safe because the deployer is effectively stateless during pay/cashout operations — it reads `_tiered721HookOf` and `_extraDataHookOf` mappings but never writes them outside of deployment functions.
34
51
 
@@ -53,7 +70,7 @@
53
70
 
54
71
  ### 8.1 Controller validation skipped during `launchProjectFor` (by design)
55
72
 
56
- `_validateController` checks `controller.DIRECTORY().controllerOf(projectId)` to verify the provided controller matches the project's registered controller. During `launchProjectFor`, the project does not yet exist, so no directory entry exists. Validation is skipped, relying on `controller.launchProjectFor()` to return the correct project ID. This is accepted because: (1) the project is created atomically within the same transaction, (2) the caller provides the controller address, so they choose their own trust boundary, and (3) validating against a non-existent project would always fail, making the check useless.
73
+ `_validateController` checks `controller.DIRECTORY().controllerOf(projectId)` to verify the provided controller matches the project's registered controller. During `launchProjectFor`, the project does not yet exist, so no directory entry exists and no pre-launch validation is possible. The accepted safeguard is the explicit `ProjectIdMismatch` check immediately after `controller.launchProjectFor()` returns. This is accepted because: (1) the project is created atomically within the same transaction, (2) the caller provides the controller address, so they choose their own trust boundary, and (3) validating against a non-existent project would always fail, making the check useless.
57
74
 
58
75
  ### 8.2 Suckers receive 0% cashout tax (shared with revnet-core)
59
76