@bananapus/omnichain-deployers-v6 0.0.29 → 0.0.30

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.
Files changed (27) hide show
  1. package/RISKS.md +5 -0
  2. package/package.json +3 -3
  3. package/src/JBOmnichainDeployer.sol +50 -13
  4. package/test/JBOmnichainDeployerGuard.t.sol +44 -8
  5. package/test/OmnichainDeployerEdgeCases.t.sol +8 -6
  6. package/test/OmnichainDeployerReentrancy.t.sol +8 -8
  7. package/test/Tiered721HookComposition.t.sol +11 -6
  8. package/test/audit/AuditFixesC2H6M14.t.sol +6 -21
  9. package/test/audit/CarryForwardRejectedHook.t.sol +5 -5
  10. package/test/audit/CashOutSpecMerge.t.sol +372 -0
  11. package/test/audit/{CodexNemesisDeterministicDrift.t.sol → DeterministicDrift.t.sol} +1 -1
  12. package/test/audit/{CodexNemesisDeterministicPeerDrift.t.sol → DeterministicPeerDrift.t.sol} +3 -3
  13. package/test/audit/ExtraCashOutHookZeroReclaim.t.sol +340 -0
  14. package/test/audit/{CodexNemesisForwardedPermissions.t.sol → ForwardedPermissions.t.sol} +1 -1
  15. package/test/audit/JBOmnichainDeployer.t.sol +6 -6
  16. package/test/audit/{CodexNemesisNftCashoutSupplyMismatch.t.sol → NftCashoutSupplyMismatch.t.sol} +11 -9
  17. package/test/audit/{CodexNemesisAudit.t.sol → OmnichainAudit.t.sol} +1 -2
  18. package/test/audit/SplitCreditWeight.t.sol +437 -0
  19. package/test/audit/WeightScalingComparison.t.sol +11 -12
  20. package/test/fork/OmnichainForkTestBase.sol +2 -1
  21. package/test/fork/TestOmnichain721QueueAndAdjust.t.sol +5 -5
  22. package/test/fork/TestOmnichainCashOutFork.t.sol +42 -40
  23. package/test/fork/TestOmnichainStressFork.t.sol +87 -95
  24. package/test/fork/TestOmnichainWeightFork.t.sol +9 -27
  25. package/test/invariants/CrossChainDeployerInvariant.t.sol +6 -2
  26. package/test/invariants/OmnichainDeployerInvariant.t.sol +3 -5
  27. package/test/invariants/handlers/CrossChainDeployerHandler.sol +0 -1
package/RISKS.md CHANGED
@@ -86,6 +86,11 @@ Extra data hooks provided by the project owner in `_setup721` configuration can
86
86
  **Missing hook721 alias check enables double invocation.** *(Minor)*
87
87
  If the project owner configures the 721 hook as both the primary hook and as an extra data hook, it could be invoked twice. Accepted because this is self-inflicted misconfiguration — the deployer correctly processes each hook independently.
88
88
 
89
+ ### Hook Selection
90
+
91
+ **ApprovalExpected rulesets excluded from hook carry-forward.**
92
+ When no new tiers are provided, the deployer carries forward the 721 hook from the most recent approved ruleset. Rulesets with `ApprovalExpected` status are intentionally excluded even though they may become active. Hook selection is irreversible — if the pending ruleset is later rejected by the approval hook, we'd have locked in a hook from a ruleset that never became active. The deployer falls back to the current (already-approved) ruleset in this case.
93
+
89
94
  ### Cross-Chain Deployment
90
95
 
91
96
  **`_msgSender()` in deployment salt breaks cross-chain determinism.** *(Minor)*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/omnichain-deployers-v6",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-omnichain-deployers-v6'"
18
18
  },
19
19
  "dependencies": {
20
- "@bananapus/721-hook-v6": "^0.0.38",
20
+ "@bananapus/721-hook-v6": "^0.0.41",
21
21
  "@bananapus/buyback-hook-v6": "^0.0.30",
22
22
  "@bananapus/core-v6": "^0.0.36",
23
23
  "@bananapus/ownable-v6": "^0.0.20",
@@ -27,7 +27,7 @@
27
27
  "@uniswap/v4-core": "^1.0.2"
28
28
  },
29
29
  "devDependencies": {
30
- "@bananapus/address-registry-v6": "^0.0.17",
30
+ "@bananapus/address-registry-v6": "^0.0.20",
31
31
  "@sphinx-labs/plugins": "^0.33.2"
32
32
  }
33
33
  }
@@ -30,6 +30,7 @@ import {JBDeployerHookConfig} from "./structs/JBDeployerHookConfig.sol";
30
30
  import {JBOmnichain721Config} from "./structs/JBOmnichain721Config.sol";
31
31
  import {JBSuckerDeploymentConfig} from "./structs/JBSuckerDeploymentConfig.sol";
32
32
  import {JBTiered721HookConfig} from "./structs/JBTiered721HookConfig.sol";
33
+ import {mulDiv} from "@prb/math/src/Common.sol";
33
34
 
34
35
  /// @notice Deploys, manages, and operates Juicebox projects with suckers.
35
36
  // Project NFTs sent to this contract are not recoverable. The deployer does not
@@ -434,10 +435,10 @@ contract JBOmnichainDeployer is
434
435
  // If a 721 hook is set and opted into cash out handling, let it adjust the cash out parameters.
435
436
  if (address(tiered721Config.hook) != address(0) && tiered721Config.useDataHookForCashOut) {
436
437
  // Forward to the 721 hook. It may change the tax rate, count, and return hook specs.
437
- // We discard the inner hook's effectiveSurplusValue — this contract computes the cross-chain values.
438
- // We also discard its totalSupply since this contract computes the cross-chain supply.
438
+ // Capture the 721 hook's totalSupply and effectiveSurplusValue — NFT cash-outs should use
439
+ // local-only denominators so holders reclaim against local surplus, not omnichain surplus.
439
440
  // slither-disable-next-line unused-return
440
- (cashOutTaxRate, cashOutCount,,, tiered721HookSpecifications) =
441
+ (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, tiered721HookSpecifications) =
441
442
  IJBRulesetDataHook(address(tiered721Config.hook)).beforeCashOutRecordedWith(context);
442
443
  }
443
444
 
@@ -452,16 +453,23 @@ contract JBOmnichainDeployer is
452
453
  // Build a mutable copy of the context with the latest values (possibly updated by the 721 hook).
453
454
  JBBeforeCashOutRecordedContext memory hookContext = context;
454
455
  hookContext.cashOutTaxRate = cashOutTaxRate;
455
- hookContext.cashOutCount = cashOutCount;
456
456
  hookContext.totalSupply = totalSupply;
457
457
  hookContext.surplus.value = effectiveSurplusValue;
458
458
 
459
- // Forward to the extra hook. It may further change the tax rate, count, and return hook specs.
460
- // We discard the inner hook's effectiveSurplusValue — this contract computes the cross-chain values.
461
- // We also discard its totalSupply since this contract computes the cross-chain supply.
462
- // slither-disable-next-line unused-return
463
- (cashOutTaxRate, cashOutCount,,, extraHookSpecifications) =
464
- extraHook.dataHook.beforeCashOutRecordedWith(hookContext);
459
+ // Forward to the extra hook. It may further change the tax rate and return hook specs.
460
+ // We always discard totalSupply and effectiveSurplusValue — this contract computes
461
+ // cross-chain values for both. When the 721 hook is active, we also discard cashOutCount
462
+ // because the 721 hook redefines both cashOutCount and totalSupply as NFT cash-out weights
463
+ // (sum of tier prices), not fungible token counts. Letting the extra hook override
464
+ // cashOutCount would corrupt NFT pricing in the bonding curve.
465
+ if (address(tiered721Config.hook) != address(0) && tiered721Config.useDataHookForCashOut) {
466
+ // slither-disable-next-line unused-return
467
+ (cashOutTaxRate,,,, extraHookSpecifications) = extraHook.dataHook.beforeCashOutRecordedWith(hookContext);
468
+ } else {
469
+ // slither-disable-next-line unused-return
470
+ (cashOutTaxRate, cashOutCount,,, extraHookSpecifications) =
471
+ extraHook.dataHook.beforeCashOutRecordedWith(hookContext);
472
+ }
465
473
  }
466
474
 
467
475
  // If neither hook returned any specifications, return the adjusted values with no hook specs.
@@ -510,6 +518,8 @@ contract JBOmnichainDeployer is
510
518
  bool hasTiered721Spec;
511
519
  // The weight returned by the 721 hook (already scaled for splits).
512
520
  uint256 tiered721Weight;
521
+ // The weight attributable to tier splits when issueTokensForSplits is true.
522
+ uint256 splitCreditWeight;
513
523
  // Whether a 721 hook is configured for this project's ruleset.
514
524
  bool has721Hook;
515
525
  if (address(tiered721Config.hook) != address(0)) {
@@ -526,6 +536,14 @@ contract JBOmnichainDeployer is
526
536
  hasTiered721Spec = true;
527
537
  tiered721HookSpec = tiered721HookSpecs[0];
528
538
  totalSplitAmount = tiered721HookSpec.amount;
539
+
540
+ // Decode splitCreditWeight from the 721 hook's metadata (4th field).
541
+ // When issueTokensForSplits is true and splits exist, this holds the weight portion
542
+ // attributable to tier splits — used to prevent split credit erasure if the extra
543
+ // hook (e.g. buyback) returns weight=0.
544
+ if (tiered721HookSpec.metadata.length >= 128) {
545
+ (,,, splitCreditWeight) = abi.decode(tiered721HookSpec.metadata, (address, address, bytes, uint256));
546
+ }
529
547
  }
530
548
  }
531
549
 
@@ -541,11 +559,27 @@ contract JBOmnichainDeployer is
541
559
  if (address(extraHook.dataHook) != address(0) && extraHook.useDataHookForPay) {
542
560
  JBBeforePayRecordedContext memory hookContext = context;
543
561
  hookContext.amount.value = projectAmount;
544
- // Pass the 721 hook's weight (which accounts for split deductions) so the data hook
545
- // makes its decisions (e.g. mint-vs-swap) based on the correct post-split weight.
546
- if (has721Hook) hookContext.weight = tiered721Weight;
562
+ // Pass the original context.weight NOT the 721 hook's split-adjusted weight.
563
+ // The extra hook (e.g. buyback) applies its own weight logic; using the 721 hook's
564
+ // already-split-adjusted weight would double-discount the split ratio.
547
565
  (weight, dataHookSpecs) = extraHook.dataHook.beforePayRecordedWith(hookContext);
548
566
  customHookCalled = true;
567
+
568
+ // The custom hook (e.g. buyback) returned a weight based on the original context.weight.
569
+ // If the 721 hook scaled weight down for tier splits, apply the same ratio so the terminal
570
+ // doesn't over-mint tokens relative to the funds actually entering the project.
571
+ // When issueTokensForSplits is true, tiered721Weight == context.weight and the ratio is 1x.
572
+ if (has721Hook && context.weight > 0 && tiered721Weight != context.weight) {
573
+ weight = mulDiv(weight, tiered721Weight, context.weight);
574
+ }
575
+
576
+ // When the extra hook returns weight=0 (e.g. buyback found no profitable swap) but tier
577
+ // splits exist with issueTokensForSplits=true, the split credit must still mint fungible tokens.
578
+ // The split credit weight is independent of buyback routing — it represents the token issuance
579
+ // for funds forwarded to tier split beneficiaries.
580
+ if (weight == 0 && splitCreditWeight > 0) {
581
+ weight = splitCreditWeight;
582
+ }
549
583
  }
550
584
  }
551
585
 
@@ -813,6 +847,9 @@ contract JBOmnichainDeployer is
813
847
  {
814
848
  // First try the latest queued ruleset — if it's been explicitly approved
815
849
  // (or has no approval hook), its hook config should take precedence.
850
+ // Conservative: only use Approved or Empty status. ApprovalExpected is intentionally
851
+ // excluded because hook selection is irreversible — if the pending ruleset is later rejected
852
+ // by the approval hook, we'd have locked in a hook from a ruleset that never became active.
816
853
  (JBRuleset memory latestQueued, JBApprovalStatus approvalStatus) =
817
854
  controller.RULESETS().latestQueuedOf(projectId);
818
855
  if (
@@ -263,11 +263,11 @@ contract JBOmnichainDeployerGuardTest is TestBaseWorkflow {
263
263
  .setPermissionsFor(
264
264
  owner,
265
265
  JBPermissionsData({
266
- operator: address(deployer),
267
- // forge-lint: disable-next-line(unsafe-typecast)
268
- projectId: uint64(projectId),
269
- permissionIds: permissionIds
270
- })
266
+ operator: address(deployer),
267
+ // forge-lint: disable-next-line(unsafe-typecast)
268
+ projectId: uint64(projectId),
269
+ permissionIds: permissionIds
270
+ })
271
271
  );
272
272
  }
273
273
 
@@ -303,9 +303,10 @@ contract JBOmnichainDeployerGuardTest is TestBaseWorkflow {
303
303
 
304
304
  JBRulesetConfig[] memory rulesets = _makeRulesetConfigs(1);
305
305
 
306
- // Should succeed without reverting.
307
306
  JBOmnichain721Config memory empty721;
308
- deployer.queueRulesetsOf(projectId, empty721, rulesets, "queue", IJBController(address(jbController())));
307
+ (uint256 rulesetId,) =
308
+ deployer.queueRulesetsOf(projectId, empty721, rulesets, "queue", IJBController(address(jbController())));
309
+ assertGt(rulesetId, 0, "Queued ruleset ID must be non-zero");
309
310
  }
310
311
 
311
312
  /// @notice Queue rulesets reverts when called in the same block as launch
@@ -363,6 +364,41 @@ contract JBOmnichainDeployerGuardTest is TestBaseWorkflow {
363
364
 
364
365
  // Now should succeed.
365
366
  JBOmnichain721Config memory empty721b;
366
- deployer.queueRulesetsOf(projectId, empty721b, rulesets, "ok-now", IJBController(address(jbController())));
367
+ (uint256 rulesetId,) =
368
+ deployer.queueRulesetsOf(projectId, empty721b, rulesets, "ok-now", IJBController(address(jbController())));
369
+ assertGt(rulesetId, 0, "Queued ruleset ID must be non-zero after warping past conflict");
370
+ }
371
+
372
+ /// @notice When no new tiers are provided and no latestQueued exists (single launch ruleset that
373
+ /// became the current ruleset), the hook is carried forward from the current ruleset (lines 862-864).
374
+ function test_queueRulesetsOf_carriesForwardFromCurrent_whenNoLatestQueued() public {
375
+ // Launch with 1 ruleset — this deploys a 721 hook stored for the launch ruleset.
376
+ uint256 projectId = _launchProject(1);
377
+ uint256 launchRulesetId = block.timestamp;
378
+ _grantDeployerQueuePermission(projectId);
379
+
380
+ // Verify the 721 hook was stored for the launch ruleset.
381
+ (IJB721TiersHook launchHook,) = deployer.tiered721HookOf(projectId, launchRulesetId);
382
+ assertEq(address(launchHook), mockHookAddr, "Launch ruleset should have 721 hook stored");
383
+
384
+ // Warp forward so the launched ruleset is the current active one and the guard passes.
385
+ vm.warp(block.timestamp + 1 days);
386
+
387
+ // Queue a new ruleset with NO new tiers → triggers carry-forward logic.
388
+ JBRulesetConfig[] memory rulesets = _makeRulesetConfigs(1);
389
+ JBOmnichain721Config memory empty721;
390
+ (uint256 queuedRulesetId,) = deployer.queueRulesetsOf(
391
+ projectId, empty721, rulesets, "carry-forward", IJBController(address(jbController()))
392
+ );
393
+
394
+ assertGt(queuedRulesetId, 0, "Queued ruleset ID must be non-zero");
395
+
396
+ // Verify the hook was carried forward to the new queued ruleset.
397
+ (IJB721TiersHook carriedHook,) = deployer.tiered721HookOf(projectId, queuedRulesetId);
398
+ assertEq(
399
+ address(carriedHook),
400
+ address(launchHook),
401
+ "Queued ruleset should carry forward the 721 hook from the current ruleset"
402
+ );
367
403
  }
368
404
  }
@@ -322,9 +322,10 @@ contract OmnichainDeployerEdgeCases is Test {
322
322
  ctx.amount.value = 1 ether;
323
323
  ctx.weight = 1000;
324
324
 
325
- // The custom hook's weight is used directly (no mulDiv scaling).
325
+ // The custom hook's weight is scaled by the 721 split ratio (500/1000 = 50%).
326
+ // mulDiv(type(uint256).max, 500, 1000) = type(uint256).max / 2.
326
327
  (uint256 weight,) = deployer.beforePayRecordedWith(ctx);
327
- assertEq(weight, type(uint256).max, "custom hook's large weight should pass through directly");
328
+ assertEq(weight, type(uint256).max / 2, "custom hook's weight scaled by 721 split ratio");
328
329
  }
329
330
 
330
331
  // =========================================================================
@@ -471,10 +472,11 @@ contract OmnichainDeployerEdgeCases is Test {
471
472
  ) = deployer.beforeCashOutRecordedWith(ctx);
472
473
 
473
474
  assertEq(cashOutTaxRate, 2000, "Custom hook should receive and override 721-adjusted tax rate");
474
- assertEq(cashOutCount, 500, "Custom hook should receive and override 721-adjusted cashOutCount");
475
- // The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
476
- // With no suckers, this equals context.totalSupply.
477
- assertEq(totalSupply, ctx.totalSupply, "Should return cross-chain totalSupply (context value with no suckers)");
475
+ // After the security fix, the 721 hook's cashOutCount is preserved — the extra hook cannot override it.
476
+ assertEq(cashOutCount, 700, "721 hook cashOutCount must be preserved (extra hook cannot override NFT pricing)");
477
+ // The deployer captures the 721 hook's totalSupply (9000) — NFT cash-outs use local-only
478
+ // denominators. The extra hook's totalSupply is discarded, preserving the 721 hook's value.
479
+ assertEq(totalSupply, 9000, "Should return 721 hook totalSupply (local denominator)");
478
480
  assertEq(hookSpecifications.length, 2, "721 and custom cash out specs should both be returned");
479
481
  assertEq(address(hookSpecifications[0].hook), mock721, "721 hook spec should come first");
480
482
  assertEq(hookSpecifications[0].amount, 11, "721 hook spec amount should be preserved");
@@ -240,14 +240,14 @@ contract OmnichainDeployerReentrancy is OmnichainForkTestBase {
240
240
  vm.prank(address(hook));
241
241
  try jbMultiTerminal()
242
242
  .cashOutTokensOf({
243
- holder: address(hook),
244
- projectId: projectId,
245
- cashOutCount: hookTokens / 2,
246
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
247
- minTokensReclaimed: 0,
248
- beneficiary: payable(address(hook)),
249
- metadata: ""
250
- }) {
243
+ holder: address(hook),
244
+ projectId: projectId,
245
+ cashOutCount: hookTokens / 2,
246
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
247
+ minTokensReclaimed: 0,
248
+ beneficiary: payable(address(hook)),
249
+ metadata: ""
250
+ }) {
251
251
  // If it succeeds, check conservation.
252
252
  uint256 surplusAfter = _terminalBalance(projectId, JBConstants.NATIVE_TOKEN);
253
253
  uint256 remainingTokens = jbTokens().totalBalanceOf(address(hook), projectId);
@@ -302,7 +302,8 @@ contract Tiered721HookComposition is Test {
302
302
  );
303
303
  JBBeforePayRecordedContext memory context = _makePayContext(projectId, block.timestamp);
304
304
  (uint256 weight, JBPayHookSpecification[] memory specs) = deployer.beforePayRecordedWith(context);
305
- assertEq(weight, 555, "weight = buyback hook's returned weight (721 weight passed as context)");
305
+ // Buyback returned weight=555, 721 split ratio=750/1000: mulDiv(555, 750, 1000) = 416.
306
+ assertEq(weight, 416, "weight = buyback weight scaled by 721 split ratio");
306
307
  assertEq(specs.length, 2, "721 spec + buyback spec");
307
308
  assertEq(address(specs[0].hook), hookAddr, "first = 721 hook");
308
309
  assertEq(specs[0].amount, splitAmount, "721 split amount preserved");
@@ -622,7 +623,8 @@ contract Tiered721HookComposition is Test {
622
623
  );
623
624
  JBBeforePayRecordedContext memory context = _makePayContext(projectId, block.timestamp);
624
625
  (uint256 weight,) = deployer.beforePayRecordedWith(context);
625
- assertEq(weight, 2000, "weight = custom hook's returned weight (721 weight passed as context)");
626
+ // Custom hook returned 2000, 721 split ratio=600/1000: mulDiv(2000, 600, 1000) = 1200.
627
+ assertEq(weight, 1200, "weight = custom hook weight scaled by 721 split ratio");
626
628
  }
627
629
 
628
630
  function test_beforePay_fullSplit_weightZero() public {
@@ -690,7 +692,8 @@ contract Tiered721HookComposition is Test {
690
692
  );
691
693
  JBBeforePayRecordedContext memory context = _makePayContext(projectId, block.timestamp);
692
694
  (uint256 weight, JBPayHookSpecification[] memory specs) = deployer.beforePayRecordedWith(context);
693
- assertEq(weight, 2000, "weight = buyback hook's returned weight (721 weight passed as context)");
695
+ // Buyback returned 2000, 721 split ratio=600/1000: mulDiv(2000, 600, 1000) = 1200.
696
+ assertEq(weight, 1200, "weight = buyback weight scaled by 721 split ratio");
694
697
  assertEq(specs.length, 2, "721 spec + buyback spec");
695
698
  assertEq(address(specs[0].hook), hookAddr, "first = 721 hook");
696
699
  assertEq(specs[0].amount, 0.4 ether, "721 split amount");
@@ -709,8 +712,9 @@ contract Tiered721HookComposition is Test {
709
712
  abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
710
713
  abi.encode(uint256(800), hookSpecs)
711
714
  );
712
- // Buyback mint path returns context.weight (which is now the 721 hook's weight = 800).
713
- uint256 mintPathWeight = 800;
715
+ // Buyback mint path returns context.weight unchanged. The deployer passes the original
716
+ // context.weight (1000) to the buyback hook, so the mint path returns 1000.
717
+ uint256 mintPathWeight = 1000;
714
718
  JBPayHookSpecification[] memory emptyBuybackSpecs = new JBPayHookSpecification[](0);
715
719
  vm.mockCall(
716
720
  buybackHookAddr,
@@ -719,7 +723,8 @@ contract Tiered721HookComposition is Test {
719
723
  );
720
724
  JBBeforePayRecordedContext memory context = _makePayContext(projectId, block.timestamp);
721
725
  (uint256 weight, JBPayHookSpecification[] memory specs) = deployer.beforePayRecordedWith(context);
722
- assertEq(weight, 800, "weight = 721 hook's weight (buyback mint path returns context.weight)");
726
+ // mulDiv(1000, 800, 1000) = 800: the 721 split ratio is correctly applied.
727
+ assertEq(weight, 800, "weight = buyback weight scaled by 721 split ratio (matches 721 hook weight)");
723
728
  assertEq(specs.length, 1, "only 721 spec (buyback empty)");
724
729
  assertEq(address(specs[0].hook), hookAddr, "spec = 721 hook");
725
730
  assertEq(specs[0].amount, 0.2 ether, "721 split amount");
@@ -8,14 +8,11 @@ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
8
8
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
9
9
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
10
10
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
11
- import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
12
11
  import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
13
- import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
14
12
  import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
15
13
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
16
14
  import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
17
15
  import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
18
- import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
19
16
  import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
20
17
  import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
21
18
  import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
@@ -379,19 +376,13 @@ contract AuditFixesC2H6M14 is Test {
379
376
  JBRulesetConfig[] memory configs = new JBRulesetConfig[](1);
380
377
  configs[0] = _rulesetConfig();
381
378
 
382
- // 721 hook mock returns empty cash-out specs by default.
383
- JBCashOutHookSpecification[] memory emptySpecs = new JBCashOutHookSpecification[](0);
384
- vm.mockCall(
385
- hookAddr,
386
- abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
387
- abi.encode(uint256(5000), uint256(1000), uint256(10_000), uint256(0), emptySpecs)
388
- );
389
-
379
+ // Disable the 721 hook for cash-out so the deployer computes cross-chain surplus itself.
380
+ // These tests verify C-2 (surplus aggregation), not NFT cashout behavior.
390
381
  deployer.launchProjectFor({
391
382
  owner: projectOwner,
392
383
  projectUri: "test",
393
384
  deploy721Config: JBOmnichain721Config({
394
- deployTiersHookConfig: _empty721HookConfig(), useDataHookForCashOut: true, salt: bytes32(0)
385
+ deployTiersHookConfig: _empty721HookConfig(), useDataHookForCashOut: false, salt: bytes32(0)
395
386
  }),
396
387
  rulesetConfigurations: configs,
397
388
  terminalConfigurations: new JBTerminalConfig[](0),
@@ -420,19 +411,13 @@ contract AuditFixesC2H6M14 is Test {
420
411
  configs[0].metadata.dataHook = extraHookAddr;
421
412
  configs[0].metadata.useDataHookForCashOut = true;
422
413
 
423
- // 721 hook mock returns empty cash-out specs.
424
- JBCashOutHookSpecification[] memory emptySpecs = new JBCashOutHookSpecification[](0);
425
- vm.mockCall(
426
- hookAddr,
427
- abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
428
- abi.encode(uint256(5000), uint256(1000), uint256(10_000), uint256(0), emptySpecs)
429
- );
430
-
414
+ // Disable the 721 hook for cash-out so the deployer computes cross-chain surplus itself.
415
+ // These tests verify H-6 (extra hook forwarding), not NFT cashout behavior.
431
416
  deployer.launchProjectFor({
432
417
  owner: projectOwner,
433
418
  projectUri: "test",
434
419
  deploy721Config: JBOmnichain721Config({
435
- deployTiersHookConfig: _empty721HookConfig(), useDataHookForCashOut: true, salt: bytes32(0)
420
+ deployTiersHookConfig: _empty721HookConfig(), useDataHookForCashOut: false, salt: bytes32(0)
436
421
  }),
437
422
  rulesetConfigurations: configs,
438
423
  terminalConfigurations: new JBTerminalConfig[](0),
@@ -318,11 +318,11 @@ contract CarryForwardRejectedHookTest is TestBaseWorkflow {
318
318
  .setPermissionsFor(
319
319
  owner,
320
320
  JBPermissionsData({
321
- operator: address(deployer),
322
- // forge-lint: disable-next-line(unsafe-typecast)
323
- projectId: uint64(projectId),
324
- permissionIds: permissionIds
325
- })
321
+ operator: address(deployer),
322
+ // forge-lint: disable-next-line(unsafe-typecast)
323
+ projectId: uint64(projectId),
324
+ permissionIds: permissionIds
325
+ })
326
326
  );
327
327
  }
328
328
  }