@bananapus/721-hook-v6 0.0.37 → 0.0.38

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/ADMINISTRATION.md CHANGED
@@ -36,6 +36,18 @@
36
36
  - `setMetadata(...)`
37
37
  - deployer setup and hook ownership transfer paths
38
38
 
39
+ ## Deployer Permission Model
40
+
41
+ `JB721TiersHookProjectDeployer` requires callers to hold the correct Juicebox permission for each operation:
42
+
43
+ | Function | Required Permissions |
44
+ | --- | --- |
45
+ | `launchProjectFor(...)` | None (creates a new project) |
46
+ | `launchRulesetsFor(...)` | `LAUNCH_RULESETS` + `SET_TERMINALS` |
47
+ | `queueRulesetsOf(...)` | `QUEUE_RULESETS` |
48
+
49
+ Permissions are checked against the project owner via `_requirePermissionFrom`. The deployer calls the controller on the caller's behalf, so the controller sees the deployer as `msg.sender`.
50
+
39
51
  ## Immutable And One-Way
40
52
 
41
53
  - many tier properties are immutable once created
package/CHANGELOG.md CHANGED
@@ -20,6 +20,10 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
20
20
  - The repo now carries a dedicated helper library to keep the hook surface manageable and to support the larger v6 feature set.
21
21
  - The repo was upgraded from the v5 Solidity baseline to `0.8.28`.
22
22
 
23
+ ## Local audit remediations
24
+
25
+ - `JB721TiersHookProjectDeployer.launchRulesetsFor` now checks `LAUNCH_RULESETS` instead of `QUEUE_RULESETS`. The previous check was semantically wrong — launching active rulesets should require the launch permission, not the queue permission.
26
+
23
27
  ## Verified deltas
24
28
 
25
29
  - `IJB721TiersHook.pricingContext()` changed from a three-value return to `(currency, decimals)`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,7 +24,7 @@ import {JBQueueRulesetsConfig} from "./structs/JBQueueRulesetsConfig.sol";
24
24
 
25
25
  /// @title JB721TiersHookProjectDeployer
26
26
  /// @notice Deploys a project and a 721 tiers hook for it. Can be used to queue rulesets for the project if given
27
- /// `JBPermissionIds.QUEUE_RULESETS`.
27
+ /// `JBPermissionIds.QUEUE_RULESETS` or `JBPermissionIds.LAUNCH_RULESETS`.
28
28
  contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721TiersHookProjectDeployer {
29
29
  //*********************************************************************//
30
30
  // --------------- public immutable stored properties ---------------- //
@@ -102,7 +102,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
102
102
  }
103
103
 
104
104
  /// @notice Launches rulesets for a project with an attached 721 tiers hook.
105
- /// @dev Only a project's owner or an operator with the `QUEUE_RULESETS & SET_TERMINALS` permission can launch its
105
+ /// @dev Only a project's owner or an operator with the `LAUNCH_RULESETS & SET_TERMINALS` permission can launch its
106
106
  /// rulesets.
107
107
  /// @param projectId The ID of the project that rulesets are being launched for.
108
108
  /// @param deployTiersHookConfig Configuration which dictates the behavior of the 721 tiers hook which is being
@@ -128,7 +128,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
128
128
 
129
129
  // Enforce permissions.
130
130
  _requirePermissionFrom({
131
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.QUEUE_RULESETS
131
+ account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.LAUNCH_RULESETS
132
132
  });
133
133
 
134
134
  _requirePermissionFrom({
@@ -0,0 +1,273 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import "../utils/UnitTestSetup.sol";
5
+
6
+ import {JB721TiersHookProjectDeployer} from "../../src/JB721TiersHookProjectDeployer.sol";
7
+ import {JBDeploy721TiersHookConfig} from "../../src/structs/JBDeploy721TiersHookConfig.sol";
8
+ import {JBLaunchRulesetsConfig} from "../../src/structs/JBLaunchRulesetsConfig.sol";
9
+ import {JBQueueRulesetsConfig} from "../../src/structs/JBQueueRulesetsConfig.sol";
10
+ import {JBPayDataHookRulesetConfig} from "../../src/structs/JBPayDataHookRulesetConfig.sol";
11
+ import {JBPayDataHookRulesetMetadata} from "../../src/structs/JBPayDataHookRulesetMetadata.sol";
12
+ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
13
+ import {JB721InitTiersConfig} from "../../src/structs/JB721InitTiersConfig.sol";
14
+ import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
15
+ import {IJB721TiersHook} from "../../src/interfaces/IJB721TiersHook.sol";
16
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
17
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
18
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
19
+ import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
20
+ import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
21
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
22
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
23
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
24
+ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
25
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
26
+
27
+ contract MockProjectsForNemesis {
28
+ uint256 internal _count;
29
+ address internal _owner;
30
+
31
+ function setup(uint256 count_, address owner_) external {
32
+ _count = count_;
33
+ _owner = owner_;
34
+ }
35
+
36
+ function count() external view returns (uint256) {
37
+ return _count;
38
+ }
39
+
40
+ function ownerOf(uint256) external view returns (address) {
41
+ return _owner;
42
+ }
43
+
44
+ fallback() external {}
45
+ }
46
+
47
+ contract StrictControllerForNemesis {
48
+ address internal immutable EXPECTED_CALLER;
49
+
50
+ error UnexpectedCaller(address caller);
51
+
52
+ constructor(address expectedCaller) {
53
+ EXPECTED_CALLER = expectedCaller;
54
+ }
55
+
56
+ fallback() external payable {
57
+ if (msg.sender != EXPECTED_CALLER) revert UnexpectedCaller(msg.sender);
58
+ bytes memory result = abi.encode(uint256(42));
59
+ assembly {
60
+ return(add(result, 32), mload(result))
61
+ }
62
+ }
63
+ }
64
+
65
+ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
66
+ JB721TiersHookProjectDeployer internal projectDeployer;
67
+ address internal operator = address(0xBEEF);
68
+ uint256 internal testProjectId = 5;
69
+
70
+ function setUp() public override {
71
+ super.setUp();
72
+
73
+ MockProjectsForNemesis projects = new MockProjectsForNemesis();
74
+ vm.etch(mockJBProjects, address(projects).code);
75
+ MockProjectsForNemesis(mockJBProjects).setup(testProjectId, owner);
76
+
77
+ vm.mockCall(mockJBDirectory, abi.encodeWithSelector(IJBDirectory.PROJECTS.selector), abi.encode(mockJBProjects));
78
+
79
+ projectDeployer = new JB721TiersHookProjectDeployer(
80
+ IJBDirectory(mockJBDirectory), IJBPermissions(mockJBPermissions), jbHookDeployer, address(0)
81
+ );
82
+ }
83
+
84
+ function test_launchRulesetsFor_revertsBecauseControllerSeesProjectDeployerAsCaller() external {
85
+ (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
86
+ _launchConfig(testProjectId);
87
+
88
+ StrictControllerForNemesis controller = new StrictControllerForNemesis(owner);
89
+
90
+ vm.prank(owner);
91
+ vm.expectRevert(
92
+ abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
93
+ );
94
+ projectDeployer.launchRulesetsFor(
95
+ testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
96
+ );
97
+ }
98
+
99
+ function test_queueRulesetsOf_revertsBecauseControllerSeesProjectDeployerAsCaller() external {
100
+ (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig) =
101
+ _queueConfig(testProjectId);
102
+
103
+ StrictControllerForNemesis controller = new StrictControllerForNemesis(owner);
104
+
105
+ vm.prank(owner);
106
+ vm.expectRevert(
107
+ abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
108
+ );
109
+ projectDeployer.queueRulesetsOf(
110
+ testProjectId, hookConfig, queueConfig, IJBController(address(controller)), bytes32(0)
111
+ );
112
+ }
113
+
114
+ function test_launchRulesetsFor_checksLaunchPermission() external {
115
+ (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
116
+ _launchConfig(testProjectId);
117
+
118
+ address account = owner;
119
+ StrictControllerForNemesis controller = new StrictControllerForNemesis(owner);
120
+
121
+ // Grant LAUNCH_RULESETS and SET_TERMINALS, deny QUEUE_RULESETS.
122
+ vm.mockCall(
123
+ mockJBPermissions,
124
+ abi.encodeWithSelector(
125
+ IJBPermissions.hasPermission.selector,
126
+ operator,
127
+ account,
128
+ testProjectId,
129
+ JBPermissionIds.QUEUE_RULESETS,
130
+ true,
131
+ true
132
+ ),
133
+ abi.encode(false)
134
+ );
135
+ vm.mockCall(
136
+ mockJBPermissions,
137
+ abi.encodeWithSelector(
138
+ IJBPermissions.hasPermission.selector,
139
+ operator,
140
+ account,
141
+ testProjectId,
142
+ JBPermissionIds.LAUNCH_RULESETS,
143
+ true,
144
+ true
145
+ ),
146
+ abi.encode(true)
147
+ );
148
+ vm.mockCall(
149
+ mockJBPermissions,
150
+ abi.encodeWithSelector(
151
+ IJBPermissions.hasPermission.selector,
152
+ operator,
153
+ account,
154
+ testProjectId,
155
+ JBPermissionIds.SET_TERMINALS,
156
+ true,
157
+ true
158
+ ),
159
+ abi.encode(true)
160
+ );
161
+
162
+ // The permission check passes with LAUNCH_RULESETS. The call proceeds to the controller, which reverts
163
+ // because it sees the deployer contract as the caller rather than the original operator.
164
+ vm.prank(operator);
165
+ vm.expectRevert(
166
+ abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
167
+ );
168
+ projectDeployer.launchRulesetsFor(
169
+ testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
170
+ );
171
+ }
172
+
173
+ function _launchConfig(uint256 projectId_)
174
+ internal
175
+ view
176
+ returns (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig)
177
+ {
178
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
179
+ tierConfigs[0] = JB721TierConfig({
180
+ price: uint104(10),
181
+ initialSupply: uint32(100),
182
+ votingUnits: uint16(0),
183
+ reserveFrequency: uint16(0),
184
+ reserveBeneficiary: reserveBeneficiary,
185
+ encodedIPFSUri: tokenUris[0],
186
+ category: uint24(1),
187
+ discountPercent: uint8(0),
188
+ flags: JB721TierConfigFlags({
189
+ allowOwnerMint: false,
190
+ useReserveBeneficiaryAsDefault: false,
191
+ transfersPausable: false,
192
+ useVotingUnits: false,
193
+ cantBeRemoved: false,
194
+ cantIncreaseDiscountPercent: false,
195
+ cantBuyWithCredits: false
196
+ }),
197
+ splitPercent: 0,
198
+ splits: new JBSplit[](0)
199
+ });
200
+
201
+ hookConfig = JBDeploy721TiersHookConfig({
202
+ name: name,
203
+ symbol: symbol,
204
+ baseUri: baseUri,
205
+ tokenUriResolver: IJB721TokenUriResolver(address(0)),
206
+ contractUri: contractUri,
207
+ tiersConfig: JB721InitTiersConfig({
208
+ tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
209
+ }),
210
+ flags: JB721TiersHookFlags({
211
+ preventOverspending: false,
212
+ issueTokensForSplits: false,
213
+ noNewTiersWithReserves: true,
214
+ noNewTiersWithVotes: false,
215
+ noNewTiersWithOwnerMinting: true
216
+ })
217
+ });
218
+
219
+ JBPayDataHookRulesetConfig[] memory rulesetConfigs = new JBPayDataHookRulesetConfig[](1);
220
+ rulesetConfigs[0].mustStartAtOrAfter = 0;
221
+ rulesetConfigs[0].duration = 14;
222
+ rulesetConfigs[0].weight = 1e18;
223
+ rulesetConfigs[0].weightCutPercent = 0;
224
+ rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
225
+ rulesetConfigs[0].metadata = JBPayDataHookRulesetMetadata({
226
+ reservedPercent: 5000,
227
+ cashOutTaxRate: 5000,
228
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
229
+ pausePay: false,
230
+ pauseCreditTransfers: false,
231
+ allowOwnerMinting: false,
232
+ allowSetCustomToken: false,
233
+ allowTerminalMigration: false,
234
+ allowSetTerminals: false,
235
+ allowSetController: false,
236
+ ownerMustSendPayouts: false,
237
+ allowAddAccountingContext: false,
238
+ allowAddPriceFeed: false,
239
+ holdFees: false,
240
+ useTotalSurplusForCashOuts: false,
241
+ useDataHookForCashOut: false,
242
+ metadata: 0x00
243
+ });
244
+
245
+ JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
246
+ accountingContexts[0] = JBAccountingContext({
247
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18, token: JBConstants.NATIVE_TOKEN
248
+ });
249
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
250
+ terminalConfigs[0] = JBTerminalConfig({
251
+ terminal: IJBTerminal(mockTerminalAddress), accountingContextsToAccept: accountingContexts
252
+ });
253
+
254
+ launchConfig = JBLaunchRulesetsConfig({
255
+ projectId: uint56(projectId_),
256
+ rulesetConfigurations: rulesetConfigs,
257
+ terminalConfigurations: terminalConfigs,
258
+ memo: "launch"
259
+ });
260
+ }
261
+
262
+ function _queueConfig(uint256 projectId_)
263
+ internal
264
+ view
265
+ returns (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig)
266
+ {
267
+ JBLaunchRulesetsConfig memory launchConfig;
268
+ (hookConfig, launchConfig) = _launchConfig(projectId_);
269
+ queueConfig = JBQueueRulesetsConfig({
270
+ projectId: uint56(projectId_), rulesetConfigurations: launchConfig.rulesetConfigurations, memo: "queue"
271
+ });
272
+ }
273
+ }