@bananapus/omnichain-deployers-v6 0.0.18 → 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 +26 -0
- package/ARCHITECTURE.md +48 -85
- package/AUDIT_INSTRUCTIONS.md +52 -366
- package/CHANGELOG.md +61 -0
- package/README.md +45 -189
- package/RISKS.md +23 -6
- package/SKILLS.md +27 -240
- package/STYLE_GUIDE.md +71 -19
- package/USER_JOURNEYS.md +45 -417
- package/package.json +2 -2
- package/references/operations.md +27 -0
- package/references/runtime.md +28 -0
- package/test/fork/OmnichainForkTestBase.sol +10 -7
- package/test/fork/TestOmnichain721QueueAndAdjust.t.sol +28 -21
- package/CHANGE_LOG.md +0 -341
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
|
-
#
|
|
1
|
+
# Architecture
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## Boundaries
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
13
|
+
## Main Components
|
|
24
14
|
|
|
25
|
-
|
|
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
|
-
|
|
21
|
+
## Runtime Model
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
### Launch
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
## Where Complexity Lives
|
|
92
52
|
|
|
93
|
-
|
|
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
|
-
|
|
57
|
+
## Dependencies
|
|
96
58
|
|
|
97
|
-
|
|
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
|
-
|
|
64
|
+
## Safe Change Guide
|
|
100
65
|
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
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.
|
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -1,386 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Audit Instructions
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## Objective
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
22
|
+
Key dependencies:
|
|
23
|
+
- `nana-core-v6`
|
|
24
|
+
- `nana-721-hook-v6`
|
|
25
|
+
- `nana-suckers-v6`
|
|
332
26
|
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
# Install dependencies
|
|
350
|
-
npm install
|
|
36
|
+
## Critical Invariants
|
|
351
37
|
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
```
|
|
47
|
+
4. Ownership transfer is complete
|
|
48
|
+
Deployer-created hooks and helper contracts must not retain silent control after initialization.
|
|
364
49
|
|
|
365
|
-
|
|
50
|
+
## Threat Model
|
|
366
51
|
|
|
367
|
-
|
|
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
|
-
|
|
59
|
+
## Build And Verification
|
|
370
60
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
66
|
+
Existing tests emphasize:
|
|
67
|
+
- reentrancy and attack paths
|
|
68
|
+
- hook composition
|
|
69
|
+
- weight scaling
|
|
70
|
+
- omnichain fork and stress scenarios
|
|
380
71
|
|
|
381
|
-
|
|
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.
|