@rev-net/core-v6 0.0.36 → 0.0.39

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 (101) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +6 -7
  3. package/foundry.toml +1 -1
  4. package/package.json +23 -16
  5. package/references/operations.md +1 -1
  6. package/references/runtime.md +1 -1
  7. package/script/Deploy.s.sol +12 -9
  8. package/src/REVDeployer.sol +60 -65
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +134 -90
  11. package/src/REVOwner.sol +124 -17
  12. package/src/interfaces/IREVDeployer.sol +2 -1
  13. package/src/interfaces/IREVHiddenTokens.sol +4 -1
  14. package/src/interfaces/IREVOwner.sol +5 -0
  15. package/ADMINISTRATION.md +0 -73
  16. package/ARCHITECTURE.md +0 -116
  17. package/AUDIT_INSTRUCTIONS.md +0 -90
  18. package/RISKS.md +0 -97
  19. package/SKILLS.md +0 -46
  20. package/STYLE_GUIDE.md +0 -610
  21. package/USER_JOURNEYS.md +0 -195
  22. package/foundry.lock +0 -11
  23. package/slither-ci.config.json +0 -10
  24. package/sphinx.lock +0 -507
  25. package/test/REV.integrations.t.sol +0 -573
  26. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  27. package/test/REVDeployerRegressions.t.sol +0 -396
  28. package/test/REVInvincibility.t.sol +0 -1371
  29. package/test/REVInvincibilityHandler.sol +0 -387
  30. package/test/REVLifecycle.t.sol +0 -420
  31. package/test/REVLoans.invariants.t.sol +0 -724
  32. package/test/REVLoansAttacks.t.sol +0 -816
  33. package/test/REVLoansFeeRecovery.t.sol +0 -783
  34. package/test/REVLoansFindings.t.sol +0 -711
  35. package/test/REVLoansRegressions.t.sol +0 -364
  36. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  37. package/test/REVLoansSourced.t.sol +0 -1839
  38. package/test/REVLoansUnSourced.t.sol +0 -409
  39. package/test/TestAuditFixVerification.t.sol +0 -675
  40. package/test/TestBurnHeldTokens.t.sol +0 -394
  41. package/test/TestCEIPattern.t.sol +0 -508
  42. package/test/TestCashOutCallerValidation.t.sol +0 -452
  43. package/test/TestConversionDocumentation.t.sol +0 -368
  44. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  45. package/test/TestCrossSourceReallocation.t.sol +0 -361
  46. package/test/TestERC2771MetaTx.t.sol +0 -585
  47. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  48. package/test/TestFlashLoanSurplus.t.sol +0 -365
  49. package/test/TestHiddenTokens.t.sol +0 -474
  50. package/test/TestHookArrayOOB.t.sol +0 -278
  51. package/test/TestLiquidationBehavior.t.sol +0 -398
  52. package/test/TestLoanSourceRotation.t.sol +0 -553
  53. package/test/TestLoansCashOutDelay.t.sol +0 -493
  54. package/test/TestLongTailEconomics.t.sol +0 -677
  55. package/test/TestLowFindings.t.sol +0 -677
  56. package/test/TestMixedFixes.t.sol +0 -593
  57. package/test/TestPermit2Signatures.t.sol +0 -683
  58. package/test/TestReallocationSandwich.t.sol +0 -412
  59. package/test/TestRevnetRegressions.t.sol +0 -350
  60. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  61. package/test/TestSplitWeightE2E.t.sol +0 -605
  62. package/test/TestSplitWeightFork.t.sol +0 -855
  63. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  64. package/test/TestSwapTerminalPermission.t.sol +0 -262
  65. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  66. package/test/TestUint112Overflow.t.sol +0 -311
  67. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  68. package/test/TestZeroRepayment.t.sol +0 -354
  69. package/test/audit/CodexCrossChainBuybackRouteMismatch.t.sol +0 -184
  70. package/test/audit/CodexPhantomSurplusTerminal.t.sol +0 -367
  71. package/test/audit/CodexREVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -142
  72. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  73. package/test/audit/NemesisOperatorDelegation.t.sol +0 -356
  74. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  75. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  76. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  77. package/test/fork/ForkTestBase.sol +0 -727
  78. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  79. package/test/fork/TestCashOutFork.t.sol +0 -253
  80. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  81. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  82. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  83. package/test/fork/TestLoanERC20Fork.t.sol +0 -465
  84. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  85. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  86. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  87. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  88. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  89. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  90. package/test/helpers/MaliciousContracts.sol +0 -247
  91. package/test/helpers/REVEmpty721Config.sol +0 -45
  92. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  93. package/test/mock/MockBuybackDataHook.sol +0 -112
  94. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  95. package/test/mock/MockSuckerRegistry.sol +0 -17
  96. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  97. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  98. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  99. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  100. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  101. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
package/src/REVLoans.sol CHANGED
@@ -359,10 +359,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
359
359
  // Get the total amount of tokens in circulation.
360
360
  uint256 totalSupply = CONTROLLER.totalTokenSupplyWithReservedTokensOf(revnetId);
361
361
 
362
- // Get a refeerence to the collateral being used to secure loans.
362
+ // Get a reference to the collateral being used to secure loans.
363
363
  uint256 totalCollateral = totalCollateralOf[revnetId];
364
364
 
365
- // The local supply includes both circulating tokens and tokens locked as loan collateral.
365
+ // Hidden tokens are intentionally excluded from borrowing math. Operators can hide tokens as a security
366
+ // handle without changing the fair loan market for visible token holders.
366
367
  uint256 localSupply = totalSupply + totalCollateral;
367
368
 
368
369
  // The local surplus includes both the treasury surplus and the outstanding borrowed amounts.
@@ -469,10 +470,13 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
469
470
  revert REVLoans_LoanExpired(timeSinceLoanCreated, LOAN_LIQUIDATION_DURATION);
470
471
  }
471
472
 
472
- // Get a reference to the amount prepaid for the full loan.
473
- uint256 prepaid = JBFees.feeAmountFrom({amountBeforeFee: loan.amount, feePercent: loan.prepaidFeePercent});
473
+ // Get a reference to the amount prepaid for the full loan. This is an app-level loan fee, so keep it
474
+ // floor-rounded instead of applying the protocol fee helper's dust minimum.
475
+ uint256 prepaid = JBFees.feeAmountFromFloor({amountBeforeFee: loan.amount, feePercent: loan.prepaidFeePercent});
474
476
 
475
- uint256 fullSourceFeeAmount = JBFees.feeAmountFrom({
477
+ // This source fee ramps with elapsed time. Use the floor-rounded fee helper so a one-second elapsed window
478
+ // with zero fee percent stays free instead of inheriting the protocol fee helper's anti-dust minimum.
479
+ uint256 fullSourceFeeAmount = JBFees.feeAmountFromFloor({
476
480
  amountBeforeFee: loan.amount - prepaid,
477
481
  feePercent: mulDiv({
478
482
  x: timeSinceLoanCreated - loan.prepaidDuration,
@@ -627,93 +631,15 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
627
631
  // Note: the operator controls `beneficiary`, so they can direct borrowed funds to any address.
628
632
  _requirePermissionFrom({account: holder, projectId: revnetId, permissionId: JBPermissionIds.OPEN_LOAN});
629
633
 
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,
634
+ return _borrowFrom({
706
635
  revnetId: revnetId,
707
- loan: loan,
708
636
  source: source,
709
- borrowAmount: borrowAmount,
637
+ minBorrowAmount: minBorrowAmount,
710
638
  collateralCount: collateralCount,
711
- sourceFeeAmount: sourceFeeAmount,
712
639
  beneficiary: beneficiary,
713
- caller: _msgSender()
640
+ prepaidFeePercent: prepaidFeePercent,
641
+ holder: holder
714
642
  });
715
-
716
- return (loanId, loan);
717
643
  }
718
644
 
719
645
  /// @notice Liquidates loans that have exceeded the 10-year liquidation duration.
@@ -838,7 +764,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
838
764
 
839
765
  // Make a new loan with the leftover collateral from reallocating.
840
766
  // The loan owner is the holder for the new loan (their tokens are used as collateral).
841
- (newLoanId, newLoan) = borrowFrom({
767
+ // Uses _borrowFrom to skip the OPEN_LOAN permission check — the caller already proved REALLOCATE_LOAN
768
+ // permission above, and requiring OPEN_LOAN here would block operators with only REALLOCATE_LOAN.
769
+ (newLoanId, newLoan) = _borrowFrom({
842
770
  revnetId: revnetId,
843
771
  source: source,
844
772
  minBorrowAmount: minBorrowAmount,
@@ -1093,10 +1021,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
1093
1021
  // Keep a reference to the fee terminal.
1094
1022
  IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: REV_ID, token: loan.source.token});
1095
1023
 
1096
- // Get the amount of additional fee to take for REV.
1024
+ // Get the amount of additional fee to take for REV. This is an app-level loan fee, not the terminal's
1025
+ // protocol fee, so keep it floor-rounded instead of applying the protocol fee helper's dust minimum.
1097
1026
  uint256 revFeeAmount = address(feeTerminal) == address(0)
1098
1027
  ? 0
1099
- : JBFees.feeAmountFrom({amountBeforeFee: addedBorrowAmount, feePercent: REV_PREPAID_FEE_PERCENT});
1028
+ : JBFees.feeAmountFromFloor({amountBeforeFee: addedBorrowAmount, feePercent: REV_PREPAID_FEE_PERCENT});
1100
1029
 
1101
1030
  // Try to pay the REV fee. If it fails, revFeeAmount is zeroed so the borrower receives it instead.
1102
1031
  if (revFeeAmount > 0) {
@@ -1234,6 +1163,121 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
1234
1163
  IERC20(token).forceApprove({spender: to, value: 0});
1235
1164
  }
1236
1165
 
1166
+ /// @notice Internal implementation of loan creation, without the OPEN_LOAN permission check.
1167
+ /// @dev Called by `borrowFrom` (after its own permission check) and by `reallocateCollateralFromLoan`
1168
+ /// (which only requires REALLOCATE_LOAN permission).
1169
+ /// @param revnetId The ID of the revnet being borrowed from.
1170
+ /// @param source The source of the loan being borrowed.
1171
+ /// @param minBorrowAmount The minimum amount being borrowed.
1172
+ /// @param collateralCount The amount of tokens to use as collateral for the loan.
1173
+ /// @param beneficiary The address that'll receive the borrowed funds and the tokens resulting from fee payments.
1174
+ /// @param prepaidFeePercent The fee percent that will be charged upfront.
1175
+ /// @param holder The address whose tokens are used as collateral and who receives the loan NFT.
1176
+ /// @return loanId The ID of the loan created from borrowing.
1177
+ /// @return loan The loan created from borrowing.
1178
+ function _borrowFrom(
1179
+ uint256 revnetId,
1180
+ REVLoanSource calldata source,
1181
+ uint256 minBorrowAmount,
1182
+ uint256 collateralCount,
1183
+ address payable beneficiary,
1184
+ uint256 prepaidFeePercent,
1185
+ address holder
1186
+ )
1187
+ internal
1188
+ returns (uint256 loanId, REVLoan memory)
1189
+ {
1190
+ // A loan needs to have collateral.
1191
+ if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
1192
+
1193
+ // Make sure the source terminal is registered in the directory for this revnet.
1194
+ if (!DIRECTORY.isTerminalOf({projectId: revnetId, terminal: IJBTerminal(address(source.terminal))})) {
1195
+ revert REVLoans_InvalidTerminal(address(source.terminal), revnetId);
1196
+ }
1197
+
1198
+ // Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
1199
+ // an 16 year loan can be paid upfront with a
1200
+ // payment of 50% of the borrowed assets, the cheapest possible rate.
1201
+ if (prepaidFeePercent < MIN_PREPAID_FEE_PERCENT || prepaidFeePercent > MAX_PREPAID_FEE_PERCENT) {
1202
+ revert REVLoans_InvalidPrepaidFeePercent(
1203
+ prepaidFeePercent, MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT
1204
+ );
1205
+ }
1206
+
1207
+ // Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
1208
+ JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
1209
+
1210
+ // Enforce the cash out delay.
1211
+ {
1212
+ uint256 cashOutDelay = _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
1213
+ if (cashOutDelay > block.timestamp) {
1214
+ revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
1215
+ }
1216
+ }
1217
+
1218
+ // Prevent the loan number from exceeding the ID namespace for this revnet.
1219
+ if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
1220
+
1221
+ // Get a reference to the loan ID.
1222
+ loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
1223
+
1224
+ // Get a reference to the loan being created.
1225
+ REVLoan storage loan = _loanOf[loanId];
1226
+
1227
+ // Set the loan's values.
1228
+ loan.source = source;
1229
+ loan.createdAt = uint48(block.timestamp);
1230
+ // forge-lint: disable-next-line(unsafe-typecast)
1231
+ loan.prepaidFeePercent = uint16(prepaidFeePercent);
1232
+ loan.prepaidDuration =
1233
+ uint32(mulDiv({x: prepaidFeePercent, y: LOAN_LIQUIDATION_DURATION, denominator: MAX_PREPAID_FEE_PERCENT}));
1234
+
1235
+ // Get the amount of the loan, using the cached ruleset.
1236
+ uint256 borrowAmount = _borrowAmountFrom({
1237
+ loan: loan, revnetId: revnetId, collateralCount: collateralCount, currentRuleset: currentRuleset
1238
+ });
1239
+
1240
+ // Revert if the bonding curve returns zero to prevent creating zero-amount loans.
1241
+ if (borrowAmount == 0) revert REVLoans_ZeroBorrowAmount();
1242
+
1243
+ // Make sure the minimum borrow amount is met.
1244
+ if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
1245
+
1246
+ // Get the amount of additional fee to take for the revnet issuing the loan. This is an app-level loan fee,
1247
+ // not the terminal's protocol fee, so keep it floor-rounded instead of applying the protocol fee helper's dust
1248
+ // minimum.
1249
+ uint256 sourceFeeAmount =
1250
+ JBFees.feeAmountFromFloor({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
1251
+
1252
+ // Borrow the amount.
1253
+ _adjust({
1254
+ loan: loan,
1255
+ revnetId: revnetId,
1256
+ newBorrowAmount: borrowAmount,
1257
+ newCollateralCount: collateralCount,
1258
+ sourceFeeAmount: sourceFeeAmount,
1259
+ beneficiary: beneficiary,
1260
+ holder: holder
1261
+ });
1262
+
1263
+ // Mint the loan NFT to the holder.
1264
+ _mint({to: holder, tokenId: loanId});
1265
+
1266
+ emit Borrow({
1267
+ loanId: loanId,
1268
+ revnetId: revnetId,
1269
+ loan: loan,
1270
+ source: source,
1271
+ borrowAmount: borrowAmount,
1272
+ collateralCount: collateralCount,
1273
+ sourceFeeAmount: sourceFeeAmount,
1274
+ beneficiary: beneficiary,
1275
+ caller: _msgSender()
1276
+ });
1277
+
1278
+ return (loanId, loan);
1279
+ }
1280
+
1237
1281
  /// @notice Reallocates collateral from a loan by making a new loan based on the original, with reduced collateral.
1238
1282
  /// @param loanId The ID of the loan to reallocate collateral from.
1239
1283
  /// @param revnetId The ID of the revnet the loan is from.
package/src/REVOwner.sol CHANGED
@@ -9,12 +9,14 @@ import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDa
9
9
  import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
10
10
  import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
11
11
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
12
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
12
13
  import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
13
14
  import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
14
15
  import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
15
16
  import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
16
17
  import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
17
18
  import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
19
+ import {IJBPeerChainAdjustedAccounts} from "@bananapus/suckers-v6/src/interfaces/IJBPeerChainAdjustedAccounts.sol";
18
20
  import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
19
21
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
20
22
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
@@ -22,11 +24,14 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
22
24
  import {mulDiv} from "@prb/math/src/Common.sol";
23
25
 
24
26
  import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
27
+ import {IREVHiddenTokens} from "./interfaces/IREVHiddenTokens.sol";
28
+ import {IREVLoans} from "./interfaces/IREVLoans.sol";
29
+ import {REVLoanSource} from "./structs/REVLoanSource.sol";
25
30
 
26
31
  /// @notice Handles the runtime data hook and cash out hook behavior for revnets.
27
32
  /// @dev Separated from `REVDeployer` to stay within the EIP-170 contract size limit.
28
33
  /// This contract is set as the `dataHook` in each revnet's ruleset metadata.
29
- contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
34
+ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAccounts {
30
35
  // A library that adds default safety checks to ERC20 functionality.
31
36
  using SafeERC20 for IERC20;
32
37
 
@@ -61,10 +66,10 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
61
66
  uint256 public immutable FEE_REVNET_ID;
62
67
 
63
68
  /// @notice The hidden tokens contract used by all revnets.
64
- address public immutable HIDDEN_TOKENS;
69
+ IREVHiddenTokens public immutable HIDDEN_TOKENS;
65
70
 
66
71
  /// @notice The loan contract used by all revnets.
67
- address public immutable LOANS;
72
+ IREVLoans public immutable LOANS;
68
73
 
69
74
  /// @notice Deploys and tracks suckers for revnets.
70
75
  IJBSuckerRegistry public immutable SUCKER_REGISTRY;
@@ -102,23 +107,21 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
102
107
  /// @param directory The directory of terminals and controllers.
103
108
  /// @param feeRevnetId The Juicebox project ID of the fee revnet.
104
109
  /// @param suckerRegistry The sucker registry.
105
- /// @param loans The loan contract address.
106
- /// @param hiddenTokens The hidden tokens contract address.
110
+ /// @param loans The loan contract.
111
+ /// @param hiddenTokens The hidden tokens contract.
107
112
  constructor(
108
113
  IJBBuybackHookRegistry buybackHook,
109
114
  IJBDirectory directory,
110
115
  uint256 feeRevnetId,
111
116
  IJBSuckerRegistry suckerRegistry,
112
- address loans,
113
- address hiddenTokens
117
+ IREVLoans loans,
118
+ IREVHiddenTokens hiddenTokens
114
119
  ) {
115
120
  BUYBACK_HOOK = buybackHook;
116
121
  DIRECTORY = directory;
117
122
  FEE_REVNET_ID = feeRevnetId;
118
123
  SUCKER_REGISTRY = suckerRegistry;
119
- // slither-disable-next-line missing-zero-check
120
124
  LOANS = loans;
121
- // slither-disable-next-line missing-zero-check
122
125
  HIDDEN_TOKENS = hiddenTokens;
123
126
  _DEPLOYER_BINDER = msg.sender;
124
127
  }
@@ -153,11 +156,23 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
153
156
  JBCashOutHookSpecification[] memory hookSpecifications
154
157
  )
155
158
  {
159
+ // Treat outstanding local loans as temporarily off-terminal revnet assets. Borrowed funds are owed back to
160
+ // the revnet, while burned loan collateral can be re-minted on repayment, so both affect fair cash-out math.
161
+ (uint256 totalBorrowed, uint256 totalCollateral) = _localLoanStateOf({
162
+ revnetId: context.projectId, decimals: context.surplus.decimals, currency: context.surplus.currency
163
+ });
164
+
156
165
  // If the cash out is from a sucker, return the full cash out amount without taxes or fees.
157
166
  // This relies on the sucker registry to only contain trusted sucker contracts deployed via
158
167
  // the registry's own deploySuckersFor flow — external addresses cannot register as suckers.
159
168
  if (_isSuckerOf({revnetId: context.projectId, addr: context.holder})) {
160
- return (0, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
169
+ return (
170
+ 0,
171
+ context.cashOutCount,
172
+ context.totalSupply + totalCollateral,
173
+ context.surplus.value + totalBorrowed,
174
+ hookSpecifications
175
+ );
161
176
  }
162
177
 
163
178
  // Keep a reference to the cash out delay of the revnet.
@@ -173,12 +188,12 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
173
188
 
174
189
  // Compute the cross-chain total supply (local + remote peer chain supplies) for cross-chain-aware bonding
175
190
  // curve.
176
- totalSupply = context.totalSupply + SUCKER_REGISTRY.remoteTotalSupplyOf(context.projectId);
177
- effectiveSurplusValue = context.surplus.value
191
+ totalSupply = context.totalSupply + totalCollateral + SUCKER_REGISTRY.remoteTotalSupplyOf(context.projectId);
192
+ effectiveSurplusValue = context.surplus.value + totalBorrowed
178
193
  + SUCKER_REGISTRY.remoteSurplusOf({
179
194
  projectId: context.projectId,
180
195
  decimals: context.surplus.decimals,
181
- currency: uint256(uint160(context.surplus.token))
196
+ currency: uint256(context.surplus.currency)
182
197
  });
183
198
 
184
199
  // If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
@@ -350,11 +365,35 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
350
365
  {
351
366
  // The loans contract, hidden tokens contract, buyback hook (and its delegates), and suckers are allowed to mint
352
367
  // the revnet's tokens.
353
- return addr == LOANS || addr == HIDDEN_TOKENS || addr == address(BUYBACK_HOOK)
368
+ return addr == address(LOANS) || addr == address(HIDDEN_TOKENS) || addr == address(BUYBACK_HOOK)
354
369
  || BUYBACK_HOOK.hasMintPermissionFor({projectId: revnetId, ruleset: ruleset, addr: addr})
355
370
  || _isSuckerOf({revnetId: revnetId, addr: addr});
356
371
  }
357
372
 
373
+ /// @notice Additional revnet accounts that peer-chain snapshots should include.
374
+ /// @dev Hidden tokens are intentionally excluded. Revnet operators can hide tokens as a security handle without
375
+ /// changing loan or cash-out math for other holders. Outstanding loan debt is counted as both surplus and balance:
376
+ /// it is value owed back to this chain's revnet and should travel to peer snapshots with the collateral supply.
377
+ /// @param revnetId The ID of the revnet being snapshotted.
378
+ /// @param decimals The decimals the returned surplus should use.
379
+ /// @param currency The currency the returned surplus should be in terms of.
380
+ /// @return supply The loan-collateral supply to include in the peer snapshot.
381
+ /// @return surplus The outstanding loan debt to include in `sourceSurplus`.
382
+ /// @return balance The outstanding loan debt to include in `sourceBalance`.
383
+ function peerChainAdjustedAccountsOf(
384
+ uint256 revnetId,
385
+ uint256 decimals,
386
+ uint256 currency
387
+ )
388
+ external
389
+ view
390
+ override
391
+ returns (uint256 supply, uint256 surplus, uint256 balance)
392
+ {
393
+ (surplus, supply) = _localLoanStateOf({revnetId: revnetId, decimals: decimals, currency: currency});
394
+ balance = surplus;
395
+ }
396
+
358
397
  //*********************************************************************//
359
398
  // --------------------- external transactions ----------------------- //
360
399
  //*********************************************************************//
@@ -397,8 +436,8 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
397
436
  if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
398
437
  IERC20(context.forwardedAmount.token)
399
438
  .safeDecreaseAllowance({
400
- spender: address(feeTerminal), requestedDecrease: context.forwardedAmount.value
401
- });
439
+ spender: address(feeTerminal), requestedDecrease: context.forwardedAmount.value
440
+ });
402
441
  }
403
442
 
404
443
  // If the fee can't be processed, return the funds to the project.
@@ -460,7 +499,8 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
460
499
  /// @return A flag indicating if the provided interface ID is supported.
461
500
  function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
462
501
  return interfaceId == type(IERC165).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
463
- || interfaceId == type(IJBCashOutHook).interfaceId;
502
+ || interfaceId == type(IJBCashOutHook).interfaceId
503
+ || interfaceId == type(IJBPeerChainAdjustedAccounts).interfaceId;
464
504
  }
465
505
 
466
506
  //*********************************************************************//
@@ -475,6 +515,73 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
475
515
  return SUCKER_REGISTRY.isSuckerOf({projectId: revnetId, addr: addr});
476
516
  }
477
517
 
518
+ /// @notice Total outstanding local loan debt and collateral for a revnet.
519
+ /// @dev This is included in cash-out and peer-snapshot math because borrowed funds are still owed to the revnet
520
+ /// and collateral can re-enter supply when the loan is repaid.
521
+ /// @param revnetId The ID of the revnet to check.
522
+ /// @param decimals The decimals the resulting fixed point debt value should use.
523
+ /// @param currency The currency the resulting debt value should be in terms of.
524
+ /// @return borrowedAmount The local outstanding loan debt converted into `currency`.
525
+ /// @return collateralCount The local burned loan collateral count.
526
+ function _localLoanStateOf(
527
+ uint256 revnetId,
528
+ uint256 decimals,
529
+ uint256 currency
530
+ )
531
+ internal
532
+ view
533
+ returns (uint256 borrowedAmount, uint256 collateralCount)
534
+ {
535
+ IREVLoans loans = LOANS;
536
+ if (address(loans) == address(0) || address(loans).code.length == 0) return (0, 0);
537
+
538
+ collateralCount = loans.totalCollateralOf(revnetId);
539
+
540
+ REVLoanSource[] memory sources = loans.loanSourcesOf(revnetId);
541
+ // Loan sources are project configuration, and this read-only aggregation needs the latest terminal/pricing
542
+ // state for each configured source.
543
+ for (uint256 i; i < sources.length; i++) {
544
+ REVLoanSource memory source = sources[i];
545
+ // Each configured source must be queried live so cash-out math includes current outstanding debt.
546
+ // slither-disable-next-line calls-loop
547
+ uint256 tokensLoaned =
548
+ loans.totalBorrowedFrom({revnetId: revnetId, terminal: source.terminal, token: source.token});
549
+ if (tokensLoaned == 0) continue;
550
+
551
+ // Read the source token's accounting context so debt can be normalized before cross-currency conversion.
552
+ // slither-disable-next-line calls-loop
553
+ JBAccountingContext memory accountingContext =
554
+ source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
555
+
556
+ // Normalize each source from its native token decimals into the caller's requested decimals.
557
+ uint256 normalizedTokens;
558
+ if (accountingContext.decimals > decimals) {
559
+ normalizedTokens = tokensLoaned / (10 ** (accountingContext.decimals - decimals));
560
+ } else if (accountingContext.decimals < decimals) {
561
+ normalizedTokens = tokensLoaned * (10 ** (decimals - accountingContext.decimals));
562
+ } else {
563
+ normalizedTokens = tokensLoaned;
564
+ }
565
+
566
+ if (accountingContext.currency == currency) {
567
+ borrowedAmount += normalizedTokens;
568
+ } else {
569
+ // Convert source-token debt into the requested currency using the loans contract's shared prices.
570
+ // slither-disable-next-line calls-loop
571
+ uint256 pricePerUnit = loans.PRICES()
572
+ .pricePerUnitOf({
573
+ projectId: revnetId,
574
+ pricingCurrency: accountingContext.currency,
575
+ unitCurrency: currency,
576
+ decimals: decimals
577
+ });
578
+ if (pricePerUnit == 0) continue;
579
+
580
+ borrowedAmount += mulDiv({x: normalizedTokens, y: 10 ** decimals, denominator: pricePerUnit});
581
+ }
582
+ }
583
+ }
584
+
478
585
  //*********************************************************************//
479
586
  // --------------------- internal transactions ----------------------- //
480
587
  //*********************************************************************//
@@ -16,6 +16,7 @@ import {REVConfig} from "../structs/REVConfig.sol";
16
16
  import {REVCroptopAllowedPost} from "../structs/REVCroptopAllowedPost.sol";
17
17
  import {REVDeploy721TiersHookConfig} from "../structs/REVDeploy721TiersHookConfig.sol";
18
18
  import {REVSuckerDeploymentConfig} from "../structs/REVSuckerDeploymentConfig.sol";
19
+ import {IREVLoans} from "./IREVLoans.sol";
19
20
 
20
21
  /// @notice Deploys and manages revnets -- Juicebox projects with pre-configured tokenomics.
21
22
  interface IREVDeployer {
@@ -143,7 +144,7 @@ interface IREVDeployer {
143
144
 
144
145
  /// @notice The loan contract used by all revnets.
145
146
  /// @return The loans contract address.
146
- function LOANS() external view returns (address);
147
+ function LOANS() external view returns (IREVLoans);
147
148
 
148
149
  /// @notice The runtime data hook contract that handles pay and cash out callbacks for revnets.
149
150
  /// @return The owner contract address.
@@ -3,7 +3,10 @@ pragma solidity ^0.8.0;
3
3
 
4
4
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
5
5
 
6
- /// @notice Manages hiding (burning) and revealing (re-minting) revnet tokens to exclude them from totalSupply.
6
+ /// @notice Manages hiding (burning) and revealing (re-minting) revnet tokens to exclude them from live totalSupply.
7
+ /// @dev Hidden balances are an operator-controlled security handle. They remain revealable, but cash-out and loan
8
+ /// accounting intentionally excludes `totalHiddenOf` so hidden inventory cannot dilute other holders' access to
9
+ /// revnet capital.
7
10
  interface IREVHiddenTokens {
8
11
  /// @notice Emitted when a holder is allowed or disallowed to hide their own tokens.
9
12
  /// @param revnetId The ID of the revnet.
@@ -2,9 +2,14 @@
2
2
  pragma solidity ^0.8.0;
3
3
 
4
4
  import {IREVDeployer} from "./IREVDeployer.sol";
5
+ import {IREVHiddenTokens} from "./IREVHiddenTokens.sol";
5
6
 
6
7
  /// @notice Interface for the REVOwner contract that handles runtime data hook and cash out hook behavior for revnets.
7
8
  interface IREVOwner {
9
+ /// @notice The hidden tokens contract used by the revnet owner hook.
10
+ /// @return The hidden tokens contract.
11
+ function HIDDEN_TOKENS() external view returns (IREVHiddenTokens);
12
+
8
13
  /// @notice The timestamp of when cashouts will become available to a specific revnet's participants.
9
14
  /// @param revnetId The ID of the revnet.
10
15
  /// @return The cash out delay timestamp.
package/ADMINISTRATION.md DELETED
@@ -1,73 +0,0 @@
1
- # Administration
2
-
3
- ## At A Glance
4
-
5
- | Item | Details |
6
- | --- | --- |
7
- | Scope | Revnet deployment shape, bounded runtime operators, loan-owner cosmetics, and optional integration control surfaces |
8
- | Control posture | Intentionally narrow and mostly deployment-defined |
9
- | Highest-risk actions | Bad stage design, wrong split-operator assignment, and misunderstanding which runtime surfaces stay live after launch |
10
- | Recovery posture | Usually replacement, not patching; the design intentionally avoids easy admin escape hatches |
11
-
12
- ## Purpose
13
-
14
- `revnet-core-v6` is designed to collapse ordinary post-launch governance into deployment-time decisions plus a small set of bounded runtime roles. The main administration task is understanding which power still exists and which power was intentionally removed.
15
-
16
- ## Control Model
17
-
18
- - `REVDeployer` holds the project NFT and therefore remains part of the ownership model.
19
- - Revnet economics are mainly fixed at deployment through staged rulesets.
20
- - `REVOwner` provides live runtime policy, but not broad human governance.
21
- - Split operators can hold narrow powers depending on stage and deployment config.
22
- - `REVLoans` has a cosmetic global owner surface, but loan economics are still bounded by revnet logic.
23
-
24
- ## Roles
25
-
26
- | Role | How Assigned | Scope | Notes |
27
- | --- | --- | --- | --- |
28
- | `REVDeployer` | Deployed singleton | Global launcher and project-NFT holder | Part of the ownership model |
29
- | Split operator | Deployment config | Per revnet | Holds only the allowed operator envelope |
30
- | Auto-issuance beneficiary | Deployment config | Per stage | Can receive preconfigured stage issuance |
31
- | Borrower or delegated loan operator | Token holder plus permission | Per holder or loan | Can open or manage loans within loan rules |
32
- | `REVLoans` owner | Constructor owner | Global cosmetic/admin surface | Does not turn Revnets back into ordinary governed projects |
33
-
34
- ## Privileged Surfaces
35
-
36
- - `deployFor(...)` defines the revnet's long-lived shape
37
- - split-operator paths can manage only the permissions left open by deployment
38
- - `autoIssueFor(...)` consumes preconfigured stage issuance
39
- - loan operators can redirect borrowed value if a holder delegates loan permissions
40
- - hidden-token flows require the holder's permission grant and mint permission wiring through `REVOwner`
41
-
42
- ## Immutable And One-Way
43
-
44
- - Stage configuration is effectively permanent after deployment.
45
- - The deployer-held project NFT is not a normal owner-recovery tool.
46
- - Loan collateral is burned at borrow time and only reminted through repayment or documented flows.
47
- - Hidden-token balances change visible supply until reveal.
48
-
49
- ## Operational Notes
50
-
51
- - Treat revnet launch as the real governance decision.
52
- - Validate stage timing, split-operator scope, and optional integrations before deployment.
53
- - Review cash-out delay, hidden-token semantics, and loan permissions together.
54
- - Do not assume there is a broad admin override for bad economics after launch.
55
-
56
- ## Machine Notes
57
-
58
- - Do not describe Revnets as fully adminless if the deployer-held NFT still matters for the trust model.
59
- - Also do not describe them as ordinary owner-controlled projects. The point is that the available control surface is intentionally narrow.
60
- - If a question is about runtime cash-outs, buybacks, or mint permissions, inspect `REVOwner` before inferring behavior from deployment prose.
61
-
62
- ## Recovery
63
-
64
- - If launch-time economics are wrong, recovery usually means replacement, not in-place repair.
65
- - If optional integrations are misconfigured, fix only where the code still exposes a valid path.
66
- - If the design intentionally omitted a recovery path, do not invent one in documentation or ops guidance.
67
-
68
- ## Admin Boundaries
69
-
70
- - No ordinary owner can casually rewrite staged economics after launch.
71
- - Split operators are not general-purpose governors.
72
- - Loan mechanics, hidden-token mechanics, and cash-out policy remain bounded by the deployed revnet logic.
73
- - This repo should not be documented as if it had a normal mutable project-owner model.