@bananapus/omnichain-deployers-v6 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +30 -2
- 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 +4 -5
- package/references/operations.md +28 -0
- package/references/runtime.md +28 -0
- package/src/JBOmnichainDeployer.sol +7 -2
- package/src/interfaces/IJBOmnichainDeployer.sol +3 -0
- package/test/JBOmnichainDeployer.t.sol +23 -0
- package/test/OmnichainDeployerEdgeCases.t.sol +10 -1
- package/test/TestAuditGaps.sol +23 -0
- package/test/audit/CarryForwardRejectedHook.t.sol +305 -0
- package/test/audit/JBOmnichainDeployer.t.sol +10 -0
- package/test/fork/OmnichainForkTestBase.sol +10 -7
- package/test/fork/TestOmnichain721QueueAndAdjust.t.sol +28 -21
- package/CHANGE_LOG.md +0 -341
- package/assets/findings/nana-omnichain-deployers-v6-pashov-ai-audit-report-20260330-103536.md +0 -39
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.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
This file describes the verified change from `nana-omnichain-deployers-v5` to the current `nana-omnichain-deployers-v6` repo.
|
|
6
|
+
|
|
7
|
+
## Current v6 surface
|
|
8
|
+
|
|
9
|
+
- `JBOmnichainDeployer`
|
|
10
|
+
- `IJBOmnichainDeployer`
|
|
11
|
+
- `JBDeployerHookConfig`
|
|
12
|
+
- `JBOmnichain721Config`
|
|
13
|
+
- `JBTiered721HookConfig`
|
|
14
|
+
|
|
15
|
+
## Summary
|
|
16
|
+
|
|
17
|
+
- The deployer now assumes a 721 hook is part of the standard deployment path instead of a special-case path.
|
|
18
|
+
- Hook composition is more explicit. The current repo separates 721-hook behavior from extra data-hook behavior and combines them deliberately.
|
|
19
|
+
- The v6 test suite includes dedicated coverage for ownership transfer, controller validation, empty ruleset edge cases, hook composition, and invariants that were not present in the small v5 tree.
|
|
20
|
+
- The repo moved to the v6 Solidity and dependency baseline.
|
|
21
|
+
|
|
22
|
+
## Verified deltas
|
|
23
|
+
|
|
24
|
+
- `launch721ProjectFor(...)`, `launch721RulesetsFor(...)`, and `queue721RulesetsOf(...)` no longer define the public API shape.
|
|
25
|
+
- Their role is covered by overloaded `launchProjectFor(...)`, `launchRulesetsFor(...)`, and `queueRulesetsOf(...)` entry points that accept `JBOmnichain721Config`.
|
|
26
|
+
- `extraDataHookOf(...)` and `tiered721HookOf(...)` replace the older single `dataHookOf(...)` view model.
|
|
27
|
+
- The overloaded launch and queue functions now return the `IJB721TiersHook` they deploy or carry forward.
|
|
28
|
+
|
|
29
|
+
## Breaking ABI changes
|
|
30
|
+
|
|
31
|
+
- The 721-specific launch and queue entry points were removed from the public API shape.
|
|
32
|
+
- `dataHookOf(...)` was replaced by `extraDataHookOf(...)` plus `tiered721HookOf(...)`.
|
|
33
|
+
- `launchProjectFor(...)`, `launchRulesetsFor(...)`, and `queueRulesetsOf(...)` now have overloads that return the hook.
|
|
34
|
+
- `JBOmnichain721Config` replaces the old direct 721 deploy config entrypoint model.
|
|
35
|
+
|
|
36
|
+
## Indexer impact
|
|
37
|
+
|
|
38
|
+
- Hook composition is now split across two tracked hook sources instead of one.
|
|
39
|
+
- Launch and queue flows should expect a 721 hook in the returned state and in the deployer's stored per-ruleset data.
|
|
40
|
+
|
|
41
|
+
## Migration notes
|
|
42
|
+
|
|
43
|
+
- Update any code that expected separate "with 721" and "without 721" deployment paths to behave like v5.
|
|
44
|
+
- Re-check ownership assumptions after hook deployment. The current repo is stricter and more explicit about that flow.
|
|
45
|
+
- If you decode launch or queue inputs, use the current v6 structs instead of v5 layouts.
|
|
46
|
+
|
|
47
|
+
## ABI appendix
|
|
48
|
+
|
|
49
|
+
- Removed public API shape
|
|
50
|
+
- `launch721ProjectFor(...)`
|
|
51
|
+
- `launch721RulesetsFor(...)`
|
|
52
|
+
- `queue721RulesetsOf(...)`
|
|
53
|
+
- Replaced with overload families
|
|
54
|
+
- `launchProjectFor(...)`
|
|
55
|
+
- `launchRulesetsFor(...)`
|
|
56
|
+
- `queueRulesetsOf(...)`
|
|
57
|
+
- Replaced hook lookup model
|
|
58
|
+
- `dataHookOf(...)` -> `extraDataHookOf(...)` + `tiered721HookOf(...)`
|
|
59
|
+
- New migration-sensitive structs
|
|
60
|
+
- `JBOmnichain721Config`
|
|
61
|
+
- `JBTiered721HookConfig`
|