@bananapus/core-v6 0.0.7 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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.2",
29
+ "@bananapus/permission-ids-v6": "^0.0.4",
30
30
  "@chainlink/contracts": "^1.3.0",
31
31
  "@openzeppelin/contracts": "^5.2.0",
32
32
  "@prb/math": "^4.1.0",
@@ -44,6 +44,14 @@ contract DeployPeriphery is Script, Sphinx {
44
44
  /// @notice The nonce that gets used across all chains to sync deployment addresses and allow for new deployments of
45
45
  /// the same bytecode.
46
46
  uint256 private CORE_DEPLOYMENT_NONCE = 6;
47
+
48
+ /// @notice The address of the omnichain ruleset operator contract (e.g. JBOmnichainDeployer).
49
+ /// @dev TRUST ASSUMPTION: This address is granted implicit permission to launch rulesets, set terminals, and queue
50
+ /// rulesets on any project via the JBController (bypassing normal JBPermissions checks). A compromised or
51
+ /// incorrect operator address could manipulate any project's rulesets across chains.
52
+ /// @dev This address should correspond to the deterministic CREATE2 deployment of the omnichain deployer contract
53
+ /// from the nana-omnichain-deployers-v6 repository. Verify it matches the deployed address on all target chains
54
+ /// before running this script.
47
55
  address private OMNICHAIN_RULESET_OPERATOR = address(0x8f5DED85c40b50d223269C1F922A056E72101590);
48
56
 
49
57
  function configureSphinx() public override {
@@ -66,6 +74,9 @@ contract DeployPeriphery is Script, Sphinx {
66
74
  }
67
75
 
68
76
  function deploy() public sphinx {
77
+ // Validate the omnichain ruleset operator is set. See TRUST ASSUMPTION above.
78
+ require(OMNICHAIN_RULESET_OPERATOR != address(0), "Omnichain ruleset operator not set");
79
+
69
80
  // Deploy the ETH/USD price feed.
70
81
  IJBPriceFeed feed;
71
82
 
@@ -115,7 +115,7 @@ library JBMetadataResolver {
115
115
 
116
116
  // Add the new entry after the last entry of the table, the new offset is the last offset + the number of words
117
117
  // taken by the last data
118
- // M-21: Compute in uint256 first — casting directly to uint8 silently wraps offsets > 255.
118
+ // Compute in uint256 first — casting directly to uint8 silently wraps offsets > 255.
119
119
  uint256 newOffset = lastOffset + numberOfWordslastData;
120
120
  if (newOffset > 255) revert JBMetadataResolver_MetadataTooLong();
121
121
  newMetadata = abi.encodePacked(newMetadata, idToAdd, bytes1(uint8(newOffset)));
@@ -282,7 +282,7 @@ library JBMetadataResolver {
282
282
  assembly ("memory-safe") {
283
283
  let length := sub(end, start)
284
284
 
285
- // M-20: Allocate memory at freemem — round up to 32-byte boundary so subsequent
285
+ // Allocate memory at freemem — round up to 32-byte boundary so subsequent
286
286
  // allocations do not overlap the tail of this buffer.
287
287
  slicedBytes := mload(0x40)
288
288
  mstore(0x40, and(add(add(add(slicedBytes, length), 0x20), 0x1f), not(0x1f)))
@@ -296,7 +296,7 @@ library JBMetadataResolver {
296
296
  // same for the out array
297
297
  let sliceBytesStartOfData := add(slicedBytes, 0x20)
298
298
 
299
- // M-20: Copy data — bound is `length` not `end`. Using `end` (an absolute source offset)
299
+ // Copy data — bound is `length` not `end`. Using `end` (an absolute source offset)
300
300
  // would over-copy by `start` bytes past the allocated buffer.
301
301
  for { let i := 0 } lt(i, length) { i := add(i, 0x20) } {
302
302
  mstore(add(sliceBytesStartOfData, i), mload(add(startBytes, i)))
@@ -4,9 +4,9 @@ pragma solidity ^0.8.6;
4
4
  import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
5
5
  import {JBCashOuts} from "../src/libraries/JBCashOuts.sol";
6
6
 
7
- /// @notice Exploit PoC tests for critical vulnerability scenarios in Juicebox V5.
8
- /// @dev These tests target specific edge cases and potential vulnerabilities identified during audit.
9
- contract AuditExploits_Local is TestBaseWorkflow {
7
+ /// @notice Tests for edge case scenarios in Juicebox V5.
8
+ /// @dev These tests target specific edge cases and potential vulnerabilities.
9
+ contract EdgeCases_Local is TestBaseWorkflow {
10
10
  //*********************************************************************//
11
11
  // ----------------------------- storage ----------------------------- //
12
12
  //*********************************************************************//
@@ -292,7 +292,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
292
292
  }
293
293
 
294
294
  //*********************************************************************//
295
- // --- Test 2: REVDeployer beforePayRecordedWith OOB (C-2) ---------- //
295
+ // --- Test 2: REVDeployer beforePayRecordedWith OOB ---------------- //
296
296
  //*********************************************************************//
297
297
 
298
298
  /// @notice Demonstrates that REVDeployer.beforePayRecordedWith writes to index [1] of the
@@ -937,14 +937,14 @@ contract AuditExploits_Local is TestBaseWorkflow {
937
937
  }
938
938
 
939
939
  //*********************************************************************//
940
- // ====== CRITICAL EXPLOIT PoC TESTS ================================ //
940
+ // ====== EDGE CASE TESTS ============================================ //
941
941
  //*********************************************************************//
942
942
 
943
943
  //*********************************************************************//
944
- // --- [C-5] Zero-Supply Cash Out Drains Entire Surplus ------------- //
944
+ // --- Zero-Supply Cash Out Drains Entire Surplus ------------------- //
945
945
  //*********************************************************************//
946
946
 
947
- /// @notice PoC: When totalSupply == 0 and surplus > 0, calling cashOutTokensOf with
947
+ /// @notice When totalSupply == 0 and surplus > 0, calling cashOutTokensOf with
948
948
  /// cashOutCount = 0 returns the ENTIRE surplus without burning any tokens.
949
949
  /// @dev Attack flow:
950
950
  /// 1. Project is funded via addToBalanceOf (no tokens minted)
@@ -952,7 +952,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
952
952
  /// 3. JBCashOuts.cashOutFrom: 0 >= 0 → true → returns full surplus
953
953
  /// 4. JBMultiTerminal: cashOutCount != 0 is false → no burn
954
954
  /// 5. Attacker receives full surplus minus fee
955
- function test_C5_zeroSupplyCashOut_drainsEntireSurplus() public {
955
+ function test_zeroSupplyCashOut_drainsEntireSurplus() public {
956
956
  // --- Setup: create a project with 0% cash out tax ---
957
957
  uint256 projectId = _launchProject({cashOutTaxRate: 0, reservedPercent: 0});
958
958
 
@@ -982,7 +982,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
982
982
  // After fix: cashOutCount == 0 → early return 0
983
983
  uint256 reclaimFromLibrary =
984
984
  JBCashOuts.cashOutFrom({surplus: fundAmount, cashOutCount: 0, totalSupply: 0, cashOutTaxRate: 0});
985
- assertEq(reclaimFromLibrary, 0, "FIX CONFIRMED: cashOutFrom(surplus, 0, 0, rate) returns 0");
985
+ assertEq(reclaimFromLibrary, 0, "cashOutFrom(surplus, 0, 0, rate) returns 0");
986
986
 
987
987
  // --- Verify the exploit is now blocked ---
988
988
  address attacker = makeAddr("attacker");
@@ -1006,22 +1006,22 @@ contract AuditExploits_Local is TestBaseWorkflow {
1006
1006
  uint256 ethReceived = attackerEthAfter - attackerEthBefore;
1007
1007
 
1008
1008
  // --- Verify the exploit is blocked ---
1009
- assertEq(reclaimAmount, 0, "FIX: attacker receives nothing when cashing out 0 tokens");
1010
- assertEq(ethReceived, 0, "FIX: no ETH transferred to attacker");
1009
+ assertEq(reclaimAmount, 0, "attacker receives nothing when cashing out 0 tokens");
1010
+ assertEq(ethReceived, 0, "no ETH transferred to attacker");
1011
1011
 
1012
1012
  // Terminal balance should be unchanged — not drained.
1013
1013
  uint256 terminalBalanceAfter =
1014
1014
  jbTerminalStore().balanceOf(address(_terminal), projectId, JBConstants.NATIVE_TOKEN);
1015
- assertEq(terminalBalanceAfter, fundAmount, "FIX: terminal balance preserved");
1015
+ assertEq(terminalBalanceAfter, fundAmount, "terminal balance preserved");
1016
1016
 
1017
1017
  // Attacker never had any tokens.
1018
1018
  uint256 attackerTokens = _tokens.totalBalanceOf(attacker, projectId);
1019
1019
  assertEq(attackerTokens, 0, "attacker never held any tokens");
1020
1020
  }
1021
1021
 
1022
- /// @notice PoC variant: Same attack but with a cash out tax rate.
1022
+ /// @notice Same scenario but with a cash out tax rate.
1023
1023
  /// Even with a tax, the attacker drains surplus (fee goes to project #1).
1024
- function test_C5_zeroSupplyCashOut_withTaxRate() public {
1024
+ function test_zeroSupplyCashOut_withTaxRate() public {
1025
1025
  // 50% cash out tax rate
1026
1026
  uint256 projectId = _launchProject({cashOutTaxRate: 5000, reservedPercent: 0});
1027
1027
 
@@ -1043,7 +1043,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
1043
1043
  // After fix: cashOutCount == 0 → early return 0, regardless of tax rate.
1044
1044
  uint256 reclaimFromLibrary =
1045
1045
  JBCashOuts.cashOutFrom({surplus: fundAmount, cashOutCount: 0, totalSupply: 0, cashOutTaxRate: 5000});
1046
- assertEq(reclaimFromLibrary, 0, "FIX: 0 returned even with 50% tax when cashOutCount=0");
1046
+ assertEq(reclaimFromLibrary, 0, "0 returned even with 50% tax when cashOutCount=0");
1047
1047
 
1048
1048
  // Verify exploit is blocked.
1049
1049
  address attacker = makeAddr("attacker");
@@ -1063,12 +1063,12 @@ contract AuditExploits_Local is TestBaseWorkflow {
1063
1063
  uint256 ethReceived = attacker.balance - attackerEthBefore;
1064
1064
 
1065
1065
  // After fix: attacker gets nothing.
1066
- assertEq(ethReceived, 0, "FIX: attacker receives nothing with 0 tokens burned");
1067
- assertEq(reclaimAmount, 0, "FIX: reclaim is 0");
1066
+ assertEq(ethReceived, 0, "attacker receives nothing with 0 tokens burned");
1067
+ assertEq(reclaimAmount, 0, "reclaim is 0");
1068
1068
  }
1069
1069
 
1070
- /// @notice PoC variant: Repeatable attack. After all tokens are burned, attacker drains surplus.
1071
- function test_C5_zeroSupplyCashOut_afterAllTokensBurned() public {
1070
+ /// @notice After all tokens are burned, calling cashOut with 0 returns nothing.
1071
+ function test_zeroSupplyCashOut_afterAllTokensBurned() public {
1072
1072
  uint256 projectId = _launchProject({cashOutTaxRate: 0, reservedPercent: 0});
1073
1073
 
1074
1074
  vm.prank(_projectOwner);
@@ -1119,18 +1119,18 @@ contract AuditExploits_Local is TestBaseWorkflow {
1119
1119
  });
1120
1120
 
1121
1121
  // After fix: cashing out 0 tokens returns 0, treasury is safe.
1122
- assertEq(stolen, 0, "FIX: attacker cannot steal funds after tokens were burned");
1123
- assertEq(attacker.balance, 0, "FIX: attacker receives no ETH");
1122
+ assertEq(stolen, 0, "cannot extract funds after tokens were burned");
1123
+ assertEq(attacker.balance, 0, "no ETH received");
1124
1124
  }
1125
1125
 
1126
1126
  //*********************************************************************//
1127
- // --- [C-2] REVDeployer.beforePayRecordedWith Array OOB (Enhanced)-- //
1127
+ // --- REVDeployer.beforePayRecordedWith Array OOB (Enhanced)-------- //
1128
1128
  //*********************************************************************//
1129
1129
 
1130
- /// @notice PoC: Demonstrates the exact Solidity panic that occurs in REVDeployer when
1130
+ /// @notice Demonstrates the exact Solidity panic that occurs in REVDeployer when
1131
1131
  /// usesTiered721Hook=false and usesBuybackHook=true.
1132
1132
  /// This mirrors REVDeployer.sol lines 248-258 exactly.
1133
- function test_C2_beforePayRecordedWith_fullOOBPattern() public {
1133
+ function test_beforePayRecordedWith_fullOOBPattern() public {
1134
1134
  // Simulate all 4 combinations and verify only the problematic one reverts.
1135
1135
  bool[4] memory tiered = [false, true, false, true];
1136
1136
  bool[4] memory buyback = [false, false, true, true];
@@ -1165,13 +1165,13 @@ contract AuditExploits_Local is TestBaseWorkflow {
1165
1165
  }
1166
1166
 
1167
1167
  //*********************************************************************//
1168
- // --- [C-4] REVDeployer.hasMintPermissionFor address(0) call ------- //
1168
+ // --- REVDeployer.hasMintPermissionFor address(0) call ------------- //
1169
1169
  //*********************************************************************//
1170
1170
 
1171
- /// @notice PoC: Demonstrates that calling a function on address(0) reverts.
1171
+ /// @notice Demonstrates that calling a function on address(0) reverts.
1172
1172
  /// This mirrors REVDeployer.sol line 353 where buybackHook.hasMintPermissionFor(...)
1173
1173
  /// is called when buybackHookOf[revnetId] == address(0).
1174
- function test_C4_hasMintPermissionFor_callOnAddressZero() public {
1174
+ function test_hasMintPermissionFor_callOnAddressZero() public {
1175
1175
  // The bug is in the short-circuit evaluation of:
1176
1176
  // addr == loansOf[revnetId] // false (addr is some real address)
1177
1177
  // || addr == address(buybackHook) // false (buybackHook is address(0), addr is not)
@@ -1206,22 +1206,22 @@ contract AuditExploits_Local is TestBaseWorkflow {
1206
1206
  // The call succeeds (returns false from empty contract) but has no return data.
1207
1207
  // Solidity's interface-based call would revert because it expects abi-encoded bool.
1208
1208
  if (success && data.length < 32) {
1209
- revert("C-4 confirmed: call to address(0) returns empty data, ABI decode reverts");
1209
+ revert("confirmed: call to address(0) returns empty data, ABI decode reverts");
1210
1210
  }
1211
1211
  if (!success) {
1212
- revert("C-4 confirmed: call to address(0) reverted directly");
1212
+ revert("confirmed: call to address(0) reverted directly");
1213
1213
  }
1214
1214
  }
1215
1215
 
1216
1216
  //*********************************************************************//
1217
- // --- [C-1] REVLoans uint112 Truncation Pattern -------------------- //
1217
+ // --- REVLoans uint112 Truncation Pattern --------------------------- //
1218
1218
  //*********************************************************************//
1219
1219
 
1220
- /// @notice PoC: Demonstrates silent uint112 truncation.
1220
+ /// @notice Demonstrates silent uint112 truncation.
1221
1221
  /// This mirrors REVLoans.sol lines 922-923 where:
1222
1222
  /// loan.amount = uint112(newBorrowAmount);
1223
1223
  /// loan.collateral = uint112(newCollateralCount);
1224
- function test_C1_uint112SilentTruncation() public pure {
1224
+ function test_uint112SilentTruncation() public pure {
1225
1225
  // Values that exceed uint112.max
1226
1226
  uint256 hugeCollateral = uint256(type(uint112).max) + 1000 ether;
1227
1227
  uint256 hugeBorrow = uint256(type(uint112).max) + 500 ether;
@@ -1249,14 +1249,14 @@ contract AuditExploits_Local is TestBaseWorkflow {
1249
1249
  // If attacker repays truncatedBorrow (tiny), they get back truncatedCollateral (tiny).
1250
1250
  // But they already received hugeBorrow worth of funds from the surplus.
1251
1251
  // Net theft = hugeBorrow - truncatedBorrow
1252
- assertGt(borrowProfit, 999 ether, "EXPLOIT: attacker steals >999 ETH from truncation of 1000 ETH overflow");
1252
+ assertGt(borrowProfit, 999 ether, "attacker extracts >999 ETH from truncation of 1000 ETH overflow");
1253
1253
  }
1254
1254
 
1255
1255
  //*********************************************************************//
1256
- // --- [C-3] REVLoans Reentrancy Pattern ----------------------------- //
1256
+ // --- REVLoans Reentrancy Pattern ----------------------------------- //
1257
1257
  //*********************************************************************//
1258
1258
 
1259
- /// @notice PoC: Demonstrates the CEI violation pattern in REVLoans._adjust.
1259
+ /// @notice Demonstrates the CEI violation pattern in REVLoans._adjust.
1260
1260
  /// The contract makes external calls (useAllowanceOf, feeTerminal.pay, _transferFrom)
1261
1261
  /// BEFORE writing loan.amount and loan.collateral at lines 921-923.
1262
1262
  ///
@@ -1264,7 +1264,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
1264
1264
  ///
1265
1265
  /// @dev The actual exploit requires a full REVLoans deployment which is in revnet-core-v5.
1266
1266
  /// This test demonstrates the PATTERN: external call before state write.
1267
- function test_C3_reentrancyPattern_externalCallBeforeStateWrite() public {
1267
+ function test_reentrancyPattern_externalCallBeforeStateWrite() public {
1268
1268
  // Deploy a mock "victim" contract that follows the vulnerable pattern.
1269
1269
  ReentrancyVictim victim = new ReentrancyVictim();
1270
1270
 
@@ -1280,26 +1280,22 @@ contract AuditExploits_Local is TestBaseWorkflow {
1280
1280
  attacker.attack();
1281
1281
 
1282
1282
  // The attacker extracted more than they should have.
1283
- assertGt(
1284
- address(attacker).balance,
1285
- 10 ether,
1286
- "EXPLOIT: attacker extracted more than single borrow allows (re-entered)"
1287
- );
1283
+ assertGt(address(attacker).balance, 10 ether, "attacker extracted more than single borrow allows (re-entered)");
1288
1284
 
1289
1285
  // The victim was drained below expected.
1290
1286
  assertLt(address(victim).balance, 10 ether, "victim was over-drained");
1291
1287
  }
1292
1288
 
1293
1289
  //*********************************************************************//
1294
- // --- [M-1] Held Fee Reentrancy ----------------------------------- //
1290
+ // --- Held Fee Reentrancy ----------------------------------------- //
1295
1291
  //*********************************************************************//
1296
1292
 
1297
- /// @notice PoC: Verifies that the M-1 held fee reentrancy fix works correctly.
1293
+ /// @notice Verifies that the held fee reentrancy fix works correctly.
1298
1294
  /// A reentrant fee terminal calls back into processHeldFeesOf during pay().
1299
1295
  /// Before the fix, the same fee would be processed twice. After the fix,
1300
1296
  /// the index is advanced before the external call (CEI pattern), so the
1301
1297
  /// reentrant call processes zero fees.
1302
- function test_M1_heldFeeReentrancy_blocked() public {
1298
+ function test_heldFeeReentrancy_blocked() public {
1303
1299
  // --- Step 1: Launch fee project (project #1) with the standard terminal ---
1304
1300
  JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
1305
1301
  JBAccountingContext[] memory tokensToAccept = new JBAccountingContext[](1);
@@ -1466,9 +1462,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
1466
1462
  // With the fix (CEI): the reentrant call sees the index already advanced,
1467
1463
  // so it only processes the remaining fees (not the same fee again).
1468
1464
  // Total pay calls should be exactly 2 (one per held fee), not 3+ (which would indicate double-processing).
1469
- assertEq(
1470
- reentrantTerminal.payCallCount(), 2, "M-1 FIX: exactly 2 pay calls (no double-processing from reentrancy)"
1471
- );
1465
+ assertEq(reentrantTerminal.payCallCount(), 2, "exactly 2 pay calls (no double-processing from reentrancy)");
1472
1466
 
1473
1467
  // Held fees should all be consumed.
1474
1468
  JBFee[] memory remainingFees = _terminal.heldFeesOf(projectId, JBConstants.NATIVE_TOKEN, 100);
@@ -1476,7 +1470,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
1476
1470
  }
1477
1471
 
1478
1472
  /// @notice Verify processHeldFeesOf correctly handles partial unlock (some locked, some unlocked).
1479
- function test_M1_partialLockedFees() public {
1473
+ function test_partialLockedFees() public {
1480
1474
  // Launch fee project.
1481
1475
  JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
1482
1476
  JBAccountingContext[] memory tokensToAccept = new JBAccountingContext[](1);
@@ -1637,14 +1631,14 @@ contract AuditExploits_Local is TestBaseWorkflow {
1637
1631
  }
1638
1632
 
1639
1633
  //*********************************************************************//
1640
- // --- [H-3] Approval Hook Revert DoS (NEW - no test existed) ------- //
1634
+ // --- Approval Hook Revert DoS ------------------------------------ //
1641
1635
  //*********************************************************************//
1642
1636
 
1643
- /// @notice PoC: A malicious approval hook that reverts on approvalStatusOf() causes
1637
+ /// @notice A malicious approval hook that reverts on approvalStatusOf() causes
1644
1638
  /// currentOf() to revert when the NEXT ruleset (queued or auto-cycled) needs
1645
1639
  /// approval from the hook. This permanently freezes the project.
1646
1640
  /// Affects JBRulesets (NOT fixed).
1647
- function test_H3_approvalHookRevertDoS() public {
1641
+ function test_approvalHookRevertDoS() public {
1648
1642
  // Deploy a malicious approval hook that always reverts
1649
1643
  MaliciousApprovalHook maliciousHook = new MaliciousApprovalHook();
1650
1644
 
@@ -1759,30 +1753,30 @@ contract AuditExploits_Local is TestBaseWorkflow {
1759
1753
  // currentOf no longer reverts — it treats the approval as Failed and falls back
1760
1754
  // to the most recent approved ruleset. The project is NOT frozen.
1761
1755
  JBRuleset memory currentRuleset = jbRulesets().currentOf(projectId);
1762
- assertGt(currentRuleset.id, 0, "H-3 FIX: project is not frozen, currentOf succeeds");
1756
+ assertGt(currentRuleset.id, 0, "project is not frozen, currentOf succeeds");
1763
1757
  }
1764
1758
 
1765
1759
  //*********************************************************************//
1766
- // --- [C-5] V5.1 Regression: Verify Fix Works ---------------------- //
1760
+ // --- V5.1 Regression: Verify Fix Works ----------------------------- //
1767
1761
  //*********************************************************************//
1768
1762
 
1769
- /// @notice Verify that the C-5 zero-supply cash-out exploit is still present in V5.0
1770
- /// but the V5.1 rulesets should handle rulesets correctly (C-5 is a different
1771
- /// bug from the rulesets fix - C-5 is in JBCashOuts library).
1772
- /// @dev Confirms the C-5 fix: cashing out 0 tokens returns 0, not the full surplus.
1773
- function test_C5_v51_zeroSupplyCashOut_fixedInLibrary() public pure {
1774
- // C-5 was in JBCashOuts.cashOutFrom: when cashOutCount=0 and totalSupply=0,
1763
+ /// @notice Verify that the zero-supply cash-out exploit is still present in V5.0
1764
+ /// but the V5.1 rulesets should handle rulesets correctly (the zero-supply bug
1765
+ /// is a different bug from the rulesets fix - it is in JBCashOuts library).
1766
+ /// @dev Confirms the fix: cashing out 0 tokens returns 0, not the full surplus.
1767
+ function test_v51_zeroSupplyCashOut_fixedInLibrary() public pure {
1768
+ // The bug was in JBCashOuts.cashOutFrom: when cashOutCount=0 and totalSupply=0,
1775
1769
  // the function returned the full surplus instead of 0.
1776
1770
  // The fix adds an early return of 0 when cashOutCount == 0.
1777
1771
  uint256 reclaimFromLibrary =
1778
1772
  JBCashOuts.cashOutFrom({surplus: 10 ether, cashOutCount: 0, totalSupply: 0, cashOutTaxRate: 0});
1779
1773
 
1780
1774
  // Cashing out 0 tokens should return 0, not the full surplus.
1781
- assertEq(reclaimFromLibrary, 0, "C-5 FIX: cashOutFrom(surplus, 0, 0, rate) should return 0");
1775
+ assertEq(reclaimFromLibrary, 0, "cashOutFrom(surplus, 0, 0, rate) should return 0");
1782
1776
  }
1783
1777
 
1784
- /// @notice Verify C-5 does not manifest when totalSupply > 0 (normal operation).
1785
- function test_C5_v51_normalOperation_cashOut_safe() public pure {
1778
+ /// @notice Verify the zero-supply bug does not manifest when totalSupply > 0 (normal operation).
1779
+ function test_v51_normalOperation_cashOut_safe() public pure {
1786
1780
  // With non-zero supply and non-zero cashOutCount, the bonding curve works correctly
1787
1781
  uint256 reclaimable =
1788
1782
  JBCashOuts.cashOutFrom({surplus: 10 ether, cashOutCount: 500e18, totalSupply: 1000e18, cashOutTaxRate: 0});
@@ -1792,11 +1786,11 @@ contract AuditExploits_Local is TestBaseWorkflow {
1792
1786
  }
1793
1787
 
1794
1788
  //*********************************************************************//
1795
- // --- [L-5] Held Fees Storage Cleanup ------------------------------ //
1789
+ // --- Held Fees Storage Cleanup ------------------------------------ //
1796
1790
  //*********************************************************************//
1797
1791
 
1798
1792
  /// @notice Verifies that processHeldFeesOf deletes processed entries and resets array+index when done.
1799
- function test_L5_heldFeesStorageCleanup() public {
1793
+ function test_heldFeesStorageCleanup() public {
1800
1794
  // Launch fee project.
1801
1795
  JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
1802
1796
  JBAccountingContext[] memory tokensToAccept = new JBAccountingContext[](1);
@@ -1930,7 +1924,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
1930
1924
 
1931
1925
  // After processing all, the array should be fully reset (not just emptied).
1932
1926
  JBFee[] memory remaining = _terminal.heldFeesOf(projectId, JBConstants.NATIVE_TOKEN, 100);
1933
- assertEq(remaining.length, 0, "L-5: all fees processed and array cleaned");
1927
+ assertEq(remaining.length, 0, "all fees processed and array cleaned");
1934
1928
 
1935
1929
  // Creating new fees after cleanup should work from index 0 (array was reset).
1936
1930
  vm.deal(address(this), 2 ether);
@@ -1957,16 +1951,16 @@ contract AuditExploits_Local is TestBaseWorkflow {
1957
1951
 
1958
1952
  // Should have 1 new fee (not 4 = 3 deleted + 1 new).
1959
1953
  JBFee[] memory newFees = _terminal.heldFeesOf(projectId, JBConstants.NATIVE_TOKEN, 100);
1960
- assertEq(newFees.length, 1, "L-5: new fee after array reset works correctly");
1954
+ assertEq(newFees.length, 1, "new fee after array reset works correctly");
1961
1955
  }
1962
1956
 
1963
1957
  //*********************************************************************//
1964
- // --- [L-8] Ruleset Start Time After Base Ruleset ------------------ //
1958
+ // --- Ruleset Start Time After Base Ruleset ------------------------ //
1965
1959
  //*********************************************************************//
1966
1960
 
1967
1961
  /// @notice Verifies that a queued ruleset's start time is bumped to at least
1968
- /// the base ruleset's start time (L-8 fix).
1969
- function test_L8_rulesetStartsAfterBaseRuleset() public {
1962
+ /// the base ruleset's start time.
1963
+ function test_rulesetStartsAfterBaseRuleset() public {
1970
1964
  // Launch fee project.
1971
1965
  JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
1972
1966
  JBAccountingContext[] memory tokensToAccept = new JBAccountingContext[](1);
@@ -2026,7 +2020,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
2026
2020
  assertGt(firstStart, 0, "first ruleset should have a start time");
2027
2021
 
2028
2022
  // Queue a second ruleset with mustStartAtOrAfter=0 (meaning "as soon as possible").
2029
- // Without the L-8 fix, this could theoretically derive a start before the base ruleset.
2023
+ // Without the fix, this could theoretically derive a start before the base ruleset.
2030
2024
  JBRulesetConfig[] memory secondConfig = new JBRulesetConfig[](1);
2031
2025
  secondConfig[0].mustStartAtOrAfter = 0; // Key: asking for earliest possible
2032
2026
  secondConfig[0].duration = 30 days;
@@ -2041,15 +2035,15 @@ contract AuditExploits_Local is TestBaseWorkflow {
2041
2035
  // Get the queued ruleset.
2042
2036
  (JBRuleset memory queued,) = jbRulesets().latestQueuedOf(projectId);
2043
2037
 
2044
- // L-8 fix: the queued ruleset must start at or after the base ruleset's start.
2045
- assertGe(queued.start, firstStart, "L-8: queued ruleset must start at or after base ruleset");
2038
+ // Fix: the queued ruleset must start at or after the base ruleset's start.
2039
+ assertGe(queued.start, firstStart, "queued ruleset must start at or after base ruleset");
2046
2040
 
2047
2041
  // It should start at the next cycle boundary (firstStart + duration).
2048
2042
  assertEq(queued.start, firstStart + 30 days, "queued ruleset starts at next cycle boundary");
2049
2043
  }
2050
2044
 
2051
2045
  //*********************************************************************//
2052
- // --- [H-4] Pending Reserves Inflate cashOutWeight (property test)-- //
2046
+ // --- Pending Reserves Inflate cashOutWeight (property test)-------- //
2053
2047
  //*********************************************************************//
2054
2048
 
2055
2049
  //*********************************************************************//
@@ -2507,13 +2501,13 @@ contract AuditExploits_Local is TestBaseWorkflow {
2507
2501
  }
2508
2502
 
2509
2503
  //*********************************************************************//
2510
- // --- [H-4] Pending Reserves Inflate cashOutWeight (property test)-- //
2504
+ // --- Pending Reserves Inflate cashOutWeight (property test)-------- //
2511
2505
  //*********************************************************************//
2512
2506
 
2513
2507
  /// @notice Property: cashOut reclaim with pending reserves <= cashOut reclaim after reserves distributed.
2514
2508
  /// @dev When there are pending reserved tokens that haven't been distributed, they should
2515
2509
  /// not inflate the denominator used for cash-out calculations.
2516
- function test_H4_pendingReserves_cashOutProperty() public {
2510
+ function test_pendingReserves_cashOutProperty() public {
2517
2511
  // Launch project with 50% reserved rate
2518
2512
  uint256 projectId = _launchProject({cashOutTaxRate: 0, reservedPercent: 5000});
2519
2513
 
@@ -2529,7 +2523,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
2529
2523
  token: JBConstants.NATIVE_TOKEN,
2530
2524
  beneficiary: _beneficiary,
2531
2525
  minReturnedTokens: 0,
2532
- memo: "H-4 test",
2526
+ memo: "test",
2533
2527
  metadata: new bytes(0)
2534
2528
  });
2535
2529
 
@@ -2589,7 +2583,7 @@ contract AuditExploits_Local is TestBaseWorkflow {
2589
2583
  // share decreases, so reclaimable should decrease or stay the same.
2590
2584
  // With 0% cashOutTaxRate and linear redemption: reclaimable = surplus * tokens / totalSupply
2591
2585
  // After reserves: totalSupply is larger, so reclaimable is smaller.
2592
- assertLe(reclaimAfter, reclaimBefore, "H-4 property: reclaim after reserves distributed <= reclaim before");
2586
+ assertLe(reclaimAfter, reclaimBefore, "reclaim after reserves distributed <= reclaim before");
2593
2587
  }
2594
2588
  }
2595
2589
 
@@ -2638,7 +2632,7 @@ contract ReentrancyAttacker {
2638
2632
  }
2639
2633
  }
2640
2634
 
2641
- /// @notice Malicious fee terminal that reenters processHeldFeesOf during pay(), demonstrating M-1.
2635
+ /// @notice Malicious fee terminal that reenters processHeldFeesOf during pay().
2642
2636
  contract ReentrantFeeTerminal is ERC165 {
2643
2637
  IJBMultiTerminal public immutable victim;
2644
2638
  uint256 public immutable targetProjectId;
@@ -2694,7 +2688,7 @@ contract ReentrantFeeTerminal is ERC165 {
2694
2688
  receive() external payable {}
2695
2689
  }
2696
2690
 
2697
- /// @notice Malicious approval hook that always reverts, demonstrating H-3 DoS.
2691
+ /// @notice Malicious approval hook that always reverts.
2698
2692
  contract MaliciousApprovalHook is ERC165, IJBRulesetApprovalHook {
2699
2693
  function DURATION() external pure override returns (uint256) {
2700
2694
  return 1 days;
@@ -262,15 +262,15 @@ contract FlashLoanAttacks_Local is TestBaseWorkflow {
262
262
  }
263
263
 
264
264
  // ═══════════════════════════════════════════════════════════════════
265
- // Test 5: C-5 regression — cashOut(0) with totalSupply==0 must return 0
265
+ // Test 5: Regression — cashOut(0) with totalSupply==0 must return 0
266
266
  // ═══════════════════════════════════════════════════════════════════
267
267
 
268
- /// @notice C-5 was a V5 audit finding where cashOut(0) with totalSupply==0 returned the entire surplus.
268
+ /// @notice Regression test: cashOut(0) with totalSupply==0 previously returned the entire surplus.
269
269
  /// @dev In V5, `cashOutCount >= totalSupply` (0 >= 0) was true and returned the full surplus before
270
270
  /// checking for zero cashOutCount. Fixed since V5.1: `JBCashOuts.cashOutFrom` returns 0 when
271
271
  /// cashOutCount==0 (line 31) before reaching the `cashOutCount >= totalSupply` check (line 37).
272
272
  /// This test verifies the fix holds.
273
- function test_C5_variant_addToBalance_zeroCashOut() public {
273
+ function test_variant_addToBalance_zeroCashOut() public {
274
274
  // Add to balance when no tokens exist
275
275
  vm.deal(address(0xD000), 5 ether);
276
276
  vm.prank(address(0xD000));
@@ -297,7 +297,7 @@ contract FlashLoanAttacks_Local is TestBaseWorkflow {
297
297
  metadata: new bytes(0)
298
298
  });
299
299
 
300
- assertEq(reclaimAmount, 0, "C-5 regression: cashOut(0) must return 0");
300
+ assertEq(reclaimAmount, 0, "Regression: cashOut(0) must return 0");
301
301
  }
302
302
 
303
303
  // ═══════════════════════════════════════════════════════════════════
@@ -350,11 +350,11 @@ contract FlashLoanAttacks_Local is TestBaseWorkflow {
350
350
  }
351
351
 
352
352
  // ═══════════════════════════════════════════════════════════════════
353
- // Test 8: H-4 reserved token inflation — cashOut timing
353
+ // Test 8: Reserved token inflation — cashOut timing
354
354
  // ═══════════════════════════════════════════════════════════════════
355
355
 
356
356
  function test_reservedTokenInflation_cashOutTiming() public {
357
- // Launch a project with 20% reserved to test H-4
357
+ // Launch a project with 20% reserved to test inflation
358
358
  JBRulesetConfig[] memory rulesetConfig = new JBRulesetConfig[](1);
359
359
  rulesetConfig[0].mustStartAtOrAfter = 0;
360
360
  rulesetConfig[0].duration = 0;
@@ -4,7 +4,7 @@ pragma solidity ^0.8.6;
4
4
  import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
5
5
  import {JBCashOuts} from "../src/libraries/JBCashOuts.sol";
6
6
 
7
- /// @notice Edge case tests for cross-ruleset cash outs and pending reserves inflation (H-4).
7
+ /// @notice Edge case tests for cross-ruleset cash outs and pending reserves inflation.
8
8
  /// Demonstrates that pending reserved tokens inflate totalSupply in cash-out calculations,
9
9
  /// systematically undervaluing cash-outs until reserves are distributed.
10
10
  contract TestCashOutTimingEdge_Local is TestBaseWorkflow {
@@ -92,7 +92,7 @@ contract TestCashOutTimingEdge_Local is TestBaseWorkflow {
92
92
  _controller.deployERC20For(_projectId, "Test", "TST", bytes32(0));
93
93
  }
94
94
 
95
- /// @notice H-4 CONFIRMATION: Pending reserves inflate totalSupply, reducing cash-out value.
95
+ /// @notice Pending reserves inflate totalSupply, reducing cash-out value.
96
96
  /// When reserves exist but haven't been distributed, totalTokenSupplyWithReservedTokensOf
97
97
  /// includes them in the denominator, making each token worth less.
98
98
  function test_pendingReserves_inflateSupply_reduceCashOut() external {
@@ -134,8 +134,8 @@ contract TestCashOutTimingEdge_Local is TestBaseWorkflow {
134
134
  surplus: surplus, cashOutCount: payerTokens, totalSupply: circulatingSupply, cashOutTaxRate: 5000
135
135
  });
136
136
 
137
- // H-4 CONFIRMED: Cash-out with pending reserves is LESS than without.
138
- assertLt(reclaimWithInflation, reclaimWithoutInflation, "H-4: Pending reserves reduce cash-out value");
137
+ // Cash-out with pending reserves is LESS than without.
138
+ assertLt(reclaimWithInflation, reclaimWithoutInflation, "Pending reserves reduce cash-out value");
139
139
 
140
140
  // Quantify the impact.
141
141
  uint256 lostValue = reclaimWithoutInflation - reclaimWithInflation;
@@ -177,7 +177,7 @@ contract TestCashOutTimingEdge_Local is TestBaseWorkflow {
177
177
  assertEq(totalAfter, totalBefore, "Total supply unchanged after distribution");
178
178
 
179
179
  // Cash-out value should be the same since total didn't change.
180
- // The fix for H-4 would be: don't include pending reserves in totalSupply.
180
+ // One approach: don't include pending reserves in totalSupply.
181
181
  uint256 reclaimAfter = JBCashOuts.cashOutFrom({
182
182
  surplus: surplus, cashOutCount: payerTokens, totalSupply: totalAfter, cashOutTaxRate: 5000
183
183
  });
@@ -21,7 +21,7 @@ contract JBRulesetsHarness is JBRulesets {
21
21
  }
22
22
  }
23
23
 
24
- /// @notice Tests for M-19: duration underflow fix in `_simulateCycledRulesetBasedOn`.
24
+ /// @notice Tests for duration underflow fix in `_simulateCycledRulesetBasedOn`.
25
25
  ///
26
26
  /// The fix guards against arithmetic underflow when `baseRuleset.duration >= block.timestamp`.
27
27
  /// Without the fix, `block.timestamp - baseRuleset.duration + 1` wraps around to ~2^256,
package/test/TestFees.sol CHANGED
@@ -481,7 +481,7 @@ contract TestFees_Local is TestBaseWorkflow {
481
481
  assertEq(_persistingFee.length, 1);
482
482
  }
483
483
 
484
- function test_AuditFinding4POC() external {
484
+ function test_doubleFeeEdgeCase() external {
485
485
  vm.skip(true);
486
486
 
487
487
  // Setup: Pay the zero project so the terminal has balance of 1 eth from another project.
@@ -537,10 +537,10 @@ contract TestFees_Local is TestBaseWorkflow {
537
537
  // Check the unlock timestamp
538
538
  assertEq(_checkOgFee[0].unlockTimestamp, block.timestamp + 2_419_200);
539
539
 
540
- // Audit Example correctly asserts that the fee amount is 1 ETH
540
+ // The fee amount is 1 ETH
541
541
  assertEq(_checkOgFee[0].amount, _nativeDistLimit);
542
542
 
543
- // Audit Example correctly asserts that the projects balance in terminal store will be zero.
543
+ // The project's balance in terminal store will be zero.
544
544
  uint256 project2BalanceStore =
545
545
  jbTerminalStore().balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
546
546
  assertEq(project2BalanceStore, 0);
@@ -3,7 +3,7 @@ pragma solidity ^0.8.6;
3
3
 
4
4
  import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
5
5
 
6
- /// @notice Confirms M-16: held fees are stranded when a terminal migrates.
6
+ /// @notice Confirms held fees are stranded when a terminal migrates.
7
7
  /// The migration calls addToBalanceOf with shouldReturnHeldFees: false,
8
8
  /// leaving held fees in the old terminal with no balance to process them.
9
9
  contract TestMigrationHeldFees_Local is TestBaseWorkflow {
@@ -116,7 +116,7 @@ contract TestMigrationHeldFees_Local is TestBaseWorkflow {
116
116
  });
117
117
  }
118
118
 
119
- /// @notice M-16 CONFIRMED: Pay → distribute payouts (holds fees) → migrate → fees stranded.
119
+ /// @notice Pay → distribute payouts (holds fees) → migrate → fees stranded.
120
120
  function test_migration_heldFeesStranded() external {
121
121
  // Step 1: Pay the project.
122
122
  _terminal.pay{value: PAY_AMOUNT}({
@@ -156,7 +156,7 @@ contract TestMigrationHeldFees_Local is TestBaseWorkflow {
156
156
 
157
157
  // Held fees still exist in old terminal.
158
158
  JBFee[] memory feesAfterMigration = _terminal.heldFeesOf(_projectId, JBConstants.NATIVE_TOKEN, 100);
159
- assertEq(feesAfterMigration.length, heldFees.length, "M-16: Held fees remain in old terminal");
159
+ assertEq(feesAfterMigration.length, heldFees.length, "Held fees remain in old terminal");
160
160
  }
161
161
 
162
162
  /// @notice Process held fees FIRST, then migrate — correct approach.
@@ -244,6 +244,6 @@ contract TestMigrationHeldFees_Local is TestBaseWorkflow {
244
244
  // The fee processing attempted but the terminal has no actual ETH.
245
245
  // The _recordAddedBalanceFor in the catch block inflates the store balance
246
246
  // without actual funds — this is a phantom balance.
247
- // This documents the M-16 issue: either fees are lost OR phantom balances are created.
247
+ // This documents the issue: either fees are lost OR phantom balances are created.
248
248
  }
249
249
  }
@@ -22,7 +22,7 @@ contract MockApprovalHookConfigurable is IJBRulesetApprovalHook {
22
22
  }
23
23
  }
24
24
 
25
- /// @notice Mock approval hook that always reverts (for DoS testing — H-3 confirmation).
25
+ /// @notice Mock approval hook that always reverts (for DoS testing).
26
26
  contract RevertingApprovalHook is IJBRulesetApprovalHook {
27
27
  uint256 public constant override DURATION = 3 days;
28
28
 
@@ -296,10 +296,10 @@ contract MockApprovalHookConfigurable is IJBRulesetApprovalHook {
296
296
  assertGt(current.cycleNumber, 1, "Should have rolled over multiple cycles");
297
297
  }
298
298
 
299
- /// @notice H-3 FIX VERIFICATION: Reverting approval hook no longer causes DoS.
299
+ /// @notice Reverting approval hook no longer causes DoS.
300
300
  /// The try/catch in _approvalStatusOf catches the revert and returns Failed status,
301
301
  /// so currentOf succeeds and falls back to the previous ruleset.
302
- function test_approvalHook_revert_causesDoS_H3() external {
302
+ function test_approvalHook_revert_causesDoS() external {
303
303
  RevertingApprovalHook revertHook = new RevertingApprovalHook();
304
304
 
305
305
  uint256 pid = _launchProject(FOURTEEN_DAYS, INITIAL_WEIGHT, 0, IJBRulesetApprovalHook(address(revertHook)));
@@ -310,7 +310,7 @@ contract MockApprovalHookConfigurable is IJBRulesetApprovalHook {
310
310
  // Warp past current cycle so the queued one is checked.
311
311
  vm.warp(block.timestamp + FOURTEEN_DAYS);
312
312
 
313
- // H-3 FIX: The reverting hook is caught by try/catch, treated as Failed.
313
+ // The reverting hook is caught by try/catch, treated as Failed.
314
314
  // currentOf succeeds and falls back to the original ruleset (weight not doubled).
315
315
  JBRuleset memory current = _rulesets.currentOf(pid);
316
316
  assertGt(current.id, 0, "currentOf should succeed, not revert");
@@ -192,7 +192,7 @@ contract BondingCurveProperties is Test {
192
192
  uint256 secondResult = JBCashOuts.cashOutFrom(remainingSurplus, b, remainingSupply, cashOutTaxRate);
193
193
 
194
194
  // NOTE: Strict subadditivity (firstResult + secondResult <= singleResult) was proven to be
195
- // violated due to mulDiv rounding accumulation (FV-1 in AUDIT_FINDINGS.md).
195
+ // violated due to mulDiv rounding accumulation.
196
196
  // The violation is bounded by rounding precision and is economically insignificant (~0.00001%).
197
197
  // We verify the weaker property: the excess is bounded by rounding tolerance.
198
198
  if (firstResult + secondResult > singleResult) {
@@ -228,7 +228,7 @@ contract BondingCurveProperties is Test {
228
228
 
229
229
  uint256 secondResult = JBCashOuts.cashOutFrom(remainingSurplus, b, remainingSupply, cashOutTaxRate);
230
230
 
231
- // NOTE: Strict subadditivity violated due to mulDiv rounding (FV-1 in AUDIT_FINDINGS.md).
231
+ // NOTE: Strict subadditivity violated due to mulDiv rounding.
232
232
  // Verify the weaker property: excess bounded by rounding tolerance (< 0.01%).
233
233
  if (firstResult + secondResult > singleResult) {
234
234
  uint256 excess = (firstResult + secondResult) - singleResult;
@@ -52,7 +52,7 @@ contract TestFundAccessLimitsEdge_Local is JBTest {
52
52
  // Query all limits — should have 2 entries now (both with currency=1, amount=1000).
53
53
  JBCurrencyAmount[] memory allLimits = limits.payoutLimitsOf(PROJECT_ID, RULESET_ID, TERMINAL, TOKEN);
54
54
 
55
- // BUG CONFIRMED: Two entries instead of one.
55
+ // Two entries instead of one.
56
56
  assertEq(allLimits.length, 2, "BUG: Limits accumulated instead of being replaced");
57
57
  assertEq(allLimits[0].amount, 1000, "First accumulated limit");
58
58
  assertEq(allLimits[1].amount, 1000, "Second accumulated limit");
@@ -62,7 +62,7 @@ contract M20M21Harness {
62
62
  }
63
63
  }
64
64
 
65
- /// @notice Tests for M-20 (_sliceBytes over-copy) and M-21 (addToMetadata offset overflow).
65
+ /// @notice Tests for _sliceBytes over-copy and addToMetadata offset overflow.
66
66
  contract TestMetadataResolverM20M21 is JBTest {
67
67
  M20M21Harness harness;
68
68
 
@@ -71,13 +71,13 @@ contract TestMetadataResolverM20M21 is JBTest {
71
71
  }
72
72
 
73
73
  //*********************************************************************//
74
- // --- M-20: _sliceBytes memory over-copy --------------------------- //
74
+ // --- _sliceBytes memory over-copy --------------------------------- //
75
75
  //*********************************************************************//
76
76
 
77
77
  /// @notice Verifies that addToMetadata preserves all entries when adding to metadata with
78
78
  /// multiple existing entries. The _sliceBytes call at line 129 uses start > 0, which on
79
79
  /// the buggy code would over-copy and corrupt subsequent memory operations.
80
- function test_M20_addToMetadataPreservesAllEntries() external view {
80
+ function test_addToMetadataPreservesAllEntries() external view {
81
81
  bytes4 id1 = bytes4(0x11111111);
82
82
  bytes4 id2 = bytes4(0x22222222);
83
83
  bytes4 id3 = bytes4(0x33333333);
@@ -116,7 +116,7 @@ contract TestMetadataResolverM20M21 is JBTest {
116
116
 
117
117
  /// @notice Test with 4 sequential addToMetadata calls, each exercising _sliceBytes.
118
118
  /// More entries = more start > 0 cases = more chances for memory corruption.
119
- function test_M20_multipleSequentialAdds() external view {
119
+ function test_multipleSequentialAdds() external view {
120
120
  bytes4[5] memory ids =
121
121
  [bytes4(0x11111111), bytes4(0x22222222), bytes4(0x33333333), bytes4(0x44444444), bytes4(0x55555555)];
122
122
  uint256[5] memory vals = [uint256(100), uint256(200), uint256(300), uint256(400), uint256(500)];
@@ -144,7 +144,7 @@ contract TestMetadataResolverM20M21 is JBTest {
144
144
  /// @notice Verify that getDataFor returns the exact expected length for non-first entries.
145
145
  /// On buggy code, _sliceBytes copies more than needed, but the returned length should still
146
146
  /// be correct. This test verifies both length and content.
147
- function test_M20_getDataForReturnsCorrectLength() external view {
147
+ function test_getDataForReturnsCorrectLength() external view {
148
148
  bytes4 id1 = bytes4(0xAAAAAAAA);
149
149
  bytes4 id2 = bytes4(0xBBBBBBBB);
150
150
 
@@ -172,14 +172,14 @@ contract TestMetadataResolverM20M21 is JBTest {
172
172
  }
173
173
 
174
174
  //*********************************************************************//
175
- // --- M-21: addToMetadata offset overflow -------------------------- //
175
+ // --- addToMetadata offset overflow -------------------------------- //
176
176
  //*********************************************************************//
177
177
 
178
178
  /// @notice Verifies that addToMetadata reverts when the new offset would exceed 255.
179
179
  /// Uses 6 entries (table = 1 word, 30 bytes) with data totaling 253 words → total 255 words.
180
180
  /// Adding a 7th entry via addToMetadata forces the table from 1→2 words, incrementing all
181
181
  /// offsets by 1. This pushes newOffset from 255 to 256, triggering the overflow check.
182
- function test_M21_addToMetadataRevertsOnOffsetOverflow() external {
182
+ function test_addToMetadataRevertsOnOffsetOverflow() external {
183
183
  // 6 entries: table = ceil(6*5/32) = 1 word. firstOffset = 2.
184
184
  // 5 entries × 42 words + 1 entry × 43 words = 253 words of data.
185
185
  // Total = 1 (reserved) + 1 (table) + 253 (data) = 255 words.
@@ -213,7 +213,7 @@ contract TestMetadataResolverM20M21 is JBTest {
213
213
  /// @notice Verifies that addToMetadata works when the offset is exactly at the boundary (255).
214
214
  /// Same 6-entry table expansion setup but with 1 less data word (total 254 words),
215
215
  /// so the expanded offset is exactly 255 — the maximum valid value.
216
- function test_M21_addToMetadataSucceedsAtBoundary() external view {
216
+ function test_addToMetadataSucceedsAtBoundary() external view {
217
217
  // 6 entries × 42 words each = 252 words of data.
218
218
  // Total = 1 (reserved) + 1 (table) + 252 (data) = 254 words.
219
219
  bytes4[] memory ids = new bytes4[](6);
@@ -238,7 +238,7 @@ contract TestPrices_Local is JBPricesSetup {
238
238
  address(directory), abi.encodeCall(IJBDirectory.controllerOf, (PROJECT_ID)), abi.encode(address(this))
239
239
  );
240
240
 
241
- // BUG CONFIRMED: This reverts because default feed blocks project-specific feeds.
241
+ // This reverts because default feed blocks project-specific feeds.
242
242
  vm.expectRevert(abi.encodeWithSelector(JBPrices.JBPrices_PriceFeedAlreadyExists.selector, defaultFeed));
243
243
  _prices.addPriceFeedFor(PROJECT_ID, _pricingCurrency, _unitCurrency, projectFeed);
244
244
  }