@bananapus/omnichain-deployers-v6 0.0.13 → 0.0.15
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 +37 -2
- package/ARCHITECTURE.md +23 -2
- package/AUDIT_INSTRUCTIONS.md +46 -8
- package/CHANGE_LOG.md +13 -1
- package/README.md +41 -19
- package/RISKS.md +26 -7
- package/SKILLS.md +102 -23
- package/STYLE_GUIDE.md +2 -2
- package/USER_JOURNEYS.md +149 -85
- package/foundry.toml +1 -1
- package/package.json +8 -7
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/DeployersDeploymentLib.sol +1 -1
- package/src/JBOmnichainDeployer.sol +23 -16
- package/src/interfaces/IJBOmnichainDeployer.sol +1 -1
- package/src/structs/JBDeployerHookConfig.sol +1 -1
- package/src/structs/JBTiered721HookConfig.sol +1 -1
- package/test/JBOmnichainDeployer.t.sol +4 -3
- package/test/JBOmnichainDeployerGuard.t.sol +1 -1
- package/test/OmnichainDeployerAttacks.t.sol +4 -3
- package/test/OmnichainDeployerEdgeCases.t.sol +18 -10
- package/test/OmnichainDeployerReentrancy.t.sol +1 -1
- package/test/TestAuditGaps.sol +4 -3
- package/test/Tiered721HookComposition.t.sol +19 -12
- package/test/audit/WeightScalingComparison.t.sol +337 -0
- package/test/fork/OmnichainForkTestBase.sol +1 -1
- package/test/fork/TestOmnichain721QueueAndAdjust.t.sol +1 -1
- package/test/fork/TestOmnichainCashOutFork.t.sol +1 -1
- package/test/fork/TestOmnichainStressFork.t.sol +1 -1
- package/test/fork/TestOmnichainWeightFork.t.sol +1 -1
- package/test/fork/TestSuckerDeploymentFork.t.sol +1 -1
- package/test/invariants/OmnichainDeployerInvariant.t.sol +1 -1
- package/test/invariants/handlers/OmnichainDeployerHandler.sol +1 -1
- package/test/regression/EmptyRulesetConfigurations.t.sol +86 -0
- package/test/regression/HookOwnershipTransfer.t.sol +1 -1
- package/test/regression/ValidateController.t.sol +1 -1
package/ADMINISTRATION.md
CHANGED
|
@@ -18,7 +18,7 @@ Admin privileges and their scope in nana-omnichain-deployers-v6.
|
|
|
18
18
|
| Function | Required Role | Permission ID | Scope | What It Does |
|
|
19
19
|
|----------|--------------|---------------|-------|--------------|
|
|
20
20
|
| `deploySuckersFor` | Project owner or operator | `DEPLOY_SUCKERS` | Per-project | Deploys new cross-chain suckers for an existing project via the sucker registry. |
|
|
21
|
-
| `launchRulesetsFor` | Project owner or operator | `
|
|
21
|
+
| `launchRulesetsFor` | Project owner or operator | `LAUNCH_RULESETS` + `SET_TERMINALS` | Per-project | Deploys a 721 tiers hook, launches new rulesets with terminal configuration for an existing project. Has a simplified overload without `deploy721Config`. |
|
|
22
22
|
| `queueRulesetsOf` | Project owner or operator | `QUEUE_RULESETS` | Per-project | Queues new rulesets for an existing project. If tiers provided, deploys a new 721 hook. Otherwise, carries forward the 721 hook from the latest ruleset. Has a simplified overload without `deploy721Config`. |
|
|
23
23
|
|
|
24
24
|
### Permissionless Functions
|
|
@@ -26,7 +26,7 @@ Admin privileges and their scope in nana-omnichain-deployers-v6.
|
|
|
26
26
|
| Function | Who Can Call | What It Does |
|
|
27
27
|
|----------|-------------|--------------|
|
|
28
28
|
| `launchProjectFor` | Anyone | Creates a new project with a 721 tiers hook (even with 0 tiers) and suckers. The ERC-721 is minted to the specified `owner`. Returns `(projectId, hook, suckers)`. Has a simplified overload without `deploy721Config` that uses a default empty-tier 721 config. |
|
|
29
|
-
| `beforePayRecordedWith` | JBMultiTerminal (via controller) | View function: calls 721 hook for specs, then custom hook (if configured) with reduced amount. Merges results. |
|
|
29
|
+
| `beforePayRecordedWith` | JBMultiTerminal (via controller) | View function: always calls the 721 hook (when its address is non-zero) for specs, then calls the custom hook (if configured and `useDataHookForPay` is set) with the reduced amount. Merges results. |
|
|
30
30
|
| `beforeCashOutRecordedWith` | JBMultiTerminal (via controller) | View function: returns 0% cash-out tax for registered suckers. Calls 721 hook first (from `_tiered721HookOf`, if `useDataHookForCashOut: true`), then calls custom hook (from `_extraDataHookOf`, if `useDataHookForCashOut: true`) with the updated values from the 721 hook. Both hooks' specifications are merged. If neither has the flag set, returns original values. |
|
|
31
31
|
| `hasMintPermissionFor` | JBController | View function: returns true for registered suckers, otherwise checks the custom hook in `_extraDataHookOf`. |
|
|
32
32
|
| `extraDataHookOf` | Anyone | View function: returns the stored `JBDeployerHookConfig` for a project/ruleset pair (the custom data hook). |
|
|
@@ -63,6 +63,41 @@ Admin privileges and their scope in nana-omnichain-deployers-v6.
|
|
|
63
63
|
|
|
64
64
|
**Cross-chain determinism:** The salt for sucker deployment is combined with `_msgSender()` (`keccak256(abi.encode(salt, _msgSender()))`). Deploying from the same sender address with the same salt on each chain produces matching sucker addresses.
|
|
65
65
|
|
|
66
|
+
## Data Hook Proxy Pattern
|
|
67
|
+
|
|
68
|
+
`JBOmnichainDeployer` acts as a data hook proxy. When set as a project's `dataHook` in ruleset metadata, it wraps up to two inner hooks:
|
|
69
|
+
|
|
70
|
+
1. **721 tiers hook** (`_tiered721HookOf[projectId][rulesetId]`): Handles NFT-based pay/cashout logic.
|
|
71
|
+
2. **Extra data hook** (`_extraDataHookOf[projectId][rulesetId]`): An optional custom hook for additional pay/cashout logic.
|
|
72
|
+
|
|
73
|
+
### Call flow for `beforePayRecordedWith`:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Terminal -> Controller -> JBOmnichainDeployer.beforePayRecordedWith()
|
|
77
|
+
1. Call 721 hook's beforePayRecordedWith (always, when its address is non-zero)
|
|
78
|
+
-> Get pay hook specifications and the total split amount
|
|
79
|
+
2. Call extra hook's beforePayRecordedWith (if useDataHookForPay is set on extra hook config)
|
|
80
|
+
-> Amount is reduced by what the 721 hook already allocated
|
|
81
|
+
3. Scale the extra hook's weight proportionally to the project's share of the payment
|
|
82
|
+
4. Merge both hooks' specifications and return
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Call flow for `beforeCashOutRecordedWith`:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Terminal -> Controller -> JBOmnichainDeployer.beforeCashOutRecordedWith()
|
|
89
|
+
1. Check if caller is a registered sucker -> return 0% cash-out tax (fee-free bridging)
|
|
90
|
+
2. Call 721 hook's beforeCashOutRecordedWith (if useDataHookForCashOut is set on 721 config)
|
|
91
|
+
-> Get cashout hook specifications and adjusted values
|
|
92
|
+
3. Call extra hook's beforeCashOutRecordedWith (if useDataHookForCashOut is set on extra config)
|
|
93
|
+
-> Receives updated values from 721 hook
|
|
94
|
+
4. Merge both hooks' specifications and return
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**`useDataHookForCashOut` / `useDataHookForPay` flags:** These flags control whether each hook participates in a given operation. For the **extra data hook**, the flags are stored per-ruleset in the `JBDeployerHookConfig` struct -- if the flag is `false`, that hook is skipped entirely and the original values are returned unchanged for that hook's portion. The **721 hook** behaves differently: it is **always** called during `beforePayRecordedWith` when its address is non-zero (no `useDataHookForPay` check), but for `beforeCashOutRecordedWith` it respects the `useDataHookForCashOut` flag stored in `JBTiered721HookConfig`.
|
|
98
|
+
|
|
99
|
+
**Write-once storage:** Both `_tiered721HookOf` and `_extraDataHookOf` mappings are written once during `_setup721()` and never updated. New rulesets can reference different hooks, but existing ruleset-to-hook mappings are permanent.
|
|
100
|
+
|
|
66
101
|
## Immutable Configuration
|
|
67
102
|
|
|
68
103
|
These values are set at deployment and cannot be changed:
|
package/ARCHITECTURE.md
CHANGED
|
@@ -18,6 +18,16 @@ src/
|
|
|
18
18
|
└── JBSuckerDeploymentConfig.sol — Sucker deployment parameters
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Hook Storage Mappings
|
|
22
|
+
|
|
23
|
+
The deployer maintains two internal mappings, both keyed by `(projectId, rulesetId)`:
|
|
24
|
+
|
|
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.
|
|
26
|
+
|
|
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`.
|
|
28
|
+
|
|
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.
|
|
30
|
+
|
|
21
31
|
## Key Data Flows
|
|
22
32
|
|
|
23
33
|
### Omnichain Project Deployment
|
|
@@ -38,7 +48,7 @@ Payment → JBOmnichainDeployer.beforePayRecordedWith()
|
|
|
38
48
|
→ If 721 hook returned specs: include in merged output
|
|
39
49
|
→ Calls custom hook from _extraDataHookOf (if useDataHookForPay=true)
|
|
40
50
|
→ Custom hook receives reduced amount (payment - splitAmount)
|
|
41
|
-
→
|
|
51
|
+
→ Uses 721 hook's split-adjusted weight directly
|
|
42
52
|
→ Merges both hook specs (721 first if any, then custom)
|
|
43
53
|
|
|
44
54
|
Cash Out → JBOmnichainDeployer.beforeCashOutRecordedWith()
|
|
@@ -76,6 +86,18 @@ Owner → JBOmnichainDeployer.launchRulesetsFor()
|
|
|
76
86
|
| Sucker registry | `IJBSuckerRegistry` | Sucker deployment and discovery |
|
|
77
87
|
| 721 hook deployer | `IJB721TiersHookDeployer` | 721 tiers hook deployment (always used) |
|
|
78
88
|
|
|
89
|
+
## Design Decisions
|
|
90
|
+
|
|
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.
|
|
92
|
+
|
|
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.
|
|
94
|
+
|
|
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.
|
|
96
|
+
|
|
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.
|
|
98
|
+
|
|
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.
|
|
100
|
+
|
|
79
101
|
## Dependencies
|
|
80
102
|
- `@bananapus/core-v6` — Core protocol (controller, directory, permissions)
|
|
81
103
|
- `@bananapus/721-hook-v6` — NFT tier deployment
|
|
@@ -83,4 +105,3 @@ Owner → JBOmnichainDeployer.launchRulesetsFor()
|
|
|
83
105
|
- `@bananapus/permission-ids-v6` — Permission constants
|
|
84
106
|
- `@bananapus/suckers-v6` — Cross-chain sucker registry
|
|
85
107
|
- `@openzeppelin/contracts` — ERC2771, ERC721Receiver
|
|
86
|
-
- `@prb/math` — Fixed-point math (`mulDiv` for weight scaling)
|
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
# nana-omnichain-deployers-v6 -- Audit Instructions
|
|
2
2
|
|
|
3
|
+
## Previous Audit Findings
|
|
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).
|
|
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`) |
|
|
20
|
+
|
|
3
21
|
## Scope
|
|
4
22
|
|
|
5
|
-
One contract,
|
|
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.
|
|
6
24
|
|
|
7
25
|
## Architecture
|
|
8
26
|
|
|
9
27
|
| File | Lines | Role |
|
|
10
28
|
|------|------:|------|
|
|
11
|
-
| `src/JBOmnichainDeployer.sol` |
|
|
29
|
+
| `src/JBOmnichainDeployer.sol` | 872 | Main contract. Deploys projects, queues rulesets, proxies data hook calls. Implements `IJBRulesetDataHook`, `IERC721Receiver`, `JBPermissioned`, `ERC2771Context`. |
|
|
12
30
|
| `src/interfaces/IJBOmnichainDeployer.sol` | 171 | Public interface. |
|
|
13
31
|
| `src/structs/JBDeployerHookConfig.sol` | 11 | Stores custom data hook address + pay/cashout flags per ruleset. |
|
|
14
32
|
| `src/structs/JBOmnichain721Config.sol` | 16 | 721 hook deployment config: tiers config + cashout flag + salt. |
|
|
15
33
|
| `src/structs/JBSuckerDeploymentConfig.sol` | 12 | Sucker deployer configs + salt for deterministic addresses. |
|
|
16
34
|
| `src/structs/JBTiered721HookConfig.sol` | 10 | Stores 721 hook address + `useDataHookForCashOut` flag per ruleset. |
|
|
17
35
|
|
|
18
|
-
**Total source**: ~1,
|
|
36
|
+
**Total source**: ~1,092 lines.
|
|
19
37
|
|
|
20
38
|
## External Dependencies
|
|
21
39
|
|
|
@@ -111,7 +129,7 @@ function launchRulesetsFor(
|
|
|
111
129
|
) external returns (uint256 rulesetId, IJB721TiersHook hook)
|
|
112
130
|
```
|
|
113
131
|
|
|
114
|
-
**Permission checks**: Requires both `
|
|
132
|
+
**Permission checks**: Requires both `LAUNCH_RULESETS` and `SET_TERMINALS` from project owner.
|
|
115
133
|
|
|
116
134
|
**Controller validation**: `_validateController(projectId, controller)` checks `controller.DIRECTORY().controllerOf(projectId) == controller`.
|
|
117
135
|
|
|
@@ -252,7 +270,6 @@ For each ruleset config at index `i`:
|
|
|
252
270
|
| `JBOmnichainDeployer_InvalidHook` | Ruleset's `metadata.dataHook == address(this)` | `_setup721` (called by all launch/queue functions) |
|
|
253
271
|
| `JBOmnichainDeployer_ProjectIdMismatch` | `controller.launchProjectFor` returns unexpected project ID | `_launchProjectFor` |
|
|
254
272
|
| `JBOmnichainDeployer_RulesetIdsUnpredictable` | `latestRulesetIdOf(projectId) >= block.timestamp` | `_queueRulesetsOf` |
|
|
255
|
-
| `JBOmnichainDeployer_UnexpectedNFT` | **Declared but never used** (dead code). | N/A |
|
|
256
273
|
| `JBOmnichainDeployer_UnexpectedNFTReceived` | `onERC721Received` called by non-`PROJECTS` contract | `onERC721Received` |
|
|
257
274
|
|
|
258
275
|
## Priority Audit Areas
|
|
@@ -311,7 +328,7 @@ For each ruleset config at index `i`:
|
|
|
311
328
|
|
|
312
329
|
## Test Suite Overview
|
|
313
330
|
|
|
314
|
-
|
|
331
|
+
14 test files, ~5,000 lines of test code:
|
|
315
332
|
|
|
316
333
|
| Category | Files | Coverage |
|
|
317
334
|
|----------|-------|----------|
|
|
@@ -324,7 +341,7 @@ For each ruleset config at index `i`:
|
|
|
324
341
|
| Invariants | `invariants/OmnichainDeployerInvariant.t.sol` + handler | Sucker 0% tax, 721 spec ordering, fund conservation, token supply consistency, deployer ETH balance, hook storage consistency |
|
|
325
342
|
| Regression | `regression/HookOwnershipTransfer.t.sol` | Hook ownership transfer in `queueRulesetsOf` |
|
|
326
343
|
| Regression | `regression/ValidateController.t.sol` | Controller validation rejects fake controllers |
|
|
327
|
-
| Fork | `fork/TestOmnichain*.t.sol` (
|
|
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 |
|
|
328
345
|
|
|
329
346
|
## Testing Setup
|
|
330
347
|
|
|
@@ -342,7 +359,28 @@ RPC_ETHEREUM_MAINNET=<your_rpc> forge test --match-path 'test/fork/*.t.sol' -vvv
|
|
|
342
359
|
RPC_ETHEREUM_MAINNET=<your_rpc> forge test --match-contract OmnichainDeployerInvariant -vvv
|
|
343
360
|
|
|
344
361
|
# Compiler settings
|
|
345
|
-
# Solidity 0.8.
|
|
362
|
+
# Solidity 0.8.28, EVM version: cancun, via_ir: true, optimizer: 200 runs
|
|
346
363
|
```
|
|
347
364
|
|
|
348
365
|
Foundry config is at `foundry.toml`. Fuzz runs: 4096. Invariant runs: 1024, depth: 100, `fail_on_revert: false`.
|
|
366
|
+
|
|
367
|
+
## How to Report Findings
|
|
368
|
+
|
|
369
|
+
Each finding should follow this 7-point structure:
|
|
370
|
+
|
|
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.
|
|
378
|
+
|
|
379
|
+
### Severity Guide
|
|
380
|
+
|
|
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. |
|
package/CHANGE_LOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This document describes all changes between `nana-omnichain-deployers` (v5) and `nana-omnichain-deployers-v6` (v6).
|
|
4
4
|
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
- **Every project gets a 721 hook**: All projects launched through the omnichain deployer now receive a 721 hook (even without tiers), unifying the deployment model.
|
|
8
|
+
- **Dual-hook architecture**: Separate tracking of 721 hooks (`_tiered721HookOf`) and extra data hooks (`_extraDataHookOf`) enables composing both in `beforePayRecordedWith` with proportional weight scaling.
|
|
9
|
+
- **v5 ownership bug fixed**: `queue721RulesetsOf` in v5 deployed 721 hooks but never transferred ownership to the project — v6 properly calls `transferOwnershipToProject` on all paths.
|
|
10
|
+
- **Function consolidation**: `launch721ProjectFor`, `launch721RulesetsFor`, and `queue721RulesetsOf` merged into overloads of their non-721 counterparts using `JBOmnichain721Config`.
|
|
11
|
+
- **New safety checks**: Controller validation, ruleset ID collision protection, and explicit reverts replace `assert()`.
|
|
12
|
+
|
|
5
13
|
---
|
|
6
14
|
|
|
7
15
|
## 1. Breaking Changes
|
|
@@ -51,6 +59,8 @@ Both v6 overloads now return `IJB721TiersHook hook` alongside `rulesetId`.
|
|
|
51
59
|
|
|
52
60
|
v6 also fixes v5's `queue721RulesetsOf` which deployed the 721 hook but never called `JBOwnable(hook).transferOwnershipToProject(projectId)`, leaving the hook owned by the deployer contract rather than the project. In v6, all paths that deploy a 721 hook — including `_queueRulesetsOf` — properly transfer hook ownership to the project.
|
|
53
61
|
|
|
62
|
+
> **⚠️ This was a v5 bug**: Any project that used v5's `queue721RulesetsOf` to deploy a 721 hook has that hook owned by the deployer contract, not the project. Projects affected should manually transfer hook ownership.
|
|
63
|
+
|
|
54
64
|
### 1.6 `queue721RulesetsOf` removed
|
|
55
65
|
|
|
56
66
|
Replaced by the `queueRulesetsOf(... JBOmnichain721Config ...)` overload described above.
|
|
@@ -73,7 +83,7 @@ The `dataHook` field moved from last to first. This changes ABI encoding and is
|
|
|
73
83
|
### 1.9 Solidity version bumped
|
|
74
84
|
|
|
75
85
|
**v5**: `pragma solidity 0.8.23`
|
|
76
|
-
**v6**: `pragma solidity 0.8.
|
|
86
|
+
**v6**: `pragma solidity 0.8.28`
|
|
77
87
|
|
|
78
88
|
### 1.10 721 hook config types replaced
|
|
79
89
|
|
|
@@ -311,3 +321,5 @@ v6: `if (msg.sender != address(PROJECTS)) revert JBOmnichainDeployer_UnexpectedN
|
|
|
311
321
|
| `queue721RulesetsOf(projectId, deployTiersHookConfig, queueRulesetsConfig, controller, salt)` | `queueRulesetsOf(projectId, deploy721Config, rulesetConfigs, memo, controller)` | 721 config bundled into `JBOmnichain721Config`. Standard `JBRulesetConfig[]` replaces `JBQueueRulesetsConfig`. |
|
|
312
322
|
| `dataHookOf(projectId, rulesetId)` | `extraDataHookOf(projectId, rulesetId)` + `tiered721HookOf(projectId, rulesetId)` | Split into two views. `extraDataHookOf` returns `JBDeployerHookConfig memory`. `tiered721HookOf` returns `(IJB721TiersHook, bool)`. |
|
|
313
323
|
| `deploySuckersFor(projectId, suckerConfig)` | `deploySuckersFor(projectId, suckerConfig)` | Unchanged. |
|
|
324
|
+
|
|
325
|
+
> **Cross-repo impact**: Uses `LAUNCH_RULESETS` from `nana-permission-ids-v6` (split from `QUEUE_RULESETS`). The dual-hook composition pattern in `beforePayRecordedWith` uses `mulDiv` from `@prb/math` to scale weight proportionally — `revnet-core-v6` implements a similar pattern for its buyback hook + 721 hook composition.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Launching a cross-chain Juicebox project normally takes several steps: deploy th
|
|
|
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
12
|
|
|
13
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
|
|
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
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
16
|
- **Returns default values** if neither hook has the relevant flag set.
|
|
17
17
|
|
|
@@ -84,7 +84,7 @@ This means a project can have both a 721 hook (for NFT minting on payments) and
|
|
|
84
84
|
|
|
85
85
|
### Simplified Overloads
|
|
86
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. For `queueRulesetsOf`, since the default config has 0 tiers, the existing 721 hook is always carried forward.
|
|
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
88
|
|
|
89
89
|
### Deterministic Cross-Chain Addresses
|
|
90
90
|
|
|
@@ -99,6 +99,15 @@ This means:
|
|
|
99
99
|
- Different senders can't collide, even with the same salt
|
|
100
100
|
- `salt = bytes32(0)` skips sucker deployment entirely
|
|
101
101
|
|
|
102
|
+
### Supported Chains
|
|
103
|
+
|
|
104
|
+
`JBOmnichainDeployer` supports the same chains as the sucker deployers it wraps. Currently supported:
|
|
105
|
+
|
|
106
|
+
- **Mainnets**: Ethereum, Optimism, Base, Arbitrum
|
|
107
|
+
- **Testnets**: Ethereum Sepolia, Optimism Sepolia, Base Sepolia, Arbitrum Sepolia
|
|
108
|
+
|
|
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.
|
|
110
|
+
|
|
102
111
|
### Ruleset ID Prediction
|
|
103
112
|
|
|
104
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`.
|
|
@@ -155,7 +164,8 @@ Add to `remappings.txt`:
|
|
|
155
164
|
[profile.default]
|
|
156
165
|
solc = '0.8.26'
|
|
157
166
|
evm_version = 'cancun'
|
|
158
|
-
|
|
167
|
+
via_ir = true
|
|
168
|
+
optimizer_runs = 200
|
|
159
169
|
|
|
160
170
|
[fuzz]
|
|
161
171
|
runs = 4096
|
|
@@ -165,28 +175,40 @@ runs = 4096
|
|
|
165
175
|
|
|
166
176
|
```
|
|
167
177
|
src/
|
|
168
|
-
JBOmnichainDeployer.sol
|
|
178
|
+
JBOmnichainDeployer.sol # Main contract (~817 lines)
|
|
169
179
|
interfaces/
|
|
170
|
-
IJBOmnichainDeployer.sol
|
|
180
|
+
IJBOmnichainDeployer.sol # Public interface
|
|
171
181
|
structs/
|
|
172
|
-
JBDeployerHookConfig.sol
|
|
173
|
-
JBOmnichain721Config.sol
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
176
186
|
test/
|
|
177
|
-
JBOmnichainDeployer.t.sol
|
|
178
|
-
JBOmnichainDeployerGuard.t.sol
|
|
179
|
-
OmnichainDeployerAttacks.t.sol
|
|
180
|
-
OmnichainDeployerEdgeCases.t.sol
|
|
181
|
-
OmnichainDeployerReentrancy.t.sol
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
184
205
|
regression/
|
|
185
|
-
HookOwnershipTransfer.t.sol
|
|
206
|
+
HookOwnershipTransfer.t.sol # Hook ownership transfer regression
|
|
207
|
+
ValidateController.t.sol # Controller validation regression
|
|
186
208
|
script/
|
|
187
|
-
Deploy.s.sol
|
|
209
|
+
Deploy.s.sol # Sphinx deployment script
|
|
188
210
|
helpers/
|
|
189
|
-
DeployersDeploymentLib.sol
|
|
211
|
+
DeployersDeploymentLib.sol # Deployment address helper
|
|
190
212
|
```
|
|
191
213
|
|
|
192
214
|
## Permissions
|
package/RISKS.md
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
- **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
6
|
- **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 `DIRECTORY.controllerOf(projectId)
|
|
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
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.
|
|
9
9
|
|
|
10
10
|
## 2. Economic / Manipulation Risks
|
|
11
11
|
|
|
12
12
|
- **Sucker cashout bypass.** Any address registered as a sucker for a project gets 0% cashout tax rate and full reclaim. If a malicious sucker is registered (via compromised `SUCKER_REGISTRY`), it can drain the project's surplus.
|
|
13
13
|
- **Weight manipulation via extra data hook.** `beforePayRecordedWith` forwards to the extra data hook, which can return any `weight`. A malicious hook can inflate token minting or set weight=0 to block minting.
|
|
14
|
-
- **721 hook amount splitting.** The deployer computes `projectAmount = context.amount.value - totalSplitAmount
|
|
14
|
+
- **721 hook amount splitting.** The deployer computes `projectAmount = context.amount.value - totalSplitAmount`. The 721 hook's returned weight (already adjusted for splits via `JB721TiersHookLib.calculateWeight`) is used directly -- no proportional scaling is applied. If the 721 hook returns a `totalSplitAmount >= context.amount.value`, `projectAmount` is set to 0 and weight becomes 0 -- no tokens are minted for the payment.
|
|
15
15
|
|
|
16
16
|
## 3. Access Control
|
|
17
17
|
|
|
@@ -21,21 +21,40 @@
|
|
|
21
21
|
|
|
22
22
|
## 4. DoS Vectors
|
|
23
23
|
|
|
24
|
-
- **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.
|
|
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).
|
|
24
|
+
- **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.
|
|
26
26
|
- **721 hook deployment revert.** `HOOK_DEPLOYER.deployHookFor` is called without try-catch. A failing deployment blocks the entire project launch.
|
|
27
27
|
|
|
28
|
-
## 5.
|
|
28
|
+
## 5. Reentrancy Surface
|
|
29
|
+
|
|
30
|
+
- **`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.
|
|
32
|
+
- **`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
|
+
- **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
|
+
|
|
35
|
+
## 6. Integration Risks
|
|
29
36
|
|
|
30
37
|
- **Hook config keyed by predicted rulesetId.** Configs stored at `block.timestamp + i` must match the actual rulesetId assigned by the controller. If the controller assigns different IDs (e.g., due to approval hook delays), the stored configs become unreachable -- payments/cashouts fall through to default behavior (no 721 handling, no extra hook).
|
|
31
38
|
- **Carried-forward 721 hook on queue.** When `tiers.length == 0`, `queueRulesetsOf` carries forward the hook from `_tiered721HookOf[projectId][latestRulesetId]`. If the latest ruleset was not deployed through this deployer, the mapping is empty and the call reverts with `JBOmnichainDeployer_InvalidHook`.
|
|
32
39
|
- **ERC721Receiver restriction.** `onERC721Received` only accepts from `PROJECTS`. Any other NFTs sent to this contract are permanently lost.
|
|
40
|
+
- **Cross-reference: sucker registration.** The deployer grants `MAP_SUCKER_TOKEN` to `SUCKER_REGISTRY` with `projectId=0` (wildcard). This means the registry can map tokens for ALL projects deployed through this deployer. See [nana-suckers-v6 RISKS.md](../nana-suckers-v6/RISKS.md) for the full sucker lifecycle risks.
|
|
41
|
+
- **Cross-reference: core reentrancy.** The deployer delegates to `JBController` and `JBMultiTerminal` for all fund operations. See [nana-core-v6 RISKS.md](../nana-core-v6/RISKS.md) section 3 for the reentrancy surface of these contracts.
|
|
33
42
|
|
|
34
|
-
##
|
|
43
|
+
## 7. Invariants to Verify
|
|
35
44
|
|
|
36
45
|
- For any project launched through this deployer, `DIRECTORY.controllerOf(projectId)` matches the controller used during launch.
|
|
37
46
|
- `_tiered721HookOf[projectId][rulesetId]` is non-zero for every rulesetId created through this deployer.
|
|
38
47
|
- Sucker cashouts always receive 0% tax rate (no path where `isSuckerOf` returns true but tax > 0).
|
|
39
|
-
- `beforePayRecordedWith`
|
|
48
|
+
- `beforePayRecordedWith` uses the 721 hook's weight directly (already split-adjusted by `JB721TiersHookLib.calculateWeight`), so no additional scaling is applied.
|
|
40
49
|
- Self-reference prevention: `rulesetConfigurations[i].metadata.dataHook` cannot be `address(this)` after `_setup721`.
|
|
41
50
|
- Project NFT ownership: after `_launchProjectFor`, the project NFT is owned by `owner`, not the deployer.
|
|
51
|
+
|
|
52
|
+
## 8. Accepted Behaviors
|
|
53
|
+
|
|
54
|
+
### 8.1 Controller validation skipped during `launchProjectFor` (by design)
|
|
55
|
+
|
|
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.
|
|
57
|
+
|
|
58
|
+
### 8.2 Suckers receive 0% cashout tax (shared with revnet-core)
|
|
59
|
+
|
|
60
|
+
`beforeCashOutRecordedWith` returns `cashOutTaxRate = 0` for addresses registered in `SUCKER_REGISTRY`. This is the same trust model as REVDeployer. The security boundary is the sucker registry — only addresses deployed through authorized deployers receive this privilege. See [revnet-core-v6 RISKS.md](../revnet-core-v6/RISKS.md) section 4 for the full sucker bypass analysis.
|
package/SKILLS.md
CHANGED
|
@@ -29,7 +29,7 @@ Single-transaction deployment of Juicebox projects with cross-chain suckers and
|
|
|
29
29
|
| Function | What it does |
|
|
30
30
|
|----------|-------------|
|
|
31
31
|
| `beforePayRecordedWith(context)` | Calls the 721 hook first (via `_tiered721HookOf`) for its specs (including split amounts), then calls the custom hook from `_extraDataHookOf` (if `useDataHookForPay: true`) with a reduced amount context (payment minus split amount) for weight + specs. Adjusts the returned weight proportionally so the terminal only mints tokens for the amount entering the project (`weight = mulDiv(weight, amount - splitAmount, amount)`). Merges specs (721 hook specs first if any, then custom hook specs). If the 721 hook returns no specs (0 tiers), its slot is omitted from the output. |
|
|
32
|
-
| `beforeCashOutRecordedWith(context)` | If holder is a sucker: returns 0% tax immediately. Calls the 721 hook first (if `useDataHookForCashOut: true`), updating cash out parameters. Then calls the custom hook from `_extraDataHookOf` (if `useDataHookForCashOut: true`) with the already-updated values. Both hooks' specifications are merged (721 specs first, then custom hook specs). If the 721 hook has the flag set and reverts (e.g., fungible cashout), the revert propagates. If neither has the flag set, returns original values. |
|
|
32
|
+
| `beforeCashOutRecordedWith(context)` | If holder is a sucker: returns 0% tax immediately. Calls the 721 hook first (if `useDataHookForCashOut: true`), updating cash out parameters. Then calls the custom hook from `_extraDataHookOf` (if `useDataHookForCashOut: true`) with the already-updated values. Both hooks' specifications are merged (721 specs first, then custom hook specs). If the 721 hook has the flag set and reverts (e.g., fungible cashout), the revert propagates. If neither has the flag set, returns original values. Hook specifications include a `noop` field — the 721 hook always returns `noop: false` (it needs its callback), while a custom hook like the buyback hook may return `noop: true` with routing diagnostics when the protocol path wins. |
|
|
33
33
|
| `hasMintPermissionFor(projectId, ruleset, addr)` | Returns `true` for registered suckers, OR if the custom hook in `_extraDataHookOf` grants permission. Returns `false` only if it doesn't grant it. |
|
|
34
34
|
|
|
35
35
|
### Views
|
|
@@ -82,30 +82,54 @@ Single-transaction deployment of Juicebox projects with cross-chain suckers and
|
|
|
82
82
|
| `JBOmnichainDeployer_ProjectIdMismatch` | `launchProjectFor` -- the project ID returned by the controller does not match the predicted `PROJECTS.count() + 1` |
|
|
83
83
|
| `JBOmnichainDeployer_ControllerMismatch` | `launchRulesetsFor`/`queueRulesetsOf` -- the provided controller does not match the project's controller in `JBDirectory` |
|
|
84
84
|
|
|
85
|
+
## Events
|
|
86
|
+
|
|
87
|
+
`JBOmnichainDeployer` does not declare any custom events. All observable state changes (project creation, ruleset queuing, sucker deployment) are emitted by the underlying contracts it calls (`IJBController`, `IJBSuckerRegistry`, `IJB721TiersHookDeployer`).
|
|
88
|
+
|
|
89
|
+
## Constants
|
|
90
|
+
|
|
91
|
+
| Name | Value | Context |
|
|
92
|
+
|------|-------|---------|
|
|
93
|
+
| `PROJECTS` | Set at construction | `IJBProjects` -- mints project NFTs. Immutable. |
|
|
94
|
+
| `HOOK_DEPLOYER` | Set at construction | `IJB721TiersHookDeployer` -- deploys 721 tiers hooks. Immutable. |
|
|
95
|
+
| `SUCKER_REGISTRY` | Set at construction | `IJBSuckerRegistry` -- deploys/tracks suckers, `isSuckerOf` checks. Immutable. |
|
|
96
|
+
| `projectId = 0` (wildcard) | Used in constructor | `MAP_SUCKER_TOKEN` permission granted to `SUCKER_REGISTRY` with `projectId=0`, giving it token mapping rights for all projects. |
|
|
97
|
+
| `decimals = 18` | Used in `_default721Config` | Default decimal precision when 721 config is omitted (simplified overloads). |
|
|
98
|
+
| `baseCurrency` | From first ruleset | When 721 config is omitted, `baseCurrency` is read from `rulesetConfigurations[0].metadata.baseCurrency`. Reverts with `JBOmnichainDeployer_NoRulesetConfigurations` if the array is empty. |
|
|
99
|
+
|
|
85
100
|
## Gotchas
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
### Deployment
|
|
103
|
+
|
|
104
|
+
- `launchProjectFor` requires **no permissions** -- anyone can launch a project to any owner address.
|
|
105
|
+
- `queueRulesetsOf` **reverts if called in the same block** as a previous ruleset queue (whether via deployer or directly). The `launchProjectFor` function doesn't have this guard because it predicts IDs from `PROJECTS.count()`, which is always 0 for a new project.
|
|
106
|
+
- Ruleset IDs in `_extraDataHookOf` are keyed by `block.timestamp + i`. If the controller assigns different IDs than predicted, the stored hook configs will be orphaned and the deployer will behave as if no hooks were set (returning default values).
|
|
107
|
+
- Sucker deployment salts are hashed with `_msgSender()`: `keccak256(abi.encode(salt, _msgSender()))`. Cross-chain deterministic addresses require using the **same sender** on each chain. The 721 hook salt uses `keccak256(abi.encode(_msgSender(), salt))` (reversed order).
|
|
108
|
+
- `salt = bytes32(0)` **skips sucker deployment entirely**. Use a nonzero salt to deploy suckers.
|
|
109
|
+
- Hook ownership is transferred to the **project** (not the owner) via `JBOwnable.transferOwnershipToProject(projectId)`. This happens **after** the project NFT is minted.
|
|
110
|
+
- The deployer holds the project NFT temporarily during launch. If the controller's `launchProjectFor` reverts, the entire transaction reverts -- no stuck NFTs.
|
|
111
|
+
- Every project always gets a 721 hook, even with an empty tiers array. This wires up the 721 hook from the start, so tiers can be added later without reconfiguring the data hook.
|
|
112
|
+
- For `queueRulesetsOf`, if no new tiers are provided, the 721 hook from the **latest ruleset** is carried forward instead of deploying a new one. Looked up from `_tiered721HookOf[projectId][latestRulesetId]`.
|
|
113
|
+
|
|
114
|
+
### Data Hook Behavior
|
|
115
|
+
|
|
116
|
+
- The deployer **always forces `useDataHookForCashOut = true`** at the protocol level so it can intercept cash outs for sucker tax exemption. However, the 721 hook's `useDataHookForCashOut` flag (stored in `_tiered721HookOf`) and the custom hook's flag (stored in `_extraDataHookOf`) each control whether that hook processes cash outs. Set `useDataHookForCashOut: false` on the 721 config to skip it for fungible cashouts (it reverts with `JB721Hook_UnexpectedTokenCashedOut` otherwise).
|
|
117
|
+
- Suckers get an **early return** in `beforeCashOutRecordedWith` -- they bypass all stored hooks entirely. Suckers can cash out even if any hook would revert.
|
|
118
|
+
- If no custom hook is stored or it doesn't grant permission, `hasMintPermissionFor` returns `false` for non-suckers. Only the custom hook in `_extraDataHookOf` is checked -- the 721 hook is not consulted.
|
|
119
|
+
- `_setup721()` sets `metadata.dataHook = address(this)`, `metadata.useDataHookForPay = true`, and `metadata.useDataHookForCashOut = true` on every ruleset. These cannot be overridden.
|
|
120
|
+
- All data hook functions (`beforePayRecordedWith`, `beforeCashOutRecordedWith`, `hasMintPermissionFor`) are `view`. If the project's real hook needs to modify state in these functions, it will fail.
|
|
121
|
+
- Setting a hook's `dataHook` to `address(this)` (the deployer itself) reverts with `JBOmnichainDeployer_InvalidHook`. This prevents infinite forwarding loops.
|
|
122
|
+
- The 721 hook is stored per-ruleset in `_tiered721HookOf[projectId][rulesetId]` with its `useDataHookForCashOut` flag. The custom data hook (if any) is stored separately in `_extraDataHookOf[projectId][rulesetId]`. They are never in the same mapping.
|
|
123
|
+
- The `JBOmnichain721Config` parameter bundles the 721 hook deployment config, the `useDataHookForCashOut` flag, and the `salt`. Custom data hooks are read from each ruleset's `metadata.dataHook` field.
|
|
124
|
+
|
|
125
|
+
### Permissions
|
|
126
|
+
|
|
127
|
+
- The constructor grants `MAP_SUCKER_TOKEN` permission to `SUCKER_REGISTRY` with `projectId=0`, meaning the registry can map tokens for **any project** deployed through this deployer.
|
|
128
|
+
|
|
129
|
+
### Edge Cases
|
|
130
|
+
|
|
131
|
+
- `onERC721Received` only accepts NFTs from the `PROJECTS` contract. Sending any other ERC-721 to the deployer will revert.
|
|
132
|
+
- ERC2771 meta-transaction support allows gasless deployments via a trusted forwarder. Salt hashing uses `_msgSender()` (not `msg.sender`), so forwarder-relayed transactions use the original sender's address for deterministic sucker addresses.
|
|
109
133
|
|
|
110
134
|
## Example Integration
|
|
111
135
|
|
|
@@ -172,3 +196,58 @@ JBOmnichain721Config memory queue721Config = JBOmnichain721Config({
|
|
|
172
196
|
controller: controller
|
|
173
197
|
});
|
|
174
198
|
```
|
|
199
|
+
|
|
200
|
+
## Buyback Hook + 721 Hook Composition
|
|
201
|
+
|
|
202
|
+
The most complex use case: a project with NFT tiers (721 hook) AND a buyback hook, both running on every payment. The deployer composes them automatically.
|
|
203
|
+
|
|
204
|
+
```solidity
|
|
205
|
+
import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
|
|
206
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
207
|
+
|
|
208
|
+
// --- Key concept: the buyback hook goes in metadata.dataHook ---
|
|
209
|
+
// The deployer extracts it, stores it in _extraDataHookOf, and replaces
|
|
210
|
+
// metadata.dataHook with address(this) so it can intercept all calls.
|
|
211
|
+
|
|
212
|
+
// 1. Configure the buyback hook as the ruleset's custom data hook.
|
|
213
|
+
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
214
|
+
rulesetConfigs[0].metadata = JBRulesetMetadata({
|
|
215
|
+
// ... other metadata fields ...
|
|
216
|
+
dataHook: address(buybackHook), // <-- deployer extracts this
|
|
217
|
+
useDataHookForPay: true, // buyback hook processes payments
|
|
218
|
+
useDataHookForCashOut: false // buyback hook does NOT process cashouts
|
|
219
|
+
// ...
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// 2. Configure the 721 hook with NFT tiers.
|
|
223
|
+
JBOmnichain721Config memory deploy721Config = JBOmnichain721Config({
|
|
224
|
+
deployTiersHookConfig: tiersHookConfig, // your NFT tier config
|
|
225
|
+
useDataHookForCashOut: false, // false = skip 721 on fungible cashouts
|
|
226
|
+
salt: bytes32("my-hook-salt")
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// 3. Launch -- both hooks are wired up automatically.
|
|
230
|
+
(uint256 projectId, IJB721TiersHook hook, address[] memory suckers) =
|
|
231
|
+
omnichainDeployer.launchProjectFor({
|
|
232
|
+
owner: msg.sender,
|
|
233
|
+
projectUri: "ipfs://metadata",
|
|
234
|
+
deploy721Config: deploy721Config,
|
|
235
|
+
rulesetConfigurations: rulesetConfigs,
|
|
236
|
+
terminalConfigurations: terminalConfigs,
|
|
237
|
+
memo: "Buyback + 721 project",
|
|
238
|
+
suckerDeploymentConfiguration: suckerConfig,
|
|
239
|
+
controller: controller
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// --- What happens on each payment: ---
|
|
243
|
+
// 1. beforePayRecordedWith calls the 721 hook first (tier matching, NFT minting specs)
|
|
244
|
+
// 2. Reduces the payment amount by the 721 split amount
|
|
245
|
+
// 3. Calls the buyback hook with the reduced amount (so it only buys back with leftover)
|
|
246
|
+
// 4. Adjusts weight proportionally: weight = mulDiv(weight, amount - splitAmount, amount)
|
|
247
|
+
// 5. Merges specs: 721 specs first, then buyback specs
|
|
248
|
+
//
|
|
249
|
+
// --- What happens on cashout: ---
|
|
250
|
+
// With useDataHookForCashOut: false on both hooks:
|
|
251
|
+
// - Suckers still get 0% tax (the deployer intercepts before any hook)
|
|
252
|
+
// - Regular users get standard bonding curve cashout (no hook interference)
|
|
253
|
+
```
|