@bananapus/core-v6 0.0.36 → 0.0.37

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 CHANGED
@@ -44,7 +44,7 @@ This file covers the main accounting, permission, and liveness risks in the core
44
44
 
45
45
  ### Weight Decay
46
46
 
47
- - **Stale weight cache can block a project.** Short-duration rulesets with nonzero `weightCutPercent` can hit `WeightCacheRequired` after enough cycles.
47
+ - **Stale weight cache can block a project.** Short-duration rulesets with nonzero `weightCutPercent` can hit `WeightCacheRequired` after 20,000 elapsed cycles (`_WEIGHT_CUT_MULTIPLE_CACHE_LOOKUP_THRESHOLD`). Projects approaching this limit must call `updateRulesetWeightCache()` to pre-cache decayed weights.
48
48
  - **Weight-cache correctness matters more than overflow.** Overflow is already bounded at queue time. The real risk is stale or wrongly-updated cache state.
49
49
 
50
50
  ### Surplus Manipulation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.36",
3
+ "version": "0.0.37",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
27
27
  },
28
28
  "dependencies": {
29
- "@bananapus/permission-ids-v6": "^0.0.17",
29
+ "@bananapus/permission-ids-v6": "^0.0.19",
30
30
  "@chainlink/contracts": "^1.5.0",
31
31
  "@openzeppelin/contracts": "^5.6.1",
32
32
  "@prb/math": "^4.1.1",
@@ -525,6 +525,10 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
525
525
  // Cache common values used in both permission checks.
526
526
  address sender = _msgSender();
527
527
  bool senderIsTerminal = _isTerminalOf(projectId, sender);
528
+ bool senderIsTerminalOrDataHook = senderIsTerminal || sender == ruleset.dataHook();
529
+ // Only query the data hook if the sender isn't already a terminal or the data hook itself.
530
+ bool senderHasDataHookMintPermission =
531
+ !senderIsTerminalOrDataHook && _hasDataHookMintPermissionFor(projectId, ruleset, sender);
528
532
 
529
533
  // Minting is restricted to: the project's owner, addresses with permission to `MINT_TOKENS`, the project's
530
534
  // terminals, and the project's data hook.
@@ -532,15 +536,14 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
532
536
  account: PROJECTS.ownerOf(projectId),
533
537
  projectId: projectId,
534
538
  permissionId: JBPermissionIds.MINT_TOKENS,
535
- alsoGrantAccessIf: senderIsTerminal || sender == ruleset.dataHook()
536
- || _hasDataHookMintPermissionFor(projectId, ruleset, sender)
539
+ alsoGrantAccessIf: senderIsTerminalOrDataHook || senderHasDataHookMintPermission
537
540
  });
538
541
 
539
542
  // If the message sender is not the project's terminal or data hook, the ruleset must have `allowOwnerMinting`
540
543
  // set to `true`.
541
544
  if (
542
- ruleset.id != 0 && !ruleset.allowOwnerMinting() && !senderIsTerminal && sender != ruleset.dataHook()
543
- && !_hasDataHookMintPermissionFor(projectId, ruleset, sender)
545
+ ruleset.id != 0 && !ruleset.allowOwnerMinting() && !senderIsTerminalOrDataHook
546
+ && !senderHasDataHookMintPermission
544
547
  ) {
545
548
  revert JBController_MintNotAllowedAndNotTerminalOrHook(sender);
546
549
  }
@@ -1150,20 +1150,24 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1150
1150
  // Cache whether the beneficiary is feeless.
1151
1151
  bool beneficiaryIsFeeless = _isFeeless(beneficiary);
1152
1152
 
1153
- // Record the cash out.
1154
- (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.recordCashOutFor({
1155
- holder: holder,
1156
- projectId: projectId,
1157
- cashOutCount: cashOutCount,
1158
- tokenToReclaim: tokenToReclaim,
1159
- beneficiaryIsFeeless: beneficiaryIsFeeless,
1160
- metadata: metadata
1161
- });
1153
+ {
1154
+ // Cache the controller to avoid a redundant external call (also used inside STORE.recordCashOutFor).
1155
+ IJBController controller = _controllerOf(projectId);
1156
+
1157
+ // Record the cash out.
1158
+ (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.recordCashOutFor({
1159
+ holder: holder,
1160
+ projectId: projectId,
1161
+ cashOutCount: cashOutCount,
1162
+ tokenToReclaim: tokenToReclaim,
1163
+ beneficiaryIsFeeless: beneficiaryIsFeeless,
1164
+ metadata: metadata
1165
+ });
1162
1166
 
1163
- // Burn the project tokens.
1164
- if (cashOutCount != 0) {
1165
- _controllerOf(projectId)
1166
- .burnTokensOf({holder: holder, projectId: projectId, tokenCount: cashOutCount, memo: ""});
1167
+ // Burn the project tokens.
1168
+ if (cashOutCount != 0) {
1169
+ controller.burnTokensOf({holder: holder, projectId: projectId, tokenCount: cashOutCount, memo: ""});
1170
+ }
1167
1171
  }
1168
1172
 
1169
1173
  // Keep a reference to the amount being reclaimed that is subject to fees.
@@ -0,0 +1,163 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.6;
3
+
4
+ import {TestBaseWorkflow} from "../helpers/TestBaseWorkflow.sol";
5
+ import {JBMultiTerminal} from "../../src/JBMultiTerminal.sol";
6
+ import {JBDirectory} from "../../src/JBDirectory.sol";
7
+ import {JBTerminalStore} from "../../src/JBTerminalStore.sol";
8
+ import {IJBController} from "../../src/interfaces/IJBController.sol";
9
+ import {IJBTerminal} from "../../src/interfaces/IJBTerminal.sol";
10
+ import {IJBRulesetApprovalHook} from "../../src/interfaces/IJBRulesetApprovalHook.sol";
11
+ import {JBConstants} from "../../src/libraries/JBConstants.sol";
12
+ import {JBFees} from "../../src/libraries/JBFees.sol";
13
+ import {JBAccountingContext} from "../../src/structs/JBAccountingContext.sol";
14
+ import {JBCurrencyAmount} from "../../src/structs/JBCurrencyAmount.sol";
15
+ import {JBFundAccessLimitGroup} from "../../src/structs/JBFundAccessLimitGroup.sol";
16
+ import {JBRulesetConfig} from "../../src/structs/JBRulesetConfig.sol";
17
+ import {JBRulesetMetadata} from "../../src/structs/JBRulesetMetadata.sol";
18
+ import {JBSplitGroup} from "../../src/structs/JBSplitGroup.sol";
19
+ import {JBTerminalConfig} from "../../src/structs/JBTerminalConfig.sol";
20
+
21
+ contract CodexMigrationFeeFailure is TestBaseWorkflow {
22
+ IJBController private _controller;
23
+ JBDirectory private _directory;
24
+ JBTerminalStore private _store;
25
+ JBMultiTerminal private _terminalA;
26
+ JBMultiTerminal private _terminalB;
27
+
28
+ address private _feeProjectOwner = address(420);
29
+ address private _projectOwner;
30
+ address private _payer;
31
+ uint256 private _projectId;
32
+
33
+ function setUp() public override {
34
+ super.setUp();
35
+
36
+ _controller = jbController();
37
+ _directory = jbDirectory();
38
+ _store = jbTerminalStore();
39
+ _terminalA = jbMultiTerminal();
40
+ _terminalB = jbMultiTerminal2();
41
+ _projectOwner = multisig();
42
+ _payer = beneficiary();
43
+
44
+ JBRulesetMetadata memory metadata = JBRulesetMetadata({
45
+ reservedPercent: 0,
46
+ cashOutTaxRate: 0,
47
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
48
+ pausePay: false,
49
+ pauseCreditTransfers: false,
50
+ allowOwnerMinting: false,
51
+ allowSetCustomToken: false,
52
+ allowTerminalMigration: true,
53
+ allowSetTerminals: true,
54
+ allowSetController: false,
55
+ allowAddAccountingContext: false,
56
+ allowAddPriceFeed: false,
57
+ ownerMustSendPayouts: false,
58
+ holdFees: false,
59
+ useTotalSurplusForCashOuts: false,
60
+ useDataHookForPay: false,
61
+ useDataHookForCashOut: false,
62
+ dataHook: address(0),
63
+ metadata: 0
64
+ });
65
+
66
+ JBRulesetConfig[] memory rulesetConfig = new JBRulesetConfig[](1);
67
+ rulesetConfig[0].mustStartAtOrAfter = 0;
68
+ rulesetConfig[0].duration = 0;
69
+ rulesetConfig[0].weight = 1000 * 10 ** 18;
70
+ rulesetConfig[0].weightCutPercent = 0;
71
+ rulesetConfig[0].approvalHook = IJBRulesetApprovalHook(address(0));
72
+ rulesetConfig[0].metadata = metadata;
73
+ rulesetConfig[0].splitGroups = new JBSplitGroup[](0);
74
+ rulesetConfig[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
75
+
76
+ JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
77
+ contexts[0] = JBAccountingContext({
78
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
79
+ });
80
+
81
+ JBTerminalConfig[] memory feeTerminalConfigs = new JBTerminalConfig[](1);
82
+ feeTerminalConfigs[0] = JBTerminalConfig({terminal: _terminalA, accountingContextsToAccept: contexts});
83
+
84
+ JBTerminalConfig[] memory projectTerminalConfigs = new JBTerminalConfig[](2);
85
+ projectTerminalConfigs[0] = JBTerminalConfig({terminal: _terminalA, accountingContextsToAccept: contexts});
86
+ projectTerminalConfigs[1] = JBTerminalConfig({terminal: _terminalB, accountingContextsToAccept: contexts});
87
+
88
+ _controller.launchProjectFor({
89
+ owner: _feeProjectOwner,
90
+ projectUri: "fee-project",
91
+ rulesetConfigurations: rulesetConfig,
92
+ terminalConfigurations: feeTerminalConfigs,
93
+ memo: ""
94
+ });
95
+
96
+ _projectId = _controller.launchProjectFor({
97
+ owner: _projectOwner,
98
+ projectUri: "migrating-project",
99
+ rulesetConfigurations: rulesetConfig,
100
+ terminalConfigurations: projectTerminalConfigs,
101
+ memo: ""
102
+ });
103
+ }
104
+
105
+ function test_migrationFeeFailure_strandsForgivenFeeAndChargesItAgainOnCleanup() external {
106
+ uint256 payAmount = 10 ether;
107
+ uint256 expectedFee = JBFees.feeAmountFrom({amountBeforeFee: payAmount, feePercent: _terminalA.FEE()});
108
+
109
+ vm.deal(_payer, payAmount);
110
+ vm.prank(_payer);
111
+ _terminalA.pay{value: payAmount}(_projectId, JBConstants.NATIVE_TOKEN, payAmount, _payer, 0, "", "");
112
+
113
+ // Break fee routing by removing every terminal from fee project #1.
114
+ vm.prank(_feeProjectOwner);
115
+ _directory.setTerminalsOf(1, new IJBTerminal[](0));
116
+
117
+ vm.prank(_projectOwner);
118
+ _terminalA.migrateBalanceOf(_projectId, JBConstants.NATIVE_TOKEN, _terminalB);
119
+
120
+ // The failed fee is credited back on terminal A instead of migrating with the project.
121
+ assertEq(
122
+ _store.balanceOf(address(_terminalA), _projectId, JBConstants.NATIVE_TOKEN),
123
+ expectedFee,
124
+ "failed migration fee remains on the source terminal"
125
+ );
126
+ assertEq(
127
+ _store.balanceOf(address(_terminalB), _projectId, JBConstants.NATIVE_TOKEN),
128
+ payAmount - expectedFee,
129
+ "only the post-fee amount reaches the destination terminal"
130
+ );
131
+ assertEq(
132
+ _store.balanceOf(address(_terminalA), 1, JBConstants.NATIVE_TOKEN),
133
+ 0,
134
+ "fee project did not receive the forgiven migration fee"
135
+ );
136
+
137
+ // Restore fee routing and sweep the residual balance.
138
+ IJBTerminal[] memory feeTerminals = new IJBTerminal[](1);
139
+ feeTerminals[0] = IJBTerminal(address(_terminalA));
140
+ vm.prank(_feeProjectOwner);
141
+ _directory.setTerminalsOf(1, feeTerminals);
142
+
143
+ vm.prank(_projectOwner);
144
+ _terminalA.migrateBalanceOf(_projectId, JBConstants.NATIVE_TOKEN, _terminalB);
145
+
146
+ uint256 secondFee = JBFees.feeAmountFrom({amountBeforeFee: expectedFee, feePercent: _terminalA.FEE()});
147
+ assertEq(
148
+ _store.balanceOf(address(_terminalA), _projectId, JBConstants.NATIVE_TOKEN),
149
+ 0,
150
+ "cleanup migration clears the stranded source balance"
151
+ );
152
+ assertEq(
153
+ _store.balanceOf(address(_terminalB), _projectId, JBConstants.NATIVE_TOKEN),
154
+ payAmount - secondFee,
155
+ "the previously forgiven fee is charged again during cleanup"
156
+ );
157
+ assertEq(
158
+ _store.balanceOf(address(_terminalA), 1, JBConstants.NATIVE_TOKEN),
159
+ secondFee,
160
+ "fee project only receives the second migration fee"
161
+ );
162
+ }
163
+ }