@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 +2 -2
- package/script/DeployPeriphery.s.sol +11 -0
- package/src/libraries/JBMetadataResolver.sol +3 -3
- package/test/AuditExploits.t.sol +73 -79
- package/test/FlashLoanAttacks.t.sol +6 -6
- package/test/TestCashOutTimingEdge.sol +5 -5
- package/test/TestDurationUnderflow.sol +1 -1
- package/test/TestFees.sol +3 -3
- package/test/TestMigrationHeldFees.sol +4 -4
- package/test/TestRulesetQueuingStress.sol +4 -4
- package/test/formal/BondingCurveProperties.t.sol +2 -2
- package/test/units/static/JBFundAccessLimits/TestFundAccessLimitsEdge.sol +1 -1
- package/test/units/static/JBMetadataResolver/TestMetadataResolverM20M21.sol +8 -8
- package/test/units/static/JBPrices/TestPrices.sol +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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)))
|
package/test/AuditExploits.t.sol
CHANGED
|
@@ -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
|
|
8
|
-
/// @dev These tests target specific edge cases and potential vulnerabilities
|
|
9
|
-
contract
|
|
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
|
|
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
|
-
// ======
|
|
940
|
+
// ====== EDGE CASE TESTS ============================================ //
|
|
941
941
|
//*********************************************************************//
|
|
942
942
|
|
|
943
943
|
//*********************************************************************//
|
|
944
|
-
// ---
|
|
944
|
+
// --- Zero-Supply Cash Out Drains Entire Surplus ------------------- //
|
|
945
945
|
//*********************************************************************//
|
|
946
946
|
|
|
947
|
-
/// @notice
|
|
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
|
|
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, "
|
|
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, "
|
|
1010
|
-
assertEq(ethReceived, 0, "
|
|
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, "
|
|
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
|
|
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
|
|
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, "
|
|
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, "
|
|
1067
|
-
assertEq(reclaimAmount, 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
|
|
1071
|
-
function
|
|
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, "
|
|
1123
|
-
assertEq(attacker.balance, 0, "
|
|
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
|
-
// ---
|
|
1127
|
+
// --- REVDeployer.beforePayRecordedWith Array OOB (Enhanced)-------- //
|
|
1128
1128
|
//*********************************************************************//
|
|
1129
1129
|
|
|
1130
|
-
/// @notice
|
|
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
|
|
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
|
-
// ---
|
|
1168
|
+
// --- REVDeployer.hasMintPermissionFor address(0) call ------------- //
|
|
1169
1169
|
//*********************************************************************//
|
|
1170
1170
|
|
|
1171
|
-
/// @notice
|
|
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
|
|
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("
|
|
1209
|
+
revert("confirmed: call to address(0) returns empty data, ABI decode reverts");
|
|
1210
1210
|
}
|
|
1211
1211
|
if (!success) {
|
|
1212
|
-
revert("
|
|
1212
|
+
revert("confirmed: call to address(0) reverted directly");
|
|
1213
1213
|
}
|
|
1214
1214
|
}
|
|
1215
1215
|
|
|
1216
1216
|
//*********************************************************************//
|
|
1217
|
-
// ---
|
|
1217
|
+
// --- REVLoans uint112 Truncation Pattern --------------------------- //
|
|
1218
1218
|
//*********************************************************************//
|
|
1219
1219
|
|
|
1220
|
-
/// @notice
|
|
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
|
|
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, "
|
|
1252
|
+
assertGt(borrowProfit, 999 ether, "attacker extracts >999 ETH from truncation of 1000 ETH overflow");
|
|
1253
1253
|
}
|
|
1254
1254
|
|
|
1255
1255
|
//*********************************************************************//
|
|
1256
|
-
// ---
|
|
1256
|
+
// --- REVLoans Reentrancy Pattern ----------------------------------- //
|
|
1257
1257
|
//*********************************************************************//
|
|
1258
1258
|
|
|
1259
|
-
/// @notice
|
|
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
|
|
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
|
-
// ---
|
|
1290
|
+
// --- Held Fee Reentrancy ----------------------------------------- //
|
|
1295
1291
|
//*********************************************************************//
|
|
1296
1292
|
|
|
1297
|
-
/// @notice
|
|
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
|
|
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
|
|
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
|
-
// ---
|
|
1634
|
+
// --- Approval Hook Revert DoS ------------------------------------ //
|
|
1641
1635
|
//*********************************************************************//
|
|
1642
1636
|
|
|
1643
|
-
/// @notice
|
|
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
|
|
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, "
|
|
1756
|
+
assertGt(currentRuleset.id, 0, "project is not frozen, currentOf succeeds");
|
|
1763
1757
|
}
|
|
1764
1758
|
|
|
1765
1759
|
//*********************************************************************//
|
|
1766
|
-
// ---
|
|
1760
|
+
// --- V5.1 Regression: Verify Fix Works ----------------------------- //
|
|
1767
1761
|
//*********************************************************************//
|
|
1768
1762
|
|
|
1769
|
-
/// @notice Verify that the
|
|
1770
|
-
/// but the V5.1 rulesets should handle rulesets correctly (
|
|
1771
|
-
/// bug from the rulesets fix -
|
|
1772
|
-
/// @dev Confirms the
|
|
1773
|
-
function
|
|
1774
|
-
//
|
|
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, "
|
|
1775
|
+
assertEq(reclaimFromLibrary, 0, "cashOutFrom(surplus, 0, 0, rate) should return 0");
|
|
1782
1776
|
}
|
|
1783
1777
|
|
|
1784
|
-
/// @notice Verify
|
|
1785
|
-
function
|
|
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
|
-
// ---
|
|
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
|
|
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, "
|
|
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, "
|
|
1954
|
+
assertEq(newFees.length, 1, "new fee after array reset works correctly");
|
|
1961
1955
|
}
|
|
1962
1956
|
|
|
1963
1957
|
//*********************************************************************//
|
|
1964
|
-
// ---
|
|
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
|
|
1969
|
-
function
|
|
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
|
|
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
|
-
//
|
|
2045
|
-
assertGe(queued.start, firstStart, "
|
|
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
|
-
// ---
|
|
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
|
-
// ---
|
|
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
|
|
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: "
|
|
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, "
|
|
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()
|
|
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
|
|
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:
|
|
265
|
+
// Test 5: Regression — cashOut(0) with totalSupply==0 must return 0
|
|
266
266
|
// ═══════════════════════════════════════════════════════════════════
|
|
267
267
|
|
|
268
|
-
/// @notice
|
|
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
|
|
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, "
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
138
|
-
assertLt(reclaimWithInflation, reclaimWithoutInflation, "
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
540
|
+
// The fee amount is 1 ETH
|
|
541
541
|
assertEq(_checkOgFee[0].amount, _nativeDistLimit);
|
|
542
542
|
|
|
543
|
-
//
|
|
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
|
|
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
|
|
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, "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
// ---
|
|
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
|
|
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
|
|
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
|
|
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
|
-
// ---
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
}
|