@bananapus/omnichain-deployers-v6 0.0.24 → 0.0.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/omnichain-deployers-v6",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,12 +17,12 @@
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.32",
21
- "@bananapus/buyback-hook-v6": "^0.0.26",
22
- "@bananapus/core-v6": "^0.0.32",
20
+ "@bananapus/721-hook-v6": "^0.0.33",
21
+ "@bananapus/buyback-hook-v6": "^0.0.27",
22
+ "@bananapus/core-v6": "^0.0.34",
23
23
  "@bananapus/ownable-v6": "^0.0.17",
24
- "@bananapus/permission-ids-v6": "^0.0.15",
25
- "@bananapus/suckers-v6": "^0.0.22",
24
+ "@bananapus/permission-ids-v6": "^0.0.17",
25
+ "@bananapus/suckers-v6": "^0.0.25",
26
26
  "@openzeppelin/contracts": "^5.6.1",
27
27
  "@uniswap/v4-core": "^1.0.2"
28
28
  },
@@ -30,4 +30,4 @@
30
30
  "@bananapus/address-registry-v6": "^0.0.17",
31
31
  "@sphinx-labs/plugins": "^0.33.2"
32
32
  }
33
- }
33
+ }
@@ -133,13 +133,14 @@ contract JBOmnichainDeployer is
133
133
  // ------------------------- external views -------------------------- //
134
134
  //*********************************************************************//
135
135
 
136
- /// @notice Allow cash outs from suckers without a tax.
136
+ /// @notice Allow cash outs from suckers without a tax, and compute cross-chain tax supply for non-sucker cash outs.
137
137
  /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
138
138
  /// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
139
139
  /// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
140
140
  /// out.
141
141
  /// @return cashOutCount The number of project tokens that are cashed out.
142
- /// @return totalSupply The total project token supply.
142
+ /// @return totalSupply The total token supply across all chains (for both proportional reclaim and tax).
143
+ /// @return effectiveSurplusValue The global surplus across all chains for proportional reclaim.
143
144
  /// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
144
145
  function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
145
146
  external
@@ -149,18 +150,28 @@ contract JBOmnichainDeployer is
149
150
  uint256 cashOutTaxRate,
150
151
  uint256 cashOutCount,
151
152
  uint256 totalSupply,
153
+ uint256 effectiveSurplusValue,
152
154
  JBCashOutHookSpecification[] memory hookSpecifications
153
155
  )
154
156
  {
155
157
  // If the cash out is from a sucker, bypass all taxes and fees.
158
+ // Pass through the local surplus so the reclaim calculation can compute the pro-rata share.
156
159
  if (SUCKER_REGISTRY.isSuckerOf({projectId: context.projectId, addr: context.holder})) {
157
- return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
160
+ return (0, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
158
161
  }
159
162
 
160
163
  // Start with the values from the context. Hooks below may override these.
161
164
  cashOutTaxRate = context.cashOutTaxRate;
162
165
  cashOutCount = context.cashOutCount;
163
- totalSupply = context.totalSupply;
166
+
167
+ // Compute the cross-chain total supply: local supply + sum of known peer chain supplies.
168
+ // This prevents the cash out tax from vanishing when a holder dominates the local supply.
169
+ totalSupply = context.totalSupply + SUCKER_REGISTRY.remoteTotalSupplyOf(context.projectId);
170
+
171
+ // Compute the cross-chain surplus: local surplus + sum of known peer chain surpluses.
172
+ // This prevents disproportionate reclaim when tokens bridge away but surplus stays.
173
+ effectiveSurplusValue = context.surplus.value
174
+ + SUCKER_REGISTRY.remoteSurplusOf(context.projectId, 18, uint256(uint160(context.surplus.token)));
164
175
 
165
176
  // Will hold the 721 hook's cash out specifications (always 0 or 1 element).
166
177
  JBCashOutHookSpecification[] memory tiered721HookSpecifications;
@@ -170,8 +181,11 @@ contract JBOmnichainDeployer is
170
181
 
171
182
  // If a 721 hook is set and opted into cash out handling, let it adjust the cash out parameters.
172
183
  if (address(tiered721Config.hook) != address(0) && tiered721Config.useDataHookForCashOut) {
173
- // Forward to the 721 hook. It may change the tax rate, count, supply, and return hook specs.
174
- (cashOutTaxRate, cashOutCount, totalSupply, tiered721HookSpecifications) =
184
+ // Forward to the 721 hook. It may change the tax rate, count, and return hook specs.
185
+ // We discard the inner hook's effectiveSurplusValue — this contract computes the cross-chain values.
186
+ // We also discard its totalSupply since this contract computes the cross-chain supply.
187
+ // slither-disable-next-line unused-return
188
+ (cashOutTaxRate, cashOutCount,,, tiered721HookSpecifications) =
175
189
  IJBRulesetDataHook(address(tiered721Config.hook)).beforeCashOutRecordedWith(context);
176
190
  }
177
191
 
@@ -189,14 +203,17 @@ contract JBOmnichainDeployer is
189
203
  hookContext.cashOutCount = cashOutCount;
190
204
  hookContext.totalSupply = totalSupply;
191
205
 
192
- // Forward to the extra hook. It may further change the tax rate, count, supply, and return hook specs.
193
- (cashOutTaxRate, cashOutCount, totalSupply, extraHookSpecifications) =
206
+ // Forward to the extra hook. It may further change the tax rate, count, and return hook specs.
207
+ // We discard the inner hook's effectiveSurplusValue — this contract computes the cross-chain values.
208
+ // We also discard its totalSupply since this contract computes the cross-chain supply.
209
+ // slither-disable-next-line unused-return
210
+ (cashOutTaxRate, cashOutCount,,, extraHookSpecifications) =
194
211
  extraHook.dataHook.beforeCashOutRecordedWith(hookContext);
195
212
  }
196
213
 
197
214
  // If neither hook returned any specifications, return the adjusted values with no hook specs.
198
215
  if (tiered721HookSpecifications.length == 0 && extraHookSpecifications.length == 0) {
199
- return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
216
+ return (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
200
217
  }
201
218
 
202
219
  // Merge both hooks' specifications: 721 spec (if any) first, then extra hook specs.
@@ -215,7 +232,7 @@ contract JBOmnichainDeployer is
215
232
  hookSpecifications = extraHookSpecifications;
216
233
  }
217
234
 
218
- return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
235
+ return (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
219
236
  }
220
237
 
221
238
  /// @notice Forward the call to the original data hook.
@@ -614,6 +631,10 @@ contract JBOmnichainDeployer is
614
631
  });
615
632
  }
616
633
 
634
+ //*********************************************************************//
635
+ // -------------------------- internal views ------------------------ //
636
+ //*********************************************************************//
637
+
617
638
  //*********************************************************************//
618
639
  // ------------------------ internal functions ----------------------- //
619
640
  //*********************************************************************//
@@ -89,6 +89,18 @@ contract TestJBOmnichainDeployer is Test {
89
89
  abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
90
90
  abi.encode(uint256(1000), new JBPayHookSpecification[](0))
91
91
  );
92
+
93
+ // Default: no remote supply or surplus (non-omnichain project).
94
+ vm.mockCall(
95
+ address(suckerRegistry),
96
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
97
+ abi.encode(uint256(0))
98
+ );
99
+ vm.mockCall(
100
+ address(suckerRegistry),
101
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
102
+ abi.encode(uint256(0))
103
+ );
92
104
  }
93
105
 
94
106
  //*********************************************************************//
@@ -207,7 +219,7 @@ contract TestJBOmnichainDeployer is Test {
207
219
 
208
220
  JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, rulesetId, sucker);
209
221
 
210
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) =
222
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) =
211
223
  deployer.beforeCashOutRecordedWith(context);
212
224
 
213
225
  assertEq(cashOutTaxRate, 0, "sucker should get 0 tax");
@@ -223,7 +235,7 @@ contract TestJBOmnichainDeployer is Test {
223
235
 
224
236
  JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, rulesetId, randomAddr);
225
237
 
226
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) =
238
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) =
227
239
  deployer.beforeCashOutRecordedWith(context);
228
240
 
229
241
  assertEq(cashOutTaxRate, context.cashOutTaxRate, "non-sucker should get original tax");
@@ -82,6 +82,18 @@ contract MockSuckerRegistry is IJBSuckerRegistry {
82
82
  return new address[](0);
83
83
  }
84
84
 
85
+ function remoteTotalSupplyOf(uint256) external pure override returns (uint256) {
86
+ return 0;
87
+ }
88
+
89
+ function remoteBalanceOf(uint256, uint256, uint256) external pure override returns (uint256) {
90
+ return 0;
91
+ }
92
+
93
+ function remoteSurplusOf(uint256, uint256, uint256) external pure override returns (uint256) {
94
+ return 0;
95
+ }
96
+
85
97
  function removeDeprecatedSucker(uint256, address) external override {}
86
98
  function removeSuckerDeployer(address) external override {}
87
99
  }
@@ -43,7 +43,7 @@ contract RevertingDataHook is IJBRulesetDataHook {
43
43
  external
44
44
  pure
45
45
  override
46
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
46
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
47
47
  {
48
48
  revert("Hook always reverts");
49
49
  }
@@ -72,9 +72,11 @@ contract InflatingDataHook is IJBRulesetDataHook {
72
72
  external
73
73
  pure
74
74
  override
75
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
75
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
76
76
  {
77
- return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, new JBCashOutHookSpecification[](0));
77
+ return (
78
+ context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
79
+ );
78
80
  }
79
81
 
80
82
  function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
@@ -142,6 +144,18 @@ contract OmnichainDeployerAttacks is Test {
142
144
  abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
143
145
  abi.encode(uint256(1000), new JBPayHookSpecification[](0))
144
146
  );
147
+
148
+ // Default: no remote supply or surplus (non-omnichain project).
149
+ vm.mockCall(
150
+ address(suckerRegistry),
151
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
152
+ abi.encode(uint256(0))
153
+ );
154
+ vm.mockCall(
155
+ address(suckerRegistry),
156
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
157
+ abi.encode(uint256(0))
158
+ );
145
159
  }
146
160
 
147
161
  // =========================================================================
@@ -166,7 +180,7 @@ contract OmnichainDeployerAttacks is Test {
166
180
 
167
181
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, sucker);
168
182
 
169
- (uint256 cashOutTaxRate,,,) = deployer.beforeCashOutRecordedWith(ctx);
183
+ (uint256 cashOutTaxRate,,,,) = deployer.beforeCashOutRecordedWith(ctx);
170
184
  assertEq(cashOutTaxRate, 0, "Sucker should get 0 tax");
171
185
  }
172
186
 
@@ -182,7 +196,7 @@ contract OmnichainDeployerAttacks is Test {
182
196
 
183
197
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
184
198
 
185
- (uint256 cashOutTaxRate,,,) = deployer.beforeCashOutRecordedWith(ctx);
199
+ (uint256 cashOutTaxRate,,,,) = deployer.beforeCashOutRecordedWith(ctx);
186
200
  assertEq(cashOutTaxRate, 5000, "Non-sucker should get original tax");
187
201
  }
188
202
 
@@ -291,7 +305,7 @@ contract OmnichainDeployerAttacks is Test {
291
305
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, storedRulesetId, sucker);
292
306
 
293
307
  // Sucker gets early return with 0 tax — never hits the reverting hook.
294
- (uint256 cashOutTaxRate,,,) = deployer.beforeCashOutRecordedWith(ctx);
308
+ (uint256 cashOutTaxRate,,,,) = deployer.beforeCashOutRecordedWith(ctx);
295
309
  assertEq(cashOutTaxRate, 0, "Sucker bypasses hook and gets 0 tax");
296
310
  }
297
311
 
@@ -78,7 +78,7 @@ contract CustomCashOutHook is IJBRulesetDataHook {
78
78
  external
79
79
  view
80
80
  override
81
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
81
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
82
82
  {
83
83
  JBCashOutHookSpecification[] memory hookSpecifications;
84
84
 
@@ -92,7 +92,7 @@ contract CustomCashOutHook is IJBRulesetDataHook {
92
92
  });
93
93
  }
94
94
 
95
- return (cashOutTaxRateReturn, cashOutCountReturn, totalSupplyReturn, hookSpecifications);
95
+ return (cashOutTaxRateReturn, cashOutCountReturn, totalSupplyReturn, 0, hookSpecifications);
96
96
  }
97
97
 
98
98
  function hasMintPermissionFor(uint256, JBRuleset calldata, address) external view override returns (bool) {
@@ -145,6 +145,18 @@ contract OmnichainDeployerEdgeCases is Test {
145
145
  address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
146
146
  );
147
147
 
148
+ // Default: no remote supply or surplus (non-omnichain project).
149
+ vm.mockCall(
150
+ address(suckerRegistry),
151
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
152
+ abi.encode(uint256(0))
153
+ );
154
+ vm.mockCall(
155
+ address(suckerRegistry),
156
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
157
+ abi.encode(uint256(0))
158
+ );
159
+
148
160
  // Hook deployer mocks (every path now deploys a 721 hook).
149
161
  vm.mockCall(
150
162
  address(hookDeployer),
@@ -324,7 +336,7 @@ contract OmnichainDeployerEdgeCases is Test {
324
336
 
325
337
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
326
338
 
327
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
339
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
328
340
  assertEq(cashOutTaxRate, 5000, "Should return original tax rate");
329
341
  assertEq(cashOutCount, 1000, "Should return original cashOutCount");
330
342
  assertEq(totalSupply, 10_000, "Should return original totalSupply");
@@ -341,10 +353,12 @@ contract OmnichainDeployerEdgeCases is Test {
341
353
 
342
354
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
343
355
 
344
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
356
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
345
357
  assertEq(cashOutTaxRate, 2000, "Should return custom hook tax rate");
346
358
  assertEq(cashOutCount, 500, "Should return custom hook cashOutCount");
347
- assertEq(totalSupply, 8000, "Should return custom hook totalSupply");
359
+ // The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
360
+ // With no suckers, this equals context.totalSupply.
361
+ assertEq(totalSupply, ctx.totalSupply, "Should return cross-chain totalSupply (context value with no suckers)");
348
362
  }
349
363
 
350
364
  // =========================================================================
@@ -370,7 +384,7 @@ contract OmnichainDeployerEdgeCases is Test {
370
384
 
371
385
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, sucker);
372
386
 
373
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
387
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
374
388
  assertEq(cashOutTaxRate, 0, "Sucker should get 0 tax regardless of hooks");
375
389
  assertEq(cashOutCount, 1000, "Sucker should get original cashOutCount");
376
390
  assertEq(totalSupply, 10_000, "Sucker should get original totalSupply");
@@ -391,14 +405,14 @@ contract OmnichainDeployerEdgeCases is Test {
391
405
  vm.mockCall(
392
406
  mock721,
393
407
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
394
- abi.encode(uint256(9999), uint256(1), uint256(1), new JBCashOutHookSpecification[](0))
408
+ abi.encode(uint256(9999), uint256(1), uint256(1), uint256(0), new JBCashOutHookSpecification[](0))
395
409
  );
396
410
 
397
411
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
398
412
 
399
413
  // Since useDataHookForCashOut is false, the 721 hook should NOT be called.
400
414
  // Original values should be returned.
401
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
415
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
402
416
  assertEq(cashOutTaxRate, 5000, "Should return original tax rate, not 721 hook's 9999");
403
417
  assertEq(cashOutCount, 1000, "Should return original cashOutCount, not 721 hook's 1");
404
418
  assertEq(totalSupply, 10_000, "Should return original totalSupply, not 721 hook's 1");
@@ -417,10 +431,12 @@ contract OmnichainDeployerEdgeCases is Test {
417
431
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
418
432
 
419
433
  // Since useDataHookForCashOut is true, the custom hook SHOULD be called.
420
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
434
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
421
435
  assertEq(cashOutTaxRate, 9999, "Should return custom hook's tax rate");
422
436
  assertEq(cashOutCount, 1, "Should return custom hook's cashOutCount");
423
- assertEq(totalSupply, 1, "Should return custom hook's totalSupply");
437
+ // The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
438
+ // With no suckers, this equals context.totalSupply.
439
+ assertEq(totalSupply, ctx.totalSupply, "Should return cross-chain totalSupply (context value with no suckers)");
424
440
  }
425
441
 
426
442
  function test_beforeCashOut_merges721AndCustomHookSpecifications() public {
@@ -441,7 +457,7 @@ contract OmnichainDeployerEdgeCases is Test {
441
457
  vm.mockCall(
442
458
  mock721,
443
459
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
444
- abi.encode(uint256(4000), uint256(700), uint256(9000), specs)
460
+ abi.encode(uint256(4000), uint256(700), uint256(9000), uint256(0), specs)
445
461
  );
446
462
 
447
463
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
@@ -449,13 +465,15 @@ contract OmnichainDeployerEdgeCases is Test {
449
465
  (
450
466
  uint256 cashOutTaxRate,
451
467
  uint256 cashOutCount,
452
- uint256 totalSupply,
468
+ uint256 totalSupply,,
453
469
  JBCashOutHookSpecification[] memory hookSpecifications
454
470
  ) = deployer.beforeCashOutRecordedWith(ctx);
455
471
 
456
472
  assertEq(cashOutTaxRate, 2000, "Custom hook should receive and override 721-adjusted tax rate");
457
473
  assertEq(cashOutCount, 500, "Custom hook should receive and override 721-adjusted cashOutCount");
458
- assertEq(totalSupply, 8000, "Custom hook should receive and override 721-adjusted totalSupply");
474
+ // The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
475
+ // With no suckers, this equals context.totalSupply.
476
+ assertEq(totalSupply, ctx.totalSupply, "Should return cross-chain totalSupply (context value with no suckers)");
459
477
  assertEq(hookSpecifications.length, 2, "721 and custom cash out specs should both be returned");
460
478
  assertEq(address(hookSpecifications[0].hook), mock721, "721 hook spec should come first");
461
479
  assertEq(hookSpecifications[0].amount, 11, "721 hook spec amount should be preserved");
@@ -477,7 +495,7 @@ contract OmnichainDeployerEdgeCases is Test {
477
495
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
478
496
 
479
497
  // Since useDataHookForCashOut is false, the custom hook should NOT be called.
480
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
498
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
481
499
  assertEq(cashOutTaxRate, 5000, "Should return original tax rate, not custom hook's 2000");
482
500
  assertEq(cashOutCount, 1000, "Should return original cashOutCount, not custom hook's 1");
483
501
  assertEq(totalSupply, 10_000, "Should return original totalSupply, not custom hook's 1");
@@ -494,7 +512,7 @@ contract OmnichainDeployerEdgeCases is Test {
494
512
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
495
513
 
496
514
  // No 721 hook, no custom hook — should fall through to original values.
497
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
515
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
498
516
  assertEq(cashOutTaxRate, 5000, "Should return original tax rate");
499
517
  assertEq(cashOutCount, 1000, "Should return original cashOutCount");
500
518
  assertEq(totalSupply, 10_000, "Should return original totalSupply");
@@ -55,9 +55,11 @@ contract PayRevertingHook is IJBRulesetDataHook {
55
55
  external
56
56
  pure
57
57
  override
58
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
58
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
59
59
  {
60
- return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, new JBCashOutHookSpecification[](0));
60
+ return (
61
+ context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
62
+ );
61
63
  }
62
64
 
63
65
  function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
@@ -86,7 +88,7 @@ contract CashOutRevertingHook is IJBRulesetDataHook {
86
88
  external
87
89
  pure
88
90
  override
89
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
91
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
90
92
  {
91
93
  revert CustomCashOutError();
92
94
  }
@@ -117,9 +119,11 @@ contract MintPermissionRevertingHook is IJBRulesetDataHook {
117
119
  external
118
120
  pure
119
121
  override
120
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
122
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
121
123
  {
122
- return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, new JBCashOutHookSpecification[](0));
124
+ return (
125
+ context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
126
+ );
123
127
  }
124
128
 
125
129
  function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
@@ -146,10 +150,10 @@ contract ExtremeCashOutHook is IJBRulesetDataHook {
146
150
  external
147
151
  pure
148
152
  override
149
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
153
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
150
154
  {
151
155
  // Return extreme values: max tax rate, zero count, max supply.
152
- return (type(uint256).max, 0, type(uint256).max, new JBCashOutHookSpecification[](0));
156
+ return (type(uint256).max, 0, type(uint256).max, 0, new JBCashOutHookSpecification[](0));
153
157
  }
154
158
 
155
159
  function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
@@ -189,9 +193,11 @@ contract ManySpecsHook is IJBRulesetDataHook {
189
193
  external
190
194
  pure
191
195
  override
192
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
196
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
193
197
  {
194
- return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, new JBCashOutHookSpecification[](0));
198
+ return (
199
+ context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
200
+ );
195
201
  }
196
202
 
197
203
  function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
@@ -219,9 +225,11 @@ contract ZeroWeightHook is IJBRulesetDataHook {
219
225
  external
220
226
  pure
221
227
  override
222
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
228
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
223
229
  {
224
- return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, new JBCashOutHookSpecification[](0));
230
+ return (
231
+ context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
232
+ );
225
233
  }
226
234
 
227
235
  function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
@@ -310,6 +318,18 @@ contract TestAuditGaps is Test {
310
318
  address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
311
319
  );
312
320
 
321
+ // Default: no remote supply or surplus (non-omnichain project).
322
+ vm.mockCall(
323
+ address(suckerRegistry),
324
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
325
+ abi.encode(uint256(0))
326
+ );
327
+ vm.mockCall(
328
+ address(suckerRegistry),
329
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
330
+ abi.encode(uint256(0))
331
+ );
332
+
313
333
  // Default 721 hook mock: returns context weight and empty specs (0 tiers, no splits).
314
334
  // A real 721 hook with no tiers returns contextWeight unchanged.
315
335
  vm.mockCall(
@@ -369,7 +389,7 @@ contract TestAuditGaps is Test {
369
389
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, storedRulesetId, sucker);
370
390
 
371
391
  // Sucker gets early return -- never hits the reverting hook.
372
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
392
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
373
393
  assertEq(cashOutTaxRate, 0, "Sucker should get 0 tax even with reverting hook");
374
394
  assertEq(cashOutCount, ctx.cashOutCount, "Sucker cashOutCount should pass through");
375
395
  assertEq(totalSupply, ctx.totalSupply, "Sucker totalSupply should pass through");
@@ -427,10 +447,12 @@ contract TestAuditGaps is Test {
427
447
 
428
448
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, storedRulesetId, attacker);
429
449
 
430
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
450
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
431
451
  assertEq(cashOutTaxRate, type(uint256).max, "Should pass through max tax rate from hook");
432
452
  assertEq(cashOutCount, 0, "Should pass through 0 count from hook");
433
- assertEq(totalSupply, type(uint256).max, "Should pass through max supply from hook");
453
+ // The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
454
+ // With no suckers, this equals context.totalSupply.
455
+ assertEq(totalSupply, ctx.totalSupply, "Should return cross-chain totalSupply (context value with no suckers)");
434
456
  }
435
457
 
436
458
  // -------------------------------------------------------------------------
@@ -482,7 +504,7 @@ contract TestAuditGaps is Test {
482
504
  // Cash out should work since the hook only reverts for pay (useDataHookForCashOut = false).
483
505
  JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, storedRulesetId, attacker);
484
506
 
485
- (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(ctx);
507
+ (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
486
508
  assertEq(cashOutTaxRate, ctx.cashOutTaxRate, "Cash out should return original tax rate");
487
509
  assertEq(cashOutCount, ctx.cashOutCount, "Cash out should return original count");
488
510
  assertEq(totalSupply, ctx.totalSupply, "Cash out should return original supply");
@@ -106,6 +106,16 @@ contract Tiered721HookComposition is Test {
106
106
  vm.mockCall(
107
107
  address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
108
108
  );
109
+ vm.mockCall(
110
+ address(suckerRegistry),
111
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
112
+ abi.encode(uint256(0))
113
+ );
114
+ vm.mockCall(
115
+ address(suckerRegistry),
116
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
117
+ abi.encode(uint256(0))
118
+ );
109
119
  JBPayHookSpecification[] memory default721Specs = new JBPayHookSpecification[](1);
110
120
  default721Specs[0] =
111
121
  JBPayHookSpecification({hook: IJBPayHook(hookAddr), noop: false, amount: 0, metadata: bytes("")});
@@ -330,7 +340,7 @@ contract Tiered721HookComposition is Test {
330
340
  abi.encode(true)
331
341
  );
332
342
  JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, block.timestamp, sucker);
333
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(context);
343
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
334
344
  assertEq(taxRate, 0, "sucker gets 0 tax");
335
345
  assertEq(cashOutCount, context.cashOutCount);
336
346
  assertEq(totalSupply, context.totalSupply);
@@ -349,13 +359,15 @@ contract Tiered721HookComposition is Test {
349
359
  vm.mockCall(
350
360
  buybackHookAddr,
351
361
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
352
- abi.encode(uint256(3000), uint256(500), uint256(5000), cashOutSpecs)
362
+ abi.encode(uint256(3000), uint256(500), uint256(5000), uint256(0), cashOutSpecs)
353
363
  );
354
364
  JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, block.timestamp, randomAddr);
355
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(context);
365
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
356
366
  assertEq(taxRate, 3000, "buyback hook's tax rate");
357
367
  assertEq(cashOutCount, 500, "buyback hook's cashOutCount");
358
- assertEq(totalSupply, 5000, "buyback hook's totalSupply");
368
+ // The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
369
+ // With no suckers, this equals context.totalSupply.
370
+ assertEq(totalSupply, context.totalSupply, "cross-chain totalSupply (context value with no suckers)");
359
371
  }
360
372
 
361
373
  function test_beforeCashOut_zeroTiers_forwardsToUserHook() public {
@@ -375,13 +387,15 @@ contract Tiered721HookComposition is Test {
375
387
  vm.mockCall(
376
388
  customHookAddr,
377
389
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
378
- abi.encode(uint256(2000), uint256(100), uint256(1000), cashOutSpecs)
390
+ abi.encode(uint256(2000), uint256(100), uint256(1000), uint256(0), cashOutSpecs)
379
391
  );
380
392
  JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, block.timestamp, randomAddr);
381
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(context);
393
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
382
394
  assertEq(taxRate, 2000, "user hook's tax rate");
383
395
  assertEq(cashOutCount, 100, "user hook's cashOutCount");
384
- assertEq(totalSupply, 1000, "user hook's totalSupply");
396
+ // The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
397
+ // With no suckers, this equals context.totalSupply.
398
+ assertEq(totalSupply, context.totalSupply, "cross-chain totalSupply (context value with no suckers)");
385
399
  }
386
400
 
387
401
  function test_beforeCashOut_zeroTiers_noUserHook_returnsOriginal() public {
@@ -398,7 +412,7 @@ contract Tiered721HookComposition is Test {
398
412
  controller
399
413
  );
400
414
  JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, block.timestamp, randomAddr);
401
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(context);
415
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
402
416
  assertEq(taxRate, context.cashOutTaxRate, "original tax rate");
403
417
  assertEq(cashOutCount, context.cashOutCount);
404
418
  assertEq(totalSupply, context.totalSupply);
@@ -126,6 +126,18 @@ contract MockSuckerRegistryCarryForward is IJBSuckerRegistry {
126
126
  return new address[](0);
127
127
  }
128
128
 
129
+ function remoteTotalSupplyOf(uint256) external pure override returns (uint256) {
130
+ return 0;
131
+ }
132
+
133
+ function remoteBalanceOf(uint256, uint256, uint256) external pure override returns (uint256) {
134
+ return 0;
135
+ }
136
+
137
+ function remoteSurplusOf(uint256, uint256, uint256) external pure override returns (uint256) {
138
+ return 0;
139
+ }
140
+
129
141
  function removeDeprecatedSucker(uint256, address) external override {}
130
142
  function removeSuckerDeployer(address) external override {}
131
143
  }
@@ -0,0 +1,158 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+
6
+ import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
7
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
8
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
9
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
10
+ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
11
+ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
12
+ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
13
+ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
14
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
15
+ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
16
+ import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookProjectDeployer.sol";
17
+ import {IJBOwnable} from "@bananapus/ownable-v6/src/interfaces/IJBOwnable.sol";
18
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
19
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
20
+ import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
21
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
22
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
23
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
24
+
25
+ import {JBOmnichainDeployer} from "../../src/JBOmnichainDeployer.sol";
26
+ import {JBOmnichain721Config} from "../../src/structs/JBOmnichain721Config.sol";
27
+ import {JBSuckerDeploymentConfig} from "../../src/structs/JBSuckerDeploymentConfig.sol";
28
+
29
+ contract CodexNemesisAudit is Test {
30
+ IJBPermissions internal permissions = IJBPermissions(makeAddr("permissions"));
31
+ IJBProjects internal projects = IJBProjects(makeAddr("projects"));
32
+ IJB721TiersHookDeployer internal hookDeployer = IJB721TiersHookDeployer(makeAddr("hookDeployer"));
33
+ IJBSuckerRegistry internal mockSuckerRegistry = IJBSuckerRegistry(makeAddr("suckerRegistry"));
34
+ IJBController internal controller = IJBController(makeAddr("controller"));
35
+ IJBDirectory internal directory = IJBDirectory(makeAddr("directory"));
36
+ address internal hookAddr = makeAddr("hook721");
37
+ address internal projectOwner = makeAddr("projectOwner");
38
+ address internal operator = makeAddr("operator");
39
+
40
+ uint256 internal constant PROJECT_ID = 42;
41
+
42
+ function setUp() public {
43
+ vm.mockCall(
44
+ address(permissions), abi.encodeWithSelector(IJBPermissions.setPermissionsFor.selector), abi.encode()
45
+ );
46
+ vm.mockCall(
47
+ address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true)
48
+ );
49
+ vm.mockCall(
50
+ address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, PROJECT_ID), abi.encode(projectOwner)
51
+ );
52
+ vm.mockCall(
53
+ address(hookDeployer),
54
+ abi.encodeWithSelector(IJB721TiersHookDeployer.deployHookFor.selector),
55
+ abi.encode(IJB721TiersHook(hookAddr))
56
+ );
57
+ vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.transferOwnershipToProject.selector), abi.encode());
58
+ }
59
+
60
+ function test_poc_launchRulesetsFor_revertsWhenProjectHasNoControllerYet() public {
61
+ JBOmnichainDeployer deployer =
62
+ new JBOmnichainDeployer(mockSuckerRegistry, hookDeployer, permissions, projects, address(0));
63
+
64
+ vm.mockCall(
65
+ address(controller), abi.encodeWithSelector(IJBController.DIRECTORY.selector), abi.encode(directory)
66
+ );
67
+ // A freshly created project with no controller yet is valid for core launchRulesetsFor,
68
+ // but the deployer rejects it before the controller can set itself as first controller.
69
+ vm.mockCall(
70
+ address(directory),
71
+ abi.encodeWithSelector(IJBDirectory.controllerOf.selector, PROJECT_ID),
72
+ abi.encode(IERC165(address(0)))
73
+ );
74
+
75
+ JBRulesetConfig[] memory configs = new JBRulesetConfig[](1);
76
+ configs[0] = _makeRulesetConfig();
77
+
78
+ vm.prank(projectOwner);
79
+ vm.expectRevert(JBOmnichainDeployer.JBOmnichainDeployer_ControllerMismatch.selector);
80
+ deployer.launchRulesetsFor(PROJECT_ID, configs, new JBTerminalConfig[](0), "memo", controller);
81
+ }
82
+
83
+ function test_poc_deploySuckersFor_requiresHiddenPermissionForDeployerItself() public {
84
+ vm.mockCall(address(directory), abi.encodeWithSelector(IJBDirectory.PROJECTS.selector), abi.encode(projects));
85
+
86
+ JBSuckerRegistry registry = new JBSuckerRegistry(directory, permissions, address(this), address(0));
87
+ JBOmnichainDeployer deployer = new JBOmnichainDeployer(
88
+ IJBSuckerRegistry(address(registry)), hookDeployer, permissions, projects, address(0)
89
+ );
90
+
91
+ vm.mockCall(
92
+ address(permissions),
93
+ abi.encodeWithSelector(
94
+ IJBPermissions.hasPermission.selector,
95
+ operator,
96
+ projectOwner,
97
+ PROJECT_ID,
98
+ JBPermissionIds.DEPLOY_SUCKERS,
99
+ true,
100
+ true
101
+ ),
102
+ abi.encode(true)
103
+ );
104
+ vm.mockCall(
105
+ address(permissions),
106
+ abi.encodeWithSelector(
107
+ IJBPermissions.hasPermission.selector,
108
+ address(deployer),
109
+ projectOwner,
110
+ PROJECT_ID,
111
+ JBPermissionIds.DEPLOY_SUCKERS,
112
+ true,
113
+ true
114
+ ),
115
+ abi.encode(false)
116
+ );
117
+
118
+ JBSuckerDeploymentConfig memory config = JBSuckerDeploymentConfig({
119
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32("SUCKER_SALT")
120
+ });
121
+
122
+ vm.prank(operator);
123
+ vm.expectRevert(
124
+ abi.encodeWithSelector(
125
+ JBPermissioned.JBPermissioned_Unauthorized.selector,
126
+ projectOwner,
127
+ address(deployer),
128
+ PROJECT_ID,
129
+ JBPermissionIds.DEPLOY_SUCKERS
130
+ )
131
+ );
132
+ deployer.deploySuckersFor(PROJECT_ID, config);
133
+ }
134
+
135
+ function _makeRulesetConfig() internal pure returns (JBRulesetConfig memory config) {
136
+ config.metadata = JBRulesetMetadata({
137
+ reservedPercent: 0,
138
+ cashOutTaxRate: 0,
139
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
140
+ pausePay: false,
141
+ pauseCreditTransfers: false,
142
+ allowOwnerMinting: false,
143
+ allowSetCustomToken: false,
144
+ allowTerminalMigration: false,
145
+ allowSetTerminals: false,
146
+ allowSetController: false,
147
+ allowAddAccountingContext: false,
148
+ allowAddPriceFeed: false,
149
+ ownerMustSendPayouts: false,
150
+ holdFees: false,
151
+ useTotalSurplusForCashOuts: false,
152
+ useDataHookForPay: false,
153
+ useDataHookForCashOut: false,
154
+ dataHook: address(0),
155
+ metadata: 0
156
+ });
157
+ }
158
+ }
@@ -145,6 +145,14 @@ contract JBOmnichainDeployerTest is Test {
145
145
  abi.encode()
146
146
  );
147
147
  vm.mockCall(suckerRegistry, abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false));
148
+ vm.mockCall(
149
+ suckerRegistry,
150
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
151
+ abi.encode(uint256(0))
152
+ );
153
+ vm.mockCall(
154
+ suckerRegistry, abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector), abi.encode(uint256(0))
155
+ );
148
156
 
149
157
  JBOmnichainDeployer deployer =
150
158
  new JBOmnichainDeployer(IJBSuckerRegistry(suckerRegistry), hookDeployer, permissions, projects, address(0));
@@ -153,7 +161,7 @@ contract JBOmnichainDeployerTest is Test {
153
161
  vm.mockCall(
154
162
  hookAddr,
155
163
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
156
- abi.encode(uint256(1234), uint256(55), uint256(999), emptySpecs)
164
+ abi.encode(uint256(1234), uint256(55), uint256(999), uint256(0), emptySpecs)
157
165
  );
158
166
 
159
167
  JBRulesetConfig[] memory launchConfigs = new JBRulesetConfig[](1);
@@ -177,11 +185,13 @@ contract JBOmnichainDeployerTest is Test {
177
185
  assertTrue(initialUseForCashOut, "launch should store the initial cash-out flag");
178
186
 
179
187
  JBBeforeCashOutRecordedContext memory initialContext = _cashOutContext(initialRulesetId);
180
- (uint256 initialTaxRate, uint256 initialCashOutCount, uint256 initialTotalSupply,) =
188
+ (uint256 initialTaxRate, uint256 initialCashOutCount, uint256 initialTotalSupply,,) =
181
189
  deployer.beforeCashOutRecordedWith(initialContext);
182
190
  assertEq(initialTaxRate, 1234, "initial ruleset should forward cash-outs into the 721 hook");
183
191
  assertEq(initialCashOutCount, 55, "initial ruleset should use the 721 hook cash-out count");
184
- assertEq(initialTotalSupply, 999, "initial ruleset should use the 721 hook total supply");
192
+ // The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
193
+ // With no suckers, this equals context.totalSupply (777).
194
+ assertEq(initialTotalSupply, 777, "initial ruleset should use cross-chain totalSupply (context value)");
185
195
 
186
196
  vm.mockCall(
187
197
  address(controller), abi.encodeWithSelector(IJBController.DIRECTORY.selector), abi.encode(directory)
@@ -239,13 +249,15 @@ contract JBOmnichainDeployerTest is Test {
239
249
  assertTrue(queuedUseForCashOut, "queue should preserve the existing 721 cash-out flag");
240
250
 
241
251
  JBBeforeCashOutRecordedContext memory queuedContext = _cashOutContext(queuedRulesetId);
242
- (uint256 queuedTaxRate, uint256 queuedCashOutCount, uint256 queuedTotalSupply,) =
252
+ (uint256 queuedTaxRate, uint256 queuedCashOutCount, uint256 queuedTotalSupply,,) =
243
253
  deployer.beforeCashOutRecordedWith(queuedContext);
244
254
 
245
255
  // The 721 hook is properly consulted for cash-outs in the queued ruleset.
246
256
  assertEq(queuedTaxRate, 1234, "queued ruleset should forward cash-outs into the 721 hook");
247
257
  assertEq(queuedCashOutCount, 55, "queued ruleset should use the 721 hook cash-out count");
248
- assertEq(queuedTotalSupply, 999, "queued ruleset should use the 721 hook total supply");
258
+ // The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
259
+ // With no suckers, this equals context.totalSupply (777).
260
+ assertEq(queuedTotalSupply, 777, "queued ruleset should use cross-chain totalSupply (context value)");
249
261
  }
250
262
 
251
263
  function _cashOutContext(uint256 rulesetId) internal pure returns (JBBeforeCashOutRecordedContext memory context) {
@@ -70,6 +70,18 @@ contract WeightScalingComparisonTest is Test {
70
70
  address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
71
71
  );
72
72
 
73
+ // Default: no remote supply or surplus (non-omnichain project).
74
+ vm.mockCall(
75
+ address(suckerRegistry),
76
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
77
+ abi.encode(uint256(0))
78
+ );
79
+ vm.mockCall(
80
+ address(suckerRegistry),
81
+ abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
82
+ abi.encode(uint256(0))
83
+ );
84
+
73
85
  // Mock hook deployer to return our mock hook address.
74
86
  vm.mockCall(
75
87
  address(hookDeployer),
@@ -27,6 +27,7 @@ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
27
27
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
28
28
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
29
29
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
30
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
30
31
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
31
32
  import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookProjectDeployer.sol";
32
33
  import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
@@ -181,7 +182,14 @@ abstract contract OmnichainForkTestBase is TestBaseWorkflow {
181
182
  suckerRegistry = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
182
183
  hookStore = new JB721TiersHookStore();
183
184
  exampleHook = new JB721TiersHook(
184
- jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), hookStore, jbSplits(), address(0)
185
+ jbDirectory(),
186
+ jbPermissions(),
187
+ jbPrices(),
188
+ jbRulesets(),
189
+ hookStore,
190
+ jbSplits(),
191
+ new JB721CheckpointsDeployer(),
192
+ address(0)
185
193
  );
186
194
  addressRegistry = new JBAddressRegistry();
187
195
  hookDeployer721 = new JB721TiersHookDeployer(exampleHook, hookStore, addressRegistry, multisig());
@@ -179,7 +179,7 @@ contract TestSuckerDeploymentFork is OmnichainForkTestBase {
179
179
  });
180
180
 
181
181
  // Call beforeCashOutRecordedWith directly on the omnichain deployer.
182
- (uint256 returnedTaxRate, uint256 returnedCashOutCount, uint256 returnedTotalSupply,) =
182
+ (uint256 returnedTaxRate, uint256 returnedCashOutCount, uint256 returnedTotalSupply,,) =
183
183
  omnichainDeployer.beforeCashOutRecordedWith(context);
184
184
 
185
185
  // Sucker should get 0% tax.