@bananapus/omnichain-deployers-v6 0.0.29 → 0.0.31
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/RISKS.md +5 -0
- package/package.json +3 -3
- package/src/JBOmnichainDeployer.sol +50 -12
- package/test/JBOmnichainDeployerGuard.t.sol +44 -8
- package/test/OmnichainDeployerEdgeCases.t.sol +8 -6
- package/test/OmnichainDeployerReentrancy.t.sol +8 -8
- package/test/Tiered721HookComposition.t.sol +11 -6
- package/test/audit/AuditFixesC2H6M14.t.sol +6 -21
- package/test/audit/CarryForwardRejectedHook.t.sol +5 -5
- package/test/audit/CashOutCountPropagation.t.sol +232 -0
- package/test/audit/CashOutSpecMerge.t.sol +372 -0
- package/test/audit/{CodexNemesisDeterministicDrift.t.sol → DeterministicDrift.t.sol} +1 -1
- package/test/audit/{CodexNemesisDeterministicPeerDrift.t.sol → DeterministicPeerDrift.t.sol} +3 -3
- package/test/audit/ExtraCashOutHookZeroReclaim.t.sol +340 -0
- package/test/audit/{CodexNemesisForwardedPermissions.t.sol → ForwardedPermissions.t.sol} +1 -1
- package/test/audit/JBOmnichainDeployer.t.sol +6 -6
- package/test/audit/{CodexNemesisNftCashoutSupplyMismatch.t.sol → NftCashoutSupplyMismatch.t.sol} +11 -9
- package/test/audit/{CodexNemesisAudit.t.sol → OmnichainAudit.t.sol} +1 -2
- package/test/audit/SplitCreditWeight.t.sol +437 -0
- package/test/audit/WeightScalingComparison.t.sol +11 -12
- package/test/fork/OmnichainForkTestBase.sol +2 -1
- package/test/fork/TestOmnichain721QueueAndAdjust.t.sol +5 -5
- package/test/fork/TestOmnichainCashOutFork.t.sol +42 -40
- package/test/fork/TestOmnichainStressFork.t.sol +87 -95
- package/test/fork/TestOmnichainWeightFork.t.sol +9 -27
- package/test/invariants/CrossChainDeployerInvariant.t.sol +6 -2
- package/test/invariants/OmnichainDeployerInvariant.t.sol +3 -5
- 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.
|
|
3
|
+
"version": "0.0.31",
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
438
|
-
//
|
|
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
|
|
441
|
+
(cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, tiered721HookSpecifications) =
|
|
441
442
|
IJBRulesetDataHook(address(tiered721Config.hook)).beforeCashOutRecordedWith(context);
|
|
442
443
|
}
|
|
443
444
|
|
|
@@ -456,12 +457,20 @@ contract JBOmnichainDeployer is
|
|
|
456
457
|
hookContext.totalSupply = totalSupply;
|
|
457
458
|
hookContext.surplus.value = effectiveSurplusValue;
|
|
458
459
|
|
|
459
|
-
// Forward to the extra hook. It may further change the tax rate
|
|
460
|
-
// We discard
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
(
|
|
464
|
-
|
|
460
|
+
// Forward to the extra hook. It may further change the tax rate and return hook specs.
|
|
461
|
+
// We always discard totalSupply and effectiveSurplusValue — this contract computes
|
|
462
|
+
// cross-chain values for both. When the 721 hook is active, we also discard cashOutCount
|
|
463
|
+
// because the 721 hook redefines both cashOutCount and totalSupply as NFT cash-out weights
|
|
464
|
+
// (sum of tier prices), not fungible token counts. Letting the extra hook override
|
|
465
|
+
// cashOutCount would corrupt NFT pricing in the bonding curve.
|
|
466
|
+
if (address(tiered721Config.hook) != address(0) && tiered721Config.useDataHookForCashOut) {
|
|
467
|
+
// slither-disable-next-line unused-return
|
|
468
|
+
(cashOutTaxRate,,,, extraHookSpecifications) = extraHook.dataHook.beforeCashOutRecordedWith(hookContext);
|
|
469
|
+
} else {
|
|
470
|
+
// slither-disable-next-line unused-return
|
|
471
|
+
(cashOutTaxRate, cashOutCount,,, extraHookSpecifications) =
|
|
472
|
+
extraHook.dataHook.beforeCashOutRecordedWith(hookContext);
|
|
473
|
+
}
|
|
465
474
|
}
|
|
466
475
|
|
|
467
476
|
// If neither hook returned any specifications, return the adjusted values with no hook specs.
|
|
@@ -510,6 +519,8 @@ contract JBOmnichainDeployer is
|
|
|
510
519
|
bool hasTiered721Spec;
|
|
511
520
|
// The weight returned by the 721 hook (already scaled for splits).
|
|
512
521
|
uint256 tiered721Weight;
|
|
522
|
+
// The weight attributable to tier splits when issueTokensForSplits is true.
|
|
523
|
+
uint256 splitCreditWeight;
|
|
513
524
|
// Whether a 721 hook is configured for this project's ruleset.
|
|
514
525
|
bool has721Hook;
|
|
515
526
|
if (address(tiered721Config.hook) != address(0)) {
|
|
@@ -526,6 +537,14 @@ contract JBOmnichainDeployer is
|
|
|
526
537
|
hasTiered721Spec = true;
|
|
527
538
|
tiered721HookSpec = tiered721HookSpecs[0];
|
|
528
539
|
totalSplitAmount = tiered721HookSpec.amount;
|
|
540
|
+
|
|
541
|
+
// Decode splitCreditWeight from the 721 hook's metadata (4th field).
|
|
542
|
+
// When issueTokensForSplits is true and splits exist, this holds the weight portion
|
|
543
|
+
// attributable to tier splits — used to prevent split credit erasure if the extra
|
|
544
|
+
// hook (e.g. buyback) returns weight=0.
|
|
545
|
+
if (tiered721HookSpec.metadata.length >= 128) {
|
|
546
|
+
(,,, splitCreditWeight) = abi.decode(tiered721HookSpec.metadata, (address, address, bytes, uint256));
|
|
547
|
+
}
|
|
529
548
|
}
|
|
530
549
|
}
|
|
531
550
|
|
|
@@ -541,11 +560,27 @@ contract JBOmnichainDeployer is
|
|
|
541
560
|
if (address(extraHook.dataHook) != address(0) && extraHook.useDataHookForPay) {
|
|
542
561
|
JBBeforePayRecordedContext memory hookContext = context;
|
|
543
562
|
hookContext.amount.value = projectAmount;
|
|
544
|
-
// Pass the
|
|
545
|
-
//
|
|
546
|
-
|
|
563
|
+
// Pass the original context.weight — NOT the 721 hook's split-adjusted weight.
|
|
564
|
+
// The extra hook (e.g. buyback) applies its own weight logic; using the 721 hook's
|
|
565
|
+
// already-split-adjusted weight would double-discount the split ratio.
|
|
547
566
|
(weight, dataHookSpecs) = extraHook.dataHook.beforePayRecordedWith(hookContext);
|
|
548
567
|
customHookCalled = true;
|
|
568
|
+
|
|
569
|
+
// The custom hook (e.g. buyback) returned a weight based on the original context.weight.
|
|
570
|
+
// If the 721 hook scaled weight down for tier splits, apply the same ratio so the terminal
|
|
571
|
+
// doesn't over-mint tokens relative to the funds actually entering the project.
|
|
572
|
+
// When issueTokensForSplits is true, tiered721Weight == context.weight and the ratio is 1x.
|
|
573
|
+
if (has721Hook && context.weight > 0 && tiered721Weight != context.weight) {
|
|
574
|
+
weight = mulDiv(weight, tiered721Weight, context.weight);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// When the extra hook returns weight=0 (e.g. buyback found no profitable swap) but tier
|
|
578
|
+
// splits exist with issueTokensForSplits=true, the split credit must still mint fungible tokens.
|
|
579
|
+
// The split credit weight is independent of buyback routing — it represents the token issuance
|
|
580
|
+
// for funds forwarded to tier split beneficiaries.
|
|
581
|
+
if (weight == 0 && splitCreditWeight > 0) {
|
|
582
|
+
weight = splitCreditWeight;
|
|
583
|
+
}
|
|
549
584
|
}
|
|
550
585
|
}
|
|
551
586
|
|
|
@@ -813,6 +848,9 @@ contract JBOmnichainDeployer is
|
|
|
813
848
|
{
|
|
814
849
|
// First try the latest queued ruleset — if it's been explicitly approved
|
|
815
850
|
// (or has no approval hook), its hook config should take precedence.
|
|
851
|
+
// Conservative: only use Approved or Empty status. ApprovalExpected is intentionally
|
|
852
|
+
// excluded because hook selection is irreversible — if the pending ruleset is later rejected
|
|
853
|
+
// by the approval hook, we'd have locked in a hook from a ruleset that never became active.
|
|
816
854
|
(JBRuleset memory latestQueued, JBApprovalStatus approvalStatus) =
|
|
817
855
|
controller.RULESETS().latestQueuedOf(projectId);
|
|
818
856
|
if (
|
|
@@ -263,11 +263,11 @@ contract JBOmnichainDeployerGuardTest is TestBaseWorkflow {
|
|
|
263
263
|
.setPermissionsFor(
|
|
264
264
|
owner,
|
|
265
265
|
JBPermissionsData({
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
//
|
|
477
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
713
|
-
|
|
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
|
-
|
|
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
|
|
383
|
-
|
|
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:
|
|
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
|
|
424
|
-
|
|
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:
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
8
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
9
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
10
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
11
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
12
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
13
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
14
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
15
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
16
|
+
import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
17
|
+
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
18
|
+
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
19
|
+
|
|
20
|
+
import {JBOmnichainDeployer} from "../../src/JBOmnichainDeployer.sol";
|
|
21
|
+
|
|
22
|
+
/// @notice Regression test: cashOutCount from the 721 hook must be propagated
|
|
23
|
+
/// to the extra data hook's context. Before fix, the extra hook receives the stale
|
|
24
|
+
/// context.cashOutCount instead of the 721-adjusted value.
|
|
25
|
+
contract CashOutCountPropagationTest is Test {
|
|
26
|
+
uint256 internal constant PROJECT_ID = 1;
|
|
27
|
+
uint256 internal constant RULESET_ID = 123;
|
|
28
|
+
|
|
29
|
+
// The 721 hook will change cashOutCount from ORIGINAL to ADJUSTED.
|
|
30
|
+
uint256 internal constant ORIGINAL_CASH_OUT_COUNT = 50 ether;
|
|
31
|
+
uint256 internal constant ADJUSTED_CASH_OUT_COUNT = 7 ether;
|
|
32
|
+
uint256 internal constant NFT_TOTAL_SUPPLY = 21 ether;
|
|
33
|
+
uint256 internal constant LOCAL_SURPLUS = 10 ether;
|
|
34
|
+
|
|
35
|
+
address internal holder = makeAddr("holder");
|
|
36
|
+
address internal permissions = makeAddr("permissions");
|
|
37
|
+
address internal projects = makeAddr("projects");
|
|
38
|
+
address internal hookDeployer = makeAddr("hookDeployer");
|
|
39
|
+
address internal suckerRegistry = makeAddr("suckerRegistry");
|
|
40
|
+
address internal directory = makeAddr("directory");
|
|
41
|
+
|
|
42
|
+
JBOmnichainDeployer internal deployer;
|
|
43
|
+
Mock721Hook internal nftHook;
|
|
44
|
+
address internal extraHookAddr;
|
|
45
|
+
|
|
46
|
+
function setUp() public {
|
|
47
|
+
vm.mockCall(permissions, abi.encodeWithSelector(IJBPermissions.setPermissionsFor.selector), abi.encode());
|
|
48
|
+
|
|
49
|
+
deployer = new JBOmnichainDeployer(
|
|
50
|
+
IJBSuckerRegistry(suckerRegistry),
|
|
51
|
+
IJB721TiersHookDeployer(hookDeployer),
|
|
52
|
+
IJBPermissions(permissions),
|
|
53
|
+
IJBProjects(projects),
|
|
54
|
+
IJBDirectory(directory),
|
|
55
|
+
address(0)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
nftHook = new Mock721Hook(ADJUSTED_CASH_OUT_COUNT, NFT_TOTAL_SUPPLY);
|
|
59
|
+
extraHookAddr = makeAddr("extraHook");
|
|
60
|
+
|
|
61
|
+
_storeTiered721Hook(address(nftHook), true);
|
|
62
|
+
_storeExtraDataHook(extraHookAddr, true);
|
|
63
|
+
_mockSuckerRegistry();
|
|
64
|
+
_mockExtraHook();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// @notice The extra data hook should receive the 721-adjusted cashOutCount,
|
|
68
|
+
/// not the original context.cashOutCount.
|
|
69
|
+
function test_extraHookReceivesAdjustedCashOutCount() public {
|
|
70
|
+
// Build the expected context that the extra hook should receive.
|
|
71
|
+
// The deployer copies the original context and patches cashOutTaxRate, totalSupply, surplus.value.
|
|
72
|
+
// With the fix, it should also patch cashOutCount.
|
|
73
|
+
// NOTE: We build expectedContext separately (not via pointer alias) to avoid Solidity memory aliasing.
|
|
74
|
+
JBBeforeCashOutRecordedContext memory expectedContext = JBBeforeCashOutRecordedContext({
|
|
75
|
+
terminal: address(0x1234),
|
|
76
|
+
holder: holder,
|
|
77
|
+
projectId: PROJECT_ID,
|
|
78
|
+
rulesetId: RULESET_ID,
|
|
79
|
+
cashOutCount: ADJUSTED_CASH_OUT_COUNT, // THIS IS THE FIX — must be the 721-adjusted value
|
|
80
|
+
totalSupply: NFT_TOTAL_SUPPLY, // from 721 hook
|
|
81
|
+
surplus: JBTokenAmount({
|
|
82
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
83
|
+
decimals: 18,
|
|
84
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
85
|
+
value: LOCAL_SURPLUS // 721 hook passes through surplus value
|
|
86
|
+
}),
|
|
87
|
+
useTotalSurplus: false,
|
|
88
|
+
cashOutTaxRate: 5000, // 721 hook passes through
|
|
89
|
+
beneficiaryIsFeeless: false,
|
|
90
|
+
metadata: ""
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Build the input context with the ORIGINAL cashOutCount.
|
|
94
|
+
JBBeforeCashOutRecordedContext memory context = _cashOutContext();
|
|
95
|
+
|
|
96
|
+
// Sanity: the original context has a different cashOutCount than what the 721 hook returns.
|
|
97
|
+
assertEq(context.cashOutCount, ORIGINAL_CASH_OUT_COUNT);
|
|
98
|
+
assertTrue(ORIGINAL_CASH_OUT_COUNT != ADJUSTED_CASH_OUT_COUNT);
|
|
99
|
+
|
|
100
|
+
// Expect the extra hook to be called with the adjusted context (including adjusted cashOutCount).
|
|
101
|
+
vm.expectCall(extraHookAddr, abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (expectedContext)));
|
|
102
|
+
|
|
103
|
+
deployer.beforeCashOutRecordedWith(context);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _cashOutContext() internal view returns (JBBeforeCashOutRecordedContext memory context) {
|
|
107
|
+
context = JBBeforeCashOutRecordedContext({
|
|
108
|
+
terminal: address(0x1234),
|
|
109
|
+
holder: holder,
|
|
110
|
+
projectId: PROJECT_ID,
|
|
111
|
+
rulesetId: RULESET_ID,
|
|
112
|
+
cashOutCount: ORIGINAL_CASH_OUT_COUNT,
|
|
113
|
+
totalSupply: 100 ether,
|
|
114
|
+
surplus: JBTokenAmount({
|
|
115
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
116
|
+
decimals: 18,
|
|
117
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
118
|
+
value: LOCAL_SURPLUS
|
|
119
|
+
}),
|
|
120
|
+
useTotalSurplus: false,
|
|
121
|
+
cashOutTaxRate: 5000,
|
|
122
|
+
beneficiaryIsFeeless: false,
|
|
123
|
+
metadata: ""
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _mockSuckerRegistry() internal {
|
|
128
|
+
vm.mockCall(
|
|
129
|
+
suckerRegistry,
|
|
130
|
+
abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector, PROJECT_ID, holder),
|
|
131
|
+
abi.encode(false)
|
|
132
|
+
);
|
|
133
|
+
vm.mockCall(
|
|
134
|
+
suckerRegistry,
|
|
135
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector, PROJECT_ID),
|
|
136
|
+
abi.encode(0)
|
|
137
|
+
);
|
|
138
|
+
vm.mockCall(
|
|
139
|
+
suckerRegistry,
|
|
140
|
+
abi.encodeWithSelector(
|
|
141
|
+
IJBSuckerRegistry.remoteSurplusOf.selector,
|
|
142
|
+
PROJECT_ID,
|
|
143
|
+
uint256(18),
|
|
144
|
+
uint256(uint32(uint160(JBConstants.NATIVE_TOKEN)))
|
|
145
|
+
),
|
|
146
|
+
abi.encode(0)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// @dev Mock the extra hook to return passthrough values.
|
|
151
|
+
function _mockExtraHook() internal {
|
|
152
|
+
// We use a broad mock: any call to beforeCashOutRecordedWith returns passthrough values.
|
|
153
|
+
// The vm.expectCall will check the exact arguments.
|
|
154
|
+
JBCashOutHookSpecification[] memory specs = new JBCashOutHookSpecification[](0);
|
|
155
|
+
vm.mockCall(
|
|
156
|
+
extraHookAddr,
|
|
157
|
+
abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
|
|
158
|
+
abi.encode(uint256(5000), uint256(0), uint256(0), uint256(0), specs)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// @dev Store the 721 hook at _tiered721HookOf[PROJECT_ID][RULESET_ID] (slot 1).
|
|
163
|
+
function _storeTiered721Hook(address hook, bool useDataHookForCashOut) internal {
|
|
164
|
+
bytes32 outerSlot = keccak256(abi.encode(PROJECT_ID, uint256(1)));
|
|
165
|
+
bytes32 valueSlot = keccak256(abi.encode(RULESET_ID, outerSlot));
|
|
166
|
+
|
|
167
|
+
uint256 packed = uint256(uint160(hook));
|
|
168
|
+
if (useDataHookForCashOut) packed |= uint256(1) << 160;
|
|
169
|
+
|
|
170
|
+
vm.store(address(deployer), valueSlot, bytes32(packed));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// @dev Store the extra data hook at _extraDataHookOf[PROJECT_ID][RULESET_ID] (slot 0).
|
|
174
|
+
/// JBDeployerHookConfig has: IJBRulesetDataHook dataHook (20 bytes), bool useDataHookForPay (1 byte),
|
|
175
|
+
/// bool useDataHookForCashOut (1 byte). All pack into a single slot.
|
|
176
|
+
function _storeExtraDataHook(address hook, bool useDataHookForCashOut) internal {
|
|
177
|
+
bytes32 outerSlot = keccak256(abi.encode(PROJECT_ID, uint256(0)));
|
|
178
|
+
bytes32 valueSlot = keccak256(abi.encode(RULESET_ID, outerSlot));
|
|
179
|
+
|
|
180
|
+
uint256 packed = uint256(uint160(hook));
|
|
181
|
+
// useDataHookForPay is at bit 160, useDataHookForCashOut is at bit 168.
|
|
182
|
+
if (useDataHookForCashOut) packed |= uint256(1) << 168;
|
|
183
|
+
|
|
184
|
+
vm.store(address(deployer), valueSlot, bytes32(packed));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// @notice Mock 721 hook that returns adjusted cashOutCount and totalSupply values.
|
|
189
|
+
contract Mock721Hook is IJBRulesetDataHook {
|
|
190
|
+
uint256 internal immutable _cashOutCount;
|
|
191
|
+
uint256 internal immutable _totalSupply;
|
|
192
|
+
|
|
193
|
+
constructor(uint256 cashOutCount_, uint256 totalSupply_) {
|
|
194
|
+
_cashOutCount = cashOutCount_;
|
|
195
|
+
_totalSupply = totalSupply_;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
199
|
+
external
|
|
200
|
+
view
|
|
201
|
+
returns (
|
|
202
|
+
uint256 cashOutTaxRate,
|
|
203
|
+
uint256 effectiveCashOutCount,
|
|
204
|
+
uint256 effectiveTotalSupply,
|
|
205
|
+
uint256 effectiveSurplusValue,
|
|
206
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
207
|
+
)
|
|
208
|
+
{
|
|
209
|
+
cashOutTaxRate = context.cashOutTaxRate;
|
|
210
|
+
effectiveCashOutCount = _cashOutCount;
|
|
211
|
+
effectiveTotalSupply = _totalSupply;
|
|
212
|
+
effectiveSurplusValue = context.surplus.value;
|
|
213
|
+
hookSpecifications = new JBCashOutHookSpecification[](0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
217
|
+
external
|
|
218
|
+
pure
|
|
219
|
+
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
220
|
+
{
|
|
221
|
+
weight = context.weight;
|
|
222
|
+
hookSpecifications = new JBPayHookSpecification[](0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasMintPermissionFor(uint256, JBRuleset memory, address) external pure returns (bool) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function supportsInterface(bytes4) external pure returns (bool) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|