@rev-net/core-v6 0.0.58 → 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/package.json +10 -10
- package/references/operations.md +8 -8
- package/references/runtime.md +2 -3
- package/script/Deploy.s.sol +23 -20
- package/src/REVDeployer.sol +80 -74
- package/src/REVLoans.sol +197 -189
- package/src/REVOwner.sol +56 -46
- package/src/interfaces/IREVDeployer.sol +14 -4
- package/src/interfaces/IREVLoans.sol +24 -30
- package/src/interfaces/IREVOwner.sol +4 -0
- package/src/libraries/REVLoansSourceFees.sol +57 -0
- package/src/structs/REVLoan.sol +2 -4
- package/src/structs/REVLoanSource.sol +0 -11
package/src/REVLoans.sol
CHANGED
|
@@ -32,8 +32,8 @@ import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
|
32
32
|
|
|
33
33
|
import {IREVLoans} from "./interfaces/IREVLoans.sol";
|
|
34
34
|
import {IREVOwner} from "./interfaces/IREVOwner.sol";
|
|
35
|
+
import {REVLoansSourceFees} from "./libraries/REVLoansSourceFees.sol";
|
|
35
36
|
import {REVLoan} from "./structs/REVLoan.sol";
|
|
36
|
-
import {REVLoanSource} from "./structs/REVLoanSource.sol";
|
|
37
37
|
|
|
38
38
|
/// @notice Allows revnet token holders to borrow against their tokens instead of cashing out. The borrowable amount
|
|
39
39
|
/// equals what a cash-out would return. Collateral tokens are burned on borrow and re-minted on repayment, keeping the
|
|
@@ -58,8 +58,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
58
58
|
|
|
59
59
|
error REVLoans_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
|
|
60
60
|
error REVLoans_CollateralExceedsLoan(uint256 collateralToReturn, uint256 loanCollateral);
|
|
61
|
+
error REVLoans_InvalidAccountingContext(uint256 revnetId, address token);
|
|
61
62
|
error REVLoans_InvalidPrepaidFeePercent(uint256 prepaidFeePercent, uint256 min, uint256 max);
|
|
62
|
-
error REVLoans_InvalidTerminal(address terminal, uint256 revnetId);
|
|
63
63
|
error REVLoans_LoanExpired(uint256 timeSinceLoanCreated, uint256 loanLiquidationDuration);
|
|
64
64
|
error REVLoans_LoanIdOverflow(uint256 revnetId, uint256 loanNumber, uint256 maxLoanNumber);
|
|
65
65
|
error REVLoans_LoanOwnerChanged(uint256 loanId, address expectedOwner, address actualOwner);
|
|
@@ -71,12 +71,12 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
71
71
|
error REVLoans_OverflowAlert(uint256 value, uint256 limit);
|
|
72
72
|
error REVLoans_PermitAllowanceNotEnough(uint256 allowanceAmount, uint256 requiredAmount);
|
|
73
73
|
error REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(uint256 newBorrowAmount, uint256 loanAmount);
|
|
74
|
-
error
|
|
75
|
-
|
|
76
|
-
);
|
|
74
|
+
error REVLoans_ReentrantLoanAction();
|
|
75
|
+
error REVLoans_SourceMismatch(address expectedToken, address actualToken);
|
|
77
76
|
error REVLoans_UnderMinBorrowAmount(uint256 minBorrowAmount, uint256 borrowAmount);
|
|
78
77
|
error REVLoans_ZeroBorrowAmount(uint256 revnetId, uint256 collateralCount);
|
|
79
78
|
error REVLoans_ZeroCollateralLoanIsInvalid(uint256 collateralCount);
|
|
79
|
+
error REVLoans_ZeroPrice(uint256 revnetId, uint256 pricingCurrency, uint256 unitCurrency);
|
|
80
80
|
|
|
81
81
|
//*********************************************************************//
|
|
82
82
|
// ------------------------- public constants ------------------------ //
|
|
@@ -108,15 +108,15 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
108
108
|
// --------------- public immutable stored properties ---------------- //
|
|
109
109
|
//*********************************************************************//
|
|
110
110
|
|
|
111
|
-
/// @notice The Permit2 contract used for token approvals and transfers.
|
|
112
|
-
IPermit2 public immutable override PERMIT2;
|
|
113
|
-
|
|
114
111
|
/// @notice The controller of revnets that use this loans contract.
|
|
115
112
|
IJBController public immutable override CONTROLLER;
|
|
116
113
|
|
|
117
114
|
/// @notice The directory of terminals and controllers for revnets.
|
|
118
115
|
IJBDirectory public immutable override DIRECTORY;
|
|
119
116
|
|
|
117
|
+
/// @notice The Permit2 contract used for token approvals and transfers.
|
|
118
|
+
IPermit2 public immutable override PERMIT2;
|
|
119
|
+
|
|
120
120
|
/// @notice A contract that stores prices for each revnet.
|
|
121
121
|
IJBPrices public immutable override PRICES;
|
|
122
122
|
|
|
@@ -126,62 +126,66 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
126
126
|
/// @notice The sucker registry used to discover peer chain suckers for cross-chain awareness.
|
|
127
127
|
IJBSuckerRegistry public immutable override SUCKER_REGISTRY;
|
|
128
128
|
|
|
129
|
+
/// @notice The canonical payout terminal that holds revnet treasury balances and sources all revnet loans.
|
|
130
|
+
IJBPayoutTerminal public immutable override TERMINAL;
|
|
131
|
+
|
|
129
132
|
//*********************************************************************//
|
|
130
133
|
// --------------------- public stored properties -------------------- //
|
|
131
134
|
//*********************************************************************//
|
|
132
135
|
|
|
133
|
-
/// @notice An indication if a revnet currently has outstanding loans from the specified
|
|
134
|
-
/// token.
|
|
136
|
+
/// @notice An indication if a revnet currently has outstanding loans from the specified token source.
|
|
135
137
|
/// @custom:param revnetId The ID of the revnet to check.
|
|
136
|
-
/// @custom:param
|
|
137
|
-
|
|
138
|
-
mapping(uint256 revnetId => mapping(IJBPayoutTerminal terminal => mapping(address token => bool)))
|
|
139
|
-
public
|
|
140
|
-
override isLoanSourceOf;
|
|
141
|
-
|
|
142
|
-
/// @notice The cumulative number of loans ever created for a revnet, used as a loan ID sequence counter.
|
|
143
|
-
/// @dev This counter only increments (on borrow, repay-with-new-loan, and reallocation) and never decrements.
|
|
144
|
-
/// It does NOT represent the number of currently active loans. Repaid and liquidated loans leave permanent gaps
|
|
145
|
-
/// in the ID sequence. Integrators should not use this to count active loans.
|
|
146
|
-
/// @custom:param revnetId The ID of the revnet to check.
|
|
147
|
-
mapping(uint256 revnetId => uint256) public override totalLoansBorrowedFor;
|
|
138
|
+
/// @custom:param token The token source to check.
|
|
139
|
+
mapping(uint256 revnetId => mapping(address token => bool)) public override isLoanSourceOf;
|
|
148
140
|
|
|
149
141
|
/// @notice The contract resolving each project ID to its ERC721 URI.
|
|
150
142
|
IJBTokenUriResolver public override tokenUriResolver;
|
|
151
143
|
|
|
152
|
-
/// @notice The total amount loaned out by a revnet from a specified
|
|
144
|
+
/// @notice The total amount loaned out by a revnet from a specified token source.
|
|
153
145
|
/// @custom:param revnetId The ID of the revnet to check.
|
|
154
|
-
/// @custom:param
|
|
155
|
-
|
|
156
|
-
mapping(uint256 revnetId => mapping(IJBPayoutTerminal terminal => mapping(address token => uint256)))
|
|
157
|
-
public
|
|
158
|
-
override totalBorrowedFrom;
|
|
146
|
+
/// @custom:param token The token source to check.
|
|
147
|
+
mapping(uint256 revnetId => mapping(address token => uint256)) public override totalBorrowedFrom;
|
|
159
148
|
|
|
160
149
|
/// @notice The total amount of collateral supporting a revnet's loans.
|
|
161
150
|
/// @custom:param revnetId The ID of the revnet to check.
|
|
162
151
|
mapping(uint256 revnetId => uint256) public override totalCollateralOf;
|
|
163
152
|
|
|
153
|
+
/// @notice The cumulative number of loans ever created for a revnet, used as a loan ID sequence counter.
|
|
154
|
+
/// @dev This counter only increments (on borrow, repay-with-new-loan, and reallocation) and never decrements.
|
|
155
|
+
/// It does NOT represent the number of currently active loans. Repaid and liquidated loans leave permanent gaps
|
|
156
|
+
/// in the ID sequence. Integrators should not use this to count active loans.
|
|
157
|
+
/// @custom:param revnetId The ID of the revnet to check.
|
|
158
|
+
mapping(uint256 revnetId => uint256) public override totalLoansBorrowedFor;
|
|
159
|
+
|
|
164
160
|
//*********************************************************************//
|
|
165
161
|
// --------------------- internal stored properties ------------------ //
|
|
166
162
|
//*********************************************************************//
|
|
167
163
|
|
|
168
|
-
/// @notice The sources of each revnet's loan.
|
|
169
|
-
/// @dev This array grows monotonically -- entries are appended when a new (terminal, token) pair is first used for
|
|
170
|
-
/// borrowing, but are never removed. The `isLoanSourceOf` mapping tracks whether a source has been registered.
|
|
171
|
-
/// Since the number of distinct (terminal, token) pairs per revnet is practically bounded (typically < 10),
|
|
172
|
-
/// the gas cost of iterating this array in `loanSourcesOf` remains manageable.
|
|
173
|
-
/// @custom:member revnetId The ID of the revnet to look up.
|
|
174
|
-
mapping(uint256 revnetId => REVLoanSource[]) internal _loanSourcesOf;
|
|
175
|
-
|
|
176
164
|
/// @notice The loans.
|
|
177
165
|
/// @custom:member The ID of the loan.
|
|
178
166
|
mapping(uint256 loanId => REVLoan) internal _loanOf;
|
|
179
167
|
|
|
168
|
+
/// @notice The sources of each revnet's loan.
|
|
169
|
+
/// @dev This array grows monotonically -- entries are appended when a token is first used for borrowing, but are
|
|
170
|
+
/// never removed. The `isLoanSourceOf` mapping tracks whether a source has been registered. Since sources are
|
|
171
|
+
/// bounded to the revnet's accepted accounting contexts on the canonical multi terminal, iteration remains
|
|
172
|
+
/// manageable.
|
|
173
|
+
/// @custom:member revnetId The ID of the revnet to look up.
|
|
174
|
+
mapping(uint256 revnetId => address[]) internal _loanSourceTokensOf;
|
|
175
|
+
|
|
176
|
+
//*********************************************************************//
|
|
177
|
+
// ------------------- transient stored properties ------------------- //
|
|
178
|
+
//*********************************************************************//
|
|
179
|
+
|
|
180
|
+
/// @notice Whether a loan-changing entrypoint is currently executing.
|
|
181
|
+
bool transient _loanActionEntered;
|
|
182
|
+
|
|
180
183
|
//*********************************************************************//
|
|
181
184
|
// -------------------------- constructor ---------------------------- //
|
|
182
185
|
//*********************************************************************//
|
|
183
186
|
|
|
184
187
|
/// @param controller The controller that manages revnets using this loans contract.
|
|
188
|
+
/// @param terminal The canonical payout terminal that holds revnet treasury balances and sources loans.
|
|
185
189
|
/// @param suckerRegistry The registry used to discover peer chain suckers for cross-chain supply/surplus awareness.
|
|
186
190
|
/// @param revId The ID of the REV revnet that will receive the fees.
|
|
187
191
|
/// @param owner The owner of the contract that can set the URI resolver.
|
|
@@ -189,6 +193,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
189
193
|
/// @param trustedForwarder A trusted forwarder of transactions to this contract.
|
|
190
194
|
constructor(
|
|
191
195
|
IJBController controller,
|
|
196
|
+
IJBPayoutTerminal terminal,
|
|
192
197
|
IJBSuckerRegistry suckerRegistry,
|
|
193
198
|
uint256 revId,
|
|
194
199
|
address owner,
|
|
@@ -202,12 +207,26 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
202
207
|
{
|
|
203
208
|
CONTROLLER = controller;
|
|
204
209
|
DIRECTORY = controller.DIRECTORY();
|
|
210
|
+
TERMINAL = terminal;
|
|
205
211
|
PRICES = controller.PRICES();
|
|
206
212
|
REV_ID = revId;
|
|
207
213
|
PERMIT2 = permit2;
|
|
208
214
|
SUCKER_REGISTRY = suckerRegistry;
|
|
209
215
|
}
|
|
210
216
|
|
|
217
|
+
//*********************************************************************//
|
|
218
|
+
// ---------------------------- modifiers ---------------------------- //
|
|
219
|
+
//*********************************************************************//
|
|
220
|
+
|
|
221
|
+
/// @notice Prevent nested loan-changing calls while an external callback is in progress.
|
|
222
|
+
modifier nonReentrantLoanAction() {
|
|
223
|
+
if (_loanActionEntered) revert REVLoans_ReentrantLoanAction();
|
|
224
|
+
|
|
225
|
+
_loanActionEntered = true;
|
|
226
|
+
_;
|
|
227
|
+
_loanActionEntered = false;
|
|
228
|
+
}
|
|
229
|
+
|
|
211
230
|
//*********************************************************************//
|
|
212
231
|
// ------------------------- external views -------------------------- //
|
|
213
232
|
//*********************************************************************//
|
|
@@ -240,7 +259,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
240
259
|
collateralCount: collateralCount,
|
|
241
260
|
decimals: decimals,
|
|
242
261
|
currency: currency,
|
|
243
|
-
|
|
262
|
+
multiTerminal: TERMINAL,
|
|
244
263
|
currentStage: currentRuleset
|
|
245
264
|
});
|
|
246
265
|
}
|
|
@@ -251,12 +270,12 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
251
270
|
return _loanOf[loanId];
|
|
252
271
|
}
|
|
253
272
|
|
|
254
|
-
/// @notice The
|
|
273
|
+
/// @notice The source tokens of each revnet's loans.
|
|
255
274
|
/// @dev This array only grows -- sources are never removed. The number of distinct sources is practically bounded
|
|
256
|
-
/// by the number of
|
|
275
|
+
/// by the number of accepted token accounting contexts, which is typically small.
|
|
257
276
|
/// @param revnetId The ID of the revnet to look up.
|
|
258
|
-
function
|
|
259
|
-
return
|
|
277
|
+
function loanSourceTokensOf(uint256 revnetId) external view override returns (address[] memory) {
|
|
278
|
+
return _loanSourceTokensOf[revnetId];
|
|
260
279
|
}
|
|
261
280
|
|
|
262
281
|
//*********************************************************************//
|
|
@@ -331,7 +350,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
331
350
|
/// @param collateralCount The amount of collateral to secure the loan with.
|
|
332
351
|
/// @param decimals The decimals to use for the resulting fixed point value.
|
|
333
352
|
/// @param currency The currency to denominate the resulting amount in.
|
|
334
|
-
/// @param
|
|
353
|
+
/// @param multiTerminal The canonical multi terminal to borrow from.
|
|
335
354
|
/// @param currentStage The pre-fetched current ruleset.
|
|
336
355
|
/// @return borrowableAmount The amount that can be borrowed from the revnet.
|
|
337
356
|
function _borrowableAmountFrom(
|
|
@@ -339,21 +358,27 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
339
358
|
uint256 collateralCount,
|
|
340
359
|
uint256 decimals,
|
|
341
360
|
uint256 currency,
|
|
342
|
-
IJBTerminal
|
|
361
|
+
IJBTerminal multiTerminal,
|
|
343
362
|
JBRuleset memory currentStage
|
|
344
363
|
)
|
|
345
364
|
internal
|
|
346
365
|
view
|
|
347
366
|
returns (uint256)
|
|
348
367
|
{
|
|
349
|
-
// Get the surplus of
|
|
368
|
+
// Get the surplus of the revnet's canonical multi terminal in terms of the requested currency.
|
|
350
369
|
uint256 totalSurplus = JBSurplus.currentSurplusOf({
|
|
351
|
-
projectId: revnetId,
|
|
370
|
+
projectId: revnetId,
|
|
371
|
+
terminals: _singleTerminalArray(multiTerminal),
|
|
372
|
+
tokens: new address[](0),
|
|
373
|
+
decimals: decimals,
|
|
374
|
+
currency: currency
|
|
352
375
|
});
|
|
353
376
|
|
|
354
377
|
// Get the total amount the revnet currently has loaned out, in terms of the native currency with 18
|
|
355
378
|
// decimals.
|
|
356
|
-
uint256 totalBorrowed = _totalBorrowedFrom({
|
|
379
|
+
uint256 totalBorrowed = _totalBorrowedFrom({
|
|
380
|
+
revnetId: revnetId, decimals: decimals, currency: currency, multiTerminal: multiTerminal
|
|
381
|
+
});
|
|
357
382
|
|
|
358
383
|
// Get the total amount of tokens in circulation.
|
|
359
384
|
uint256 totalSupply = CONTROLLER.totalTokenSupplyWithReservedTokensOf(revnetId);
|
|
@@ -361,8 +386,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
361
386
|
// Get a reference to the collateral being used to secure loans.
|
|
362
387
|
uint256 totalCollateral = totalCollateralOf[revnetId];
|
|
363
388
|
|
|
364
|
-
//
|
|
365
|
-
//
|
|
389
|
+
// Only live token supply is counted here, then loan collateral is added back because loans burn collateral
|
|
390
|
+
// while borrowers still have a repayable claim on it. Ordinary voluntary burns are not tracked as hidden
|
|
391
|
+
// supply in v6; they destroy the holder's claim and do not need to be added back.
|
|
366
392
|
uint256 localSupply = totalSupply + totalCollateral;
|
|
367
393
|
|
|
368
394
|
// The local surplus includes both the treasury surplus and the outstanding borrowed amounts.
|
|
@@ -411,19 +437,16 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
411
437
|
// If there's no collateral, there's no loan.
|
|
412
438
|
if (collateralCount == 0) return 0;
|
|
413
439
|
|
|
414
|
-
//
|
|
415
|
-
JBAccountingContext memory
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
// Keep a reference to the revnet's terminals.
|
|
419
|
-
IJBTerminal[] memory terminals = _terminalsOf(revnetId);
|
|
440
|
+
// Keep a reference to the token's accounting context from the canonical treasury terminal.
|
|
441
|
+
JBAccountingContext memory context =
|
|
442
|
+
TERMINAL.accountingContextForTokenOf({projectId: revnetId, token: loan.sourceToken});
|
|
420
443
|
|
|
421
444
|
return _borrowableAmountFrom({
|
|
422
445
|
revnetId: revnetId,
|
|
423
446
|
collateralCount: collateralCount,
|
|
424
|
-
decimals:
|
|
425
|
-
currency:
|
|
426
|
-
|
|
447
|
+
decimals: context.decimals,
|
|
448
|
+
currency: context.currency,
|
|
449
|
+
multiTerminal: TERMINAL,
|
|
427
450
|
currentStage: currentRuleset
|
|
428
451
|
});
|
|
429
452
|
}
|
|
@@ -460,36 +483,17 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
460
483
|
/// @param amount The amount to pay off.
|
|
461
484
|
/// @return The source fee amount for the loan.
|
|
462
485
|
function _determineSourceFeeAmount(REVLoan memory loan, uint256 amount) internal view returns (uint256) {
|
|
463
|
-
// Keep a reference to the
|
|
486
|
+
// Keep a reference to the loan age here because production uses the live block timestamp while formal proofs
|
|
487
|
+
// pass explicit elapsed-time values into the same source-fee library.
|
|
464
488
|
uint256 timeSinceLoanCreated = block.timestamp - loan.createdAt;
|
|
465
489
|
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (timeSinceLoanCreated > LOAN_LIQUIDATION_DURATION) {
|
|
473
|
-
revert REVLoans_LoanExpired({
|
|
474
|
-
timeSinceLoanCreated: timeSinceLoanCreated, loanLiquidationDuration: LOAN_LIQUIDATION_DURATION
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Get a reference to the amount prepaid for the full loan.
|
|
479
|
-
uint256 prepaid = JBFees.feeAmountFrom({amountBeforeFee: loan.amount, feePercent: loan.prepaidFeePercent});
|
|
480
|
-
|
|
481
|
-
// This source fee ramps with elapsed time.
|
|
482
|
-
uint256 fullSourceFeeAmount = JBFees.feeAmountFrom({
|
|
483
|
-
amountBeforeFee: loan.amount - prepaid,
|
|
484
|
-
feePercent: mulDiv({
|
|
485
|
-
x: timeSinceLoanCreated - loan.prepaidDuration,
|
|
486
|
-
y: JBConstants.MAX_FEE,
|
|
487
|
-
denominator: LOAN_LIQUIDATION_DURATION - loan.prepaidDuration
|
|
488
|
-
})
|
|
490
|
+
// Delegate the arithmetic so Halmos can prove the exact fee schedule without loading the full loan contract.
|
|
491
|
+
return REVLoansSourceFees.sourceFeeAmountFrom({
|
|
492
|
+
loan: loan,
|
|
493
|
+
amount: amount,
|
|
494
|
+
timeSinceLoanCreated: timeSinceLoanCreated,
|
|
495
|
+
loanLiquidationDuration: LOAN_LIQUIDATION_DURATION
|
|
489
496
|
});
|
|
490
|
-
|
|
491
|
-
// Calculate the source fee amount for the amount being paid off.
|
|
492
|
-
return mulDiv({x: fullSourceFeeAmount, y: amount, denominator: loan.amount});
|
|
493
497
|
}
|
|
494
498
|
|
|
495
499
|
/// @notice Generate an ID for a loan given a revnet ID and a loan number within that revnet.
|
|
@@ -515,19 +519,20 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
515
519
|
return ERC2771Context._msgSender();
|
|
516
520
|
}
|
|
517
521
|
|
|
518
|
-
/// @notice Returns
|
|
519
|
-
/// @param
|
|
520
|
-
/// @return
|
|
521
|
-
function
|
|
522
|
-
|
|
522
|
+
/// @notice Returns a single-terminal array for surplus calculations.
|
|
523
|
+
/// @param terminal The terminal to place in the array.
|
|
524
|
+
/// @return terminals The one-item terminal array.
|
|
525
|
+
function _singleTerminalArray(IJBTerminal terminal) internal pure returns (IJBTerminal[] memory terminals) {
|
|
526
|
+
terminals = new IJBTerminal[](1);
|
|
527
|
+
terminals[0] = terminal;
|
|
523
528
|
}
|
|
524
529
|
|
|
525
530
|
/// @notice The total borrowed amount from a revnet, aggregated across all loan sources.
|
|
526
531
|
/// @dev Each source's `totalBorrowedFrom` is stored in the source token's native decimals (e.g. 6 for USDC,
|
|
527
532
|
/// 18 for ETH). Before aggregation, each amount is normalized to the target `decimals` to prevent mixed-decimal
|
|
528
533
|
/// arithmetic errors. For cross-currency sources, the normalized amount is then converted via the price feed.
|
|
529
|
-
/// @dev
|
|
530
|
-
///
|
|
534
|
+
/// @dev Cross-currency sources fail closed if the price is zero. Core `JBPrices` reverts before returning zero;
|
|
535
|
+
/// the local zero check below covers mocked or nonconforming price modules so a source is never silently ignored.
|
|
531
536
|
/// @param revnetId The ID of the revnet to check.
|
|
532
537
|
/// @param decimals The decimals to use for the resulting fixed point value.
|
|
533
538
|
/// @param currency The currency to denominate the resulting value in.
|
|
@@ -535,7 +540,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
535
540
|
function _totalBorrowedFrom(
|
|
536
541
|
uint256 revnetId,
|
|
537
542
|
uint256 decimals,
|
|
538
|
-
uint256 currency
|
|
543
|
+
uint256 currency,
|
|
544
|
+
IJBTerminal multiTerminal
|
|
539
545
|
)
|
|
540
546
|
internal
|
|
541
547
|
view
|
|
@@ -543,50 +549,50 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
543
549
|
{
|
|
544
550
|
// Keep a reference to all sources being used to loaned out from this revnet.
|
|
545
551
|
// Use storage ref to avoid bulk-copying the entire array to memory.
|
|
546
|
-
|
|
552
|
+
address[] storage sources = _loanSourceTokensOf[revnetId];
|
|
547
553
|
|
|
548
554
|
// Iterate over all sources being used to loaned out.
|
|
549
555
|
for (uint256 i; i < sources.length; i++) {
|
|
550
556
|
// Get a reference to the token being iterated on.
|
|
551
|
-
|
|
557
|
+
address sourceToken = sources[i];
|
|
552
558
|
|
|
553
559
|
// Get a reference to the amount of tokens loaned out.
|
|
554
|
-
uint256 tokensLoaned = totalBorrowedFrom[revnetId][
|
|
560
|
+
uint256 tokensLoaned = totalBorrowedFrom[revnetId][sourceToken];
|
|
555
561
|
|
|
556
|
-
// Skip if no tokens are loaned from this source.
|
|
557
|
-
// reverting on stale sources whose terminals may no longer support this token.
|
|
562
|
+
// Skip if no tokens are loaned from this source.
|
|
558
563
|
if (tokensLoaned == 0) continue;
|
|
559
564
|
|
|
560
|
-
// Get
|
|
561
|
-
JBAccountingContext memory
|
|
562
|
-
|
|
565
|
+
// Get the current accounting context for the source token from the terminal being evaluated.
|
|
566
|
+
JBAccountingContext memory context =
|
|
567
|
+
multiTerminal.accountingContextForTokenOf({projectId: revnetId, token: sourceToken});
|
|
563
568
|
|
|
564
569
|
// Normalize the token amount from the source's decimals to the target decimals.
|
|
565
570
|
uint256 normalizedTokens;
|
|
566
|
-
if (
|
|
567
|
-
normalizedTokens = tokensLoaned / (10 ** (
|
|
568
|
-
} else if (
|
|
569
|
-
normalizedTokens = tokensLoaned * (10 ** (decimals -
|
|
571
|
+
if (context.decimals > decimals) {
|
|
572
|
+
normalizedTokens = tokensLoaned / (10 ** (context.decimals - decimals));
|
|
573
|
+
} else if (context.decimals < decimals) {
|
|
574
|
+
normalizedTokens = tokensLoaned * (10 ** (decimals - context.decimals));
|
|
570
575
|
} else {
|
|
571
576
|
normalizedTokens = tokensLoaned;
|
|
572
577
|
}
|
|
573
578
|
|
|
574
579
|
// If the currency matches, add the normalized amount directly.
|
|
575
|
-
if (
|
|
580
|
+
if (context.currency == currency) {
|
|
576
581
|
borrowedAmount += normalizedTokens;
|
|
577
582
|
} else {
|
|
578
|
-
// Otherwise, convert via the price feed.
|
|
583
|
+
// Otherwise, convert via the price feed. `JBPrices` itself rejects a zero price, but the explicit
|
|
584
|
+
// local check keeps the same fail-closed behavior if tests or future modules return 0 directly.
|
|
579
585
|
uint256 pricePerUnit = PRICES.pricePerUnitOf({
|
|
580
|
-
projectId: revnetId,
|
|
581
|
-
pricingCurrency: accountingContext.currency,
|
|
582
|
-
unitCurrency: currency,
|
|
583
|
-
decimals: decimals
|
|
586
|
+
projectId: revnetId, pricingCurrency: context.currency, unitCurrency: currency, decimals: decimals
|
|
584
587
|
});
|
|
585
588
|
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
589
|
+
// A zero denominator would either panic below or, if skipped, hide outstanding debt. Revert instead
|
|
590
|
+
// so misconfigured cross-currency sources cannot make borrowers appear safer than they are.
|
|
591
|
+
if (pricePerUnit == 0) {
|
|
592
|
+
revert REVLoans_ZeroPrice({
|
|
593
|
+
revnetId: revnetId, pricingCurrency: context.currency, unitCurrency: currency
|
|
594
|
+
});
|
|
595
|
+
}
|
|
590
596
|
|
|
591
597
|
borrowedAmount += mulDiv({x: normalizedTokens, y: 10 ** decimals, denominator: pricePerUnit});
|
|
592
598
|
}
|
|
@@ -606,9 +612,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
606
612
|
/// @dev A delegated operator (with OPEN_LOAN permission) can set `beneficiary` to any address, directing borrowed
|
|
607
613
|
/// funds away from the holder. Holders should only grant OPEN_LOAN to fully trusted operators.
|
|
608
614
|
/// @param revnetId The ID of the revnet to borrow from.
|
|
609
|
-
/// @param
|
|
610
|
-
/// @param minBorrowAmount The minimum amount to borrow, denominated in
|
|
611
|
-
/// context.
|
|
615
|
+
/// @param token The token to borrow from the revnet's canonical multi terminal.
|
|
616
|
+
/// @param minBorrowAmount The minimum amount to borrow, denominated in `token`.
|
|
612
617
|
/// @param collateralCount The amount of tokens to use as collateral for the loan.
|
|
613
618
|
/// @param beneficiary The address that will receive the borrowed funds and the tokens resulting from fee payments.
|
|
614
619
|
/// @param prepaidFeePercent The fee percent to charge upfront. Prepaying a fee is cheaper than paying later.
|
|
@@ -616,7 +621,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
616
621
|
/// @return loan The loan created.
|
|
617
622
|
function borrowFrom(
|
|
618
623
|
uint256 revnetId,
|
|
619
|
-
|
|
624
|
+
address token,
|
|
620
625
|
uint256 minBorrowAmount,
|
|
621
626
|
uint256 collateralCount,
|
|
622
627
|
address payable beneficiary,
|
|
@@ -625,6 +630,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
625
630
|
)
|
|
626
631
|
public
|
|
627
632
|
override
|
|
633
|
+
nonReentrantLoanAction
|
|
628
634
|
returns (uint256 loanId, REVLoan memory)
|
|
629
635
|
{
|
|
630
636
|
// Only the holder or a permissioned operator can open a loan on the holder's behalf.
|
|
@@ -633,7 +639,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
633
639
|
|
|
634
640
|
return _borrowFrom({
|
|
635
641
|
revnetId: revnetId,
|
|
636
|
-
|
|
642
|
+
token: token,
|
|
637
643
|
minBorrowAmount: minBorrowAmount,
|
|
638
644
|
collateralCount: collateralCount,
|
|
639
645
|
beneficiary: beneficiary,
|
|
@@ -655,7 +661,15 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
655
661
|
/// @param revnetId The ID of the revnet to liquidate loans from.
|
|
656
662
|
/// @param startingLoanId The loan number to start iterating from.
|
|
657
663
|
/// @param count The number of loans to iterate over.
|
|
658
|
-
function liquidateExpiredLoansFrom(
|
|
664
|
+
function liquidateExpiredLoansFrom(
|
|
665
|
+
uint256 revnetId,
|
|
666
|
+
uint256 startingLoanId,
|
|
667
|
+
uint256 count
|
|
668
|
+
)
|
|
669
|
+
external
|
|
670
|
+
override
|
|
671
|
+
nonReentrantLoanAction
|
|
672
|
+
{
|
|
659
673
|
// Prevent cross-revnet accounting corruption: loan numbers must stay within the revnet's ID namespace.
|
|
660
674
|
uint256 endLoanNumber = startingLoanId + count;
|
|
661
675
|
if (endLoanNumber > _ONE_TRILLION) {
|
|
@@ -700,7 +714,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
700
714
|
|
|
701
715
|
if (loan.amount > 0) {
|
|
702
716
|
// Decrement the amount loaned.
|
|
703
|
-
totalBorrowedFrom[revnetId][loan.
|
|
717
|
+
totalBorrowedFrom[revnetId][loan.sourceToken] -= loan.amount;
|
|
704
718
|
}
|
|
705
719
|
|
|
706
720
|
emit Liquidate({loanId: loanId, revnetId: revnetId, loan: loan, caller: sender});
|
|
@@ -716,9 +730,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
716
730
|
/// borrowed funds from the new loan away from the loan owner. Grant this permission only to trusted operators.
|
|
717
731
|
/// @param loanId The ID of the loan to reallocate collateral from.
|
|
718
732
|
/// @param collateralCountToTransfer The amount of collateral to transfer from the original loan.
|
|
719
|
-
/// @param
|
|
720
|
-
/// @param minBorrowAmount The minimum amount to borrow, denominated in
|
|
721
|
-
/// context.
|
|
733
|
+
/// @param token The token of the new loan. Must match the existing loan's source token.
|
|
734
|
+
/// @param minBorrowAmount The minimum amount to borrow, denominated in `token`.
|
|
722
735
|
/// @param collateralCountToAdd The amount of collateral to add to the new loan from your balance.
|
|
723
736
|
/// @param beneficiary The address that will receive the borrowed funds and the tokens resulting from fee payments.
|
|
724
737
|
/// @param prepaidFeePercent The fee percent to charge upfront for the new loan.
|
|
@@ -729,7 +742,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
729
742
|
function reallocateCollateralFromLoan(
|
|
730
743
|
uint256 loanId,
|
|
731
744
|
uint256 collateralCountToTransfer,
|
|
732
|
-
|
|
745
|
+
address token,
|
|
733
746
|
uint256 minBorrowAmount,
|
|
734
747
|
uint256 collateralCountToAdd,
|
|
735
748
|
address payable beneficiary,
|
|
@@ -737,6 +750,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
737
750
|
)
|
|
738
751
|
external
|
|
739
752
|
override
|
|
753
|
+
nonReentrantLoanAction
|
|
740
754
|
returns (uint256 reallocatedLoanId, uint256 newLoanId, REVLoan memory reallocatedLoan, REVLoan memory newLoan)
|
|
741
755
|
{
|
|
742
756
|
// Keep a reference to the revnet ID of the loan being reallocated.
|
|
@@ -766,14 +780,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
766
780
|
|
|
767
781
|
// Make sure the new loan's source matches the existing loan's source to prevent cross-source value extraction.
|
|
768
782
|
{
|
|
769
|
-
|
|
770
|
-
if (
|
|
771
|
-
revert REVLoans_SourceMismatch({
|
|
772
|
-
expectedToken: existingSource.token,
|
|
773
|
-
actualToken: source.token,
|
|
774
|
-
expectedTerminal: address(existingSource.terminal),
|
|
775
|
-
actualTerminal: address(source.terminal)
|
|
776
|
-
});
|
|
783
|
+
address existingToken = _loanOf[loanId].sourceToken;
|
|
784
|
+
if (token != existingToken) {
|
|
785
|
+
revert REVLoans_SourceMismatch({expectedToken: existingToken, actualToken: token});
|
|
777
786
|
}
|
|
778
787
|
}
|
|
779
788
|
|
|
@@ -790,7 +799,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
790
799
|
// permission above, and requiring OPEN_LOAN here would block operators with only REALLOCATE_LOAN.
|
|
791
800
|
(newLoanId, newLoan) = _borrowFrom({
|
|
792
801
|
revnetId: revnetId,
|
|
793
|
-
|
|
802
|
+
token: token,
|
|
794
803
|
minBorrowAmount: minBorrowAmount,
|
|
795
804
|
collateralCount: collateralCountToTransfer + collateralCountToAdd,
|
|
796
805
|
beneficiary: beneficiary,
|
|
@@ -820,6 +829,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
820
829
|
external
|
|
821
830
|
payable
|
|
822
831
|
override
|
|
832
|
+
nonReentrantLoanAction
|
|
823
833
|
returns (uint256 paidOffLoanId, REVLoan memory paidOffloan)
|
|
824
834
|
{
|
|
825
835
|
// Cache the sender to avoid repeated ERC2771 context reads.
|
|
@@ -891,7 +901,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
891
901
|
|
|
892
902
|
// Accept the funds that'll be used to pay off loans.
|
|
893
903
|
maxRepayBorrowAmount =
|
|
894
|
-
_acceptFundsFor({token: loan.
|
|
904
|
+
_acceptFundsFor({token: loan.sourceToken, amount: maxRepayBorrowAmount, allowance: allowance});
|
|
895
905
|
|
|
896
906
|
// Re-check ownership: an ERC-777/ERC-1363 source token can reenter during the transfer above and transfer
|
|
897
907
|
// the loan NFT to another account. Without this check, `_repayLoan` would burn the new owner's NFT while
|
|
@@ -911,7 +921,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
911
921
|
}
|
|
912
922
|
|
|
913
923
|
// Cache the source token before _repayLoan deletes the loan storage.
|
|
914
|
-
address sourceToken = loan.
|
|
924
|
+
address sourceToken = loan.sourceToken;
|
|
915
925
|
|
|
916
926
|
(paidOffLoanId, paidOffloan) = _repayLoan({
|
|
917
927
|
loanId: loanId,
|
|
@@ -1027,39 +1037,39 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1027
1037
|
)
|
|
1028
1038
|
internal
|
|
1029
1039
|
{
|
|
1040
|
+
address sourceToken = loan.sourceToken;
|
|
1041
|
+
|
|
1030
1042
|
// Register the source if this is the first time its being used for this revnet.
|
|
1031
|
-
// Note: Sources are only appended, never removed. Gas accumulation from iteration is bounded
|
|
1032
|
-
//
|
|
1033
|
-
if (!isLoanSourceOf[revnetId][
|
|
1034
|
-
isLoanSourceOf[revnetId][
|
|
1035
|
-
|
|
1043
|
+
// Note: Sources are only appended, never removed. Gas accumulation from iteration is bounded by the revnet's
|
|
1044
|
+
// accepted accounting contexts.
|
|
1045
|
+
if (!isLoanSourceOf[revnetId][sourceToken]) {
|
|
1046
|
+
isLoanSourceOf[revnetId][sourceToken] = true;
|
|
1047
|
+
_loanSourceTokensOf[revnetId].push(sourceToken);
|
|
1036
1048
|
}
|
|
1037
1049
|
|
|
1038
|
-
// Increment the amount of the token borrowed from the revnet
|
|
1039
|
-
totalBorrowedFrom[revnetId][
|
|
1050
|
+
// Increment the amount of the token borrowed from the revnet.
|
|
1051
|
+
totalBorrowedFrom[revnetId][sourceToken] += addedBorrowAmount;
|
|
1040
1052
|
|
|
1041
1053
|
uint256 netAmountPaidOut;
|
|
1042
1054
|
{
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
loan.source.terminal.accountingContextForTokenOf({projectId: revnetId, token: loan.source.token});
|
|
1055
|
+
JBAccountingContext memory context =
|
|
1056
|
+
TERMINAL.accountingContextForTokenOf({projectId: revnetId, token: sourceToken});
|
|
1046
1057
|
|
|
1047
1058
|
// Pull the amount to be loaned out of the revnet. This will incure the protocol fee.
|
|
1048
|
-
netAmountPaidOut =
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
});
|
|
1059
|
+
netAmountPaidOut = TERMINAL.useAllowanceOf({
|
|
1060
|
+
projectId: revnetId,
|
|
1061
|
+
token: sourceToken,
|
|
1062
|
+
amount: addedBorrowAmount,
|
|
1063
|
+
currency: context.currency,
|
|
1064
|
+
minTokensPaidOut: 0,
|
|
1065
|
+
beneficiary: payable(address(this)),
|
|
1066
|
+
feeBeneficiary: beneficiary,
|
|
1067
|
+
memo: ""
|
|
1068
|
+
});
|
|
1059
1069
|
}
|
|
1060
1070
|
|
|
1061
1071
|
// Keep a reference to the fee terminal.
|
|
1062
|
-
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: REV_ID, token:
|
|
1072
|
+
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: REV_ID, token: sourceToken});
|
|
1063
1073
|
|
|
1064
1074
|
// Get the amount of additional fee to take for REV.
|
|
1065
1075
|
uint256 revFeeAmount = address(feeTerminal) == address(0)
|
|
@@ -1071,7 +1081,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1071
1081
|
if (!_tryPayFee({
|
|
1072
1082
|
terminal: feeTerminal,
|
|
1073
1083
|
projectId: REV_ID,
|
|
1074
|
-
token:
|
|
1084
|
+
token: sourceToken,
|
|
1075
1085
|
amount: revFeeAmount,
|
|
1076
1086
|
beneficiary: beneficiary,
|
|
1077
1087
|
metadataProjectId: revnetId
|
|
@@ -1087,17 +1097,15 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1087
1097
|
_transferFrom({
|
|
1088
1098
|
from: address(this),
|
|
1089
1099
|
to: beneficiary,
|
|
1090
|
-
token:
|
|
1100
|
+
token: sourceToken,
|
|
1091
1101
|
amount: netAmountPaidOut - revFeeAmount - sourceFeeAmount
|
|
1092
1102
|
});
|
|
1093
1103
|
}
|
|
1094
1104
|
|
|
1095
1105
|
/// @notice Adjust a loan -- pay it back, add more, or return excess collateral.
|
|
1096
|
-
/// @dev
|
|
1097
|
-
///
|
|
1098
|
-
///
|
|
1099
|
-
/// checks that should fail. Practically infeasible — requires an adversarial pay hook on the revnet's own
|
|
1100
|
-
/// terminal that calls back into `borrowFrom`, which is not a realistic deployment configuration.
|
|
1106
|
+
/// @dev `borrowFrom`, `reallocateCollateralFromLoan`, and `repayLoan` hold a transient lock across this function.
|
|
1107
|
+
/// External terminal, token, and beneficiary callbacks may observe in-progress loan state, but they cannot nest
|
|
1108
|
+
/// another loan-changing action before aggregate collateral and borrowed accounting have finished updating.
|
|
1101
1109
|
/// @param loan The loan to adjust.
|
|
1102
1110
|
/// @param revnetId The ID of the revnet the loan is in.
|
|
1103
1111
|
/// @param newBorrowAmount The new amount of the loan, denominated in the token of the source's accounting
|
|
@@ -1118,8 +1126,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1118
1126
|
internal
|
|
1119
1127
|
{
|
|
1120
1128
|
// Cache frequently-read storage fields to avoid repeated SLOAD.
|
|
1121
|
-
address sourceToken = loan.
|
|
1122
|
-
IJBPayoutTerminal sourceTerminal = loan.source.terminal;
|
|
1129
|
+
address sourceToken = loan.sourceToken;
|
|
1123
1130
|
|
|
1124
1131
|
// Snapshot deltas from current state before writing.
|
|
1125
1132
|
uint256 addedBorrowAmount = newBorrowAmount > loan.amount ? newBorrowAmount - loan.amount : 0;
|
|
@@ -1168,7 +1175,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1168
1175
|
// Try to pay the source fee. If it fails, transfer the amount to the beneficiary instead.
|
|
1169
1176
|
if (sourceFeeAmount > 0) {
|
|
1170
1177
|
if (!_tryPayFee({
|
|
1171
|
-
terminal:
|
|
1178
|
+
terminal: TERMINAL,
|
|
1172
1179
|
projectId: revnetId,
|
|
1173
1180
|
token: sourceToken,
|
|
1174
1181
|
amount: sourceFeeAmount,
|
|
@@ -1205,7 +1212,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1205
1212
|
/// @dev Called by `borrowFrom` (after its own permission check) and by `reallocateCollateralFromLoan`
|
|
1206
1213
|
/// (which only requires REALLOCATE_LOAN permission).
|
|
1207
1214
|
/// @param revnetId The ID of the revnet to borrow from.
|
|
1208
|
-
/// @param
|
|
1215
|
+
/// @param token The token to borrow.
|
|
1209
1216
|
/// @param minBorrowAmount The minimum amount to borrow.
|
|
1210
1217
|
/// @param collateralCount The amount of tokens to use as collateral for the loan.
|
|
1211
1218
|
/// @param beneficiary The address that will receive the borrowed funds and fee payment tokens.
|
|
@@ -1215,7 +1222,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1215
1222
|
/// @return loan The loan created.
|
|
1216
1223
|
function _borrowFrom(
|
|
1217
1224
|
uint256 revnetId,
|
|
1218
|
-
|
|
1225
|
+
address token,
|
|
1219
1226
|
uint256 minBorrowAmount,
|
|
1220
1227
|
uint256 collateralCount,
|
|
1221
1228
|
address payable beneficiary,
|
|
@@ -1228,10 +1235,14 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1228
1235
|
// A loan needs to have collateral.
|
|
1229
1236
|
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid({collateralCount: collateralCount});
|
|
1230
1237
|
|
|
1231
|
-
//
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1238
|
+
// Cache the current ruleset once — used by source validation, _cashOutDelayOf, and _borrowAmountFrom.
|
|
1239
|
+
JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
|
|
1240
|
+
|
|
1241
|
+
// Make sure the token's accounting context exists on the canonical multi terminal for this revnet. An
|
|
1242
|
+
// unaccepted token reads as an empty accounting context from the terminal store, which must not be treated as
|
|
1243
|
+
// a valid zero-decimal/zero-currency loan source.
|
|
1244
|
+
JBAccountingContext memory context = TERMINAL.accountingContextForTokenOf({projectId: revnetId, token: token});
|
|
1245
|
+
if (context.token != token) revert REVLoans_InvalidAccountingContext({revnetId: revnetId, token: token});
|
|
1235
1246
|
|
|
1236
1247
|
// Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
|
|
1237
1248
|
// an 16 year loan can be paid upfront with a
|
|
@@ -1242,9 +1253,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1242
1253
|
});
|
|
1243
1254
|
}
|
|
1244
1255
|
|
|
1245
|
-
// Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
|
|
1246
|
-
JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
|
|
1247
|
-
|
|
1248
1256
|
// Enforce the cash out delay.
|
|
1249
1257
|
{
|
|
1250
1258
|
uint256 cashOutDelay = _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
|
|
@@ -1261,7 +1269,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1261
1269
|
REVLoan storage loan = _loanOf[loanId];
|
|
1262
1270
|
|
|
1263
1271
|
// Set the loan's values.
|
|
1264
|
-
loan.
|
|
1272
|
+
loan.sourceToken = token;
|
|
1265
1273
|
loan.createdAt = uint48(block.timestamp);
|
|
1266
1274
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1267
1275
|
loan.prepaidFeePercent = uint16(prepaidFeePercent);
|
|
@@ -1302,7 +1310,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1302
1310
|
loanId: loanId,
|
|
1303
1311
|
revnetId: revnetId,
|
|
1304
1312
|
loan: loan,
|
|
1305
|
-
|
|
1313
|
+
token: token,
|
|
1306
1314
|
borrowAmount: borrowAmount,
|
|
1307
1315
|
collateralCount: collateralCount,
|
|
1308
1316
|
sourceFeeAmount: sourceFeeAmount,
|
|
@@ -1383,7 +1391,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1383
1391
|
reallocatedLoan.createdAt = loan.createdAt;
|
|
1384
1392
|
reallocatedLoan.prepaidFeePercent = loan.prepaidFeePercent;
|
|
1385
1393
|
reallocatedLoan.prepaidDuration = loan.prepaidDuration;
|
|
1386
|
-
reallocatedLoan.
|
|
1394
|
+
reallocatedLoan.sourceToken = loan.sourceToken;
|
|
1387
1395
|
|
|
1388
1396
|
// Reduce the collateral of the reallocated loan.
|
|
1389
1397
|
_adjust({
|
|
@@ -1419,25 +1427,25 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1419
1427
|
/// @param repaidBorrowAmount The amount to pay off, denominated in the token of the source's accounting
|
|
1420
1428
|
/// context.
|
|
1421
1429
|
function _removeFrom(REVLoan memory loan, uint256 revnetId, uint256 repaidBorrowAmount) internal {
|
|
1422
|
-
|
|
1423
|
-
|
|
1430
|
+
address sourceToken = loan.sourceToken;
|
|
1431
|
+
|
|
1432
|
+
// Decrement the total amount of a token being loaned out by the revnet.
|
|
1433
|
+
totalBorrowedFrom[revnetId][sourceToken] -= repaidBorrowAmount;
|
|
1424
1434
|
|
|
1425
1435
|
// Increase the allowance for the beneficiary.
|
|
1426
|
-
uint256 payValue = _beforeTransferTo({
|
|
1427
|
-
to: address(loan.source.terminal), token: loan.source.token, amount: repaidBorrowAmount
|
|
1428
|
-
});
|
|
1436
|
+
uint256 payValue = _beforeTransferTo({to: address(TERMINAL), token: sourceToken, amount: repaidBorrowAmount});
|
|
1429
1437
|
|
|
1430
1438
|
// Add the loaned amount back to the revnet.
|
|
1431
|
-
|
|
1439
|
+
TERMINAL.addToBalanceOf{value: payValue}({
|
|
1432
1440
|
projectId: revnetId,
|
|
1433
|
-
token:
|
|
1441
|
+
token: sourceToken,
|
|
1434
1442
|
amount: repaidBorrowAmount,
|
|
1435
1443
|
shouldReturnHeldFees: false,
|
|
1436
1444
|
memo: "",
|
|
1437
1445
|
metadata: bytes(abi.encodePacked(REV_ID))
|
|
1438
1446
|
});
|
|
1439
1447
|
|
|
1440
|
-
_afterTransferTo({to: address(
|
|
1448
|
+
_afterTransferTo({to: address(TERMINAL), token: sourceToken});
|
|
1441
1449
|
}
|
|
1442
1450
|
|
|
1443
1451
|
/// @notice Pay down a loan.
|
|
@@ -1514,7 +1522,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1514
1522
|
paidOffLoan.createdAt = loan.createdAt;
|
|
1515
1523
|
paidOffLoan.prepaidFeePercent = loan.prepaidFeePercent;
|
|
1516
1524
|
paidOffLoan.prepaidDuration = loan.prepaidDuration;
|
|
1517
|
-
paidOffLoan.
|
|
1525
|
+
paidOffLoan.sourceToken = loan.sourceToken;
|
|
1518
1526
|
|
|
1519
1527
|
// Mint the replacement loan to the loan owner FIRST so it exists before _adjust writes data.
|
|
1520
1528
|
_mint({to: loanOwner, tokenId: paidOffLoanId});
|