@rev-net/core-v6 0.0.36 → 0.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/RISKS.md +11 -1
  2. package/package.json +9 -9
  3. package/src/REVLoans.sol +120 -83
  4. package/src/REVOwner.sol +3 -3
  5. package/test/REV.integrations.t.sol +14 -14
  6. package/test/REVInvincibility.t.sol +16 -16
  7. package/test/REVLifecycle.t.sol +32 -32
  8. package/test/REVLoansSourced.t.sol +15 -15
  9. package/test/TestCashOutCallerValidation.t.sol +8 -8
  10. package/test/TestConversionDocumentation.t.sol +2 -5
  11. package/test/TestCrossCurrencyReclaim.t.sol +72 -72
  12. package/test/TestLongTailEconomics.t.sol +56 -56
  13. package/test/TestSwapTerminalPermission.t.sol +21 -21
  14. package/test/audit/HiddenSupplyCashout.t.sol +61 -0
  15. package/test/audit/NemesisVerification.t.sol +97 -0
  16. package/test/audit/REVOwnerCurrencyMismatch.t.sol +188 -0
  17. package/test/audit/{CodexREVOwnerRemoteSurplusCurrencyMismatch.t.sol → REVOwnerRemoteSurplusCurrencyMismatch.t.sol} +4 -6
  18. package/test/audit/ReallocatePermission.t.sol +363 -0
  19. package/test/audit/RemoteLoanAccountingGap.t.sol +74 -0
  20. package/test/fork/TestCashOutFork.t.sol +48 -48
  21. package/test/fork/TestLoanAdversarialFork.t.sol +744 -0
  22. package/test/fork/TestLoanERC20Fork.t.sol +2 -8
  23. package/test/fork/TestPermit2PaymentFork.t.sol +32 -32
  24. package/test/regression/TestBurnPermissionRequired.t.sol +5 -5
  25. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +8 -8
  26. /package/test/audit/{CodexCrossChainBuybackRouteMismatch.t.sol → CrossChainBuybackRouteMismatch.t.sol} +0 -0
  27. /package/test/audit/{NemesisOperatorDelegation.t.sol → OperatorDelegation.t.sol} +0 -0
  28. /package/test/audit/{CodexPhantomSurplusTerminal.t.sol → PhantomSurplusTerminal.t.sol} +0 -0
package/RISKS.md CHANGED
@@ -27,7 +27,7 @@ This file focuses on the staged-economics, runtime-hook, hidden-supply, and loan
27
27
 
28
28
  - **Stage immutability cuts both ways.** A bad stage schedule or bad cash-out tax choice is expensive to unwind.
29
29
  - **Borrowability depends on live economics.** If surplus, supply, or cross-chain state are wrong, loan capacity becomes wrong.
30
- - **Zero or degraded price feeds can undercount debt.** If a source becomes invisible to debt aggregation, later borrowing can become too permissive.
30
+ - **Zero or degraded price feeds can undercount debt.** If a source becomes invisible to debt aggregation, later borrowing can become too permissive. Specifically, `_debtOf` skips sources where `pricePerUnitOf` returns zero, treating them as if the borrower has no debt in that source. If a feed breaks or returns zero, existing debt in that currency is effectively invisible, inflating the borrower's apparent borrowable amount.
31
31
  - **Hidden-token mechanics change visible supply.** That affects per-token cash-out value and can change the economics seen by other holders.
32
32
  - **Auto-issuance dilutes holders predictably but still materially.** Timing is permissionless, even if the amounts are fixed at deployment.
33
33
  - **Omnichain expansion can corrupt surplus aggregation.** Since borrowability aggregates surplus from all registered terminals across chains, a compromised or misconfigured terminal on a remote chain affects global surplus accounting.
@@ -95,3 +95,13 @@ A project that expands to a new chain can register additional terminals on that
95
95
  ### 8.7 REVLoans CEI violation in `_adjust`
96
96
 
97
97
  In `REVLoans._adjust`, `totalCollateralOf[revnetId]` is incremented after external calls (`useAllowanceOf`, fee payment). A reentrant `borrowFrom` would see a lower `totalCollateralOf`. This is documented inline (lines 1128-1132) and requires an adversarial pay hook on the revnet's own terminal -- a trust-level configuration that is not realistic in standard deployments.
98
+
99
+ ### 8.8 Remote loan corrections not reflected in local borrowability
100
+
101
+ `_borrowableAmountFrom` adds back local `totalBorrowed` and `totalCollateral` to reconstitute pre-loan economic state for the bonding curve. However, remote chain snapshots (built by `JBSuckerLib.buildSnapshotMessage`) capture raw surplus/supply WITHOUT loan corrections from the remote chain. This is accepted because:
102
+
103
+ 1. Suckers are a general-purpose bridging layer and should not need knowledge of revnet-specific loan mechanics.
104
+ 2. The `localSurplus` cap (REVLoans line 386-387) prevents extraction beyond what the local terminal actually holds.
105
+ 3. The over-lending exposure is bounded by the difference between corrected and uncorrected remote values, which is proportional to remote outstanding loans — typically a small fraction of total surplus.
106
+
107
+ Project operators deploying cross-chain revnets with active loan markets on multiple chains should understand that local borrowability calculations do not account for remote outstanding loans.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rev-net/core-v6",
3
- "version": "0.0.36",
3
+ "version": "0.0.37",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,14 +19,14 @@
19
19
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
20
20
  },
21
21
  "dependencies": {
22
- "@bananapus/721-hook-v6": "^0.0.35",
23
- "@bananapus/buyback-hook-v6": "^0.0.27",
24
- "@bananapus/core-v6": "^0.0.34",
25
- "@bananapus/ownable-v6": "^0.0.17",
26
- "@bananapus/permission-ids-v6": "^0.0.17",
27
- "@bananapus/router-terminal-v6": "^0.0.26",
28
- "@bananapus/suckers-v6": "^0.0.25",
29
- "@croptop/core-v6": "github:mejango/croptop-core-v6",
22
+ "@bananapus/721-hook-v6": "^0.0.38",
23
+ "@bananapus/buyback-hook-v6": "^0.0.30",
24
+ "@bananapus/core-v6": "^0.0.36",
25
+ "@bananapus/ownable-v6": "^0.0.20",
26
+ "@bananapus/permission-ids-v6": "^0.0.19",
27
+ "@bananapus/router-terminal-v6": "^0.0.30",
28
+ "@bananapus/suckers-v6": "^0.0.28",
29
+ "@croptop/core-v6": "^0.0.36",
30
30
  "@openzeppelin/contracts": "^5.6.1",
31
31
  "@uniswap/v4-core": "^1.0.2",
32
32
  "@uniswap/v4-periphery": "^1.0.3"
package/src/REVLoans.sol CHANGED
@@ -627,93 +627,15 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
627
627
  // Note: the operator controls `beneficiary`, so they can direct borrowed funds to any address.
628
628
  _requirePermissionFrom({account: holder, projectId: revnetId, permissionId: JBPermissionIds.OPEN_LOAN});
629
629
 
630
- // A loan needs to have collateral.
631
- if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
632
-
633
- // Make sure the source terminal is registered in the directory for this revnet.
634
- if (!DIRECTORY.isTerminalOf({projectId: revnetId, terminal: IJBTerminal(address(source.terminal))})) {
635
- revert REVLoans_InvalidTerminal(address(source.terminal), revnetId);
636
- }
637
-
638
- // Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
639
- // an 16 year loan can be paid upfront with a
640
- // payment of 50% of the borrowed assets, the cheapest possible rate.
641
- if (prepaidFeePercent < MIN_PREPAID_FEE_PERCENT || prepaidFeePercent > MAX_PREPAID_FEE_PERCENT) {
642
- revert REVLoans_InvalidPrepaidFeePercent(
643
- prepaidFeePercent, MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT
644
- );
645
- }
646
-
647
- // Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
648
- JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
649
-
650
- // Enforce the cash out delay.
651
- {
652
- uint256 cashOutDelay = _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
653
- if (cashOutDelay > block.timestamp) {
654
- revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
655
- }
656
- }
657
-
658
- // Prevent the loan number from exceeding the ID namespace for this revnet.
659
- if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
660
-
661
- // Get a reference to the loan ID.
662
- loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
663
-
664
- // Get a reference to the loan being created.
665
- REVLoan storage loan = _loanOf[loanId];
666
-
667
- // Set the loan's values.
668
- loan.source = source;
669
- loan.createdAt = uint48(block.timestamp);
670
- // forge-lint: disable-next-line(unsafe-typecast)
671
- loan.prepaidFeePercent = uint16(prepaidFeePercent);
672
- loan.prepaidDuration =
673
- uint32(mulDiv({x: prepaidFeePercent, y: LOAN_LIQUIDATION_DURATION, denominator: MAX_PREPAID_FEE_PERCENT}));
674
-
675
- // Get the amount of the loan, using the cached ruleset.
676
- uint256 borrowAmount = _borrowAmountFrom({
677
- loan: loan, revnetId: revnetId, collateralCount: collateralCount, currentRuleset: currentRuleset
678
- });
679
-
680
- // Revert if the bonding curve returns zero to prevent creating zero-amount loans.
681
- if (borrowAmount == 0) revert REVLoans_ZeroBorrowAmount();
682
-
683
- // Make sure the minimum borrow amount is met.
684
- if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
685
-
686
- // Get the amount of additional fee to take for the revnet issuing the loan.
687
- // Fee rounding may leave a few wei of dust — economically insignificant relative to gas costs.
688
- uint256 sourceFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
689
-
690
- // Borrow the amount.
691
- _adjust({
692
- loan: loan,
693
- revnetId: revnetId,
694
- newBorrowAmount: borrowAmount,
695
- newCollateralCount: collateralCount,
696
- sourceFeeAmount: sourceFeeAmount,
697
- beneficiary: beneficiary,
698
- holder: holder
699
- });
700
-
701
- // Mint the loan NFT to the holder.
702
- _mint({to: holder, tokenId: loanId});
703
-
704
- emit Borrow({
705
- loanId: loanId,
630
+ return _borrowFrom({
706
631
  revnetId: revnetId,
707
- loan: loan,
708
632
  source: source,
709
- borrowAmount: borrowAmount,
633
+ minBorrowAmount: minBorrowAmount,
710
634
  collateralCount: collateralCount,
711
- sourceFeeAmount: sourceFeeAmount,
712
635
  beneficiary: beneficiary,
713
- caller: _msgSender()
636
+ prepaidFeePercent: prepaidFeePercent,
637
+ holder: holder
714
638
  });
715
-
716
- return (loanId, loan);
717
639
  }
718
640
 
719
641
  /// @notice Liquidates loans that have exceeded the 10-year liquidation duration.
@@ -838,7 +760,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
838
760
 
839
761
  // Make a new loan with the leftover collateral from reallocating.
840
762
  // The loan owner is the holder for the new loan (their tokens are used as collateral).
841
- (newLoanId, newLoan) = borrowFrom({
763
+ // Uses _borrowFrom to skip the OPEN_LOAN permission check — the caller already proved REALLOCATE_LOAN
764
+ // permission above, and requiring OPEN_LOAN here would block operators with only REALLOCATE_LOAN.
765
+ (newLoanId, newLoan) = _borrowFrom({
842
766
  revnetId: revnetId,
843
767
  source: source,
844
768
  minBorrowAmount: minBorrowAmount,
@@ -1234,6 +1158,119 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
1234
1158
  IERC20(token).forceApprove({spender: to, value: 0});
1235
1159
  }
1236
1160
 
1161
+ /// @notice Internal implementation of loan creation, without the OPEN_LOAN permission check.
1162
+ /// @dev Called by `borrowFrom` (after its own permission check) and by `reallocateCollateralFromLoan`
1163
+ /// (which only requires REALLOCATE_LOAN permission).
1164
+ /// @param revnetId The ID of the revnet being borrowed from.
1165
+ /// @param source The source of the loan being borrowed.
1166
+ /// @param minBorrowAmount The minimum amount being borrowed.
1167
+ /// @param collateralCount The amount of tokens to use as collateral for the loan.
1168
+ /// @param beneficiary The address that'll receive the borrowed funds and the tokens resulting from fee payments.
1169
+ /// @param prepaidFeePercent The fee percent that will be charged upfront.
1170
+ /// @param holder The address whose tokens are used as collateral and who receives the loan NFT.
1171
+ /// @return loanId The ID of the loan created from borrowing.
1172
+ /// @return loan The loan created from borrowing.
1173
+ function _borrowFrom(
1174
+ uint256 revnetId,
1175
+ REVLoanSource calldata source,
1176
+ uint256 minBorrowAmount,
1177
+ uint256 collateralCount,
1178
+ address payable beneficiary,
1179
+ uint256 prepaidFeePercent,
1180
+ address holder
1181
+ )
1182
+ internal
1183
+ returns (uint256 loanId, REVLoan memory)
1184
+ {
1185
+ // A loan needs to have collateral.
1186
+ if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
1187
+
1188
+ // Make sure the source terminal is registered in the directory for this revnet.
1189
+ if (!DIRECTORY.isTerminalOf({projectId: revnetId, terminal: IJBTerminal(address(source.terminal))})) {
1190
+ revert REVLoans_InvalidTerminal(address(source.terminal), revnetId);
1191
+ }
1192
+
1193
+ // Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
1194
+ // an 16 year loan can be paid upfront with a
1195
+ // payment of 50% of the borrowed assets, the cheapest possible rate.
1196
+ if (prepaidFeePercent < MIN_PREPAID_FEE_PERCENT || prepaidFeePercent > MAX_PREPAID_FEE_PERCENT) {
1197
+ revert REVLoans_InvalidPrepaidFeePercent(
1198
+ prepaidFeePercent, MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT
1199
+ );
1200
+ }
1201
+
1202
+ // Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
1203
+ JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
1204
+
1205
+ // Enforce the cash out delay.
1206
+ {
1207
+ uint256 cashOutDelay = _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
1208
+ if (cashOutDelay > block.timestamp) {
1209
+ revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
1210
+ }
1211
+ }
1212
+
1213
+ // Prevent the loan number from exceeding the ID namespace for this revnet.
1214
+ if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
1215
+
1216
+ // Get a reference to the loan ID.
1217
+ loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
1218
+
1219
+ // Get a reference to the loan being created.
1220
+ REVLoan storage loan = _loanOf[loanId];
1221
+
1222
+ // Set the loan's values.
1223
+ loan.source = source;
1224
+ loan.createdAt = uint48(block.timestamp);
1225
+ // forge-lint: disable-next-line(unsafe-typecast)
1226
+ loan.prepaidFeePercent = uint16(prepaidFeePercent);
1227
+ loan.prepaidDuration =
1228
+ uint32(mulDiv({x: prepaidFeePercent, y: LOAN_LIQUIDATION_DURATION, denominator: MAX_PREPAID_FEE_PERCENT}));
1229
+
1230
+ // Get the amount of the loan, using the cached ruleset.
1231
+ uint256 borrowAmount = _borrowAmountFrom({
1232
+ loan: loan, revnetId: revnetId, collateralCount: collateralCount, currentRuleset: currentRuleset
1233
+ });
1234
+
1235
+ // Revert if the bonding curve returns zero to prevent creating zero-amount loans.
1236
+ if (borrowAmount == 0) revert REVLoans_ZeroBorrowAmount();
1237
+
1238
+ // Make sure the minimum borrow amount is met.
1239
+ if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
1240
+
1241
+ // Get the amount of additional fee to take for the revnet issuing the loan.
1242
+ // Fee rounding may leave a few wei of dust — economically insignificant relative to gas costs.
1243
+ uint256 sourceFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
1244
+
1245
+ // Borrow the amount.
1246
+ _adjust({
1247
+ loan: loan,
1248
+ revnetId: revnetId,
1249
+ newBorrowAmount: borrowAmount,
1250
+ newCollateralCount: collateralCount,
1251
+ sourceFeeAmount: sourceFeeAmount,
1252
+ beneficiary: beneficiary,
1253
+ holder: holder
1254
+ });
1255
+
1256
+ // Mint the loan NFT to the holder.
1257
+ _mint({to: holder, tokenId: loanId});
1258
+
1259
+ emit Borrow({
1260
+ loanId: loanId,
1261
+ revnetId: revnetId,
1262
+ loan: loan,
1263
+ source: source,
1264
+ borrowAmount: borrowAmount,
1265
+ collateralCount: collateralCount,
1266
+ sourceFeeAmount: sourceFeeAmount,
1267
+ beneficiary: beneficiary,
1268
+ caller: _msgSender()
1269
+ });
1270
+
1271
+ return (loanId, loan);
1272
+ }
1273
+
1237
1274
  /// @notice Reallocates collateral from a loan by making a new loan based on the original, with reduced collateral.
1238
1275
  /// @param loanId The ID of the loan to reallocate collateral from.
1239
1276
  /// @param revnetId The ID of the revnet the loan is from.
package/src/REVOwner.sol CHANGED
@@ -178,7 +178,7 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
178
178
  + SUCKER_REGISTRY.remoteSurplusOf({
179
179
  projectId: context.projectId,
180
180
  decimals: context.surplus.decimals,
181
- currency: uint256(uint160(context.surplus.token))
181
+ currency: uint256(context.surplus.currency)
182
182
  });
183
183
 
184
184
  // If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
@@ -397,8 +397,8 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
397
397
  if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
398
398
  IERC20(context.forwardedAmount.token)
399
399
  .safeDecreaseAllowance({
400
- spender: address(feeTerminal), requestedDecrease: context.forwardedAmount.value
401
- });
400
+ spender: address(feeTerminal), requestedDecrease: context.forwardedAmount.value
401
+ });
402
402
  }
403
403
 
404
404
  // If the fee can't be processed, return the funds to the project.
@@ -531,25 +531,25 @@ contract REVnet_Integrations is TestBaseWorkflow {
531
531
  // The loans contract should have USE_ALLOWANCE permission for any revnet via the wildcard grant.
532
532
  bool hasPermission = jbPermissions()
533
533
  .hasPermission({
534
- operator: address(REV_DEPLOYER.LOANS()),
535
- account: address(REV_DEPLOYER),
536
- projectId: REVNET_ID,
537
- permissionId: JBPermissionIds.USE_ALLOWANCE,
538
- includeRoot: false,
539
- includeWildcardProjectId: true
540
- });
534
+ operator: address(REV_DEPLOYER.LOANS()),
535
+ account: address(REV_DEPLOYER),
536
+ projectId: REVNET_ID,
537
+ permissionId: JBPermissionIds.USE_ALLOWANCE,
538
+ includeRoot: false,
539
+ includeWildcardProjectId: true
540
+ });
541
541
  assertTrue(hasPermission, "LOANS should have USE_ALLOWANCE for deployed revnet");
542
542
 
543
543
  // Also holds for a revnet that doesn't exist yet — the wildcard covers all projects.
544
544
  bool hasPermissionForFuture = jbPermissions()
545
545
  .hasPermission({
546
- operator: address(REV_DEPLOYER.LOANS()),
547
- account: address(REV_DEPLOYER),
548
- projectId: 999,
549
- permissionId: JBPermissionIds.USE_ALLOWANCE,
550
- includeRoot: false,
551
- includeWildcardProjectId: true
552
- });
546
+ operator: address(REV_DEPLOYER.LOANS()),
547
+ account: address(REV_DEPLOYER),
548
+ projectId: 999,
549
+ permissionId: JBPermissionIds.USE_ALLOWANCE,
550
+ includeRoot: false,
551
+ includeWildcardProjectId: true
552
+ });
553
553
  assertTrue(hasPermissionForFuture, "LOANS should have USE_ALLOWANCE for any project via wildcard");
554
554
  }
555
555
 
@@ -692,14 +692,14 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
692
692
  vm.prank(USER);
693
693
  try jbMultiTerminal()
694
694
  .cashOutTokensOf({
695
- holder: USER,
696
- projectId: REVNET_ID,
697
- cashOutCount: tokens,
698
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
699
- minTokensReclaimed: 0,
700
- beneficiary: payable(USER),
701
- metadata: ""
702
- }) returns (
695
+ holder: USER,
696
+ projectId: REVNET_ID,
697
+ cashOutCount: tokens,
698
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
699
+ minTokensReclaimed: 0,
700
+ beneficiary: payable(USER),
701
+ metadata: ""
702
+ }) returns (
703
703
  uint256 reclaimAmount
704
704
  ) {
705
705
  // The reclaim amount should be bounded by the bonding curve
@@ -944,14 +944,14 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
944
944
  vm.prank(USER);
945
945
  try jbMultiTerminal()
946
946
  .cashOutTokensOf({
947
- holder: USER,
948
- projectId: REVNET_ID,
949
- cashOutCount: tokens / 2,
950
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
951
- minTokensReclaimed: 0,
952
- beneficiary: payable(USER),
953
- metadata: ""
954
- }) returns (
947
+ holder: USER,
948
+ projectId: REVNET_ID,
949
+ cashOutCount: tokens / 2,
950
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
951
+ minTokensReclaimed: 0,
952
+ beneficiary: payable(USER),
953
+ metadata: ""
954
+ }) returns (
955
955
  uint256 reclaimAmount
956
956
  ) {
957
957
  // The double fee means the fee project gets more than expected
@@ -265,14 +265,14 @@ contract REVLifecycle_Local is TestBaseWorkflow {
265
265
  vm.prank(USER1);
266
266
  uint256 reclaimed = jbMultiTerminal()
267
267
  .cashOutTokensOf({
268
- holder: USER1,
269
- projectId: REVNET_ID,
270
- cashOutCount: cashOutAmount,
271
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
272
- minTokensReclaimed: 0,
273
- beneficiary: payable(USER1),
274
- metadata: ""
275
- });
268
+ holder: USER1,
269
+ projectId: REVNET_ID,
270
+ cashOutCount: cashOutAmount,
271
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
272
+ minTokensReclaimed: 0,
273
+ beneficiary: payable(USER1),
274
+ metadata: ""
275
+ });
276
276
  assertGt(reclaimed, 0, "should reclaim some ETH");
277
277
 
278
278
  // Total supply should decrease after cash out
@@ -311,14 +311,14 @@ contract REVLifecycle_Local is TestBaseWorkflow {
311
311
  vm.prank(USER1);
312
312
  uint256 reclaimedStage0 = jbMultiTerminal()
313
313
  .cashOutTokensOf({
314
- holder: USER1,
315
- projectId: REVNET_ID,
316
- cashOutCount: halfTokens,
317
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
318
- minTokensReclaimed: 0,
319
- beneficiary: payable(USER1),
320
- metadata: ""
321
- });
314
+ holder: USER1,
315
+ projectId: REVNET_ID,
316
+ cashOutCount: halfTokens,
317
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
318
+ minTokensReclaimed: 0,
319
+ beneficiary: payable(USER1),
320
+ metadata: ""
321
+ });
322
322
  assertGt(reclaimedStage0, 0, "should reclaim in stage 0");
323
323
 
324
324
  // Cash out tax with 50% rate means you get less than proportional share
@@ -344,14 +344,14 @@ contract REVLifecycle_Local is TestBaseWorkflow {
344
344
  vm.prank(USER1);
345
345
  uint256 reclaimed = jbMultiTerminal()
346
346
  .cashOutTokensOf({
347
- holder: USER1,
348
- projectId: REVNET_ID,
349
- cashOutCount: tokens,
350
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
351
- minTokensReclaimed: 0,
352
- beneficiary: payable(USER1),
353
- metadata: ""
354
- });
347
+ holder: USER1,
348
+ projectId: REVNET_ID,
349
+ cashOutCount: tokens,
350
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
351
+ minTokensReclaimed: 0,
352
+ beneficiary: payable(USER1),
353
+ metadata: ""
354
+ });
355
355
 
356
356
  // With 50% cash out tax and single holder, reclaiming full supply
357
357
  // should return less than full amount (due to tax)
@@ -386,14 +386,14 @@ contract REVLifecycle_Local is TestBaseWorkflow {
386
386
  vm.prank(USER1);
387
387
  uint256 reclaimed1 = jbMultiTerminal()
388
388
  .cashOutTokensOf({
389
- holder: USER1,
390
- projectId: REVNET_ID,
391
- cashOutCount: tokens1,
392
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
393
- minTokensReclaimed: 0,
394
- beneficiary: payable(USER1),
395
- metadata: ""
396
- });
389
+ holder: USER1,
390
+ projectId: REVNET_ID,
391
+ cashOutCount: tokens1,
392
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
393
+ minTokensReclaimed: 0,
394
+ beneficiary: payable(USER1),
395
+ metadata: ""
396
+ });
397
397
 
398
398
  // Should reclaim proportional share (minus tax)
399
399
  assertGt(reclaimed1, 0, "user1 should reclaim some ETH");
@@ -751,11 +751,11 @@ contract REVLoansSourcedTests is TestBaseWorkflow {
751
751
 
752
752
  uint256 fullReclaimableSurplus = jbMultiTerminal().STORE()
753
753
  .currentReclaimableSurplusOf({
754
- projectId: revnetProjectId,
755
- cashOutCount: tokensToCashout,
756
- totalSupply: totalSupplyExcludingAutoMint,
757
- surplus: nativeSurplus
758
- });
754
+ projectId: revnetProjectId,
755
+ cashOutCount: tokensToCashout,
756
+ totalSupply: totalSupplyExcludingAutoMint,
757
+ surplus: nativeSurplus
758
+ });
759
759
 
760
760
  assertGe(fullReclaimableSurplus, loanable);
761
761
 
@@ -764,11 +764,11 @@ contract REVLoansSourcedTests is TestBaseWorkflow {
764
764
 
765
765
  uint256 reclaimableSurplus = jbMultiTerminal().STORE()
766
766
  .currentReclaimableSurplusOf({
767
- projectId: revnetProjectId,
768
- cashOutCount: tokensToCashout - feeTokenCount,
769
- totalSupply: totalSupplyExcludingAutoMint,
770
- surplus: nativeSurplus
771
- });
767
+ projectId: revnetProjectId,
768
+ cashOutCount: tokensToCashout - feeTokenCount,
769
+ totalSupply: totalSupplyExcludingAutoMint,
770
+ surplus: nativeSurplus
771
+ });
772
772
 
773
773
  // In the `revFee` calculation we decrease the `nativeSurplus` by the `reclaimableSurplus`
774
774
  // but due to a `stack too deep` we can't do that there, so we decrease it here.
@@ -777,11 +777,11 @@ contract REVLoansSourcedTests is TestBaseWorkflow {
777
777
 
778
778
  uint256 revFee = jbMultiTerminal().STORE()
779
779
  .currentReclaimableSurplusOf({
780
- projectId: revnetProjectId,
781
- cashOutCount: feeTokenCount,
782
- totalSupply: totalSupplyExcludingAutoMint - (tokensToCashout - feeTokenCount),
783
- surplus: nativeSurplus
784
- });
780
+ projectId: revnetProjectId,
781
+ cashOutCount: feeTokenCount,
782
+ totalSupply: totalSupplyExcludingAutoMint - (tokensToCashout - feeTokenCount),
783
+ surplus: nativeSurplus
784
+ });
785
785
 
786
786
  assertGe(fullReclaimableSurplus, mulDiv((reclaimableSurplus + revFee), 995, 1000)); // small marging for curve
787
787
  // rounding.
@@ -315,14 +315,14 @@ contract TestCashOutCallerValidation is TestBaseWorkflow {
315
315
  vm.prank(USER);
316
316
  uint256 reclaimed = jbMultiTerminal()
317
317
  .cashOutTokensOf({
318
- holder: USER,
319
- projectId: REVNET_ID,
320
- cashOutCount: tokenCount,
321
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
322
- minTokensReclaimed: 0,
323
- beneficiary: payable(USER),
324
- metadata: ""
325
- });
318
+ holder: USER,
319
+ projectId: REVNET_ID,
320
+ cashOutCount: tokenCount,
321
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
322
+ minTokensReclaimed: 0,
323
+ beneficiary: payable(USER),
324
+ metadata: ""
325
+ });
326
326
 
327
327
  assertGt(reclaimed, 0, "Should have reclaimed some ETH");
328
328
 
@@ -286,11 +286,8 @@ contract TestConversionDocumentation is TestBaseWorkflow {
286
286
  vm.prank(USER);
287
287
  jbController()
288
288
  .launchRulesetsFor({
289
- projectId: projectId,
290
- rulesetConfigurations: rulesetConfigs,
291
- terminalConfigurations: termConfigs,
292
- memo: ""
293
- });
289
+ projectId: projectId, rulesetConfigurations: rulesetConfigs, terminalConfigurations: termConfigs, memo: ""
290
+ });
294
291
 
295
292
  // Now try to convert this project to a revnet — should revert.
296
293
  // Approve NFT to REV_DEPLOYER.