@bananapus/omnichain-deployers-v6 0.0.20 → 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 CHANGED
@@ -43,7 +43,7 @@ Admin privileges and their scope in nana-omnichain-deployers-v6.
43
43
 
44
44
  | Function | Required Role | Permission ID | Scope | What It Does |
45
45
  |----------|--------------|---------------|-------|--------------|
46
- | `deploySuckersFor` | Project owner or operator | `DEPLOY_SUCKERS` | Per-project | Deploys new cross-chain suckers for an existing project via the sucker registry. |
46
+ | `deploySuckersFor` | Project owner or operator | `DEPLOY_SUCKERS` | Per-project | Deploys new cross-chain suckers for an existing project via the sucker registry. The same operation also applies token mappings on the new suckers, so existing projects must already have the registry arranged as an authorized `MAP_SUCKER_TOKEN` operator for that project. |
47
47
  | `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`. |
48
48
  | `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`. |
49
49
 
@@ -81,12 +81,14 @@ Admin privileges and their scope in nana-omnichain-deployers-v6.
81
81
 
82
82
  | Action | Who | Mechanism |
83
83
  |--------|-----|-----------|
84
- | Deploy suckers for existing project | Project owner or DEPLOY_SUCKERS operator | `deploySuckersFor` calls `SUCKER_REGISTRY.deploySuckersFor` |
84
+ | Deploy suckers for existing project | Project owner or DEPLOY_SUCKERS operator | `deploySuckersFor` calls `SUCKER_REGISTRY.deploySuckersFor`; because the registry also applies the initial mappings, existing projects must pair this with a project-level `MAP_SUCKER_TOKEN` arrangement for the registry |
85
85
  | Deploy suckers during project launch | Project deployer (anyone) | Included in `launchProjectFor` if `salt != bytes32(0)` |
86
86
  | Map sucker tokens | JBSuckerRegistry | Granted MAP_SUCKER_TOKEN at construction with projectId=0 wildcard |
87
87
  | Grant 0% cash-out tax to suckers | Automatic | `beforeCashOutRecordedWith` checks `SUCKER_REGISTRY.isSuckerOf` |
88
88
  | Grant mint permission to suckers | Automatic | `hasMintPermissionFor` checks `SUCKER_REGISTRY.isSuckerOf` |
89
89
 
90
+ **Existing-project operator note:** `deploySuckersFor` looks like a deployment-only action at the top level, but it is intentionally a deploy-and-map flow. If an existing project delegates `DEPLOY_SUCKERS` without also arranging `MAP_SUCKER_TOKEN` for the registry, the transaction will fail once the registry reaches the mapping step.
91
+
90
92
  **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.
91
93
 
92
94
  ## Data Hook Proxy Pattern
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/omnichain-deployers-v6",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,9 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@bananapus/721-hook-v6": "^0.0.30",
21
- "@bananapus/address-registry-v6": "^0.0.16",
22
- "@bananapus/buyback-hook-v6": "^0.0.24",
23
- "@bananapus/core-v6": "^0.0.30",
21
+ "@bananapus/buyback-hook-v6": "^0.0.25",
22
+ "@bananapus/core-v6": "^0.0.31",
24
23
  "@bananapus/ownable-v6": "^0.0.16",
25
24
  "@bananapus/permission-ids-v6": "^0.0.15",
26
25
  "@bananapus/suckers-v6": "^0.0.20",
@@ -19,6 +19,7 @@
19
19
  - A same-block queue assumption breaks predicted ruleset IDs and silently strands stored config.
20
20
  - A project expects custom-hook behavior on cash-out, but the wrapper flags disable it or the sucker exemption bypasses it.
21
21
  - Deterministic deployment assumptions fail because sender or salt composition changed.
22
+ - Existing-project sucker deployment is treated as a pure `DEPLOY_SUCKERS` flow even though the registry also performs token mapping, so missing `MAP_SUCKER_TOKEN` authority for the registry shows up only when the deploy path reaches `mapTokens`.
22
23
 
23
24
  ## Useful Proof Points
24
25
 
@@ -378,7 +378,9 @@ contract JBOmnichainDeployer is
378
378
  //*********************************************************************//
379
379
 
380
380
  /// @notice Deploy new suckers for an existing project.
381
- /// @dev Only the juicebox's owner can deploy new suckers.
381
+ /// @dev Only the juicebox's owner or an operator with `JBPermissionIds.DEPLOY_SUCKERS` can call this entrypoint.
382
+ /// The downstream registry call also maps the configured tokens on each newly created sucker, so the same
383
+ /// end-to-end operation depends on the project's token-mapping authority being arranged for the registry.
382
384
  /// @param projectId The ID of the project to deploy suckers for.
383
385
  /// @param suckerDeploymentConfiguration The suckers to set up for the project.
384
386
  function deploySuckersFor(
@@ -806,7 +808,10 @@ contract JBOmnichainDeployer is
806
808
  // Use the caller-provided flag when deploying a new hook.
807
809
  use721ForCashOut = deploy721Config.useDataHookForCashOut;
808
810
  } else {
809
- JBTiered721HookConfig memory previousConfig = _tiered721HookOf[projectId][latestRulesetId];
811
+ // Read the *current* (approved) ruleset — not `latestRulesetId` — because a queued-but-unapproved
812
+ // ruleset may have been rejected by the approval hook, so its hook config should not be carried forward.
813
+ uint256 currentRulesetId = controller.RULESETS().currentOf(projectId).id;
814
+ JBTiered721HookConfig memory previousConfig = _tiered721HookOf[projectId][currentRulesetId];
810
815
  hook = previousConfig.hook;
811
816
  // Revert if no hook exists to carry forward — this means no tiers were provided and
812
817
  // no previous ruleset had a 721 hook deployed through this contract.
@@ -37,6 +37,9 @@ interface IJBOmnichainDeployer {
37
37
  returns (IJB721TiersHook hook, bool useDataHookForCashOut);
38
38
 
39
39
  /// @notice Deploy new suckers for an existing project.
40
+ /// @dev This flow forwards to `JBSuckerRegistry.deploySuckersFor`, which also applies the supplied token mappings
41
+ /// on each newly deployed sucker. Existing projects therefore need both deployment authority and a project-level
42
+ /// arrangement that lets the registry perform `MAP_SUCKER_TOKEN` for the same operation.
40
43
  /// @param projectId The ID of the project to deploy suckers for.
41
44
  /// @param suckerDeploymentConfiguration The suckers to set up for the project.
42
45
  /// @return suckers The addresses of the deployed suckers.
@@ -392,6 +392,19 @@ contract TestJBOmnichainDeployer is Test {
392
392
  abi.encodeWithSelector(IJBRulesets.latestRulesetIdOf.selector, projectId),
393
393
  abi.encode(launchTimestamp)
394
394
  );
395
+
396
+ // Mock currentOf to return a ruleset whose id matches the launch so carry-forward can look up the stored 721
397
+ // hook.
398
+ {
399
+ JBRuleset memory currentRuleset;
400
+ currentRuleset.id = uint48(launchTimestamp);
401
+ vm.mockCall(
402
+ address(rulesets),
403
+ abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId),
404
+ abi.encode(currentRuleset)
405
+ );
406
+ }
407
+
395
408
  // Warp forward so latestRulesetId < block.timestamp.
396
409
  vm.warp(block.timestamp + 1);
397
410
 
@@ -472,6 +485,16 @@ contract TestJBOmnichainDeployer is Test {
472
485
  abi.encode(launchTimestamp)
473
486
  );
474
487
 
488
+ // Mock currentOf to return a ruleset whose id matches the launch so carry-forward can look up the stored 721
489
+ // hook.
490
+ JBRuleset memory currentRuleset;
491
+ currentRuleset.id = uint48(launchTimestamp);
492
+ vm.mockCall(
493
+ address(rulesets),
494
+ abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId),
495
+ abi.encode(currentRuleset)
496
+ );
497
+
475
498
  // Warp forward so latestRulesetId < block.timestamp.
476
499
  vm.warp(block.timestamp + 1);
477
500
 
@@ -614,11 +614,20 @@ contract OmnichainDeployerEdgeCases is Test {
614
614
  abi.encode(uint256(50)) // A past ruleset ID — no hook stored at this ID
615
615
  );
616
616
 
617
+ // Mock currentOf to return a ruleset with id=50 (no hook stored for this id via the deployer).
618
+ JBRuleset memory currentRuleset;
619
+ currentRuleset.id = uint48(50);
620
+ vm.mockCall(
621
+ address(rulesets),
622
+ abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId),
623
+ abi.encode(currentRuleset)
624
+ );
625
+
617
626
  JBRulesetConfig[] memory configs = new JBRulesetConfig[](1);
618
627
  configs[0] = _makeRulesetConfig(address(0), true, false);
619
628
 
620
629
  // Empty 721 config — no tiers, so it tries to carry forward an existing hook.
621
- // But no hook was stored for rulesetId=1 via this deployer.
630
+ // But no hook was stored for rulesetId=50 via this deployer.
622
631
  JBOmnichain721Config memory empty721Config;
623
632
  vm.prank(projectOwner);
624
633
  vm.expectRevert(JBOmnichainDeployer.JBOmnichainDeployer_InvalidHook.selector);
@@ -539,6 +539,7 @@ contract TestAuditGaps is Test {
539
539
  // latestRulesetIdOf still = BASE_TIME (from launch).
540
540
  // Guard: BASE_TIME >= BASE_TIME + 1 -> false -> passes.
541
541
  _mockLatestRulesetId(BASE_TIME);
542
+ _mockCurrentRulesetId(BASE_TIME);
542
543
 
543
544
  uint256 expectedQueuedId = BASE_TIME + 1;
544
545
  vm.mockCall(
@@ -565,6 +566,7 @@ contract TestAuditGaps is Test {
565
566
  // Warp forward 1 second for first queue.
566
567
  vm.warp(BASE_TIME + 1);
567
568
  _mockLatestRulesetId(BASE_TIME);
569
+ _mockCurrentRulesetId(BASE_TIME);
568
570
 
569
571
  uint256 firstQueueTime = BASE_TIME + 1;
570
572
  vm.mockCall(
@@ -672,6 +674,7 @@ contract TestAuditGaps is Test {
672
674
  // Warp past the latestRulesetId: block.timestamp = BASE_TIME + 3.
673
675
  vm.warp(BASE_TIME + 3);
674
676
  _mockLatestRulesetId(latestRulesetId);
677
+ _mockCurrentRulesetId(BASE_TIME);
675
678
 
676
679
  uint256 expectedQueuedId = BASE_TIME + 3;
677
680
  vm.mockCall(
@@ -699,6 +702,7 @@ contract TestAuditGaps is Test {
699
702
  // Warp forward.
700
703
  vm.warp(BASE_TIME + 100);
701
704
  _mockLatestRulesetId(BASE_TIME);
705
+ _mockCurrentRulesetId(BASE_TIME);
702
706
 
703
707
  uint256 expectedQueuedId = BASE_TIME + 100;
704
708
  vm.mockCall(
@@ -739,6 +743,16 @@ contract TestAuditGaps is Test {
739
743
  vm.warp(BASE_TIME + 50);
740
744
  _mockLatestRulesetId(BASE_TIME);
741
745
 
746
+ // Mock currentOf to return a ruleset whose id matches the launch ruleset so carry-forward can look up the
747
+ // stored 721 hook.
748
+ JBRuleset memory currentRuleset;
749
+ currentRuleset.id = uint48(BASE_TIME);
750
+ vm.mockCall(
751
+ address(rulesetsContract),
752
+ abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId),
753
+ abi.encode(currentRuleset)
754
+ );
755
+
742
756
  uint256 expectedQueuedId = BASE_TIME + 50;
743
757
  vm.mockCall(
744
758
  address(controller),
@@ -820,6 +834,7 @@ contract TestAuditGaps is Test {
820
834
 
821
835
  vm.warp(BASE_TIME + 10);
822
836
  _mockLatestRulesetId(BASE_TIME);
837
+ _mockCurrentRulesetId(BASE_TIME);
823
838
 
824
839
  uint256 expectedQueuedId = BASE_TIME + 10;
825
840
  vm.mockCall(
@@ -866,6 +881,14 @@ contract TestAuditGaps is Test {
866
881
  );
867
882
  }
868
883
 
884
+ function _mockCurrentRulesetId(uint256 currentRulesetId) internal {
885
+ JBRuleset memory r;
886
+ r.id = uint48(currentRulesetId);
887
+ vm.mockCall(
888
+ address(rulesetsContract), abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId), abi.encode(r)
889
+ );
890
+ }
891
+
869
892
  function _makePayContext(uint256 pid, uint256 rid) internal returns (JBBeforePayRecordedContext memory) {
870
893
  return JBBeforePayRecordedContext({
871
894
  terminal: makeAddr("terminal"),
@@ -0,0 +1,305 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
5
+ import {JBApprovalStatus} from "@bananapus/core-v6/src/enums/JBApprovalStatus.sol";
6
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
7
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
8
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
9
+ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
10
+ import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
11
+ import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
12
+ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
13
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
14
+ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
15
+ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
16
+ import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
17
+ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
18
+ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
19
+ import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
20
+ import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
21
+ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
22
+ import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
23
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
24
+ import {JBSuckersPair} from "@bananapus/suckers-v6/src/structs/JBSuckersPair.sol";
25
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
26
+ import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
27
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
28
+ import {JBOmnichainDeployer} from "../../src/JBOmnichainDeployer.sol";
29
+ import {JBOmnichain721Config} from "../../src/structs/JBOmnichain721Config.sol";
30
+ import {JBSuckerDeploymentConfig} from "../../src/structs/JBSuckerDeploymentConfig.sol";
31
+
32
+ contract RejectingApprovalHook is ERC165, IJBRulesetApprovalHook {
33
+ function DURATION() external pure override returns (uint256) {
34
+ return 0;
35
+ }
36
+
37
+ function approvalStatusOf(uint256, JBRuleset calldata) external pure override returns (JBApprovalStatus) {
38
+ return JBApprovalStatus.Failed;
39
+ }
40
+
41
+ function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
42
+ return interfaceId == type(IJBRulesetApprovalHook).interfaceId || super.supportsInterface(interfaceId);
43
+ }
44
+ }
45
+
46
+ contract HookStub {
47
+ uint256 public lastProjectId;
48
+
49
+ function transferOwnershipToProject(uint256 projectId) external {
50
+ lastProjectId = projectId;
51
+ }
52
+ }
53
+
54
+ contract SequentialHookDeployer is IJB721TiersHookDeployer {
55
+ HookStub[] internal _hooks;
56
+ uint256 internal _index;
57
+
58
+ constructor(HookStub[] memory hooks) {
59
+ _hooks = hooks;
60
+ }
61
+
62
+ function deployHookFor(
63
+ uint256,
64
+ JBDeploy721TiersHookConfig calldata,
65
+ bytes32
66
+ )
67
+ external
68
+ override
69
+ returns (IJB721TiersHook)
70
+ {
71
+ return IJB721TiersHook(address(_hooks[_index++]));
72
+ }
73
+ }
74
+
75
+ contract MockSuckerRegistryCarryForward is IJBSuckerRegistry {
76
+ function DIRECTORY() external pure override returns (IJBDirectory) {
77
+ return IJBDirectory(address(0));
78
+ }
79
+
80
+ function PROJECTS() external pure override returns (IJBProjects) {
81
+ return IJBProjects(address(0));
82
+ }
83
+
84
+ function isSuckerOf(uint256, address) external pure override returns (bool) {
85
+ return false;
86
+ }
87
+
88
+ function suckerDeployerIsAllowed(address) external pure override returns (bool) {
89
+ return false;
90
+ }
91
+
92
+ function suckerPairsOf(uint256) external pure override returns (JBSuckersPair[] memory) {
93
+ return new JBSuckersPair[](0);
94
+ }
95
+
96
+ function suckersOf(uint256) external pure override returns (address[] memory) {
97
+ return new address[](0);
98
+ }
99
+
100
+ function MAX_TO_REMOTE_FEE() external pure override returns (uint256) {
101
+ return 0;
102
+ }
103
+
104
+ function toRemoteFee() external pure override returns (uint256) {
105
+ return 0;
106
+ }
107
+
108
+ function setToRemoteFee(uint256) external override {}
109
+ function allowSuckerDeployer(address) external override {}
110
+ function allowSuckerDeployers(address[] calldata) external override {}
111
+
112
+ function deploySuckersFor(
113
+ uint256,
114
+ bytes32,
115
+ JBSuckerDeployerConfig[] memory
116
+ )
117
+ external
118
+ pure
119
+ override
120
+ returns (address[] memory)
121
+ {
122
+ return new address[](0);
123
+ }
124
+
125
+ function removeDeprecatedSucker(uint256, address) external override {}
126
+ function removeSuckerDeployer(address) external override {}
127
+ }
128
+
129
+ contract CarryForwardRejectedHookTest is TestBaseWorkflow {
130
+ JBOmnichainDeployer internal deployer;
131
+ RejectingApprovalHook internal rejectHook;
132
+ SequentialHookDeployer internal hookDeployer;
133
+ HookStub internal initialHook;
134
+ HookStub internal rejectedHook;
135
+ MockSuckerRegistryCarryForward internal suckerRegistry;
136
+
137
+ address internal owner;
138
+
139
+ function setUp() public override {
140
+ super.setUp();
141
+
142
+ owner = multisig();
143
+ rejectHook = new RejectingApprovalHook();
144
+ initialHook = new HookStub();
145
+ rejectedHook = new HookStub();
146
+ HookStub[] memory hooks = new HookStub[](2);
147
+ hooks[0] = initialHook;
148
+ hooks[1] = rejectedHook;
149
+ hookDeployer = new SequentialHookDeployer(hooks);
150
+ suckerRegistry = new MockSuckerRegistryCarryForward();
151
+
152
+ deployer = new JBOmnichainDeployer(
153
+ IJBSuckerRegistry(address(suckerRegistry)),
154
+ IJB721TiersHookDeployer(address(hookDeployer)),
155
+ IJBPermissions(address(jbPermissions())),
156
+ IJBProjects(address(jbProjects())),
157
+ trustedForwarder()
158
+ );
159
+
160
+ vm.prank(multisig());
161
+ jbDirectory().setIsAllowedToSetFirstController(address(deployer), true);
162
+ }
163
+
164
+ function test_queueCarryForward_usesRejectedRulesetHook_notCurrentRulesetHook() public {
165
+ JBRulesetConfig[] memory initialRulesets = _makeRulesets(rejectHook);
166
+ JBTerminalConfig[] memory terminals = new JBTerminalConfig[](0);
167
+ JBOmnichain721Config memory configWithTiers = _configWithTiers(false);
168
+
169
+ (uint256 projectId, IJB721TiersHook launchedHook,) = deployer.launchProjectFor(
170
+ owner,
171
+ "ipfs://project",
172
+ configWithTiers,
173
+ initialRulesets,
174
+ terminals,
175
+ "launch",
176
+ _emptySuckerConfig(),
177
+ IJBController(address(jbController()))
178
+ );
179
+ uint256 initialRulesetId = jbRulesets().latestRulesetIdOf(projectId);
180
+
181
+ assertEq(address(launchedHook), address(initialHook), "launch should use the first hook");
182
+
183
+ _grantQueuePermissions(projectId);
184
+
185
+ vm.warp(block.timestamp + 1);
186
+
187
+ JBOmnichain721Config memory rejectedConfig = _configWithTiers(true);
188
+ (uint256 rejectedRulesetId,) = deployer.queueRulesetsOf(
189
+ projectId,
190
+ rejectedConfig,
191
+ _makeRulesets(IJBRulesetApprovalHook(address(0))),
192
+ "rejected",
193
+ IJBController(address(jbController()))
194
+ );
195
+
196
+ JBRuleset memory currentRuleset = jbRulesets().currentOf(projectId);
197
+ assertEq(currentRuleset.id, initialRulesetId, "core should still use the last approved ruleset");
198
+
199
+ vm.warp(block.timestamp + 2);
200
+
201
+ (uint256 carriedRulesetId,) = deployer.queueRulesetsOf(
202
+ projectId,
203
+ _default721Config(),
204
+ _makeRulesets(IJBRulesetApprovalHook(address(0))),
205
+ "carry",
206
+ IJBController(address(jbController()))
207
+ );
208
+
209
+ (IJB721TiersHook carriedHook, bool carriedCashOutFlag) = deployer.tiered721HookOf(projectId, carriedRulesetId);
210
+ (IJB721TiersHook currentHook, bool currentCashOutFlag) = deployer.tiered721HookOf(projectId, initialRulesetId);
211
+ (IJB721TiersHook rejectedStoredHook, bool rejectedCashOutFlag) =
212
+ deployer.tiered721HookOf(projectId, rejectedRulesetId);
213
+
214
+ assertEq(address(currentHook), address(initialHook), "approved ruleset should point to the initial hook");
215
+ assertFalse(currentCashOutFlag, "approved ruleset should preserve its original cash-out flag");
216
+
217
+ assertEq(address(rejectedStoredHook), address(rejectedHook), "rejected ruleset should store the second hook");
218
+ assertTrue(rejectedCashOutFlag, "rejected ruleset should store the second cash-out flag");
219
+
220
+ // The carry-forward uses currentOf (the approved ruleset), not the rejected queued one.
221
+ assertEq(
222
+ address(carriedHook),
223
+ address(initialHook),
224
+ "carry-forward should inherit the current (approved) hook, not the rejected one"
225
+ );
226
+ assertFalse(carriedCashOutFlag, "carry-forward should inherit the current (approved) cash-out flag");
227
+ }
228
+
229
+ function _configWithTiers(bool useDataHookForCashOut) internal pure returns (JBOmnichain721Config memory config) {
230
+ config = _default721Config();
231
+ config.useDataHookForCashOut = useDataHookForCashOut;
232
+ config.deployTiersHookConfig.tiersConfig.tiers = new JB721TierConfig[](1);
233
+ config.deployTiersHookConfig.tiersConfig.tiers[0].price = 1 ether;
234
+ config.deployTiersHookConfig.tiersConfig.tiers[0].initialSupply = 10;
235
+ }
236
+
237
+ function _default721Config() internal pure returns (JBOmnichain721Config memory config) {
238
+ config.deployTiersHookConfig.tiersConfig.currency =
239
+ uint32(uint160(address(0x000000000000000000000000000000000000EEEe)));
240
+ config.deployTiersHookConfig.tiersConfig.decimals = 18;
241
+ }
242
+
243
+ function _makeRulesets(IJBRulesetApprovalHook approvalHook)
244
+ internal
245
+ pure
246
+ returns (JBRulesetConfig[] memory configs)
247
+ {
248
+ configs = new JBRulesetConfig[](1);
249
+ configs[0] = JBRulesetConfig({
250
+ mustStartAtOrAfter: uint48(0),
251
+ duration: uint32(0),
252
+ weight: uint112(1e18),
253
+ weightCutPercent: uint32(0),
254
+ approvalHook: approvalHook,
255
+ metadata: JBRulesetMetadata({
256
+ reservedPercent: 0,
257
+ cashOutTaxRate: 0,
258
+ baseCurrency: uint32(uint160(address(0x000000000000000000000000000000000000EEEe))),
259
+ pausePay: false,
260
+ pauseCreditTransfers: false,
261
+ allowOwnerMinting: false,
262
+ allowSetCustomToken: false,
263
+ allowTerminalMigration: false,
264
+ allowSetController: false,
265
+ allowSetTerminals: false,
266
+ allowAddAccountingContext: false,
267
+ allowAddPriceFeed: false,
268
+ ownerMustSendPayouts: false,
269
+ holdFees: false,
270
+ useTotalSurplusForCashOuts: false,
271
+ useDataHookForPay: false,
272
+ useDataHookForCashOut: false,
273
+ dataHook: address(0),
274
+ metadata: 0
275
+ }),
276
+ splitGroups: new JBSplitGroup[](0),
277
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
278
+ });
279
+ }
280
+
281
+ function _emptySuckerConfig() internal pure returns (JBSuckerDeploymentConfig memory) {
282
+ return JBSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
283
+ }
284
+
285
+ function _grantQueuePermissions(uint256 projectId) internal {
286
+ uint8[] memory permissionIds = new uint8[](1);
287
+ permissionIds[0] = JBPermissionIds.QUEUE_RULESETS;
288
+
289
+ vm.prank(owner);
290
+ jbPermissions()
291
+ .setPermissionsFor(
292
+ owner,
293
+ JBPermissionsData({operator: address(this), projectId: uint64(projectId), permissionIds: permissionIds})
294
+ );
295
+
296
+ vm.prank(owner);
297
+ jbPermissions()
298
+ .setPermissionsFor(
299
+ owner,
300
+ JBPermissionsData({
301
+ operator: address(deployer), projectId: uint64(projectId), permissionIds: permissionIds
302
+ })
303
+ );
304
+ }
305
+ }
@@ -11,6 +11,7 @@ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
11
11
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
12
12
  import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
13
13
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
14
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
14
15
  import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
15
16
  import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
16
17
  import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
@@ -196,6 +197,15 @@ contract JBOmnichainDeployerTest is Test {
196
197
  abi.encode(initialRulesetId)
197
198
  );
198
199
 
200
+ // Mock currentOf to return a JBRuleset with id = initialRulesetId so the carry-forward lookup succeeds.
201
+ JBRuleset memory currentRuleset;
202
+ currentRuleset.id = uint48(initialRulesetId);
203
+ vm.mockCall(
204
+ address(rulesets),
205
+ abi.encodeWithSelector(IJBRulesets.currentOf.selector, PROJECT_ID),
206
+ abi.encode(currentRuleset)
207
+ );
208
+
199
209
  vm.warp(block.timestamp + 1);
200
210
  uint256 queuedRulesetId = block.timestamp;
201
211
  vm.mockCall(
@@ -1,39 +0,0 @@
1
- # 🔐 Security Review — nana-omnichain-deployers-v6
2
-
3
- ---
4
-
5
- ## Scope
6
-
7
- | | |
8
- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
- | **Mode** | ALL / default |
10
- | **Files reviewed** | `script/Deploy.s.sol` · `script/helpers/DeployersDeploymentLib.sol` · `src/JBOmnichainDeployer.sol`<br>`src/structs/JBDeployerHookConfig.sol` · `src/structs/JBOmnichain721Config.sol` · `src/structs/JBSuckerDeploymentConfig.sol`<br>`src/structs/JBTiered721HookConfig.sol` |
11
- | **Confidence threshold (1-100)** | 75 |
12
-
13
- ---
14
-
15
- ## Findings
16
-
17
- None.
18
-
19
- ---
20
-
21
- Findings List
22
-
23
- | # | Confidence | Title |
24
- |---|---|---|
25
- | - | - | None |
26
-
27
- ---
28
-
29
- ## Leads
30
-
31
- _Vulnerability trails with concrete code smells where the full exploit path could not be completed in one analysis pass. These are not false positives — they are high-signal leads for manual review. Not scored._
32
-
33
- - **Reflexive controller validation overstates what it proves** — `JBOmnichainDeployer._validateController` — Code smells: validation is anchored to `controller.DIRECTORY()` supplied by the same controller being checked, rather than to an immutable trusted directory — This did not survive to a finding because the path does not update the canonical project rulesets or directory, so I could not complete a live exploit that changes real terminal execution. It is still worth tightening or clarifying because the current comment implies stronger authenticity guarantees than the code actually enforces.
34
- - **`launchRulesetsFor` semantics depend on upstream “first launch only” behavior** — `JBOmnichainDeployer._launchRulesetsFor` — Code smells: the wrapper presents a generic launch entrypoint, but upstream `JBController.launchRulesetsFor` reverts once a project already has rulesets — I did not confirm a security impact, but the API/documentation boundary is easy to misuse and should remain regression-tested so integrations do not assume it can relaunch arbitrary existing projects.
35
- - **Pay-hook composition assumes the 721 hook preserves its current return-shape invariants** — `JBOmnichainDeployer.beforePayRecordedWith` — Code smells: only `tiered721HookSpecs[0]` is consumed and `projectAmount` is clamped to zero if the returned split amount exceeds the payment amount — I could not prove an exploitable path in the current dependency set because the bundled 721 hook maintains the expected single-spec invariant. This remains a dependency-sensitive integration trail if the upstream hook contract or interface expectations ever change.
36
-
37
- ---
38
-
39
- > ⚠️ This review was performed by an AI assistant. AI analysis can never verify the complete absence of vulnerabilities and no guarantee of security is given. Team security reviews, bug bounty programs, and on-chain monitoring are strongly recommended. For a consultation regarding your projects' security, visit [https://www.pashov.com](https://www.pashov.com)