@rev-net/core-v6 0.0.57 → 0.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/REVOwner.sol CHANGED
@@ -26,7 +26,6 @@ import {mulDiv} from "@prb/math/src/Common.sol";
26
26
 
27
27
  import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
28
28
  import {IREVLoans} from "./interfaces/IREVLoans.sol";
29
- import {REVLoanSource} from "./structs/REVLoanSource.sol";
30
29
 
31
30
  /// @notice The runtime hook for all revnets — set as every revnet's `dataHook` in ruleset metadata. At pay time, it
32
31
  /// coordinates the 721 hook (NFT tier minting) with the buyback hook (secondary market swap routing) and scales weight
@@ -45,6 +44,8 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
45
44
 
46
45
  error REVOwner_AlreadyInitialized(address deployer);
47
46
  error REVOwner_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
47
+ error REVOwner_InvalidLoanSourceToken(uint256 revnetId, address token);
48
+ error REVOwner_NativeFeeValueMismatch(uint256 expected, uint256 actual);
48
49
  error REVOwner_Unauthorized(address caller, address expectedCaller);
49
50
 
50
51
  //*********************************************************************//
@@ -81,7 +82,7 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
81
82
 
82
83
  /// @notice The deployer that manages revnet state.
83
84
  /// @dev Set once via `setDeployer()` using the precomputed canonical REVDeployer address.
84
- IREVDeployer public DEPLOYER;
85
+ IREVDeployer public deployer;
85
86
 
86
87
  //*********************************************************************//
87
88
  // -------------------- private stored properties -------------------- //
@@ -99,7 +100,7 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
99
100
  /// @param feeRevnetId The Juicebox project ID of the fee revnet.
100
101
  /// @param suckerRegistry The sucker registry.
101
102
  /// @param loans The loan contract.
102
- /// @param deployer The account allowed to bind the canonical deployer via `setDeployer`. Passed explicitly
103
+ /// @param deployerAddress The account allowed to bind the canonical deployer via `setDeployer`. Passed explicitly
103
104
  /// because CREATE2 deployments set `msg.sender` to the factory, not the intended operator.
104
105
  constructor(
105
106
  IJBBuybackHookRegistry buybackHook,
@@ -107,14 +108,14 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
107
108
  uint256 feeRevnetId,
108
109
  IJBSuckerRegistry suckerRegistry,
109
110
  IREVLoans loans,
110
- address deployer
111
+ address deployerAddress
111
112
  ) {
112
113
  BUYBACK_HOOK = buybackHook;
113
114
  DIRECTORY = directory;
114
115
  FEE_REVNET_ID = feeRevnetId;
115
116
  SUCKER_REGISTRY = suckerRegistry;
116
117
  LOANS = loans;
117
- _DEPLOYER = deployer;
118
+ _DEPLOYER = deployerAddress;
118
119
  }
119
120
 
120
121
  //*********************************************************************//
@@ -153,17 +154,17 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
153
154
  revnetId: context.projectId, decimals: context.surplus.decimals, currency: context.surplus.currency
154
155
  });
155
156
 
157
+ // Start with local supply and surplus (including collateral and borrowed amounts).
158
+ totalSupply = context.totalSupply + totalCollateral;
159
+ effectiveSurplusValue = context.surplus.value + totalBorrowed;
160
+
156
161
  // If the cash out is from a sucker, return the full cash out amount without taxes or fees.
162
+ // Sucker cash-outs are the bridge accounting path: the value moving out of this chain must stay proportional
163
+ // to this chain's local backing. Do not add remote supply/surplus here, even for unscoped revnets.
157
164
  // This relies on the sucker registry to only contain trusted sucker contracts deployed via
158
165
  // the registry's own deploySuckersFor flow — external addresses cannot register as suckers.
159
166
  if (_isSuckerOf({revnetId: context.projectId, addr: context.holder})) {
160
- return (
161
- 0,
162
- context.cashOutCount,
163
- context.totalSupply + totalCollateral,
164
- context.surplus.value + totalBorrowed,
165
- hookSpecifications
166
- );
167
+ return (0, context.cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
167
168
  }
168
169
 
169
170
  // Keep a reference to the cash out delay of the revnet.
@@ -178,10 +179,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
178
179
  // Get the terminal that will receive the cash out fee.
179
180
  IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
180
181
 
181
- // Start with local supply and surplus (including collateral and borrowed amounts).
182
- totalSupply = context.totalSupply + totalCollateral;
183
- effectiveSurplusValue = context.surplus.value + totalBorrowed;
184
-
185
182
  // If the ruleset aggregates cross-chain state, add remote supply and surplus.
186
183
  if (!context.scopeCashOutsToLocalBalances) {
187
184
  totalSupply += SUCKER_REGISTRY.remoteTotalSupplyOf(context.projectId);
@@ -452,8 +449,16 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
452
449
  // No caller validation needed — this hook only pays fees to the fee project using funds forwarded by the
453
450
  // caller. A non-terminal caller would just be donating their own funds as fees. There's nothing to exploit.
454
451
 
455
- // If there's sufficient approval, transfer normally.
456
- if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
452
+ if (context.forwardedAmount.token == JBConstants.NATIVE_TOKEN) {
453
+ // Native fee processing must be value-balanced by the current call. Otherwise a non-terminal caller could
454
+ // spend ETH that was forcibly sent or accidentally stranded in this hook.
455
+ if (msg.value != context.forwardedAmount.value) {
456
+ revert REVOwner_NativeFeeValueMismatch({expected: context.forwardedAmount.value, actual: msg.value});
457
+ }
458
+ } else {
459
+ if (msg.value != 0) revert REVOwner_NativeFeeValueMismatch({expected: 0, actual: msg.value});
460
+
461
+ // If there's sufficient approval, transfer normally.
457
462
  IERC20(context.forwardedAmount.token)
458
463
  .safeTransferFrom({from: msg.sender, to: address(this), value: context.forwardedAmount.value});
459
464
  }
@@ -504,29 +509,29 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
504
509
  }
505
510
  }
506
511
 
512
+ /// @notice Store the cash out delay for a revnet.
513
+ /// @dev Only callable by the deployer.
514
+ /// @param revnetId The ID of the revnet.
515
+ /// @param cashOutDelay The timestamp after which cash outs are allowed.
516
+ function setCashOutDelayOf(uint256 revnetId, uint256 cashOutDelay) external {
517
+ if (msg.sender != address(deployer)) {
518
+ revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(deployer)});
519
+ }
520
+ cashOutDelayOf[revnetId] = cashOutDelay;
521
+ }
522
+
507
523
  /// @notice Bind the canonical deployer address exactly once.
508
524
  /// @dev The deployer address is precomputed and supplied by the account that created this REVOwner instance.
509
525
  /// Only that deploy-time binder may call this, which avoids an ambient public initializer where any first caller
510
526
  /// could seize the deployer role before the deterministic REVDeployer is actually deployed.
511
- /// @param deployer The canonical REVDeployer instance that will manage revnet runtime state.
512
- function setDeployer(IREVDeployer deployer) external {
527
+ /// @param newDeployer The canonical REVDeployer instance that will manage revnet runtime state.
528
+ function setDeployer(IREVDeployer newDeployer) external {
513
529
  // Only the account that deployed this REVOwner may complete the one-time deployer binding.
514
530
  if (msg.sender != _DEPLOYER) revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: _DEPLOYER});
515
531
  // Prevent the deployer binding from being overwritten after initialization.
516
- if (address(DEPLOYER) != address(0)) revert REVOwner_AlreadyInitialized({deployer: address(DEPLOYER)});
532
+ if (address(deployer) != address(0)) revert REVOwner_AlreadyInitialized({deployer: address(deployer)});
517
533
  // Store the canonical REVDeployer that is authorized to manage runtime hook state.
518
- DEPLOYER = deployer;
519
- }
520
-
521
- /// @notice Store the cash out delay for a revnet.
522
- /// @dev Only callable by the deployer.
523
- /// @param revnetId The ID of the revnet.
524
- /// @param cashOutDelay The timestamp after which cash outs are allowed.
525
- function setCashOutDelayOf(uint256 revnetId, uint256 cashOutDelay) external {
526
- if (msg.sender != address(DEPLOYER)) {
527
- revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(DEPLOYER)});
528
- }
529
- cashOutDelayOf[revnetId] = cashOutDelay;
534
+ deployer = newDeployer;
530
535
  }
531
536
 
532
537
  /// @notice Store the tiered ERC-721 hook for a revnet.
@@ -534,8 +539,8 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
534
539
  /// @param revnetId The ID of the revnet.
535
540
  /// @param hook The tiered ERC-721 hook.
536
541
  function setTiered721HookOf(uint256 revnetId, IJB721TiersHook hook) external {
537
- if (msg.sender != address(DEPLOYER)) {
538
- revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(DEPLOYER)});
542
+ if (msg.sender != address(deployer)) {
543
+ revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(deployer)});
539
544
  }
540
545
  tiered721HookOf[revnetId] = hook;
541
546
  }
@@ -587,38 +592,41 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
587
592
 
588
593
  collateralCount = loans.totalCollateralOf(revnetId);
589
594
 
590
- REVLoanSource[] memory sources = loans.loanSourcesOf(revnetId);
591
- // Loan sources are project configuration, and this read-only aggregation needs the latest terminal/pricing
592
- // state for each configured source.
595
+ address[] memory sources = loans.loanSourceTokensOf(revnetId);
596
+ if (sources.length == 0) return (0, collateralCount);
597
+
598
+ IJBTerminal multiTerminal = deployer.MULTI_TERMINAL();
599
+ // Loan sources are tokens whose accounting contexts live on the canonical multi terminal.
593
600
  for (uint256 i; i < sources.length; i++) {
594
- REVLoanSource memory source = sources[i];
601
+ address sourceToken = sources[i];
595
602
  // Each configured source must be queried live so cash-out math includes current outstanding debt.
596
- uint256 tokensLoaned =
597
- loans.totalBorrowedFrom({revnetId: revnetId, terminal: source.terminal, token: source.token});
603
+ uint256 tokensLoaned = loans.totalBorrowedFrom({revnetId: revnetId, token: sourceToken});
598
604
  if (tokensLoaned == 0) continue;
599
605
 
600
- // Read the source token's accounting context so debt can be normalized before cross-currency conversion.
601
- JBAccountingContext memory accountingContext =
602
- source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
606
+ JBAccountingContext memory sourceContext =
607
+ multiTerminal.accountingContextForTokenOf({projectId: revnetId, token: sourceToken});
608
+ if (sourceContext.token != sourceToken) {
609
+ revert REVOwner_InvalidLoanSourceToken({revnetId: revnetId, token: sourceToken});
610
+ }
603
611
 
604
612
  // Normalize each source from its native token decimals into the caller's requested decimals.
605
613
  uint256 normalizedTokens;
606
- if (accountingContext.decimals > decimals) {
607
- normalizedTokens = tokensLoaned / (10 ** (accountingContext.decimals - decimals));
608
- } else if (accountingContext.decimals < decimals) {
609
- normalizedTokens = tokensLoaned * (10 ** (decimals - accountingContext.decimals));
614
+ if (sourceContext.decimals > decimals) {
615
+ normalizedTokens = tokensLoaned / (10 ** (sourceContext.decimals - decimals));
616
+ } else if (sourceContext.decimals < decimals) {
617
+ normalizedTokens = tokensLoaned * (10 ** (decimals - sourceContext.decimals));
610
618
  } else {
611
619
  normalizedTokens = tokensLoaned;
612
620
  }
613
621
 
614
- if (accountingContext.currency == currency) {
622
+ if (sourceContext.currency == currency) {
615
623
  borrowedAmount += normalizedTokens;
616
624
  } else {
617
625
  // Convert source-token debt into the requested currency using the loans contract's shared prices.
618
626
  uint256 pricePerUnit = loans.PRICES()
619
627
  .pricePerUnitOf({
620
628
  projectId: revnetId,
621
- pricingCurrency: accountingContext.currency,
629
+ pricingCurrency: sourceContext.currency,
622
630
  unitCurrency: currency,
623
631
  decimals: decimals
624
632
  });
@@ -633,6 +641,14 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
633
641
  // --------------------- internal transactions ----------------------- //
634
642
  //*********************************************************************//
635
643
 
644
+ /// @notice Clears any token allowance granted by `_beforeTransferTo`.
645
+ /// @param to The address that was approved by `_beforeTransferTo`.
646
+ /// @param token The token whose allowance should be revoked.
647
+ function _afterTransferTo(address to, address token) internal {
648
+ if (token == JBConstants.NATIVE_TOKEN) return;
649
+ IERC20(token).forceApprove({spender: to, value: 0});
650
+ }
651
+
636
652
  /// @notice Logic to trigger before transferring tokens from this contract.
637
653
  /// @param to The address to transfer to.
638
654
  /// @param token The token to transfer.
@@ -645,10 +661,4 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
645
661
  IERC20(token).safeIncreaseAllowance({spender: to, value: amount});
646
662
  return 0;
647
663
  }
648
-
649
- /// @notice Clears any token allowance granted by `_beforeTransferTo`.
650
- function _afterTransferTo(address to, address token) internal {
651
- if (token == JBConstants.NATIVE_TOKEN) return;
652
- IERC20(token).forceApprove({spender: to, value: 0});
653
- }
654
664
  }
@@ -8,6 +8,8 @@ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol
8
8
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
9
9
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
10
10
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
11
+ import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
12
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
11
13
  import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
12
14
  import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
13
15
  import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
@@ -142,6 +144,10 @@ interface IREVDeployer {
142
144
  /// @return The loans contract address.
143
145
  function LOANS() external view returns (IREVLoans);
144
146
 
147
+ /// @notice The canonical terminal that holds revnet treasury balances.
148
+ /// @return The multi terminal contract.
149
+ function MULTI_TERMINAL() external view returns (IJBTerminal);
150
+
145
151
  /// @notice The runtime data hook contract that handles pay and cash out callbacks for revnets.
146
152
  /// @return The owner contract address.
147
153
  function OWNER() external view returns (address);
@@ -158,6 +164,10 @@ interface IREVDeployer {
158
164
  /// @return The publisher contract.
159
165
  function PUBLISHER() external view returns (CTPublisher);
160
166
 
167
+ /// @notice The canonical router terminal registry installed as a project terminal for alternate payment routes.
168
+ /// @return The router terminal registry contract, cast as a terminal.
169
+ function ROUTER_TERMINAL_REGISTRY() external view returns (IJBTerminal);
170
+
161
171
  /// @notice The registry that deploys and tracks suckers for revnets.
162
172
  /// @return The sucker registry contract.
163
173
  function SUCKER_REGISTRY() external view returns (IJBSuckerRegistry);
@@ -176,7 +186,7 @@ interface IREVDeployer {
176
186
  /// @dev Every revnet gets a 721 hook — pass an empty config if no tiers are needed initially.
177
187
  /// @param revnetId The ID of the Juicebox project to initialize. Send 0 to deploy a new revnet.
178
188
  /// @param configuration Core revnet configuration.
179
- /// @param terminalConfigurations The terminals to set up for the revnet.
189
+ /// @param accountingContextsToAccept The accounting contexts the canonical multi terminal should accept.
180
190
  /// @param suckerDeploymentConfiguration The suckers to set up for cross-chain token transfers.
181
191
  /// @param tiered721HookConfiguration How to configure the tiered ERC-721 hook for the revnet.
182
192
  /// @param allowedPosts Restrictions on which croptop posts to allow on the revnet's ERC-721 tiers.
@@ -185,7 +195,7 @@ interface IREVDeployer {
185
195
  function deployFor(
186
196
  uint256 revnetId,
187
197
  REVConfig memory configuration,
188
- JBTerminalConfig[] memory terminalConfigurations,
198
+ JBAccountingContext[] memory accountingContextsToAccept,
189
199
  REVSuckerDeploymentConfig memory suckerDeploymentConfiguration,
190
200
  REVDeploy721TiersHookConfig memory tiered721HookConfiguration,
191
201
  REVCroptopAllowedPost[] memory allowedPosts
@@ -197,14 +207,14 @@ interface IREVDeployer {
197
207
  /// @dev Convenience overload — constructs an empty 721 config internally and delegates to the 6-arg version.
198
208
  /// @param revnetId The ID of the Juicebox project to initialize. Send 0 to deploy a new revnet.
199
209
  /// @param configuration Core revnet configuration.
200
- /// @param terminalConfigurations The terminals to set up for the revnet.
210
+ /// @param accountingContextsToAccept The accounting contexts the canonical multi terminal should accept.
201
211
  /// @param suckerDeploymentConfiguration The suckers to set up for cross-chain token transfers.
202
212
  /// @return The ID of the newly created or initialized revnet.
203
213
  /// @return hook The tiered ERC-721 hook deployed for the revnet.
204
214
  function deployFor(
205
215
  uint256 revnetId,
206
216
  REVConfig memory configuration,
207
- JBTerminalConfig[] memory terminalConfigurations,
217
+ JBAccountingContext[] memory accountingContextsToAccept,
208
218
  REVSuckerDeploymentConfig memory suckerDeploymentConfiguration
209
219
  )
210
220
  external
@@ -10,7 +10,6 @@ import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowanc
10
10
  import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
11
11
  import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
12
12
  import {REVLoan} from "../structs/REVLoan.sol";
13
- import {REVLoanSource} from "../structs/REVLoanSource.sol";
14
13
 
15
14
  /// @notice Manages loans against revnet token collateral.
16
15
  interface IREVLoans {
@@ -18,7 +17,7 @@ interface IREVLoans {
18
17
  /// @param loanId The ID of the newly created loan.
19
18
  /// @param revnetId The ID of the revnet being borrowed from.
20
19
  /// @param loan The loan data.
21
- /// @param source The source of the loan (terminal and token).
20
+ /// @param token The token borrowed from the revnet's canonical multi terminal.
22
21
  /// @param borrowAmount The amount borrowed.
23
22
  /// @param collateralCount The amount of collateral tokens locked.
24
23
  /// @param sourceFeeAmount The fee amount charged by the source.
@@ -28,7 +27,7 @@ interface IREVLoans {
28
27
  uint256 indexed loanId,
29
28
  uint256 indexed revnetId,
30
29
  REVLoan loan,
31
- REVLoanSource source,
30
+ address token,
32
31
  uint256 borrowAmount,
33
32
  uint256 collateralCount,
34
33
  uint256 sourceFeeAmount,
@@ -124,12 +123,11 @@ interface IREVLoans {
124
123
  /// @return The directory contract.
125
124
  function DIRECTORY() external view returns (IJBDirectory);
126
125
 
127
- /// @notice Whether a revnet currently has outstanding loans from the specified terminal in the specified token.
126
+ /// @notice Whether a revnet currently has outstanding loans from the specified token.
128
127
  /// @param revnetId The ID of the revnet to check.
129
- /// @param terminal The terminal to check.
130
128
  /// @param token The token to check.
131
129
  /// @return A flag indicating if the revnet has an active loan source.
132
- function isLoanSourceOf(uint256 revnetId, IJBPayoutTerminal terminal, address token) external view returns (bool);
130
+ function isLoanSourceOf(uint256 revnetId, address token) external view returns (bool);
133
131
 
134
132
  /// @notice The duration after which a loan expires and its collateral is permanently lost.
135
133
  /// @return The loan liquidation duration in seconds.
@@ -140,13 +138,13 @@ interface IREVLoans {
140
138
  /// @return The loan data.
141
139
  function loanOf(uint256 loanId) external view returns (REVLoan memory);
142
140
 
143
- /// @notice The sources of each revnet's loans.
144
- /// @dev This array only grows -- sources are appended when a new (terminal, token) pair is first used for
145
- /// borrowing, but are never removed. Gas cost scales linearly with the number of distinct sources, though this is
146
- /// practically bounded to a small number of unique (terminal, token) pairs.
141
+ /// @notice The token sources of each revnet's loans.
142
+ /// @dev This array only grows -- sources are appended when a token is first used for borrowing, but are never
143
+ /// removed. Gas cost scales linearly with the number of distinct sources, though this is practically bounded to the
144
+ /// revnet's accepted accounting contexts.
147
145
  /// @param revnetId The ID of the revnet to get the loan sources for.
148
- /// @return The array of loan sources.
149
- function loanSourcesOf(uint256 revnetId) external view returns (REVLoanSource[] memory);
146
+ /// @return The array of loan source tokens.
147
+ function loanSourceTokensOf(uint256 revnetId) external view returns (address[] memory);
150
148
 
151
149
  /// @notice The maximum fee percent that can be prepaid when borrowing, in terms of `JBConstants.MAX_FEE`.
152
150
  /// @return The maximum prepaid fee percent.
@@ -168,13 +166,17 @@ interface IREVLoans {
168
166
  /// @return The REV revnet ID.
169
167
  function REV_ID() external view returns (uint256);
170
168
 
169
+ /// @notice The fee percent charged by the REV revnet on each loan, in terms of `JBConstants.MAX_FEE`.
170
+ /// @return The REV prepaid fee percent.
171
+ function REV_PREPAID_FEE_PERCENT() external view returns (uint256);
172
+
171
173
  /// @notice The sucker registry used to discover peer chain suckers for cross-chain supply/surplus awareness.
172
174
  /// @return The sucker registry.
173
175
  function SUCKER_REGISTRY() external view returns (IJBSuckerRegistry);
174
176
 
175
- /// @notice The fee percent charged by the REV revnet on each loan, in terms of `JBConstants.MAX_FEE`.
176
- /// @return The REV prepaid fee percent.
177
- function REV_PREPAID_FEE_PERCENT() external view returns (uint256);
177
+ /// @notice The canonical payout terminal that holds revnet treasury balances and sources all revnet loans.
178
+ /// @return The canonical payout terminal.
179
+ function TERMINAL() external view returns (IJBPayoutTerminal);
178
180
 
179
181
  /// @notice The revnet ID for a given loan ID.
180
182
  /// @param loanId The loan ID to look up.
@@ -185,19 +187,11 @@ interface IREVLoans {
185
187
  /// @return The token URI resolver.
186
188
  function tokenUriResolver() external view returns (IJBTokenUriResolver);
187
189
 
188
- /// @notice The total amount loaned out by a revnet from a specified terminal in a specified token.
190
+ /// @notice The total amount loaned out by a revnet from a specified token.
189
191
  /// @param revnetId The ID of the revnet to check.
190
- /// @param terminal The terminal the loans were issued from.
191
192
  /// @param token The token loaned.
192
193
  /// @return The total amount borrowed.
193
- function totalBorrowedFrom(
194
- uint256 revnetId,
195
- IJBPayoutTerminal terminal,
196
- address token
197
- )
198
- external
199
- view
200
- returns (uint256);
194
+ function totalBorrowedFrom(uint256 revnetId, address token) external view returns (uint256);
201
195
 
202
196
  /// @notice The total amount of collateral supporting a revnet's loans.
203
197
  /// @param revnetId The ID of the revnet.
@@ -214,8 +208,8 @@ interface IREVLoans {
214
208
 
215
209
  /// @notice Open a loan by borrowing from a revnet. Collateral tokens are burned and only re-minted upon repayment.
216
210
  /// @param revnetId The ID of the revnet to borrow from.
217
- /// @param source The source of the loan (terminal and token).
218
- /// @param minBorrowAmount The minimum amount to borrow, denominated in the source's token.
211
+ /// @param token The token to borrow from the revnet's canonical multi terminal.
212
+ /// @param minBorrowAmount The minimum amount to borrow, denominated in `token`.
219
213
  /// @param collateralCount The amount of tokens to use as collateral for the loan.
220
214
  /// @param beneficiary The address that will receive the borrowed funds and fee payment tokens.
221
215
  /// @param prepaidFeePercent The fee percent to charge upfront, in terms of `JBConstants.MAX_FEE`.
@@ -224,7 +218,7 @@ interface IREVLoans {
224
218
  /// @return The loan created.
225
219
  function borrowFrom(
226
220
  uint256 revnetId,
227
- REVLoanSource calldata source,
221
+ address token,
228
222
  uint256 minBorrowAmount,
229
223
  uint256 collateralCount,
230
224
  address payable beneficiary,
@@ -243,7 +237,7 @@ interface IREVLoans {
243
237
  /// @notice Refinance a loan by transferring extra collateral from an existing loan to a new loan.
244
238
  /// @param loanId The ID of the loan to reallocate collateral from.
245
239
  /// @param collateralCountToTransfer The amount of collateral to transfer from the original loan.
246
- /// @param source The source of the new loan (terminal and token). Must match the existing loan's source.
240
+ /// @param token The token of the new loan. Must match the existing loan's source token.
247
241
  /// @param minBorrowAmount The minimum amount to borrow for the new loan.
248
242
  /// @param collateralCountToAdd Additional collateral to add to the new loan from your balance.
249
243
  /// @param beneficiary The address that will receive the borrowed funds and fee payment tokens.
@@ -255,7 +249,7 @@ interface IREVLoans {
255
249
  function reallocateCollateralFromLoan(
256
250
  uint256 loanId,
257
251
  uint256 collateralCountToTransfer,
258
- REVLoanSource calldata source,
252
+ address token,
259
253
  uint256 minBorrowAmount,
260
254
  uint256 collateralCountToAdd,
261
255
  address payable beneficiary,
@@ -10,7 +10,11 @@ interface IREVOwner {
10
10
  /// @return The cash out delay timestamp.
11
11
  function cashOutDelayOf(uint256 revnetId) external view returns (uint256);
12
12
 
13
+ /// @notice The canonical deployer managing revnet runtime state.
14
+ /// @return The revnet deployer instance.
15
+ function deployer() external view returns (IREVDeployer);
16
+
13
17
  /// @notice Bind the canonical deployer exactly once.
14
- /// @param deployer The revnet deployer instance.
15
- function setDeployer(IREVDeployer deployer) external;
18
+ /// @param newDeployer The revnet deployer instance.
19
+ function setDeployer(IREVDeployer newDeployer) external;
16
20
  }
@@ -0,0 +1,57 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
5
+ import {JBFees} from "@bananapus/core-v6/src/libraries/JBFees.sol";
6
+ import {mulDiv} from "@prb/math/src/Common.sol";
7
+
8
+ import {REVLoan} from "../structs/REVLoan.sol";
9
+
10
+ /// @notice Source-fee arithmetic for REV loans.
11
+ library REVLoansSourceFees {
12
+ error REVLoans_LoanExpired(uint256 timeSinceLoanCreated, uint256 loanLiquidationDuration);
13
+
14
+ /// @notice Determines the source fee amount for a loan when paying off a certain amount.
15
+ /// @param loan The loan to determine the source fee for.
16
+ /// @param amount The amount of principal being paid off.
17
+ /// @param timeSinceLoanCreated The elapsed time since the loan was created.
18
+ /// @param loanLiquidationDuration The duration after which loans are expired.
19
+ /// @return sourceFeeAmount The source fee amount for the repayment.
20
+ function sourceFeeAmountFrom(
21
+ REVLoan memory loan,
22
+ uint256 amount,
23
+ uint256 timeSinceLoanCreated,
24
+ uint256 loanLiquidationDuration
25
+ )
26
+ internal
27
+ pure
28
+ returns (uint256 sourceFeeAmount)
29
+ {
30
+ // Inside the prepaid window, the borrower already paid the source fee up front.
31
+ if (timeSinceLoanCreated <= loan.prepaidDuration) return 0;
32
+
33
+ // Expired loans cannot be managed through repay/reallocate paths. Uses `>` so the exact boundary second is
34
+ // still repayable, matching the liquidation path's `<=` check.
35
+ if (timeSinceLoanCreated > loanLiquidationDuration) {
36
+ revert REVLoans_LoanExpired({
37
+ timeSinceLoanCreated: timeSinceLoanCreated, loanLiquidationDuration: loanLiquidationDuration
38
+ });
39
+ }
40
+
41
+ // The prepaid fee reduces the amount that can still accrue a time-based source fee.
42
+ uint256 prepaid = JBFees.feeAmountFrom({amountBeforeFee: loan.amount, feePercent: loan.prepaidFeePercent});
43
+
44
+ // Linearly ramp the remaining source fee from 0 after the prepaid window to 100% at liquidation.
45
+ uint256 fullSourceFeeAmount = JBFees.feeAmountFrom({
46
+ amountBeforeFee: loan.amount - prepaid,
47
+ feePercent: mulDiv({
48
+ x: timeSinceLoanCreated - loan.prepaidDuration,
49
+ y: JBConstants.MAX_FEE,
50
+ denominator: loanLiquidationDuration - loan.prepaidDuration
51
+ })
52
+ });
53
+
54
+ // Charge only the pro-rata part of the full source fee for the principal being repaid.
55
+ return mulDiv({x: fullSourceFeeAmount, y: amount, denominator: loan.amount});
56
+ }
57
+ }
@@ -1,8 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {REVLoanSource} from "./REVLoanSource.sol";
5
-
6
4
  /// @notice An active loan against a revnet. The borrower locked collateral tokens (which were burned) and received
7
5
  /// funds from the revnet's terminal. The loan can be repaid within the prepaid duration at no extra cost; after that,
8
6
  /// repayment cost increases linearly until liquidation at 10 years.
@@ -12,12 +10,12 @@ import {REVLoanSource} from "./REVLoanSource.sol";
12
10
  /// @custom:member prepaidFeePercent The percentage of fees prepaid at creation (determines prepaid duration).
13
11
  /// @custom:member prepaidDuration The duration (seconds) during which repayment costs nothing beyond the original
14
12
  /// amount.
15
- /// @custom:member source The terminal and token from which funds were drawn.
13
+ /// @custom:member sourceToken The token borrowed from the revnet's canonical multi terminal.
16
14
  struct REVLoan {
17
15
  uint112 amount;
18
16
  uint112 collateral;
19
17
  uint48 createdAt;
20
18
  uint16 prepaidFeePercent;
21
19
  uint32 prepaidDuration;
22
- REVLoanSource source;
20
+ address sourceToken;
23
21
  }
@@ -1,11 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.0;
3
-
4
- import {IJBPayoutTerminal} from "@bananapus/core-v6/src/interfaces/IJBPayoutTerminal.sol";
5
-
6
- /// @custom:member token The token to loan.
7
- /// @custom:member terminal The terminal to loan from.
8
- struct REVLoanSource {
9
- address token;
10
- IJBPayoutTerminal terminal;
11
- }