@rev-net/core-v6 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/ADMINISTRATION.md +5 -1
  2. package/ARCHITECTURE.md +69 -11
  3. package/AUDIT_INSTRUCTIONS.md +90 -7
  4. package/CHANGE_LOG.md +16 -3
  5. package/README.md +32 -7
  6. package/RISKS.md +26 -14
  7. package/SKILLS.md +168 -46
  8. package/STYLE_GUIDE.md +1 -1
  9. package/USER_JOURNEYS.md +20 -6
  10. package/foundry.toml +7 -0
  11. package/package.json +9 -10
  12. package/script/Deploy.s.sol +80 -16
  13. package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
  14. package/src/REVDeployer.sol +73 -21
  15. package/src/REVLoans.sol +27 -6
  16. package/test/REV.integrations.t.sol +1 -1
  17. package/test/REVAutoIssuanceFuzz.t.sol +1 -1
  18. package/test/REVDeployerRegressions.t.sol +7 -4
  19. package/test/REVInvincibility.t.sol +7 -19
  20. package/test/REVInvincibilityHandler.sol +1 -1
  21. package/test/REVLifecycle.t.sol +1 -1
  22. package/test/REVLoans.invariants.t.sol +1 -1
  23. package/test/REVLoansAttacks.t.sol +20 -12
  24. package/test/REVLoansFeeRecovery.t.sol +20 -12
  25. package/test/REVLoansFindings.t.sol +20 -12
  26. package/test/REVLoansRegressions.t.sol +20 -12
  27. package/test/REVLoansSourceFeeRecovery.t.sol +1 -1
  28. package/test/REVLoansSourced.t.sol +1 -9
  29. package/test/REVLoansUnSourced.t.sol +1 -1
  30. package/test/TestBurnHeldTokens.t.sol +1 -1
  31. package/test/TestCEIPattern.t.sol +1 -1
  32. package/test/TestCashOutCallerValidation.t.sol +75 -1
  33. package/test/TestConversionDocumentation.t.sol +1 -1
  34. package/test/TestCrossCurrencyReclaim.t.sol +1 -1
  35. package/test/TestCrossSourceReallocation.t.sol +1 -1
  36. package/test/TestERC2771MetaTx.t.sol +1 -1
  37. package/test/TestEmptyBuybackSpecs.t.sol +1 -1
  38. package/test/TestFlashLoanSurplus.t.sol +1 -1
  39. package/test/TestHookArrayOOB.t.sol +1 -1
  40. package/test/TestLiquidationBehavior.t.sol +1 -1
  41. package/test/TestLoanSourceRotation.t.sol +1 -1
  42. package/test/TestLongTailEconomics.t.sol +1 -1
  43. package/test/TestLowFindings.t.sol +4 -2
  44. package/test/TestMixedFixes.t.sol +7 -5
  45. package/test/TestPermit2Signatures.t.sol +1 -1
  46. package/test/TestReallocationSandwich.t.sol +1 -1
  47. package/test/TestRevnetRegressions.t.sol +1 -1
  48. package/test/TestSplitWeightAdjustment.t.sol +11 -6
  49. package/test/TestSplitWeightE2E.t.sol +1 -1
  50. package/test/TestSplitWeightFork.t.sol +9 -10
  51. package/test/TestStageTransitionBorrowable.t.sol +1 -1
  52. package/test/TestSwapTerminalPermission.t.sol +1 -1
  53. package/test/TestUint112Overflow.t.sol +1 -1
  54. package/test/TestZeroRepayment.t.sol +1 -1
  55. package/test/audit/LoanIdOverflowGuard.t.sol +497 -0
  56. package/test/fork/ForkTestBase.sol +8 -11
  57. package/test/fork/TestAutoIssuanceFork.t.sol +148 -0
  58. package/test/fork/TestCashOutFork.t.sol +23 -22
  59. package/test/fork/TestIssuanceDecayFork.t.sol +158 -0
  60. package/test/fork/TestLoanBorrowFork.t.sol +1 -1
  61. package/test/fork/TestLoanCrossRulesetFork.t.sol +1 -1
  62. package/test/fork/TestLoanERC20Fork.t.sol +463 -0
  63. package/test/fork/TestLoanLiquidationFork.t.sol +1 -1
  64. package/test/fork/TestLoanReallocateFork.t.sol +1 -1
  65. package/test/fork/TestLoanRepayFork.t.sol +3 -3
  66. package/test/fork/TestLoanTransferFork.t.sol +1 -1
  67. package/test/fork/TestPermit2PaymentFork.t.sol +299 -0
  68. package/test/fork/TestSplitWeightFork.t.sol +1 -1
  69. package/test/helpers/MaliciousContracts.sol +37 -23
  70. package/test/mock/MockBuybackCashOutRecorder.sol +82 -0
  71. package/test/mock/MockBuybackDataHook.sol +51 -7
  72. package/test/mock/MockBuybackDataHookMintPath.sol +1 -1
  73. package/test/regression/TestBurnPermissionRequired.t.sol +1 -1
  74. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +205 -0
  75. package/test/regression/TestCrossRevnetLiquidation.t.sol +1 -1
  76. package/test/regression/TestCumulativeLoanCounter.t.sol +1 -1
  77. package/test/regression/TestLiquidateGapHandling.t.sol +1 -1
  78. package/test/regression/TestZeroPriceFeed.t.sol +1 -1
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
5
5
  import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
@@ -41,7 +41,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
41
41
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
42
42
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
43
43
  import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
44
- import {mulDiv} from "@prb/math/src/Common.sol";
44
+ import {mulDiv, sqrt} from "@prb/math/src/Common.sol";
45
45
 
46
46
  import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
47
47
  import {REVAutoIssuance} from "./structs/REVAutoIssuance.sol";
@@ -235,6 +235,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
235
235
  /// @notice Determine how a cash out from a revnet should be processed.
236
236
  /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
237
237
  /// @dev If a sucker is cashing out, no taxes or fees are imposed.
238
+ /// @dev REVDeployer is intentionally not registered as a feeless address. The protocol fee (2.5%) applies on top
239
+ /// of the rev fee — this is by design. The fee hook spec amount sent to `afterCashOutRecordedWith` will have the
240
+ /// protocol fee deducted by the terminal before reaching this contract, so the rev fee is computed on the
241
+ /// post-protocol-fee amount.
238
242
  /// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
239
243
  /// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
240
244
  /// out.
@@ -271,18 +275,19 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
271
275
  IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
272
276
 
273
277
  // If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
274
- // feeless (e.g. the router terminal routing value between projects), do not charge a fee.
278
+ // feeless (e.g. the router terminal routing value between projects), proxy directly to the buyback hook.
275
279
  if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
276
- return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, hookSpecifications);
280
+ // slither-disable-next-line unused-return
281
+ return BUYBACK_HOOK.beforeCashOutRecordedWith(context);
277
282
  }
278
283
 
279
- // Get a reference to the number of tokens being used to pay the fee (out of the total being cashed out).
284
+ // Split the cashed-out tokens into a fee portion and a non-fee portion.
280
285
  // Micro cash outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero, bypassing the fee.
281
286
  // Economically insignificant: the gas cost of the transaction far exceeds the bypassed fee. No fix needed.
282
287
  uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
283
288
  uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
284
289
 
285
- // Keep a reference to the amount claimable with non-fee tokens.
290
+ // Calculate how much surplus the non-fee tokens can reclaim via the bonding curve.
286
291
  uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
287
292
  surplus: context.surplus.value,
288
293
  cashOutCount: nonFeeCashOutCount,
@@ -290,7 +295,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
290
295
  cashOutTaxRate: context.cashOutTaxRate
291
296
  });
292
297
 
293
- // Keep a reference to the fee amount after the reclaimed amount is subtracted.
298
+ // Calculate how much the fee tokens reclaim from the remaining surplus after the non-fee reclaim.
294
299
  uint256 feeAmount = JBCashOuts.cashOutFrom({
295
300
  surplus: context.surplus.value - postFeeReclaimedAmount,
296
301
  cashOutCount: feeCashOutCount,
@@ -298,15 +303,39 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
298
303
  cashOutTaxRate: context.cashOutTaxRate
299
304
  });
300
305
 
301
- // Assemble a cash out hook specification to invoke `afterCashOutRecordedWith(…)` with, to process the fee.
302
- hookSpecifications = new JBCashOutHookSpecification[](1);
303
- hookSpecifications[0] = JBCashOutHookSpecification({
304
- hook: IJBCashOutHook(address(this)), amount: feeAmount, metadata: abi.encode(feeTerminal)
306
+ // Build a context for the buyback hook using only the non-fee token count.
307
+ JBBeforeCashOutRecordedContext memory buybackHookContext = context;
308
+ buybackHookContext.cashOutCount = nonFeeCashOutCount;
309
+
310
+ // Let the buyback hook adjust the cash out parameters and optionally return a hook specification.
311
+ JBCashOutHookSpecification[] memory buybackHookSpecifications;
312
+ (cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications) =
313
+ BUYBACK_HOOK.beforeCashOutRecordedWith(buybackHookContext);
314
+
315
+ // If the fee rounds down to zero, return the buyback hook's response directly — no fee to process.
316
+ if (feeAmount == 0) return (cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications);
317
+
318
+ // Build a hook spec that routes the fee amount to this contract's `afterCashOutRecordedWith` for processing.
319
+ JBCashOutHookSpecification memory feeSpec = JBCashOutHookSpecification({
320
+ hook: IJBCashOutHook(address(this)), noop: false, amount: feeAmount, metadata: abi.encode(feeTerminal)
305
321
  });
306
322
 
307
- // Return the cash out rate and the number of revnet tokens to cash out, minus the tokens being used to pay the
308
- // fee.
309
- return (context.cashOutTaxRate, nonFeeCashOutCount, context.totalSupply, hookSpecifications);
323
+ // Compose the final hook specifications: buyback spec (if any) + fee spec.
324
+ // NOTE: Only buybackHookSpecifications[0] is used. If the buyback hook returns multiple
325
+ // specs, the additional ones are silently dropped. This is intentional — the buyback hook is
326
+ // expected to return at most one spec for the cash-out buyback swap.
327
+ if (buybackHookSpecifications.length > 0) {
328
+ // The buyback hook returned a spec — include it before the fee spec.
329
+ hookSpecifications = new JBCashOutHookSpecification[](2);
330
+ hookSpecifications[0] = buybackHookSpecifications[0];
331
+ hookSpecifications[1] = feeSpec;
332
+ } else {
333
+ // No buyback spec — only the fee spec.
334
+ hookSpecifications = new JBCashOutHookSpecification[](1);
335
+ hookSpecifications[0] = feeSpec;
336
+ }
337
+
338
+ return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
310
339
  }
311
340
 
312
341
  /// @notice Before a revnet processes an incoming payment, determine the weight and pay hooks to use.
@@ -565,14 +594,33 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
565
594
  }
566
595
  }
567
596
 
568
- /// @notice Try to initialize a Uniswap V4 buyback pool for a terminal token at a generic 1:1 price.
597
+ /// @notice Try to initialize a Uniswap V4 buyback pool for a terminal token at its fair issuance price.
569
598
  /// @dev Called after the ERC-20 token is deployed so the pool can be initialized in the PoolManager.
599
+ /// Computes `sqrtPriceX96` from `initialIssuance` so the pool starts at the same price as the bonding curve.
570
600
  /// Silently catches failures (e.g., if the pool is already initialized).
571
601
  /// @param revnetId The ID of the revnet.
572
602
  /// @param terminalToken The terminal token to initialize a buyback pool for.
573
- function _tryInitializeBuybackPoolFor(uint256 revnetId, address terminalToken) internal {
574
- // Try to initialize the pool at a generic 1:1 sqrtPriceX96 and configure the buyback hook.
575
- // The buyback hook constructs the PoolKey internally from the project token, terminal token, and pool params.
603
+ /// @param initialIssuance The initial issuance rate (project tokens per terminal token, 18 decimals).
604
+ function _tryInitializeBuybackPoolFor(uint256 revnetId, address terminalToken, uint112 initialIssuance) internal {
605
+ uint160 sqrtPriceX96;
606
+
607
+ if (initialIssuance == 0) {
608
+ sqrtPriceX96 = uint160(1 << 96);
609
+ } else {
610
+ address normalizedTerminalToken = terminalToken == JBConstants.NATIVE_TOKEN ? address(0) : terminalToken;
611
+ address projectToken = address(CONTROLLER.TOKENS().tokenOf(revnetId));
612
+
613
+ if (projectToken == address(0) || projectToken == normalizedTerminalToken) {
614
+ sqrtPriceX96 = uint160(1 << 96);
615
+ } else if (normalizedTerminalToken < projectToken) {
616
+ // token0 = terminal, token1 = project → price = issuance / 1e18
617
+ sqrtPriceX96 = uint160(sqrt(mulDiv(uint256(initialIssuance), 1 << 192, 1e18)));
618
+ } else {
619
+ // token0 = project, token1 = terminal → price = 1e18 / issuance
620
+ sqrtPriceX96 = uint160(sqrt(mulDiv(1e18, 1 << 192, uint256(initialIssuance))));
621
+ }
622
+ }
623
+
576
624
  // slither-disable-next-line calls-loop
577
625
  try BUYBACK_HOOK.initializePoolFor({
578
626
  projectId: revnetId,
@@ -580,7 +628,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
580
628
  tickSpacing: DEFAULT_BUYBACK_TICK_SPACING,
581
629
  twapWindow: DEFAULT_BUYBACK_TWAP_WINDOW,
582
630
  terminalToken: terminalToken,
583
- sqrtPriceX96: uint160(1 << 96)
631
+ sqrtPriceX96: sqrtPriceX96
584
632
  }) {}
585
633
  catch {} // Pool may already be initialized — that's OK.
586
634
  }
@@ -838,8 +886,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
838
886
 
839
887
  /// @notice Change a revnet's split operator.
840
888
  /// @dev Only a revnet's current split operator can set a new split operator.
889
+ /// @dev Passing `address(0)` as `newSplitOperator` relinquishes operator powers permanently — the permissions
890
+ /// are granted to the zero address (which cannot execute transactions), effectively burning them.
841
891
  /// @param revnetId The ID of the revnet to set the split operator of.
842
- /// @param newSplitOperator The new split operator's address.
892
+ /// @param newSplitOperator The new split operator's address. Use `address(0)` to relinquish operator powers.
843
893
  function setSplitOperatorOf(uint256 revnetId, address newSplitOperator) external override {
844
894
  // Enforce permissions.
845
895
  _checkIfIsSplitOperatorOf({revnetId: revnetId, operator: _msgSender()});
@@ -1067,7 +1117,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
1067
1117
  for (uint256 j; j < terminalConfiguration.accountingContextsToAccept.length; j++) {
1068
1118
  // slither-disable-next-line calls-loop
1069
1119
  _tryInitializeBuybackPoolFor({
1070
- revnetId: revnetId, terminalToken: terminalConfiguration.accountingContextsToAccept[j].token
1120
+ revnetId: revnetId,
1121
+ terminalToken: terminalConfiguration.accountingContextsToAccept[j].token,
1122
+ initialIssuance: configuration.stageConfigurations[0].initialIssuance
1071
1123
  });
1072
1124
  }
1073
1125
  }
package/src/REVLoans.sol CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
5
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
@@ -339,11 +339,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
339
339
 
340
340
  // Get the surplus of all the revnet's terminals in terms of the native currency.
341
341
  uint256 totalSurplus = JBSurplus.currentSurplusOf({
342
- projectId: revnetId,
343
- terminals: terminals,
344
- accountingContexts: new JBAccountingContext[](0),
345
- decimals: decimals,
346
- currency: currency
342
+ projectId: revnetId, terminals: terminals, tokens: new address[](0), decimals: decimals, currency: currency
347
343
  });
348
344
 
349
345
  // Get the total amount the revnet currently has loaned out, in terms of the native currency with 18
@@ -442,6 +438,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
442
438
  }
443
439
 
444
440
  /// @notice Generate a ID for a loan given a revnet ID and a loan number within that revnet.
441
+ /// @dev The multiplication and addition can theoretically overflow a uint256 if revnetId or loanNumber are
442
+ /// astronomically large. In practice this is infeasible — it would require 2^256 loans or project IDs, far
443
+ /// beyond any realistic usage. No overflow check is needed.
445
444
  /// @param revnetId The ID of the revnet to generate a loan ID for.
446
445
  /// @param loanNumber The loan number of the loan within the revnet.
447
446
  /// @return The token ID of the 721.
@@ -581,6 +580,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
581
580
  );
582
581
  }
583
582
 
583
+ // Prevent the loan number from exceeding the ID namespace for this revnet.
584
+ if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
585
+
584
586
  // Get a reference to the loan ID.
585
587
  loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
586
588
 
@@ -991,6 +993,10 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
991
993
  _beforeTransferTo({to: address(feeTerminal), token: loan.source.token, amount: revFeeAmount});
992
994
 
993
995
  // Pay the fee. Send the REV to the beneficiary. If fee payment fails, give the amount back to the borrower.
996
+ // NOTE: When terminal.pay() reverts (e.g. due to a misconfigured terminal or paused payments),
997
+ // the REV fee is refunded to the borrower, resulting in an effectively interest-free loan for the
998
+ // REV fee portion. This is acceptable — it requires a broken/misconfigured fee terminal and the
999
+ // borrower still pays the source fee and protocol fee.
994
1000
  // slither-disable-next-line arbitrary-send-eth,unused-return
995
1001
  try feeTerminal.pay{value: payValue}({
996
1002
  projectId: REV_ID,
@@ -1026,6 +1032,11 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
1026
1032
 
1027
1033
  /// @notice Allows the owner of a loan to pay it back, add more, or receive returned collateral no longer necessary
1028
1034
  /// to support the loan.
1035
+ /// @dev CEI ordering note: `totalCollateralOf` is not incremented until `_addCollateralTo` executes,
1036
+ /// which happens after the external calls in `_addTo` (useAllowanceOf, fee payment, transfer). A reentrant
1037
+ /// `borrowFrom` during those calls would see a lower `totalCollateralOf`, potentially passing collateral
1038
+ /// checks that should fail. Practically infeasible — requires an adversarial pay hook on the revnet's own
1039
+ /// terminal that calls back into `borrowFrom`, which is not a realistic deployment configuration.
1029
1040
  /// @param loan The loan being adjusted.
1030
1041
  /// @param revnetId The ID of the revnet the loan is being adjusted in.
1031
1042
  /// @param newBorrowAmount The new amount of the loan, denominated in the token of the source's accounting
@@ -1096,6 +1107,10 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
1096
1107
  });
1097
1108
 
1098
1109
  // Pay the fee. If it fails, reclaim the allowance and give the amount back to the borrower.
1110
+ // NOTE: When terminal.pay() reverts (e.g. due to a misconfigured terminal or paused payments),
1111
+ // the source fee is refunded to the borrower, resulting in an effectively interest-free loan for the
1112
+ // source fee portion. This is acceptable — it requires a broken/misconfigured source terminal and
1113
+ // the borrower still pays the REV fee and protocol fee.
1099
1114
  // slither-disable-next-line unused-return,arbitrary-send-eth
1100
1115
  try loan.source.terminal.pay{value: payValue}({
1101
1116
  projectId: revnetId,
@@ -1167,6 +1182,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
1167
1182
  revert REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(borrowAmount, loan.amount);
1168
1183
  }
1169
1184
 
1185
+ // Prevent the loan number from exceeding the ID namespace for this revnet.
1186
+ if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
1187
+
1170
1188
  // Get a reference to the replacement loan ID.
1171
1189
  reallocatedLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
1172
1190
 
@@ -1296,6 +1314,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
1296
1314
  return (loanId, paidOffSnapshot);
1297
1315
  } else {
1298
1316
  // Make a new loan with the remaining amount and collateral.
1317
+ // Prevent the loan number from exceeding the ID namespace for this revnet.
1318
+ if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
1319
+
1299
1320
  // Get a reference to the replacement loan ID.
1300
1321
  uint256 paidOffLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
1301
1322
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -138,7 +138,8 @@ contract REVDeployerRegressions is TestBaseWorkflow {
138
138
  assertEq(correctIndex, 0, "buyback hook should use index 0 when no tiered hook");
139
139
 
140
140
  // Write to the correct index (no revert)
141
- specs[correctIndex] = JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), amount: 1 ether, metadata: ""});
141
+ specs[correctIndex] =
142
+ JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), noop: false, amount: 1 ether, metadata: ""});
142
143
  }
143
144
 
144
145
  /// @notice Verify both hooks present works fine (no OOB).
@@ -150,8 +151,10 @@ contract REVDeployerRegressions is TestBaseWorkflow {
150
151
  assertEq(arraySize, 2, "array should be size 2");
151
152
 
152
153
  JBPayHookSpecification[] memory specs = new JBPayHookSpecification[](arraySize);
153
- specs[0] = JBPayHookSpecification({hook: IJBPayHook(address(0xdead)), amount: 1 ether, metadata: ""});
154
- specs[1] = JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), amount: 2 ether, metadata: ""});
154
+ specs[0] =
155
+ JBPayHookSpecification({hook: IJBPayHook(address(0xdead)), noop: false, amount: 1 ether, metadata: ""});
156
+ specs[1] =
157
+ JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), noop: false, amount: 2 ether, metadata: ""});
155
158
  }
156
159
 
157
160
  //*********************************************************************//
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -368,7 +368,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
368
368
 
369
369
  // Verify safe write
370
370
  JBPayHookSpecification[] memory specs = new JBPayHookSpecification[](arraySize);
371
- specs[correctIndex] = JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), amount: 1 ether, metadata: ""});
371
+ specs[correctIndex] =
372
+ JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), noop: false, amount: 1 ether, metadata: ""});
372
373
  }
373
374
 
374
375
  /// @notice Reentrancy — _adjust calls terminal.pay() BEFORE writing loan state.
@@ -909,12 +910,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
909
910
  // Record fee project balance before cash-out
910
911
  uint256 feeBalanceBefore;
911
912
  {
912
- JBAccountingContext[] memory feeCtx = new JBAccountingContext[](1);
913
- feeCtx[0] = JBAccountingContext({
914
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
915
- });
916
913
  feeBalanceBefore = jbMultiTerminal()
917
- .currentSurplusOf(FEE_PROJECT_ID, feeCtx, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
914
+ .currentSurplusOf(FEE_PROJECT_ID, new address[](0), 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
918
915
  }
919
916
 
920
917
  // Cash out
@@ -935,12 +932,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
935
932
  // because both the terminal fee AND the revnet fee route to it
936
933
  uint256 feeBalanceAfter;
937
934
  {
938
- JBAccountingContext[] memory feeCtx = new JBAccountingContext[](1);
939
- feeCtx[0] = JBAccountingContext({
940
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
941
- });
942
935
  feeBalanceAfter = jbMultiTerminal()
943
- .currentSurplusOf(FEE_PROJECT_ID, feeCtx, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
936
+ .currentSurplusOf(FEE_PROJECT_ID, new address[](0), 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
944
937
  }
945
938
 
946
939
  // Fee project should have received fees from the cash-out
@@ -1206,13 +1199,8 @@ contract REVInvincibility_Invariants is StdInvariant, TestBaseWorkflow {
1206
1199
  function invariant_REV_1_surplusCoversLoans() public {
1207
1200
  uint256 totalBorrowed = LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
1208
1201
 
1209
- JBAccountingContext[] memory ctxArray = new JBAccountingContext[](1);
1210
- ctxArray[0] = JBAccountingContext({
1211
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1212
- });
1213
-
1214
- uint256 storeBalance =
1215
- jbMultiTerminal().currentSurplusOf(REVNET_ID, ctxArray, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1202
+ uint256 storeBalance = jbMultiTerminal()
1203
+ .currentSurplusOf(REVNET_ID, new address[](0), 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1216
1204
 
1217
1205
  // Note: storeBalance is surplus (after payout limits), but the terminal holds at least this much
1218
1206
  // The total borrowed should not exceed what the terminal can cover
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -42,6 +42,8 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
42
42
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
43
43
  import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
44
44
  import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
45
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
46
+ import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
45
47
 
46
48
  /// @notice A malicious terminal that re-enters REVLoans during fee payment in _adjust().
47
49
  /// @dev Reentrancy during pay() callback in _adjust.
@@ -128,17 +130,7 @@ contract ReentrantTerminal is ERC165, IJBPayoutTerminal {
128
130
  override
129
131
  {}
130
132
 
131
- function currentSurplusOf(
132
- uint256,
133
- JBAccountingContext[] memory,
134
- uint256,
135
- uint256
136
- )
137
- external
138
- pure
139
- override
140
- returns (uint256)
141
- {
133
+ function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
142
134
  return 0;
143
135
  }
144
136
 
@@ -168,6 +160,22 @@ contract ReentrantTerminal is ERC165, IJBPayoutTerminal {
168
160
  return 0;
169
161
  }
170
162
 
163
+ function previewPayFor(
164
+ uint256,
165
+ address,
166
+ uint256,
167
+ address,
168
+ bytes calldata
169
+ )
170
+ external
171
+ pure
172
+ override
173
+ returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
174
+ {
175
+ JBRuleset memory ruleset;
176
+ return (ruleset, 0, 0, new JBPayHookSpecification[](0));
177
+ }
178
+
171
179
  function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
172
180
  return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
173
181
  || super.supportsInterface(interfaceId);
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -42,6 +42,8 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
42
42
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
43
43
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
44
44
  import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
45
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
46
+ import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
45
47
  import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
46
48
 
47
49
  /// @notice A terminal mock that always reverts on pay(), used to simulate fee payment failure.
@@ -87,17 +89,7 @@ contract RevertingFeeTerminal is ERC165, IJBPayoutTerminal {
87
89
  override
88
90
  {}
89
91
 
90
- function currentSurplusOf(
91
- uint256,
92
- JBAccountingContext[] memory,
93
- uint256,
94
- uint256
95
- )
96
- external
97
- pure
98
- override
99
- returns (uint256)
100
- {
92
+ function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
101
93
  return 0;
102
94
  }
103
95
 
@@ -127,6 +119,22 @@ contract RevertingFeeTerminal is ERC165, IJBPayoutTerminal {
127
119
  return 0;
128
120
  }
129
121
 
122
+ function previewPayFor(
123
+ uint256,
124
+ address,
125
+ uint256,
126
+ address,
127
+ bytes calldata
128
+ )
129
+ external
130
+ pure
131
+ override
132
+ returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
133
+ {
134
+ JBRuleset memory ruleset;
135
+ return (ruleset, 0, 0, new JBPayHookSpecification[](0));
136
+ }
137
+
130
138
  function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
131
139
  return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
132
140
  || super.supportsInterface(interfaceId);
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -42,6 +42,8 @@ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
42
42
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
43
43
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
44
44
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
45
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
46
+ import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
45
47
  import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
46
48
 
47
49
  /// @notice A fake terminal that returns garbage accounting contexts.
@@ -91,17 +93,7 @@ contract GarbageTerminal is ERC165, IJBPayoutTerminal {
91
93
  override
92
94
  {}
93
95
 
94
- function currentSurplusOf(
95
- uint256,
96
- JBAccountingContext[] memory,
97
- uint256,
98
- uint256
99
- )
100
- external
101
- pure
102
- override
103
- returns (uint256)
104
- {
96
+ function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
105
97
  return 0;
106
98
  }
107
99
 
@@ -130,6 +122,22 @@ contract GarbageTerminal is ERC165, IJBPayoutTerminal {
130
122
  return 0;
131
123
  }
132
124
 
125
+ function previewPayFor(
126
+ uint256,
127
+ address,
128
+ uint256,
129
+ address,
130
+ bytes calldata
131
+ )
132
+ external
133
+ pure
134
+ override
135
+ returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
136
+ {
137
+ JBRuleset memory ruleset;
138
+ return (ruleset, 0, 0, new JBPayHookSpecification[](0));
139
+ }
140
+
133
141
  function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
134
142
  return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
135
143
  || super.supportsInterface(interfaceId);
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -38,6 +38,8 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
38
38
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
39
39
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
40
40
  import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
41
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
42
+ import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
41
43
  import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
42
44
 
43
45
  /// @notice A fake terminal that tracks whether useAllowanceOf was called.
@@ -92,17 +94,7 @@ contract FakeTerminal is ERC165, IJBPayoutTerminal {
92
94
  override
93
95
  {}
94
96
 
95
- function currentSurplusOf(
96
- uint256,
97
- JBAccountingContext[] memory,
98
- uint256,
99
- uint256
100
- )
101
- external
102
- pure
103
- override
104
- returns (uint256)
105
- {
97
+ function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
106
98
  return 0;
107
99
  }
108
100
 
@@ -131,6 +123,22 @@ contract FakeTerminal is ERC165, IJBPayoutTerminal {
131
123
  return 0;
132
124
  }
133
125
 
126
+ function previewPayFor(
127
+ uint256,
128
+ address,
129
+ uint256,
130
+ address,
131
+ bytes calldata
132
+ )
133
+ external
134
+ pure
135
+ override
136
+ returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
137
+ {
138
+ JBRuleset memory ruleset;
139
+ return (ruleset, 0, 0, new JBPayHookSpecification[](0));
140
+ }
141
+
134
142
  function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
135
143
  return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
136
144
  || super.supportsInterface(interfaceId);
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";