@bananapus/core-v6 0.0.23 → 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.
Files changed (177) hide show
  1. package/ADMINISTRATION.md +16 -3
  2. package/ARCHITECTURE.md +28 -9
  3. package/AUDIT_INSTRUCTIONS.md +102 -17
  4. package/CHANGE_LOG.md +18 -8
  5. package/README.md +58 -2
  6. package/RISKS.md +13 -20
  7. package/SKILLS.md +158 -11
  8. package/STYLE_GUIDE.md +11 -6
  9. package/USER_JOURNEYS.md +53 -16
  10. package/foundry.toml +1 -1
  11. package/package.json +2 -2
  12. package/script/Deploy.s.sol +2 -2
  13. package/script/DeployPeriphery.s.sol +2 -5
  14. package/script/helpers/CoreDeploymentLib.sol +2 -2
  15. package/src/JBChainlinkV3PriceFeed.sol +1 -1
  16. package/src/JBChainlinkV3SequencerPriceFeed.sol +1 -1
  17. package/src/JBController.sol +14 -7
  18. package/src/JBDeadline.sol +1 -1
  19. package/src/JBDirectory.sol +1 -1
  20. package/src/JBERC20.sol +6 -2
  21. package/src/JBFeelessAddresses.sol +1 -1
  22. package/src/JBFundAccessLimits.sol +1 -1
  23. package/src/JBMultiTerminal.sol +53 -4
  24. package/src/JBPermissions.sol +6 -2
  25. package/src/JBPrices.sol +1 -1
  26. package/src/JBProjects.sol +1 -1
  27. package/src/JBRulesets.sol +1 -1
  28. package/src/JBSplits.sol +1 -1
  29. package/src/JBTerminalStore.sol +64 -65
  30. package/src/JBTokens.sol +5 -1
  31. package/src/interfaces/IJBController.sol +7 -1
  32. package/src/libraries/JBPayoutSplitGroupLib.sol +1 -1
  33. package/src/periphery/JBDeadline1Day.sol +1 -1
  34. package/src/periphery/JBDeadline3Days.sol +1 -1
  35. package/src/periphery/JBDeadline3Hours.sol +1 -1
  36. package/src/periphery/JBDeadline7Days.sol +1 -1
  37. package/src/periphery/JBMatchingPriceFeed.sol +1 -1
  38. package/test/TestAccessToFunds.sol +4 -4
  39. package/test/TestFeeFreeCashOutBypass.sol +332 -0
  40. package/test/TestJBERC20Inheritance.sol +1 -1
  41. package/test/TestMetadataOffsetOverflow.sol +1 -1
  42. package/test/TestMetadataParserLib.sol +1 -1
  43. package/test/TestMultiTerminalSurplus.sol +1 -1
  44. package/test/TestMultiTokenSurplus.sol +1 -1
  45. package/test/TestMultipleAccessLimits.sol +4 -4
  46. package/test/TestPermit2DataHook.t.sol +1 -1
  47. package/test/TestPermit2Terminal.sol +1 -1
  48. package/test/TestTerminalPreviewParity.sol +1 -1
  49. package/test/audit/CashOutReenterPay.t.sol +496 -0
  50. package/test/audit/FeeFreeSurplusLifecycle.t.sol +392 -0
  51. package/test/audit/FeeFreeSurplusStale.t.sol +242 -0
  52. package/test/audit/USDTVoidReturnCompat.t.sol +519 -0
  53. package/test/fork/TestChainlinkPriceFeedFork.sol +1 -1
  54. package/test/fork/TestSequencerPriceFeedFork.sol +1 -1
  55. package/test/fork/TestTerminalPreviewParityFork.sol +1 -1
  56. package/test/helpers/JBTest.sol +1 -1
  57. package/test/helpers/MetadataResolverHelper.sol +1 -1
  58. package/test/mock/MockERC20.sol +1 -1
  59. package/test/mock/MockMaliciousBeneficiary.sol +1 -1
  60. package/test/mock/MockMaliciousSplitHook.sol +1 -1
  61. package/test/mock/MockPriceFeed.sol +1 -1
  62. package/test/mock/MockUSDT.sol +80 -0
  63. package/test/regression/HoldFeesCashOutReserved.t.sol +2 -2
  64. package/test/units/static/JBChainlinkV3PriceFeed/TestPriceFeed.sol +1 -1
  65. package/test/units/static/JBController/JBControllerSetup.sol +1 -1
  66. package/test/units/static/JBController/TestBurnTokensOf.sol +1 -1
  67. package/test/units/static/JBController/TestClaimTokensFor.sol +1 -1
  68. package/test/units/static/JBController/TestDeployErc20For.sol +1 -1
  69. package/test/units/static/JBController/TestLaunchProjectFor.sol +1 -1
  70. package/test/units/static/JBController/TestLaunchRulesetsFor.sol +1 -1
  71. package/test/units/static/JBController/TestMigrateController.sol +1 -1
  72. package/test/units/static/JBController/TestMintTokensOfUnits.sol +1 -1
  73. package/test/units/static/JBController/TestOmnichainRulesetOperator.sol +324 -0
  74. package/test/units/static/JBController/TestPayReservedTokenToTerminal.sol +1 -1
  75. package/test/units/static/JBController/TestPreviewMintOf.sol +1 -1
  76. package/test/units/static/JBController/TestReceiveMigrationFrom.sol +1 -1
  77. package/test/units/static/JBController/TestRulesetViews.sol +1 -1
  78. package/test/units/static/JBController/TestSendReservedTokensToSplitsOf.sol +1 -1
  79. package/test/units/static/JBController/TestSetSplitGroupsOf.sol +1 -1
  80. package/test/units/static/JBController/TestSetTokenFor.sol +1 -1
  81. package/test/units/static/JBController/TestSetUriOf.sol +1 -1
  82. package/test/units/static/JBController/TestTransferCreditsFrom.sol +1 -1
  83. package/test/units/static/JBDeadline/TestDeadlineFuzz.sol +1 -1
  84. package/test/units/static/JBDirectory/JBDirectorySetup.sol +1 -1
  85. package/test/units/static/JBDirectory/TestPrimaryTerminalOf.sol +1 -1
  86. package/test/units/static/JBDirectory/TestSetControllerOf.sol +1 -1
  87. package/test/units/static/JBDirectory/TestSetControllerOfMigrationOrder.sol +1 -1
  88. package/test/units/static/JBDirectory/TestSetPrimaryTerminalOf.sol +1 -1
  89. package/test/units/static/JBDirectory/TestSetTerminalsOf.sol +1 -1
  90. package/test/units/static/JBERC20/JBERC20Setup.sol +11 -4
  91. package/test/units/static/JBERC20/SigUtils.sol +1 -1
  92. package/test/units/static/JBERC20/TestInitialize.sol +8 -1
  93. package/test/units/static/JBERC20/TestName.sol +1 -1
  94. package/test/units/static/JBERC20/TestNonces.sol +1 -1
  95. package/test/units/static/JBERC20/TestSymbol.sol +1 -1
  96. package/test/units/static/JBFeelessAdresses/JBFeelessSetup.sol +1 -1
  97. package/test/units/static/JBFeelessAdresses/TestInterfaces.sol +1 -1
  98. package/test/units/static/JBFeelessAdresses/TestSetFeelessAddress.sol +1 -1
  99. package/test/units/static/JBFees/TestFeesFuzz.sol +1 -1
  100. package/test/units/static/JBFixedPointNumber/TestAdjustDecimals.sol +1 -1
  101. package/test/units/static/JBFixedPointNumber/TestAdjustDecimalsFuzz.sol +1 -1
  102. package/test/units/static/JBFundAccessLimits/JBFundAccessSetup.sol +1 -1
  103. package/test/units/static/JBFundAccessLimits/TestFundAccessLimitsEdge.sol +1 -1
  104. package/test/units/static/JBFundAccessLimits/TestPayoutLimitOf.sol +1 -1
  105. package/test/units/static/JBFundAccessLimits/TestPayoutLimitsOf.sol +1 -1
  106. package/test/units/static/JBFundAccessLimits/TestSetFundAccessLimitsFor.sol +1 -1
  107. package/test/units/static/JBFundAccessLimits/TestSurplusAllowanceOf.sol +1 -1
  108. package/test/units/static/JBFundAccessLimits/TestSurplusAllowancesOf.sol +1 -1
  109. package/test/units/static/JBMetadataResolver/TestGetDataFor.sol +1 -1
  110. package/test/units/static/JBMetadataResolver/TestMetadataResolverEdgeCases.sol +1 -1
  111. package/test/units/static/JBMetadataResolver/TestMetadataResolverFuzz.sol +1 -1
  112. package/test/units/static/JBMultiTerminal/JBMultiTerminalSetup.sol +1 -1
  113. package/test/units/static/JBMultiTerminal/TestAccountingContextsOf.sol +1 -1
  114. package/test/units/static/JBMultiTerminal/TestAddAccountingContextsFor.sol +1 -1
  115. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +1 -1
  116. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +1 -1
  117. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +1 -1
  118. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +1 -1
  119. package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +1 -1
  120. package/test/units/static/JBMultiTerminal/TestPay.sol +1 -1
  121. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +1 -1
  122. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +1 -1
  123. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +1 -1
  124. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +1 -1
  125. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +1 -1
  126. package/test/units/static/JBPermissions/JBPermissionsSetup.sol +1 -1
  127. package/test/units/static/JBPermissions/TestHasPermission.sol +1 -1
  128. package/test/units/static/JBPermissions/TestHasPermissions.sol +1 -1
  129. package/test/units/static/JBPermissions/TestSetPermissionsFor.sol +1 -1
  130. package/test/units/static/JBPrices/JBPricesSetup.sol +1 -1
  131. package/test/units/static/JBPrices/TestAddPriceFeedFor.sol +1 -1
  132. package/test/units/static/JBPrices/TestPricePerUnitOf.sol +1 -1
  133. package/test/units/static/JBPrices/TestPrices.sol +1 -1
  134. package/test/units/static/JBProjects/JBProjectsSetup.sol +1 -1
  135. package/test/units/static/JBProjects/TestCreateFor.sol +1 -1
  136. package/test/units/static/JBProjects/TestInitialProject.sol +1 -1
  137. package/test/units/static/JBProjects/TestInterfaces.sol +1 -1
  138. package/test/units/static/JBProjects/TestSetResolver.sol +1 -1
  139. package/test/units/static/JBProjects/TestTokenUri.sol +1 -1
  140. package/test/units/static/JBRulesetMetadataResolver/TestSetCashOutTaxRateTo.sol +1 -1
  141. package/test/units/static/JBRulesets/JBRulesetsSetup.sol +1 -1
  142. package/test/units/static/JBRulesets/TestCurrentApprovalStatusForLatestRulesetOf.sol +1 -1
  143. package/test/units/static/JBRulesets/TestCurrentOf.sol +1 -1
  144. package/test/units/static/JBRulesets/TestGetRulesetOf.sol +1 -1
  145. package/test/units/static/JBRulesets/TestLatestQueuedRulesetOf.sol +1 -1
  146. package/test/units/static/JBRulesets/TestRulesets.sol +1 -1
  147. package/test/units/static/JBRulesets/TestRulesetsOf.sol +1 -1
  148. package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +1 -1
  149. package/test/units/static/JBRulesets/TestUpdateRulesetWeightCache.sol +1 -1
  150. package/test/units/static/JBSplits/JBSplitsSetup.sol +1 -1
  151. package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +1 -1
  152. package/test/units/static/JBSplits/TestSetSplitGroupsOf.sol +1 -1
  153. package/test/units/static/JBSplits/TestSplitsLockedEdge.sol +1 -1
  154. package/test/units/static/JBSplits/TestSplitsOf.sol +1 -1
  155. package/test/units/static/JBSplits/TestSplitsPacking.sol +1 -1
  156. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +1 -1
  157. package/test/units/static/JBTerminalStore/JBTerminalStoreSetup.sol +1 -1
  158. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +1 -1
  159. package/test/units/static/JBTerminalStore/TestCurrentSurplusOf.sol +1 -1
  160. package/test/units/static/JBTerminalStore/TestCurrentTotalSurplusOf.sol +1 -1
  161. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +1 -1
  162. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +1 -1
  163. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +1 -1
  164. package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +1 -1
  165. package/test/units/static/JBTerminalStore/TestRecordPayoutFor.sol +1 -1
  166. package/test/units/static/JBTerminalStore/TestRecordTerminalMigration.sol +1 -1
  167. package/test/units/static/JBTerminalStore/TestRecordUsedAllowanceOf.sol +1 -1
  168. package/test/units/static/JBTerminalStore/TestUint224Overflow.sol +1 -1
  169. package/test/units/static/JBTokens/JBTokensSetup.sol +1 -1
  170. package/test/units/static/JBTokens/TestBurnFrom.sol +1 -1
  171. package/test/units/static/JBTokens/TestClaimTokensFor.sol +1 -1
  172. package/test/units/static/JBTokens/TestDeployERC20ForUnits.sol +1 -1
  173. package/test/units/static/JBTokens/TestMintFor.sol +1 -1
  174. package/test/units/static/JBTokens/TestSetTokenFor.sol +1 -1
  175. package/test/units/static/JBTokens/TestTotalBalanceOf.sol +1 -1
  176. package/test/units/static/JBTokens/TestTotalSupplyOf.sol +1 -1
  177. package/test/units/static/JBTokens/TestTransferCreditsFrom.sol +1 -1
@@ -387,8 +387,8 @@ contract HoldFeesCashOutReserved_Local is TestBaseWorkflow {
387
387
  surplus: surplus, cashOutCount: payerTokens, totalSupply: circulatingSupply, cashOutTaxRate: 5000
388
388
  });
389
389
 
390
- // With pending reserves, the cashout value is lower (known behavior, H-4).
391
- assertLt(reclaimWithReserves, reclaimWithoutReserves, "Pending reserves reduce cashout value (expected, H-4)");
390
+ // With pending reserves, the cashout value is lower (known behavior).
391
+ assertLt(reclaimWithReserves, reclaimWithoutReserves, "Pending reserves reduce cashout value (expected)");
392
392
 
393
393
  // Actually perform the cashout and confirm it matches the with-reserves calculation (net of fee).
394
394
  // cashOutTokensOf returns the NET reclaim after the 2.5% fee is deducted.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBTest} from "../../../helpers/JBTest.sol";
5
5
  import {JBChainlinkV3PriceFeed} from "../../../../src/JBChainlinkV3PriceFeed.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {IJBController} from "../../../../src/interfaces/IJBController.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
5
5
  import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
5
5
  import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {IJBController} from "../../../../src/interfaces/IJBController.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {StdStorage, stdStorage} from "forge-std/StdStorage.sol";
5
5
  import {JBController} from "../../../../src/JBController.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
@@ -0,0 +1,324 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {JBController} from "../../../../src/JBController.sol";
5
+ import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
6
+ import {IJBController} from "../../../../src/interfaces/IJBController.sol";
7
+ import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
8
+ import {IJBFundAccessLimits} from "../../../../src/interfaces/IJBFundAccessLimits.sol";
9
+ import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
10
+ import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
11
+ import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
12
+ import {IJBPrices} from "../../../../src/interfaces/IJBPrices.sol";
13
+ import {IJBProjects} from "../../../../src/interfaces/IJBProjects.sol";
14
+ import {IJBSplits} from "../../../../src/interfaces/IJBSplits.sol";
15
+ import {IJBTokens} from "../../../../src/interfaces/IJBTokens.sol";
16
+ import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
17
+ import {JBRulesetMetadataResolver} from "../../../../src/libraries/JBRulesetMetadataResolver.sol";
18
+ import {JBCurrencyAmount} from "../../../../src/structs/JBCurrencyAmount.sol";
19
+ import {JBFundAccessLimitGroup} from "../../../../src/structs/JBFundAccessLimitGroup.sol";
20
+ import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
21
+ import {JBRulesetConfig} from "../../../../src/structs/JBRulesetConfig.sol";
22
+ import {JBRulesetMetadata} from "../../../../src/structs/JBRulesetMetadata.sol";
23
+ import {JBSplitGroup} from "../../../../src/structs/JBSplitGroup.sol";
24
+ import {JBTerminalConfig} from "../../../../src/structs/JBTerminalConfig.sol";
25
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
26
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
27
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
28
+ import {JBTest} from "../../../helpers/JBTest.sol";
29
+
30
+ /// @notice Tests the OMNICHAIN_RULESET_OPERATOR trust boundary — verifying the operator can bypass
31
+ /// LAUNCH_RULESETS, SET_TERMINALS, and QUEUE_RULESETS permission checks via alsoGrantAccessIf.
32
+ contract TestOmnichainRulesetOperator_Local is JBTest {
33
+ IJBController public _controller;
34
+
35
+ // Mocks
36
+ IJBPermissions public permissions = IJBPermissions(makeAddr("permissions"));
37
+ IJBDirectory public directory = IJBDirectory(makeAddr("directory"));
38
+ IJBRulesets public rulesets = IJBRulesets(makeAddr("rulesets"));
39
+ IJBSplits public splits = IJBSplits(makeAddr("splits"));
40
+ IJBFundAccessLimits public fundAccessLimits = IJBFundAccessLimits(makeAddr("limits"));
41
+
42
+ // Use makeAddr for projects so we can mock ERC721 calls
43
+ address public projectsAddr = makeAddr("projects");
44
+
45
+ address public operator = makeAddr("omnichainOperator");
46
+ address public projectOwner = makeAddr("projectOwner");
47
+ address public randomCaller = makeAddr("randomCaller");
48
+ uint256 public projectId = 1;
49
+
50
+ function setUp() public {
51
+ // Deploy controller with a non-zero OMNICHAIN_RULESET_OPERATOR
52
+ _controller = new JBController(
53
+ directory,
54
+ fundAccessLimits,
55
+ permissions,
56
+ IJBPrices(makeAddr("prices")),
57
+ IJBProjects(projectsAddr),
58
+ rulesets,
59
+ splits,
60
+ IJBTokens(makeAddr("tokens")),
61
+ operator,
62
+ makeAddr("forwarder")
63
+ );
64
+ }
65
+
66
+ // ── Helpers
67
+ // ──────────────────────────────────────────────────────────
68
+
69
+ function _genRulesetConfig() internal pure returns (JBTerminalConfig[] memory, JBRulesetConfig[] memory) {
70
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](0);
71
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
72
+
73
+ JBRulesetMetadata memory metadata = JBRulesetMetadata({
74
+ reservedPercent: 0,
75
+ cashOutTaxRate: 0,
76
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
77
+ pausePay: false,
78
+ pauseCreditTransfers: false,
79
+ allowOwnerMinting: false,
80
+ allowSetCustomToken: false,
81
+ allowTerminalMigration: false,
82
+ allowSetTerminals: false,
83
+ ownerMustSendPayouts: false,
84
+ allowSetController: false,
85
+ allowAddAccountingContext: true,
86
+ allowAddPriceFeed: false,
87
+ holdFees: false,
88
+ useTotalSurplusForCashOuts: false,
89
+ useDataHookForPay: false,
90
+ useDataHookForCashOut: false,
91
+ dataHook: address(0),
92
+ metadata: 0
93
+ });
94
+
95
+ JBFundAccessLimitGroup[] memory limitGroups = new JBFundAccessLimitGroup[](1);
96
+ JBCurrencyAmount[] memory payoutLimits = new JBCurrencyAmount[](1);
97
+ payoutLimits[0] = JBCurrencyAmount({amount: 0, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
98
+ JBCurrencyAmount[] memory surplusAllowances = new JBCurrencyAmount[](1);
99
+ surplusAllowances[0] = JBCurrencyAmount({amount: 0, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
100
+ limitGroups[0] = JBFundAccessLimitGroup({
101
+ terminal: address(0),
102
+ token: JBConstants.NATIVE_TOKEN,
103
+ payoutLimits: payoutLimits,
104
+ surplusAllowances: surplusAllowances
105
+ });
106
+
107
+ rulesetConfigs[0].mustStartAtOrAfter = 0;
108
+ rulesetConfigs[0].duration = 0;
109
+ rulesetConfigs[0].weight = 0;
110
+ rulesetConfigs[0].weightCutPercent = 0;
111
+ rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
112
+ rulesetConfigs[0].metadata = metadata;
113
+ rulesetConfigs[0].splitGroups = new JBSplitGroup[](0);
114
+ rulesetConfigs[0].fundAccessLimitGroups = limitGroups;
115
+
116
+ return (terminalConfigs, rulesetConfigs);
117
+ }
118
+
119
+ /// @dev Mock the full call chain for a successful launchRulesetsFor.
120
+ function _mockLaunchSuccess(JBRulesetConfig[] memory rulesetConfigs) internal {
121
+ uint48 ts = uint48(block.timestamp);
122
+
123
+ // ownerOf
124
+ vm.mockCall(projectsAddr, abi.encodeCall(IERC721.ownerOf, (projectId)), abi.encode(projectOwner));
125
+
126
+ // No existing rulesets
127
+ vm.mockCall(address(rulesets), abi.encodeCall(IJBRulesets.latestRulesetIdOf, (projectId)), abi.encode(0));
128
+
129
+ // setControllerOf
130
+ vm.mockCall(
131
+ address(directory),
132
+ abi.encodeCall(IJBDirectory.setControllerOf, (projectId, IERC165(address(_controller)))),
133
+ ""
134
+ );
135
+
136
+ // queueFor
137
+ JBRuleset memory ruleset = JBRuleset({
138
+ cycleNumber: 1,
139
+ id: ts,
140
+ basedOnId: 0,
141
+ start: ts,
142
+ duration: 0,
143
+ weight: 0,
144
+ weightCutPercent: 0,
145
+ approvalHook: IJBRulesetApprovalHook(address(0)),
146
+ metadata: 0
147
+ });
148
+ vm.mockCall(
149
+ address(rulesets),
150
+ abi.encodeCall(
151
+ IJBRulesets.queueFor,
152
+ (
153
+ projectId,
154
+ 0,
155
+ 0,
156
+ 0,
157
+ rulesetConfigs[0].approvalHook,
158
+ JBRulesetMetadataResolver.packRulesetMetadata(rulesetConfigs[0].metadata),
159
+ 0
160
+ )
161
+ ),
162
+ abi.encode(ruleset)
163
+ );
164
+
165
+ // setSplitGroupsOf
166
+ vm.mockCall(
167
+ address(splits),
168
+ abi.encodeCall(IJBSplits.setSplitGroupsOf, (projectId, ts, rulesetConfigs[0].splitGroups)),
169
+ ""
170
+ );
171
+
172
+ // setFundAccessLimitsFor
173
+ vm.mockCall(
174
+ address(fundAccessLimits),
175
+ abi.encodeCall(
176
+ IJBFundAccessLimits.setFundAccessLimitsFor, (projectId, ts, rulesetConfigs[0].fundAccessLimitGroups)
177
+ ),
178
+ ""
179
+ );
180
+ }
181
+
182
+ /// @dev Mock the full call chain for a successful queueRulesetsOf.
183
+ function _mockQueueSuccess(JBRulesetConfig[] memory rulesetConfigs) internal {
184
+ uint48 ts = uint48(block.timestamp);
185
+
186
+ // ownerOf
187
+ vm.mockCall(projectsAddr, abi.encodeCall(IERC721.ownerOf, (projectId)), abi.encode(projectOwner));
188
+
189
+ // queueFor
190
+ JBRuleset memory ruleset = JBRuleset({
191
+ cycleNumber: 2,
192
+ id: ts,
193
+ basedOnId: 1,
194
+ start: ts,
195
+ duration: 0,
196
+ weight: 0,
197
+ weightCutPercent: 0,
198
+ approvalHook: IJBRulesetApprovalHook(address(0)),
199
+ metadata: 0
200
+ });
201
+ vm.mockCall(
202
+ address(rulesets),
203
+ abi.encodeCall(
204
+ IJBRulesets.queueFor,
205
+ (
206
+ projectId,
207
+ 0,
208
+ 0,
209
+ 0,
210
+ rulesetConfigs[0].approvalHook,
211
+ JBRulesetMetadataResolver.packRulesetMetadata(rulesetConfigs[0].metadata),
212
+ 0
213
+ )
214
+ ),
215
+ abi.encode(ruleset)
216
+ );
217
+
218
+ // setSplitGroupsOf
219
+ vm.mockCall(
220
+ address(splits),
221
+ abi.encodeCall(IJBSplits.setSplitGroupsOf, (projectId, ts, rulesetConfigs[0].splitGroups)),
222
+ ""
223
+ );
224
+
225
+ // setFundAccessLimitsFor
226
+ vm.mockCall(
227
+ address(fundAccessLimits),
228
+ abi.encodeCall(
229
+ IJBFundAccessLimits.setFundAccessLimitsFor, (projectId, ts, rulesetConfigs[0].fundAccessLimitGroups)
230
+ ),
231
+ ""
232
+ );
233
+ }
234
+
235
+ // ── Tests
236
+ // ────────────────────────────────────────────────────────────
237
+
238
+ function test_operatorCanLaunchRulesetsWithoutPermission() external {
239
+ (JBTerminalConfig[] memory terminalConfigs, JBRulesetConfig[] memory rulesetConfigs) = _genRulesetConfig();
240
+ _mockLaunchSuccess(rulesetConfigs);
241
+
242
+ // Operator calls launchRulesetsFor without owning the project or having any permission grants.
243
+ // The alsoGrantAccessIf bypass should allow this.
244
+ vm.prank(operator);
245
+ _controller.launchRulesetsFor(projectId, rulesetConfigs, terminalConfigs, "");
246
+ }
247
+
248
+ function test_operatorCanQueueRulesetsWithoutPermission() external {
249
+ (, JBRulesetConfig[] memory rulesetConfigs) = _genRulesetConfig();
250
+ _mockQueueSuccess(rulesetConfigs);
251
+
252
+ // Operator calls queueRulesetsOf without owning the project or having any permission grants.
253
+ vm.prank(operator);
254
+ _controller.queueRulesetsOf(projectId, rulesetConfigs, "");
255
+ }
256
+
257
+ function test_nonOperatorCannotBypassPermissions() external {
258
+ (JBTerminalConfig[] memory terminalConfigs, JBRulesetConfig[] memory rulesetConfigs) = _genRulesetConfig();
259
+
260
+ // Mock ownerOf — randomCaller is not the owner
261
+ vm.mockCall(projectsAddr, abi.encodeCall(IERC721.ownerOf, (projectId)), abi.encode(projectOwner));
262
+
263
+ // Mock permission check — randomCaller has no permissions
264
+ vm.mockCall(
265
+ address(permissions),
266
+ abi.encodeCall(
267
+ IJBPermissions.hasPermission,
268
+ (randomCaller, projectOwner, projectId, JBPermissionIds.LAUNCH_RULESETS, true, true)
269
+ ),
270
+ abi.encode(false)
271
+ );
272
+
273
+ // Should revert for launchRulesetsFor
274
+ vm.prank(randomCaller);
275
+ vm.expectRevert(
276
+ abi.encodeWithSelector(
277
+ JBPermissioned.JBPermissioned_Unauthorized.selector,
278
+ projectOwner,
279
+ randomCaller,
280
+ projectId,
281
+ JBPermissionIds.LAUNCH_RULESETS
282
+ )
283
+ );
284
+ _controller.launchRulesetsFor(projectId, rulesetConfigs, terminalConfigs, "");
285
+
286
+ // Mock permission check for QUEUE_RULESETS — randomCaller has no permissions
287
+ vm.mockCall(
288
+ address(permissions),
289
+ abi.encodeCall(
290
+ IJBPermissions.hasPermission,
291
+ (randomCaller, projectOwner, projectId, JBPermissionIds.QUEUE_RULESETS, true, true)
292
+ ),
293
+ abi.encode(false)
294
+ );
295
+
296
+ // Should revert for queueRulesetsOf
297
+ vm.prank(randomCaller);
298
+ vm.expectRevert(
299
+ abi.encodeWithSelector(
300
+ JBPermissioned.JBPermissioned_Unauthorized.selector,
301
+ projectOwner,
302
+ randomCaller,
303
+ projectId,
304
+ JBPermissionIds.QUEUE_RULESETS
305
+ )
306
+ );
307
+ _controller.queueRulesetsOf(projectId, rulesetConfigs, "");
308
+ }
309
+
310
+ function test_operatorCannotLaunchIfRulesetsExist() external {
311
+ (JBTerminalConfig[] memory terminalConfigs, JBRulesetConfig[] memory rulesetConfigs) = _genRulesetConfig();
312
+
313
+ // Mock ownerOf
314
+ vm.mockCall(projectsAddr, abi.encodeCall(IERC721.ownerOf, (projectId)), abi.encode(projectOwner));
315
+
316
+ // Project already has rulesets
317
+ vm.mockCall(address(rulesets), abi.encodeCall(IJBRulesets.latestRulesetIdOf, (projectId)), abi.encode(1));
318
+
319
+ // Even the operator cannot launch if rulesets already exist
320
+ vm.prank(operator);
321
+ vm.expectRevert(abi.encodeWithSelector(JBController.JBController_RulesetsAlreadyLaunched.selector, projectId));
322
+ _controller.launchRulesetsFor(projectId, rulesetConfigs, terminalConfigs, "");
323
+ }
324
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {IJBTerminal} from "../../../../src/interfaces/IJBTerminal.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {IJBController} from "../../../../src/interfaces/IJBController.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBApprovalStatus} from "../../../../src/enums/JBApprovalStatus.sol";
5
5
  import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {StdStorage, stdStorage} from "forge-std/StdStorage.sol";
5
5
  import {JBController} from "../../../../src/JBController.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
5
5
  import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
5
5
  import {IJBController} from "../../../../src/interfaces/IJBController.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBController} from "../../../../src/JBController.sol";
5
5
  import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
5
5
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBDirectory} from "../../../../src/JBDirectory.sol";
5
5
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBDirectoryAccessControl} from "../../../../src/interfaces/IJBDirectoryAccessControl.sol";
5
5
  import {IJBTerminal} from "../../../../src/interfaces/IJBTerminal.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {StdStorage, stdStorage} from "forge-std/StdStorage.sol";
5
5
  import {JBDirectory} from "../../../../src/JBDirectory.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {StdStorage, stdStorage} from "forge-std/StdStorage.sol";
5
5
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {StdStorage, stdStorage} from "forge-std/StdStorage.sol";
5
5
  import {JBDirectory} from "../../../../src/JBDirectory.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {StdStorage, stdStorage} from "forge-std/StdStorage.sol";
5
5
  import {JBDirectory} from "../../../../src/JBDirectory.sol";
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
+ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
4
5
  import {JBERC20} from "../../../../src/JBERC20.sol";
5
6
  import {IJBToken} from "../../../../src/interfaces/IJBToken.sol";
6
7
  import {JBTest} from "../../../helpers/JBTest.sol";
@@ -12,11 +13,17 @@ Tests relative to this contract will be dependent on mock calls/emits and stdSto
12
13
  contract JBERC20Setup is JBTest {
13
14
  address _owner = makeAddr("owner");
14
15
 
15
- // Target Contract
16
+ // Implementation (constructor sets _name = "invalid", cannot be initialized)
17
+ IJBToken public _implementation;
18
+
19
+ // Target Contract (clone with empty _name, can be initialized)
16
20
  IJBToken public _erc20;
17
21
 
18
22
  function erc20Setup() public virtual {
19
- // Instantiate the contract being tested
20
- _erc20 = new JBERC20();
23
+ // Deploy the implementation
24
+ _implementation = new JBERC20();
25
+
26
+ // Clone it — clones start with empty storage, so initialize() works
27
+ _erc20 = IJBToken(Clones.clone(address(_implementation)));
21
28
  }
22
29
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  contract SigUtils {
5
5
  // forge-lint: disable-next-line(mixed-case-variable)
@@ -1,7 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
5
+ import {JBERC20} from "../../../../src/JBERC20.sol";
5
6
  import {JBERC20Setup} from "./JBERC20Setup.sol";
6
7
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7
8
 
@@ -13,6 +14,12 @@ contract TestInitialize_Local is JBERC20Setup {
13
14
  super.erc20Setup();
14
15
  }
15
16
 
17
+ function test_ImplementationCannotBeInitialized() external {
18
+ // The implementation has _name = "invalid" set in constructor, so initialize() must revert.
19
+ vm.expectRevert(JBERC20.JBERC20_AlreadyInitialized.selector);
20
+ _implementation.initialize(_name, _symbol, _owner);
21
+ }
22
+
16
23
  function test_WhenANameIsAlreadySet() external {
17
24
  // it will revert
18
25
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
5
5
  import {JBERC20Setup} from "./JBERC20Setup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
5
5
  import {JBERC20Setup} from "./JBERC20Setup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
5
5
  import {JBERC20Setup} from "./JBERC20Setup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBFeelessAddresses} from "../../../../src/JBFeelessAddresses.sol";
5
5
  import {IJBFeelessAddresses} from "../../../../src/interfaces/IJBFeelessAddresses.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBFeelessAddresses} from "../../../../src/interfaces/IJBFeelessAddresses.sol";
5
5
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";