@bananapus/omnichain-deployers-v6 0.0.37 → 0.0.39

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/README.md CHANGED
@@ -90,7 +90,7 @@ src/
90
90
  interfaces/
91
91
  structs/
92
92
  test/
93
- unit, attack, invariant, fork, audit, and regression coverage
93
+ unit, attack, invariant, fork, review, and regression coverage
94
94
  script/
95
95
  Deploy.s.sol
96
96
  helpers/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/omnichain-deployers-v6",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,10 +27,10 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@bananapus/721-hook-v6": "0.0.43",
30
- "@bananapus/core-v6": "0.0.39",
30
+ "@bananapus/core-v6": "0.0.44",
31
31
  "@bananapus/ownable-v6": "0.0.24",
32
32
  "@bananapus/permission-ids-v6": "0.0.22",
33
- "@bananapus/suckers-v6": "0.0.34",
33
+ "@bananapus/suckers-v6": "0.0.37",
34
34
  "@openzeppelin/contracts": "5.6.1"
35
35
  },
36
36
  "devDependencies": {
@@ -24,5 +24,5 @@
24
24
  ## Useful Proof Points
25
25
 
26
26
  - [`test/JBOmnichainDeployer.t.sol`](../test/JBOmnichainDeployer.t.sol) for baseline deploy and queue flows.
27
- - [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for pinned edge cases.
27
+ - [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) for pinned edge cases.
28
28
  - [`test/Tiered721HookComposition.t.sol`](../test/Tiered721HookComposition.t.sol) when cross-repo integration behavior matters more than isolated unit logic.
@@ -25,4 +25,4 @@
25
25
  - [`test/Tiered721HookComposition.t.sol`](../test/Tiered721HookComposition.t.sol) for hook-composition behavior.
26
26
  - [`test/JBOmnichainDeployerGuard.t.sol`](../test/JBOmnichainDeployerGuard.t.sol) and [`test/OmnichainDeployerAttacks.t.sol`](../test/OmnichainDeployerAttacks.t.sol) for safety properties.
27
27
  - [`test/OmnichainDeployerReentrancy.t.sol`](../test/OmnichainDeployerReentrancy.t.sol) for wrapper security assumptions.
28
- - [`test/OmnichainDeployerEdgeCases.t.sol`](../test/OmnichainDeployerEdgeCases.t.sol), [`test/JBOmnichainDeployer.t.sol`](../test/JBOmnichainDeployer.t.sol), and [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for edge behavior.
28
+ - [`test/OmnichainDeployerEdgeCases.t.sol`](../test/OmnichainDeployerEdgeCases.t.sol), [`test/JBOmnichainDeployer.t.sol`](../test/JBOmnichainDeployer.t.sol), and [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) for edge behavior.
@@ -55,24 +55,26 @@ contract JBOmnichainDeployer is
55
55
  //*********************************************************************//
56
56
 
57
57
  /// @notice Thrown when the provided controller does not match the project's controller in the directory.
58
- error JBOmnichainDeployer_ControllerMismatch();
58
+ error JBOmnichainDeployer_ControllerMismatch(
59
+ uint256 projectId, address expectedController, address actualController
60
+ );
59
61
 
60
- /// @notice Thrown when a data hook is set to this contract.
61
- error JBOmnichainDeployer_InvalidHook();
62
+ /// @notice Thrown when a data hook is invalid for the project ruleset being configured.
63
+ error JBOmnichainDeployer_InvalidHook(address hook, uint256 projectId, uint256 rulesetId);
62
64
 
63
65
  /// @notice Thrown when an empty `rulesetConfigurations` array is passed to a simplified overload that needs at
64
66
  /// least one ruleset to derive a default 721 config.
65
- error JBOmnichainDeployer_NoRulesetConfigurations();
66
-
67
- /// @notice Thrown when the project ID returned by the controller does not match the expected project ID.
68
- error JBOmnichainDeployer_ProjectIdMismatch();
67
+ error JBOmnichainDeployer_NoRulesetConfigurations(uint256 rulesetConfigurationCount);
69
68
 
70
69
  /// @notice Thrown when queueing rulesets for a project whose latest ruleset was already queued in the same block.
71
70
  /// @dev Ruleset IDs are predicted as `block.timestamp + i`. This prediction fails if
72
71
  /// `latestRulesetIdOf >= block.timestamp`, which can only happen if rulesets were already queued in the same block.
73
- error JBOmnichainDeployer_RulesetIdsUnpredictable();
72
+ error JBOmnichainDeployer_RulesetIdsUnpredictable(
73
+ uint256 projectId, uint256 latestRulesetId, uint256 currentTimestamp
74
+ );
74
75
 
75
- error JBOmnichainDeployer_UnexpectedNFTReceived();
76
+ /// @notice Thrown when this contract receives a project NFT from an unexpected sender or transfer.
77
+ error JBOmnichainDeployer_UnexpectedNFTReceived(address caller, address from, uint256 tokenId);
76
78
 
77
79
  //*********************************************************************//
78
80
  // --------------- public immutable stored properties ---------------- //
@@ -172,7 +174,6 @@ contract JBOmnichainDeployer is
172
174
  // Deploy the suckers.
173
175
  // Note: the salt includes `_msgSender()` for replay protection. Cross-chain deterministic
174
176
  // address matching requires using the same sender address on each chain.
175
- // slither-disable-next-line unused-return
176
177
  suckers = SUCKER_REGISTRY.deploySuckersFor({
177
178
  projectId: projectId,
178
179
  salt: keccak256(abi.encode(suckerDeploymentConfiguration.salt, _msgSender())),
@@ -325,10 +326,12 @@ contract JBOmnichainDeployer is
325
326
  });
326
327
  }
327
328
 
328
- /// @dev Make sure this contract can only receive project NFTs from `JBProjects`.
329
- function onERC721Received(address, address, uint256, bytes calldata) external view returns (bytes4) {
330
- // Make sure the 721 received is from the `JBProjects` contract.
331
- if (msg.sender != address(PROJECTS)) revert JBOmnichainDeployer_UnexpectedNFTReceived();
329
+ /// @dev Make sure this contract can only receive project NFTs minted from `JBProjects` (not transferred).
330
+ function onERC721Received(address, address from, uint256 tokenId, bytes calldata) external view returns (bytes4) {
331
+ // Only accept mints (from == address(0)) from the `JBProjects` contract, not arbitrary transfers.
332
+ if (msg.sender != address(PROJECTS) || from != address(0)) {
333
+ revert JBOmnichainDeployer_UnexpectedNFTReceived({caller: msg.sender, from: from, tokenId: tokenId});
334
+ }
332
335
 
333
336
  return IERC721Receiver.onERC721Received.selector;
334
337
  }
@@ -432,18 +435,19 @@ contract JBOmnichainDeployer is
432
435
  cashOutTaxRate = context.cashOutTaxRate;
433
436
  cashOutCount = context.cashOutCount;
434
437
 
435
- // Compute the cross-chain total supply: local supply + sum of known peer chain supplies.
436
- // This prevents the cash out tax from vanishing when a holder dominates the local supply.
437
- totalSupply = context.totalSupply + SUCKER_REGISTRY.remoteTotalSupplyOf(context.projectId);
438
+ // Start with local values.
439
+ totalSupply = context.totalSupply;
440
+ effectiveSurplusValue = context.surplus.value;
438
441
 
439
- // Compute the cross-chain surplus: local surplus + sum of known peer chain surpluses.
440
- // This prevents disproportionate reclaim when tokens bridge away but surplus stays.
441
- effectiveSurplusValue = context.surplus.value
442
- + SUCKER_REGISTRY.remoteSurplusOf({
442
+ // If the ruleset aggregates cross-chain state, add remote supply and surplus.
443
+ if (!context.scopeCashOutsToLocalBalances) {
444
+ totalSupply += SUCKER_REGISTRY.remoteTotalSupplyOf(context.projectId);
445
+ effectiveSurplusValue += SUCKER_REGISTRY.remoteSurplusOf({
443
446
  projectId: context.projectId,
444
447
  decimals: context.surplus.decimals,
445
448
  currency: uint256(context.surplus.currency)
446
449
  });
450
+ }
447
451
 
448
452
  // Will hold the 721 hook's cash out specifications (always 0 or 1 element).
449
453
  JBCashOutHookSpecification[] memory tiered721HookSpecifications;
@@ -456,7 +460,6 @@ contract JBOmnichainDeployer is
456
460
  // Forward to the 721 hook. It may change the tax rate, count, and return hook specs.
457
461
  // Capture the 721 hook's totalSupply and effectiveSurplusValue — NFT cash-outs should use
458
462
  // local-only denominators so holders reclaim against local surplus, not omnichain surplus.
459
- // slither-disable-next-line unused-return
460
463
  (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, tiered721HookSpecifications) =
461
464
  IJBRulesetDataHook(address(tiered721Config.hook)).beforeCashOutRecordedWith(context);
462
465
  }
@@ -483,10 +486,8 @@ contract JBOmnichainDeployer is
483
486
  // (sum of tier prices), not fungible token counts. Letting the extra hook override
484
487
  // cashOutCount would corrupt NFT pricing in the bonding curve.
485
488
  if (address(tiered721Config.hook) != address(0) && tiered721Config.useDataHookForCashOut) {
486
- // slither-disable-next-line unused-return
487
489
  (cashOutTaxRate,,,, extraHookSpecifications) = extraHook.dataHook.beforeCashOutRecordedWith(hookContext);
488
490
  } else {
489
- // slither-disable-next-line unused-return
490
491
  (cashOutTaxRate, cashOutCount,,, extraHookSpecifications) =
491
492
  extraHook.dataHook.beforeCashOutRecordedWith(hookContext);
492
493
  }
@@ -552,7 +553,6 @@ contract JBOmnichainDeployer is
552
553
  has721Hook = true;
553
554
  // Call the 721 hook directly — useDataHookForPay is always true for 721 hooks.
554
555
  JBPayHookSpecification[] memory tiered721HookSpecs;
555
- // slither-disable-next-line unused-return
556
556
  (tiered721Weight, tiered721HookSpecs) =
557
557
  IJBRulesetDataHook(address(tiered721Config.hook)).beforePayRecordedWith(context);
558
558
  // The 721 hook returns a single spec (itself) whose amount is the total split amount.
@@ -750,7 +750,6 @@ contract JBOmnichainDeployer is
750
750
 
751
751
  // Deploy a 721 hook and set up rulesets.
752
752
  hook = _deploy721Hook({projectId: projectId, config: deploy721Config});
753
- // slither-disable-next-line reentrancy-benign
754
753
  rulesetConfigurations = _setup721({
755
754
  projectId: projectId,
756
755
  rulesetConfigurations: rulesetConfigurations,
@@ -759,7 +758,6 @@ contract JBOmnichainDeployer is
759
758
  });
760
759
 
761
760
  // Launch the rulesets for the reserved project.
762
- // slither-disable-start unused-return
763
761
  controller.launchRulesetsFor({
764
762
  projectId: projectId,
765
763
  projectUri: projectUri,
@@ -767,14 +765,12 @@ contract JBOmnichainDeployer is
767
765
  terminalConfigurations: terminalConfigurations,
768
766
  memo: memo
769
767
  });
770
- // slither-disable-end unused-return
771
768
 
772
769
  // Transfer the hook's ownership to the project (now that the project NFT has been minted).
773
770
  JBOwnable(address(hook)).transferOwnershipToProject(projectId);
774
771
 
775
772
  // Deploy the suckers (if applicable).
776
773
  if (suckerDeploymentConfiguration.salt != bytes32(0)) {
777
- // slither-disable-next-line unused-return
778
774
  suckers = SUCKER_REGISTRY.deploySuckersFor({
779
775
  projectId: projectId,
780
776
  salt: keccak256(abi.encode(suckerDeploymentConfiguration.salt, _msgSender())),
@@ -820,7 +816,6 @@ contract JBOmnichainDeployer is
820
816
  // Deploy a 721 hook, transfer its ownership to the project, and set up rulesets.
821
817
  hook = _deploy721Hook({projectId: projectId, config: deploy721Config});
822
818
  JBOwnable(address(hook)).transferOwnershipToProject(projectId);
823
- // slither-disable-next-line reentrancy-benign
824
819
  rulesetConfigurations = _setup721({
825
820
  projectId: projectId,
826
821
  rulesetConfigurations: rulesetConfigurations,
@@ -861,8 +856,11 @@ contract JBOmnichainDeployer is
861
856
  // `block.timestamp + i` ruleset ID prediction incorrect.
862
857
  uint256 latestRulesetId = controller.RULESETS().latestRulesetIdOf(projectId);
863
858
  // forge-lint: disable-next-line(block-timestamp)
864
- if (latestRulesetId >= block.timestamp) {
865
- revert JBOmnichainDeployer_RulesetIdsUnpredictable();
859
+ uint256 currentTimestamp = block.timestamp;
860
+ if (latestRulesetId >= currentTimestamp) {
861
+ revert JBOmnichainDeployer_RulesetIdsUnpredictable({
862
+ projectId: projectId, latestRulesetId: latestRulesetId, currentTimestamp: currentTimestamp
863
+ });
866
864
  }
867
865
 
868
866
  // Deploy a new 721 hook if tiers are provided, otherwise carry forward the existing hook.
@@ -899,12 +897,15 @@ contract JBOmnichainDeployer is
899
897
  hook = previousConfig.hook;
900
898
  // Revert if no hook exists to carry forward — this means no tiers were provided and
901
899
  // no previous ruleset had a 721 hook deployed through this contract.
902
- if (address(hook) == address(0)) revert JBOmnichainDeployer_InvalidHook();
900
+ if (address(hook) == address(0)) {
901
+ revert JBOmnichainDeployer_InvalidHook({
902
+ hook: address(hook), projectId: projectId, rulesetId: sourceRulesetId
903
+ });
904
+ }
903
905
  // Preserve the previous ruleset's cash-out flag when carrying forward.
904
906
  use721ForCashOut = previousConfig.useDataHookForCashOut;
905
907
  }
906
908
 
907
- // slither-disable-next-line reentrancy-benign
908
909
  rulesetConfigurations = _setup721({
909
910
  projectId: projectId,
910
911
  rulesetConfigurations: rulesetConfigurations,
@@ -938,19 +939,23 @@ contract JBOmnichainDeployer is
938
939
  returns (JBRulesetConfig[] memory)
939
940
  {
940
941
  for (uint256 i; i < rulesetConfigurations.length; i++) {
942
+ // forge-lint: disable-next-line(block-timestamp)
943
+ uint256 rulesetId = block.timestamp + i;
944
+
941
945
  // Validate no self-reference.
942
- if (rulesetConfigurations[i].metadata.dataHook == address(this)) revert JBOmnichainDeployer_InvalidHook();
946
+ if (rulesetConfigurations[i].metadata.dataHook == address(this)) {
947
+ revert JBOmnichainDeployer_InvalidHook({
948
+ hook: rulesetConfigurations[i].metadata.dataHook, projectId: projectId, rulesetId: rulesetId
949
+ });
950
+ }
943
951
 
944
952
  // Store the 721 hook config per-ruleset.
945
- // slither-disable-next-line reentrancy-benign
946
- // forge-lint: disable-next-line(block-timestamp)
947
- _tiered721HookOf[projectId][block.timestamp + i] =
953
+ _tiered721HookOf[projectId][rulesetId] =
948
954
  JBTiered721HookConfig({hook: hook721, useDataHookForCashOut: use721ForCashOut});
949
955
 
950
956
  // Store custom hook from metadata (same as _setup).
951
957
  if (rulesetConfigurations[i].metadata.dataHook != address(0)) {
952
- // forge-lint: disable-next-line(block-timestamp)
953
- _extraDataHookOf[projectId][block.timestamp + i] = JBDeployerHookConfig({
958
+ _extraDataHookOf[projectId][rulesetId] = JBDeployerHookConfig({
954
959
  dataHook: IJBRulesetDataHook(rulesetConfigurations[i].metadata.dataHook),
955
960
  useDataHookForPay: rulesetConfigurations[i].metadata.useDataHookForPay,
956
961
  useDataHookForCashOut: rulesetConfigurations[i].metadata.useDataHookForCashOut
@@ -984,7 +989,11 @@ contract JBOmnichainDeployer is
984
989
  pure
985
990
  returns (JBOmnichain721Config memory config)
986
991
  {
987
- if (rulesetConfigurations.length == 0) revert JBOmnichainDeployer_NoRulesetConfigurations();
992
+ if (rulesetConfigurations.length == 0) {
993
+ revert JBOmnichainDeployer_NoRulesetConfigurations({
994
+ rulesetConfigurationCount: rulesetConfigurations.length
995
+ });
996
+ }
988
997
  config.deployTiersHookConfig.tiersConfig.currency = rulesetConfigurations[0].metadata.baseCurrency;
989
998
  config.deployTiersHookConfig.tiersConfig.decimals = 18;
990
999
  }
@@ -1010,7 +1019,9 @@ contract JBOmnichainDeployer is
1010
1019
  address current = address(DIRECTORY.controllerOf(projectId));
1011
1020
  // Allow address(0) for fresh projects that haven't launched rulesets yet.
1012
1021
  if (current != address(0) && current != address(controller)) {
1013
- revert JBOmnichainDeployer_ControllerMismatch();
1022
+ revert JBOmnichainDeployer_ControllerMismatch({
1023
+ projectId: projectId, expectedController: current, actualController: address(controller)
1024
+ });
1014
1025
  }
1015
1026
  }
1016
1027
  }