@rev-net/core-v6 0.0.29 → 0.0.31
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/ADMINISTRATION.md +19 -9
- package/ARCHITECTURE.md +3 -0
- package/AUDIT_INSTRUCTIONS.md +11 -1
- package/CHANGELOG.md +26 -0
- package/README.md +1 -0
- package/RISKS.md +28 -4
- package/SKILLS.md +2 -1
- package/USER_JOURNEYS.md +28 -3
- package/package.json +8 -8
- package/references/operations.md +1 -1
- package/script/Deploy.s.sol +26 -4
- package/src/REVDeployer.sol +4 -2
- package/src/REVHiddenTokens.sol +149 -0
- package/src/REVLoans.sol +192 -199
- package/src/REVOwner.sol +51 -14
- package/src/interfaces/IREVHiddenTokens.sol +53 -0
- package/src/interfaces/IREVLoans.sol +8 -6
- package/test/REV.integrations.t.sol +12 -2
- package/test/REVAutoIssuanceFuzz.t.sol +12 -2
- package/test/REVDeployerRegressions.t.sol +14 -3
- package/test/REVInvincibility.t.sol +27 -8
- package/test/REVInvincibilityHandler.sol +1 -1
- package/test/REVLifecycle.t.sol +14 -3
- package/test/REVLoans.invariants.t.sol +15 -4
- package/test/REVLoansAttacks.t.sol +19 -7
- package/test/REVLoansFeeRecovery.t.sol +24 -13
- package/test/REVLoansFindings.t.sol +16 -5
- package/test/REVLoansRegressions.t.sol +15 -4
- package/test/REVLoansSourceFeeRecovery.t.sol +16 -5
- package/test/REVLoansSourced.t.sol +60 -25
- package/test/REVLoansUnSourced.t.sol +15 -4
- package/test/TestBurnHeldTokens.t.sol +14 -3
- package/test/TestCEIPattern.t.sol +19 -7
- package/test/TestCashOutCallerValidation.t.sol +15 -4
- package/test/TestConversionDocumentation.t.sol +14 -3
- package/test/TestCrossCurrencyReclaim.t.sol +14 -3
- package/test/TestCrossSourceReallocation.t.sol +15 -4
- package/test/TestERC2771MetaTx.t.sol +18 -5
- package/test/TestEmptyBuybackSpecs.t.sol +14 -3
- package/test/TestFlashLoanSurplus.t.sol +15 -4
- package/test/TestHiddenTokens.t.sol +431 -0
- package/test/TestHookArrayOOB.t.sol +14 -3
- package/test/TestLiquidationBehavior.t.sol +16 -5
- package/test/TestLoanSourceRotation.t.sol +20 -7
- package/test/TestLoansCashOutDelay.t.sol +18 -7
- package/test/TestLongTailEconomics.t.sol +14 -3
- package/test/TestLowFindings.t.sol +25 -9
- package/test/TestMixedFixes.t.sol +19 -8
- package/test/TestPermit2Signatures.t.sol +15 -4
- package/test/TestReallocationSandwich.t.sol +16 -4
- package/test/TestRevnetRegressions.t.sol +16 -5
- package/test/TestSplitWeightAdjustment.t.sol +16 -4
- package/test/TestSplitWeightE2E.t.sol +18 -4
- package/test/TestSplitWeightFork.t.sol +16 -3
- package/test/TestStageTransitionBorrowable.t.sol +14 -3
- package/test/TestSwapTerminalPermission.t.sol +14 -3
- package/test/TestUint112Overflow.t.sol +15 -4
- package/test/TestZeroAmountLoanGuard.t.sol +15 -4
- package/test/TestZeroRepayment.t.sol +15 -4
- package/test/audit/CodexPhantomSurplusTerminal.t.sol +367 -0
- package/test/audit/LoanIdOverflowGuard.t.sol +16 -5
- package/test/audit/NemesisOperatorDelegation.t.sol +289 -0
- package/test/fork/ForkTestBase.sol +18 -4
- package/test/fork/TestLoanBorrowFork.t.sol +2 -1
- package/test/fork/TestLoanERC20Fork.t.sol +4 -2
- package/test/fork/TestLoanTransferFork.t.sol +12 -2
- package/test/helpers/MaliciousContracts.sol +1 -1
- package/test/mock/MockBuybackCashOutRecorder.sol +2 -0
- package/test/mock/MockBuybackDataHook.sol +3 -1
- package/test/mock/MockBuybackDataHookMintPath.sol +2 -0
- package/test/mock/MockSuckerRegistry.sol +17 -0
- package/test/regression/TestBurnPermissionRequired.t.sol +16 -5
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +16 -3
- package/test/regression/TestCrossRevnetLiquidation.t.sol +14 -3
- package/test/regression/TestCumulativeLoanCounter.t.sol +15 -4
- package/test/regression/TestLiquidateGapHandling.t.sol +15 -4
- package/test/regression/TestZeroPriceFeed.t.sol +17 -6
package/src/REVLoans.sol
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
|
+
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
4
5
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
5
6
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
6
7
|
import {IJBPayoutTerminal} from "@bananapus/core-v6/src/interfaces/IJBPayoutTerminal.sol";
|
|
8
|
+
import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
|
|
7
9
|
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
8
|
-
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
9
10
|
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
10
11
|
import {IJBTokenUriResolver} from "@bananapus/core-v6/src/interfaces/IJBTokenUriResolver.sol";
|
|
11
12
|
import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
|
|
@@ -16,6 +17,8 @@ import {JBSurplus} from "@bananapus/core-v6/src/libraries/JBSurplus.sol";
|
|
|
16
17
|
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
17
18
|
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
18
19
|
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
20
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
21
|
+
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
19
22
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
20
23
|
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
21
24
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
@@ -45,7 +48,7 @@ import {REVLoanSource} from "./structs/REVLoanSource.sol";
|
|
|
45
48
|
/// cannot be
|
|
46
49
|
/// recouped.
|
|
47
50
|
/// @dev The loaned amounts include the fees taken, meaning the amount paid back is the amount borrowed plus the fees.
|
|
48
|
-
contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
51
|
+
contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans {
|
|
49
52
|
// A library that parses the packed ruleset metadata into a friendlier format.
|
|
50
53
|
using JBRulesetMetadataResolver for JBRuleset;
|
|
51
54
|
|
|
@@ -71,7 +74,6 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
71
74
|
error REVLoans_PermitAllowanceNotEnough(uint256 allowanceAmount, uint256 requiredAmount);
|
|
72
75
|
error REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(uint256 newBorrowAmount, uint256 loanAmount);
|
|
73
76
|
error REVLoans_SourceMismatch();
|
|
74
|
-
error REVLoans_Unauthorized(address caller, address owner);
|
|
75
77
|
error REVLoans_UnderMinBorrowAmount(uint256 minBorrowAmount, uint256 borrowAmount);
|
|
76
78
|
error REVLoans_ZeroBorrowAmount();
|
|
77
79
|
error REVLoans_ZeroCollateralLoanIsInvalid();
|
|
@@ -120,12 +122,12 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
120
122
|
/// @notice A contract that stores prices for each revnet.
|
|
121
123
|
IJBPrices public immutable override PRICES;
|
|
122
124
|
|
|
123
|
-
/// @notice Mints ERC-721s that represent revnet ownership and transfers.
|
|
124
|
-
IJBProjects public immutable override PROJECTS;
|
|
125
|
-
|
|
126
125
|
/// @notice The ID of the REV revnet that will receive the fees.
|
|
127
126
|
uint256 public immutable override REV_ID;
|
|
128
127
|
|
|
128
|
+
/// @notice The sucker registry used to discover peer chain suckers for cross-chain awareness.
|
|
129
|
+
IJBSuckerRegistry public immutable override SUCKER_REGISTRY;
|
|
130
|
+
|
|
129
131
|
//*********************************************************************//
|
|
130
132
|
// --------------------- public stored properties -------------------- //
|
|
131
133
|
//*********************************************************************//
|
|
@@ -182,14 +184,14 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
182
184
|
//*********************************************************************//
|
|
183
185
|
|
|
184
186
|
/// @param controller The controller that manages revnets using this loans contract.
|
|
185
|
-
/// @param
|
|
187
|
+
/// @param suckerRegistry The registry used to discover peer chain suckers for cross-chain supply/surplus awareness.
|
|
186
188
|
/// @param revId The ID of the REV revnet that will receive the fees.
|
|
187
189
|
/// @param owner The owner of the contract that can set the URI resolver.
|
|
188
190
|
/// @param permit2 A permit2 utility.
|
|
189
191
|
/// @param trustedForwarder A trusted forwarder of transactions to this contract.
|
|
190
192
|
constructor(
|
|
191
193
|
IJBController controller,
|
|
192
|
-
|
|
194
|
+
IJBSuckerRegistry suckerRegistry,
|
|
193
195
|
uint256 revId,
|
|
194
196
|
address owner,
|
|
195
197
|
IPermit2 permit2,
|
|
@@ -197,14 +199,15 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
197
199
|
)
|
|
198
200
|
ERC721("REV Loans", "$REVLOAN")
|
|
199
201
|
ERC2771Context(trustedForwarder)
|
|
202
|
+
JBPermissioned(IJBPermissioned(address(controller)).PERMISSIONS())
|
|
200
203
|
Ownable(owner)
|
|
201
204
|
{
|
|
202
205
|
CONTROLLER = controller;
|
|
203
206
|
DIRECTORY = controller.DIRECTORY();
|
|
204
207
|
PRICES = controller.PRICES();
|
|
205
|
-
PROJECTS = projects;
|
|
206
208
|
REV_ID = revId;
|
|
207
209
|
PERMIT2 = permit2;
|
|
210
|
+
SUCKER_REGISTRY = suckerRegistry;
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
//*********************************************************************//
|
|
@@ -228,8 +231,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
228
231
|
returns (uint256)
|
|
229
232
|
{
|
|
230
233
|
// Cache the current ruleset once — used by both _cashOutDelayOf and _borrowableAmountFrom.
|
|
231
|
-
|
|
232
|
-
(JBRuleset memory currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
234
|
+
JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
|
|
233
235
|
|
|
234
236
|
// If the cash out delay hasn't passed yet, no amount is borrowable.
|
|
235
237
|
if (_cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset}) > block.timestamp) return 0;
|
|
@@ -239,7 +241,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
239
241
|
collateralCount: collateralCount,
|
|
240
242
|
decimals: decimals,
|
|
241
243
|
currency: currency,
|
|
242
|
-
terminals:
|
|
244
|
+
terminals: _terminalsOf(revnetId),
|
|
243
245
|
currentStage: currentRuleset
|
|
244
246
|
});
|
|
245
247
|
}
|
|
@@ -295,32 +297,6 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
295
297
|
// -------------------------- internal views ------------------------- //
|
|
296
298
|
//*********************************************************************//
|
|
297
299
|
|
|
298
|
-
/// @notice Returns the cash out delay timestamp for a revnet by resolving the data hook from the current ruleset.
|
|
299
|
-
/// @param revnetId The ID of the revnet.
|
|
300
|
-
/// @return The cash out delay timestamp. Returns 0 if no data hook is set or no delay exists.
|
|
301
|
-
function _cashOutDelayOf(uint256 revnetId) internal view returns (uint256) {
|
|
302
|
-
// Get the revnet's current ruleset to find its data hook (the REVOwner contract).
|
|
303
|
-
// slither-disable-next-line unused-return
|
|
304
|
-
(JBRuleset memory currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
305
|
-
|
|
306
|
-
return _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/// @notice Returns the cash out delay timestamp using a pre-fetched ruleset (avoids redundant external call).
|
|
310
|
-
/// @param revnetId The ID of the revnet.
|
|
311
|
-
/// @param currentRuleset The pre-fetched current ruleset.
|
|
312
|
-
/// @return The cash out delay timestamp. Returns 0 if no data hook is set or no delay exists.
|
|
313
|
-
function _cashOutDelayOf(uint256 revnetId, JBRuleset memory currentRuleset) internal view returns (uint256) {
|
|
314
|
-
// Extract the data hook address from the ruleset's packed metadata.
|
|
315
|
-
address dataHook = currentRuleset.dataHook();
|
|
316
|
-
|
|
317
|
-
// If there's no data hook, this isn't a revnet — no cash out delay applies.
|
|
318
|
-
if (dataHook == address(0)) return 0;
|
|
319
|
-
|
|
320
|
-
// Read the cash out delay from the REVOwner contract (the data hook).
|
|
321
|
-
return IREVOwner(dataHook).cashOutDelayOf(revnetId);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
300
|
/// @notice Checks this contract's balance of a specific token.
|
|
325
301
|
/// @param token The address of the token to get this contract's balance of.
|
|
326
302
|
/// @return This contract's balance.
|
|
@@ -357,33 +333,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
357
333
|
/// @param decimals The decimals the resulting fixed point value will include.
|
|
358
334
|
/// @param currency The currency that the resulting amount should be in terms of.
|
|
359
335
|
/// @param terminals The terminals that the funds are being borrowed from.
|
|
336
|
+
/// @param currentStage The pre-fetched current ruleset.
|
|
360
337
|
/// @return borrowableAmount The amount that can be borrowed from the revnet.
|
|
361
|
-
function _borrowableAmountFrom(
|
|
362
|
-
uint256 revnetId,
|
|
363
|
-
uint256 collateralCount,
|
|
364
|
-
uint256 decimals,
|
|
365
|
-
uint256 currency,
|
|
366
|
-
IJBTerminal[] memory terminals
|
|
367
|
-
)
|
|
368
|
-
internal
|
|
369
|
-
view
|
|
370
|
-
returns (uint256)
|
|
371
|
-
{
|
|
372
|
-
// Keep a reference to the current stage.
|
|
373
|
-
// slither-disable-next-line unused-return
|
|
374
|
-
(JBRuleset memory currentStage,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
375
|
-
|
|
376
|
-
return _borrowableAmountFrom({
|
|
377
|
-
revnetId: revnetId,
|
|
378
|
-
collateralCount: collateralCount,
|
|
379
|
-
decimals: decimals,
|
|
380
|
-
currency: currency,
|
|
381
|
-
terminals: terminals,
|
|
382
|
-
currentStage: currentStage
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/// @dev Overload that accepts a pre-fetched ruleset to avoid redundant `currentRulesetOf` calls.
|
|
387
338
|
function _borrowableAmountFrom(
|
|
388
339
|
uint256 revnetId,
|
|
389
340
|
uint256 collateralCount,
|
|
@@ -411,53 +362,37 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
411
362
|
// Get a refeerence to the collateral being used to secure loans.
|
|
412
363
|
uint256 totalCollateral = totalCollateralOf[revnetId];
|
|
413
364
|
|
|
365
|
+
// The local supply includes both circulating tokens and tokens locked as loan collateral.
|
|
366
|
+
uint256 localSupply = totalSupply + totalCollateral;
|
|
367
|
+
|
|
368
|
+
// The local surplus includes both the treasury surplus and the outstanding borrowed amounts.
|
|
369
|
+
uint256 localSurplus = totalSurplus + totalBorrowed;
|
|
370
|
+
|
|
414
371
|
// Proportional — uses the CURRENT stage's cashOutTaxRate.
|
|
415
372
|
// NOTE: When a revnet transitions between stages with different cashOutTaxRate values, the borrowable amount
|
|
416
373
|
// for the same collateral changes. A lower cashOutTaxRate in a later stage means more borrowable value per
|
|
417
374
|
// collateral. This is by design: loan value tracks the current bonding curve parameters, just as cash-out
|
|
418
375
|
// value does. Borrowers benefit from decreasing tax rates and are constrained by increasing ones.
|
|
419
|
-
|
|
420
|
-
|
|
376
|
+
// Add cross-chain remote values for proportional reclaim.
|
|
377
|
+
uint256 omnichainSurplus =
|
|
378
|
+
localSurplus + SUCKER_REGISTRY.remoteSurplusOf({projectId: revnetId, decimals: 18, currency: currency});
|
|
379
|
+
uint256 omnichainSupply = localSupply + SUCKER_REGISTRY.remoteTotalSupplyOf(revnetId);
|
|
380
|
+
uint256 reclaimable = JBCashOuts.cashOutFrom({
|
|
381
|
+
surplus: omnichainSurplus,
|
|
421
382
|
cashOutCount: collateralCount,
|
|
422
|
-
totalSupply:
|
|
383
|
+
totalSupply: omnichainSupply,
|
|
423
384
|
cashOutTaxRate: currentStage.cashOutTaxRate()
|
|
424
385
|
});
|
|
386
|
+
// Cap at local surplus — can't borrow more than what this chain's terminals actually hold.
|
|
387
|
+
return reclaimable > localSurplus ? localSurplus : reclaimable;
|
|
425
388
|
}
|
|
426
389
|
|
|
427
390
|
/// @notice The amount of the loan that should be borrowed for the given collateral amount.
|
|
428
391
|
/// @param loan The loan having its borrow amount determined.
|
|
429
392
|
/// @param revnetId The ID of the revnet to check for borrowable assets from.
|
|
430
393
|
/// @param collateralCount The amount of collateral that the loan will be collateralized with.
|
|
394
|
+
/// @param currentRuleset The pre-fetched current ruleset.
|
|
431
395
|
/// @return borrowAmount The amount of the loan that should be borrowed.
|
|
432
|
-
function _borrowAmountFrom(
|
|
433
|
-
REVLoan storage loan,
|
|
434
|
-
uint256 revnetId,
|
|
435
|
-
uint256 collateralCount
|
|
436
|
-
)
|
|
437
|
-
internal
|
|
438
|
-
view
|
|
439
|
-
returns (uint256)
|
|
440
|
-
{
|
|
441
|
-
// If there's no collateral, there's no loan.
|
|
442
|
-
if (collateralCount == 0) return 0;
|
|
443
|
-
|
|
444
|
-
// Get a reference to the accounting context for the source.
|
|
445
|
-
JBAccountingContext memory accountingContext =
|
|
446
|
-
loan.source.terminal.accountingContextForTokenOf({projectId: revnetId, token: loan.source.token});
|
|
447
|
-
|
|
448
|
-
// Keep a reference to the revnet's terminals.
|
|
449
|
-
IJBTerminal[] memory terminals = DIRECTORY.terminalsOf(revnetId);
|
|
450
|
-
|
|
451
|
-
return _borrowableAmountFrom({
|
|
452
|
-
revnetId: revnetId,
|
|
453
|
-
collateralCount: collateralCount,
|
|
454
|
-
decimals: accountingContext.decimals,
|
|
455
|
-
currency: accountingContext.currency,
|
|
456
|
-
terminals: terminals
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/// @dev Overload that accepts a pre-fetched ruleset to avoid redundant `currentRulesetOf` calls.
|
|
461
396
|
function _borrowAmountFrom(
|
|
462
397
|
REVLoan storage loan,
|
|
463
398
|
uint256 revnetId,
|
|
@@ -476,7 +411,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
476
411
|
loan.source.terminal.accountingContextForTokenOf({projectId: revnetId, token: loan.source.token});
|
|
477
412
|
|
|
478
413
|
// Keep a reference to the revnet's terminals.
|
|
479
|
-
IJBTerminal[] memory terminals =
|
|
414
|
+
IJBTerminal[] memory terminals = _terminalsOf(revnetId);
|
|
480
415
|
|
|
481
416
|
return _borrowableAmountFrom({
|
|
482
417
|
revnetId: revnetId,
|
|
@@ -488,11 +423,34 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
488
423
|
});
|
|
489
424
|
}
|
|
490
425
|
|
|
426
|
+
/// @notice Returns the cash out delay timestamp using a pre-fetched ruleset.
|
|
427
|
+
/// @param revnetId The ID of the revnet.
|
|
428
|
+
/// @param currentRuleset The pre-fetched current ruleset.
|
|
429
|
+
/// @return The cash out delay timestamp. Returns 0 if no data hook is set or no delay exists.
|
|
430
|
+
function _cashOutDelayOf(uint256 revnetId, JBRuleset memory currentRuleset) internal view returns (uint256) {
|
|
431
|
+
// Extract the data hook address from the ruleset's packed metadata.
|
|
432
|
+
address dataHook = currentRuleset.dataHook();
|
|
433
|
+
|
|
434
|
+
// If there's no data hook, this isn't a revnet — no cash out delay applies.
|
|
435
|
+
if (dataHook == address(0)) return 0;
|
|
436
|
+
|
|
437
|
+
// Read the cash out delay from the REVOwner contract (the data hook).
|
|
438
|
+
return IREVOwner(dataHook).cashOutDelayOf(revnetId);
|
|
439
|
+
}
|
|
440
|
+
|
|
491
441
|
/// @dev `ERC-2771` specifies the context as being a single address (20 bytes).
|
|
492
442
|
function _contextSuffixLength() internal view override(ERC2771Context, Context) returns (uint256) {
|
|
493
443
|
return super._contextSuffixLength();
|
|
494
444
|
}
|
|
495
445
|
|
|
446
|
+
/// @notice Returns the current ruleset for a revnet. Consolidates ABI encode/decode to a single site.
|
|
447
|
+
/// @param revnetId The ID of the revnet.
|
|
448
|
+
/// @return currentRuleset The current ruleset.
|
|
449
|
+
function _currentRulesetOf(uint256 revnetId) internal view returns (JBRuleset memory currentRuleset) {
|
|
450
|
+
// slither-disable-next-line unused-return
|
|
451
|
+
(currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
452
|
+
}
|
|
453
|
+
|
|
496
454
|
/// @notice Determines the source fee amount for a loan being paid off a certain amount.
|
|
497
455
|
/// @param loan The loan having its source fee amount determined.
|
|
498
456
|
/// @param amount The amount being paid off.
|
|
@@ -550,6 +508,13 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
550
508
|
return ERC2771Context._msgSender();
|
|
551
509
|
}
|
|
552
510
|
|
|
511
|
+
/// @notice Returns the terminals for a revnet. Consolidates ABI encode/decode to a single site.
|
|
512
|
+
/// @param revnetId The ID of the revnet.
|
|
513
|
+
/// @return The terminals registered for the revnet.
|
|
514
|
+
function _terminalsOf(uint256 revnetId) internal view returns (IJBTerminal[] memory) {
|
|
515
|
+
return DIRECTORY.terminalsOf(revnetId);
|
|
516
|
+
}
|
|
517
|
+
|
|
553
518
|
/// @notice The total borrowed amount from a revnet, aggregated across all loan sources.
|
|
554
519
|
/// @dev Each source's `totalBorrowedFrom` is stored in the source token's native decimals (e.g. 6 for USDC,
|
|
555
520
|
/// 18 for ETH). Before aggregation, each amount is normalized to the target `decimals` to prevent mixed-decimal
|
|
@@ -632,6 +597,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
632
597
|
/// @dev Collateral tokens are permanently burned when the loan is created. They are re-minted to the borrower
|
|
633
598
|
/// only upon repayment. If the loan expires (after LOAN_LIQUIDATION_DURATION), the collateral is permanently
|
|
634
599
|
/// lost and cannot be recovered.
|
|
600
|
+
/// @dev A delegated operator (with OPEN_LOAN permission) can set `beneficiary` to any address, directing borrowed
|
|
601
|
+
/// funds away from the holder. Holders should only grant OPEN_LOAN to fully trusted operators.
|
|
635
602
|
/// @param revnetId The ID of the revnet being borrowed from.
|
|
636
603
|
/// @param source The source of the loan being borrowed.
|
|
637
604
|
/// @param minBorrowAmount The minimum amount being borrowed, denominated in the token of the source's accounting
|
|
@@ -648,12 +615,17 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
648
615
|
uint256 minBorrowAmount,
|
|
649
616
|
uint256 collateralCount,
|
|
650
617
|
address payable beneficiary,
|
|
651
|
-
uint256 prepaidFeePercent
|
|
618
|
+
uint256 prepaidFeePercent,
|
|
619
|
+
address holder
|
|
652
620
|
)
|
|
653
621
|
public
|
|
654
622
|
override
|
|
655
623
|
returns (uint256 loanId, REVLoan memory)
|
|
656
624
|
{
|
|
625
|
+
// Only the holder or a permissioned operator can open a loan on the holder's behalf.
|
|
626
|
+
// Note: the operator controls `beneficiary`, so they can direct borrowed funds to any address.
|
|
627
|
+
_requirePermissionFrom({account: holder, projectId: revnetId, permissionId: JBPermissionIds.OPEN_LOAN});
|
|
628
|
+
|
|
657
629
|
// A loan needs to have collateral.
|
|
658
630
|
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
|
|
659
631
|
|
|
@@ -672,8 +644,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
672
644
|
}
|
|
673
645
|
|
|
674
646
|
// Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
|
|
675
|
-
|
|
676
|
-
(JBRuleset memory currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
647
|
+
JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
|
|
677
648
|
|
|
678
649
|
// Enforce the cash out delay.
|
|
679
650
|
{
|
|
@@ -722,14 +693,12 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
722
693
|
newBorrowAmount: borrowAmount,
|
|
723
694
|
newCollateralCount: collateralCount,
|
|
724
695
|
sourceFeeAmount: sourceFeeAmount,
|
|
725
|
-
beneficiary: beneficiary
|
|
696
|
+
beneficiary: beneficiary,
|
|
697
|
+
holder: holder
|
|
726
698
|
});
|
|
727
699
|
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
// Mint the loan.
|
|
732
|
-
_mint({to: sender, tokenId: loanId});
|
|
700
|
+
// Mint the loan NFT to the holder.
|
|
701
|
+
_mint({to: holder, tokenId: loanId});
|
|
733
702
|
|
|
734
703
|
emit Borrow({
|
|
735
704
|
loanId: loanId,
|
|
@@ -740,7 +709,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
740
709
|
collateralCount: collateralCount,
|
|
741
710
|
sourceFeeAmount: sourceFeeAmount,
|
|
742
711
|
beneficiary: beneficiary,
|
|
743
|
-
caller:
|
|
712
|
+
caller: _msgSender()
|
|
744
713
|
});
|
|
745
714
|
|
|
746
715
|
return (loanId, loan);
|
|
@@ -811,6 +780,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
811
780
|
/// @dev Refinancing a loan will burn the original and create two new loans.
|
|
812
781
|
/// @dev This function is intentionally not payable — it only moves existing collateral between loans and does
|
|
813
782
|
/// not accept new funds. Any ETH sent with the call will be rejected by the EVM.
|
|
783
|
+
/// @dev A delegated operator (with REALLOCATE_LOAN permission) can set `beneficiary` to any address, directing
|
|
784
|
+
/// borrowed funds from the new loan away from the loan owner. Grant this permission only to trusted operators.
|
|
814
785
|
/// @param loanId The ID of the loan to reallocate collateral from.
|
|
815
786
|
/// @param collateralCountToTransfer The amount of collateral to transfer from the original loan.
|
|
816
787
|
/// @param source The source of the loan to create.
|
|
@@ -836,14 +807,13 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
836
807
|
override
|
|
837
808
|
returns (uint256 reallocatedLoanId, uint256 newLoanId, REVLoan memory reallocatedLoan, REVLoan memory newLoan)
|
|
838
809
|
{
|
|
839
|
-
//
|
|
840
|
-
|
|
810
|
+
// Keep a reference to the revnet ID of the loan being reallocated.
|
|
811
|
+
uint256 revnetId = revnetIdOfLoanWith(loanId);
|
|
841
812
|
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
813
|
+
// Only the loan owner or a permissioned operator can reallocate.
|
|
814
|
+
// Note: the operator controls `beneficiary`, so they can direct new loan proceeds to any address.
|
|
815
|
+
address loanOwner = _ownerOf(loanId);
|
|
816
|
+
_requirePermissionFrom({account: loanOwner, projectId: revnetId, permissionId: JBPermissionIds.REALLOCATE_LOAN});
|
|
847
817
|
|
|
848
818
|
// Make sure the loan hasn't expired.
|
|
849
819
|
if (block.timestamp - _loanOf[loanId].createdAt > LOAN_LIQUIDATION_DURATION) {
|
|
@@ -860,27 +830,28 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
860
830
|
|
|
861
831
|
// Note: this function is not payable, so the EVM prevents sending ETH at the call level.
|
|
862
832
|
|
|
863
|
-
// Keep a reference to the revnet ID of the loan being reallocated.
|
|
864
|
-
uint256 revnetId = revnetIdOfLoanWith(loanId);
|
|
865
|
-
|
|
866
833
|
// Refinance the loan.
|
|
867
834
|
(reallocatedLoanId, reallocatedLoan) = _reallocateCollateralFromLoan({
|
|
868
|
-
loanId: loanId, revnetId: revnetId, collateralCountToRemove: collateralCountToTransfer
|
|
835
|
+
loanId: loanId, revnetId: revnetId, collateralCountToRemove: collateralCountToTransfer, loanOwner: loanOwner
|
|
869
836
|
});
|
|
870
837
|
|
|
871
838
|
// Make a new loan with the leftover collateral from reallocating.
|
|
839
|
+
// The loan owner is the holder for the new loan (their tokens are used as collateral).
|
|
872
840
|
(newLoanId, newLoan) = borrowFrom({
|
|
873
841
|
revnetId: revnetId,
|
|
874
842
|
source: source,
|
|
875
843
|
minBorrowAmount: minBorrowAmount,
|
|
876
844
|
collateralCount: collateralCountToTransfer + collateralCountToAdd,
|
|
877
845
|
beneficiary: beneficiary,
|
|
878
|
-
prepaidFeePercent: prepaidFeePercent
|
|
846
|
+
prepaidFeePercent: prepaidFeePercent,
|
|
847
|
+
holder: loanOwner
|
|
879
848
|
});
|
|
880
849
|
}
|
|
881
850
|
|
|
882
851
|
/// @notice Allows the owner of a loan to pay it back or receive returned collateral no longer necessary to support
|
|
883
852
|
/// the loan.
|
|
853
|
+
/// @dev A delegated operator (with REPAY_LOAN permission) can set `beneficiary` to any address, directing returned
|
|
854
|
+
/// collateral tokens away from the loan owner. Grant this permission only to trusted operators.
|
|
884
855
|
/// @param loanId The ID of the loan being adjusted.
|
|
885
856
|
/// @param maxRepayBorrowAmount The maximum amount being paid off, denominated in the token of the source's
|
|
886
857
|
/// accounting context.
|
|
@@ -904,11 +875,12 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
904
875
|
// Cache the sender to avoid repeated ERC2771 context reads.
|
|
905
876
|
address sender = _msgSender();
|
|
906
877
|
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
878
|
+
// Only the loan owner or a permissioned operator can repay.
|
|
879
|
+
// Note: the operator controls `beneficiary`, so they can direct returned collateral to any address.
|
|
880
|
+
address loanOwner = _ownerOf(loanId);
|
|
881
|
+
_requirePermissionFrom({
|
|
882
|
+
account: loanOwner, projectId: revnetIdOfLoanWith(loanId), permissionId: JBPermissionIds.REPAY_LOAN
|
|
883
|
+
});
|
|
912
884
|
|
|
913
885
|
// Keep a reference to the fee being iterated on.
|
|
914
886
|
REVLoan storage loan = _loanOf[loanId];
|
|
@@ -920,12 +892,18 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
920
892
|
// Get a reference to the revnet ID of the loan being repaid.
|
|
921
893
|
uint256 revnetId = revnetIdOfLoanWith(loanId);
|
|
922
894
|
|
|
895
|
+
// Cache the current ruleset once for borrow amount calculation.
|
|
896
|
+
JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
|
|
897
|
+
|
|
923
898
|
// Scope to limit newBorrowAmount's stack lifetime.
|
|
924
899
|
uint256 repayBorrowAmount;
|
|
925
900
|
{
|
|
926
901
|
// Get the new borrow amount.
|
|
927
902
|
uint256 newBorrowAmount = _borrowAmountFrom({
|
|
928
|
-
loan: loan,
|
|
903
|
+
loan: loan,
|
|
904
|
+
revnetId: revnetId,
|
|
905
|
+
collateralCount: loan.collateral - collateralCountToReturn,
|
|
906
|
+
currentRuleset: currentRuleset
|
|
929
907
|
});
|
|
930
908
|
|
|
931
909
|
// If the remaining collateral yields zero borrow amount, treat as full repay.
|
|
@@ -972,7 +950,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
972
950
|
repayBorrowAmount: repayBorrowAmount,
|
|
973
951
|
sourceFeeAmount: sourceFeeAmount,
|
|
974
952
|
collateralCountToReturn: collateralCountToReturn,
|
|
975
|
-
beneficiary: beneficiary
|
|
953
|
+
beneficiary: beneficiary,
|
|
954
|
+
loanOwner: loanOwner
|
|
976
955
|
});
|
|
977
956
|
|
|
978
957
|
// If the max repay amount is greater than the repay amount, return the difference back to the payer.
|
|
@@ -1054,14 +1033,12 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1054
1033
|
/// loan is repaid. If the loan expires and is liquidated, the burned collateral is permanently lost.
|
|
1055
1034
|
/// @param revnetId The ID of the revnet the loan is being added in.
|
|
1056
1035
|
/// @param amount The new amount of collateral being added to the loan.
|
|
1057
|
-
function _addCollateralTo(uint256 revnetId, uint256 amount) internal {
|
|
1036
|
+
function _addCollateralTo(uint256 revnetId, uint256 amount, address holder) internal {
|
|
1058
1037
|
// Increment the total amount of collateral tokens.
|
|
1059
1038
|
totalCollateralOf[revnetId] += amount;
|
|
1060
1039
|
|
|
1061
1040
|
// Permanently burn the tokens that are tracked as collateral. These are only re-minted upon repayment.
|
|
1062
|
-
CONTROLLER.burnTokensOf({
|
|
1063
|
-
holder: _msgSender(), projectId: revnetId, tokenCount: amount, memo: "Adding collateral to loan"
|
|
1064
|
-
});
|
|
1041
|
+
CONTROLLER.burnTokensOf({holder: holder, projectId: revnetId, tokenCount: amount, memo: ""});
|
|
1065
1042
|
}
|
|
1066
1043
|
|
|
1067
1044
|
/// @notice Add a new amount to the loan that is greater than the previous amount.
|
|
@@ -1108,7 +1085,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1108
1085
|
minTokensPaidOut: 0,
|
|
1109
1086
|
beneficiary: payable(address(this)),
|
|
1110
1087
|
feeBeneficiary: beneficiary,
|
|
1111
|
-
memo: "
|
|
1088
|
+
memo: ""
|
|
1112
1089
|
});
|
|
1113
1090
|
}
|
|
1114
1091
|
|
|
@@ -1120,33 +1097,16 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1120
1097
|
? 0
|
|
1121
1098
|
: JBFees.feeAmountFrom({amountBeforeFee: addedBorrowAmount, feePercent: REV_PREPAID_FEE_PERCENT});
|
|
1122
1099
|
|
|
1100
|
+
// Try to pay the REV fee. If it fails, revFeeAmount is zeroed so the borrower receives it instead.
|
|
1123
1101
|
if (revFeeAmount > 0) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
// borrower still pays the source fee and protocol fee.
|
|
1133
|
-
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
1134
|
-
try feeTerminal.pay{value: payValue}({
|
|
1135
|
-
projectId: REV_ID,
|
|
1136
|
-
token: loan.source.token,
|
|
1137
|
-
amount: revFeeAmount,
|
|
1138
|
-
beneficiary: beneficiary,
|
|
1139
|
-
minReturnedTokens: 0,
|
|
1140
|
-
memo: "Fee from loan",
|
|
1141
|
-
metadata: bytes(abi.encodePacked(revnetId))
|
|
1142
|
-
}) {}
|
|
1143
|
-
catch (bytes memory) {
|
|
1144
|
-
// If the fee can't be processed, decrease the ERC-20 allowance and zero out the fee
|
|
1145
|
-
// so the borrower receives it instead.
|
|
1146
|
-
if (loan.source.token != JBConstants.NATIVE_TOKEN) {
|
|
1147
|
-
IERC20(loan.source.token)
|
|
1148
|
-
.safeDecreaseAllowance({spender: address(feeTerminal), requestedDecrease: revFeeAmount});
|
|
1149
|
-
}
|
|
1102
|
+
if (!_tryPayFee({
|
|
1103
|
+
terminal: feeTerminal,
|
|
1104
|
+
projectId: REV_ID,
|
|
1105
|
+
token: loan.source.token,
|
|
1106
|
+
amount: revFeeAmount,
|
|
1107
|
+
beneficiary: beneficiary,
|
|
1108
|
+
metadataProjectId: revnetId
|
|
1109
|
+
})) {
|
|
1150
1110
|
revFeeAmount = 0;
|
|
1151
1111
|
}
|
|
1152
1112
|
}
|
|
@@ -1177,13 +1137,15 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1177
1137
|
/// @param newCollateralCount The new amount of collateral backing the loan.
|
|
1178
1138
|
/// @param sourceFeeAmount The amount of the fee being taken from the revnet acting as the source of the loan.
|
|
1179
1139
|
/// @param beneficiary The address receiving the returned collateral and any tokens resulting from paying fees.
|
|
1140
|
+
/// @param holder The address whose tokens are used as collateral (burned).
|
|
1180
1141
|
function _adjust(
|
|
1181
1142
|
REVLoan storage loan,
|
|
1182
1143
|
uint256 revnetId,
|
|
1183
1144
|
uint256 newBorrowAmount,
|
|
1184
1145
|
uint256 newCollateralCount,
|
|
1185
1146
|
uint256 sourceFeeAmount,
|
|
1186
|
-
address payable beneficiary
|
|
1147
|
+
address payable beneficiary,
|
|
1148
|
+
address holder
|
|
1187
1149
|
)
|
|
1188
1150
|
internal
|
|
1189
1151
|
{
|
|
@@ -1227,7 +1189,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1227
1189
|
|
|
1228
1190
|
// Add collateral if needed...
|
|
1229
1191
|
if (addedCollateralCount > 0) {
|
|
1230
|
-
_addCollateralTo({revnetId: revnetId, amount: addedCollateralCount});
|
|
1192
|
+
_addCollateralTo({revnetId: revnetId, amount: addedCollateralCount, holder: holder});
|
|
1231
1193
|
// ... or return collateral if needed.
|
|
1232
1194
|
} else if (returnedCollateralCount > 0) {
|
|
1233
1195
|
_returnCollateralFrom({
|
|
@@ -1235,35 +1197,16 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1235
1197
|
});
|
|
1236
1198
|
}
|
|
1237
1199
|
|
|
1238
|
-
//
|
|
1239
|
-
// cannot block all loan operations (matching the REV fee pattern above).
|
|
1200
|
+
// Try to pay the source fee. If it fails, transfer the amount to the beneficiary instead.
|
|
1240
1201
|
if (sourceFeeAmount > 0) {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
// the borrower still pays the REV fee and protocol fee.
|
|
1250
|
-
// slither-disable-next-line unused-return,arbitrary-send-eth
|
|
1251
|
-
try sourceTerminal.pay{value: payValue}({
|
|
1252
|
-
projectId: revnetId,
|
|
1253
|
-
token: sourceToken,
|
|
1254
|
-
amount: sourceFeeAmount,
|
|
1255
|
-
beneficiary: beneficiary,
|
|
1256
|
-
minReturnedTokens: 0,
|
|
1257
|
-
memo: "Fee from loan",
|
|
1258
|
-
metadata: bytes(abi.encodePacked(REV_ID))
|
|
1259
|
-
}) {}
|
|
1260
|
-
catch (bytes memory) {
|
|
1261
|
-
// If the fee can't be processed, decrease the ERC-20 allowance and return the amount
|
|
1262
|
-
// to the beneficiary instead.
|
|
1263
|
-
if (sourceToken != JBConstants.NATIVE_TOKEN) {
|
|
1264
|
-
IERC20(sourceToken)
|
|
1265
|
-
.safeDecreaseAllowance({spender: address(sourceTerminal), requestedDecrease: sourceFeeAmount});
|
|
1266
|
-
}
|
|
1202
|
+
if (!_tryPayFee({
|
|
1203
|
+
terminal: IJBTerminal(address(sourceTerminal)),
|
|
1204
|
+
projectId: revnetId,
|
|
1205
|
+
token: sourceToken,
|
|
1206
|
+
amount: sourceFeeAmount,
|
|
1207
|
+
beneficiary: beneficiary,
|
|
1208
|
+
metadataProjectId: REV_ID
|
|
1209
|
+
})) {
|
|
1267
1210
|
_transferFrom({from: address(this), to: beneficiary, token: sourceToken, amount: sourceFeeAmount});
|
|
1268
1211
|
}
|
|
1269
1212
|
}
|
|
@@ -1291,7 +1234,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1291
1234
|
function _reallocateCollateralFromLoan(
|
|
1292
1235
|
uint256 loanId,
|
|
1293
1236
|
uint256 revnetId,
|
|
1294
|
-
uint256 collateralCountToRemove
|
|
1237
|
+
uint256 collateralCountToRemove,
|
|
1238
|
+
address loanOwner
|
|
1295
1239
|
)
|
|
1296
1240
|
internal
|
|
1297
1241
|
returns (uint256 reallocatedLoanId, REVLoan storage reallocatedLoan)
|
|
@@ -1308,8 +1252,13 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1308
1252
|
// Keep a reference to the new collateral amount.
|
|
1309
1253
|
uint256 newCollateralCount = loan.collateral - collateralCountToRemove;
|
|
1310
1254
|
|
|
1255
|
+
// Cache the current ruleset for borrow amount calculation.
|
|
1256
|
+
JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
|
|
1257
|
+
|
|
1311
1258
|
// Keep a reference to the new borrow amount.
|
|
1312
|
-
uint256 borrowAmount = _borrowAmountFrom({
|
|
1259
|
+
uint256 borrowAmount = _borrowAmountFrom({
|
|
1260
|
+
loan: loan, revnetId: revnetId, collateralCount: newCollateralCount, currentRuleset: currentRuleset
|
|
1261
|
+
});
|
|
1313
1262
|
|
|
1314
1263
|
// Make sure the borrow amount is not less than the original loan's amount.
|
|
1315
1264
|
if (borrowAmount < loan.amount) {
|
|
@@ -1340,12 +1289,13 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1340
1289
|
newBorrowAmount: reallocatedLoan.amount, // Don't change the borrow amount.
|
|
1341
1290
|
newCollateralCount: newCollateralCount,
|
|
1342
1291
|
sourceFeeAmount: 0,
|
|
1343
|
-
beneficiary: payable(
|
|
1292
|
+
beneficiary: payable(loanOwner), // Return collateral to the loan owner, who will have the returned
|
|
1344
1293
|
// collateral tokens debited from their balance for the new loan.
|
|
1294
|
+
holder: loanOwner // Only used if collateral is added (not the case here — collateral is being returned).
|
|
1345
1295
|
});
|
|
1346
1296
|
|
|
1347
|
-
// Mint the replacement loan.
|
|
1348
|
-
_mint({to:
|
|
1297
|
+
// Mint the replacement loan to the loan owner.
|
|
1298
|
+
_mint({to: loanOwner, tokenId: reallocatedLoanId});
|
|
1349
1299
|
|
|
1350
1300
|
// Clear stale loan data for gas refund.
|
|
1351
1301
|
delete _loanOf[loanId];
|
|
@@ -1381,7 +1331,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1381
1331
|
token: loan.source.token,
|
|
1382
1332
|
amount: repaidBorrowAmount,
|
|
1383
1333
|
shouldReturnHeldFees: false,
|
|
1384
|
-
memo: "
|
|
1334
|
+
memo: "",
|
|
1385
1335
|
metadata: bytes(abi.encodePacked(REV_ID))
|
|
1386
1336
|
});
|
|
1387
1337
|
}
|
|
@@ -1394,6 +1344,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1394
1344
|
/// @param sourceFeeAmount The amount of the fee being taken from the revnet acting as the source of the loan.
|
|
1395
1345
|
/// @param collateralCountToReturn The amount of collateral being returned that the loan no longer requires.
|
|
1396
1346
|
/// @param beneficiary The address receiving the returned collateral and any tokens resulting from paying fees.
|
|
1347
|
+
/// @param loanOwner The owner of the loan NFT (receives replacement loan if partial repay).
|
|
1397
1348
|
// slither-disable-next-line reentrancy-eth,reentrancy-events
|
|
1398
1349
|
function _repayLoan(
|
|
1399
1350
|
uint256 loanId,
|
|
@@ -1402,7 +1353,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1402
1353
|
uint256 repayBorrowAmount,
|
|
1403
1354
|
uint256 sourceFeeAmount,
|
|
1404
1355
|
uint256 collateralCountToReturn,
|
|
1405
|
-
address payable beneficiary
|
|
1356
|
+
address payable beneficiary,
|
|
1357
|
+
address loanOwner
|
|
1406
1358
|
)
|
|
1407
1359
|
internal
|
|
1408
1360
|
returns (uint256, REVLoan memory)
|
|
@@ -1423,7 +1375,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1423
1375
|
newBorrowAmount: 0,
|
|
1424
1376
|
newCollateralCount: 0,
|
|
1425
1377
|
sourceFeeAmount: sourceFeeAmount,
|
|
1426
|
-
beneficiary: beneficiary
|
|
1378
|
+
beneficiary: beneficiary,
|
|
1379
|
+
holder: _msgSender() // Only used if collateral is added (not the case here — collateral is returned).
|
|
1427
1380
|
});
|
|
1428
1381
|
|
|
1429
1382
|
// Snapshot the zeroed loan for the return value (reflects post-repay state).
|
|
@@ -1466,8 +1419,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1466
1419
|
paidOffLoan.prepaidDuration = loan.prepaidDuration;
|
|
1467
1420
|
paidOffLoan.source = loan.source;
|
|
1468
1421
|
|
|
1469
|
-
// Mint the replacement loan FIRST so it exists before _adjust writes data.
|
|
1470
|
-
_mint({to:
|
|
1422
|
+
// Mint the replacement loan to the loan owner FIRST so it exists before _adjust writes data.
|
|
1423
|
+
_mint({to: loanOwner, tokenId: paidOffLoanId});
|
|
1471
1424
|
|
|
1472
1425
|
// Then adjust the loan data.
|
|
1473
1426
|
_adjust({
|
|
@@ -1476,7 +1429,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1476
1429
|
newBorrowAmount: paidOffLoan.amount - (repayBorrowAmount - sourceFeeAmount),
|
|
1477
1430
|
newCollateralCount: paidOffLoan.collateral - collateralCountToReturn,
|
|
1478
1431
|
sourceFeeAmount: sourceFeeAmount,
|
|
1479
|
-
beneficiary: beneficiary
|
|
1432
|
+
beneficiary: beneficiary,
|
|
1433
|
+
holder: _msgSender() // Only used if collateral is added (not the case here — collateral is returned).
|
|
1480
1434
|
});
|
|
1481
1435
|
|
|
1482
1436
|
emit RepayLoan({
|
|
@@ -1513,7 +1467,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1513
1467
|
projectId: revnetId,
|
|
1514
1468
|
tokenCount: collateralCount,
|
|
1515
1469
|
beneficiary: beneficiary,
|
|
1516
|
-
memo: "
|
|
1470
|
+
memo: "",
|
|
1517
1471
|
useReservedPercent: false
|
|
1518
1472
|
});
|
|
1519
1473
|
}
|
|
@@ -1546,6 +1500,45 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1546
1500
|
PERMIT2.transferFrom({from: from, to: to, amount: uint160(amount), token: token});
|
|
1547
1501
|
}
|
|
1548
1502
|
|
|
1503
|
+
/// @notice Attempts to pay a fee to a terminal. On failure, cleans up the ERC-20 allowance and returns false.
|
|
1504
|
+
/// @param terminal The terminal to pay the fee to.
|
|
1505
|
+
/// @param projectId The project receiving the fee.
|
|
1506
|
+
/// @param token The token being used to pay the fee.
|
|
1507
|
+
/// @param amount The fee amount.
|
|
1508
|
+
/// @param beneficiary The address to credit for the fee payment.
|
|
1509
|
+
/// @param metadataProjectId The project ID encoded in the payment metadata.
|
|
1510
|
+
/// @return success Whether the fee was successfully paid.
|
|
1511
|
+
function _tryPayFee(
|
|
1512
|
+
IJBTerminal terminal,
|
|
1513
|
+
uint256 projectId,
|
|
1514
|
+
address token,
|
|
1515
|
+
uint256 amount,
|
|
1516
|
+
address beneficiary,
|
|
1517
|
+
uint256 metadataProjectId
|
|
1518
|
+
)
|
|
1519
|
+
internal
|
|
1520
|
+
returns (bool success)
|
|
1521
|
+
{
|
|
1522
|
+
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1523
|
+
|
|
1524
|
+
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
1525
|
+
try terminal.pay{value: payValue}({
|
|
1526
|
+
projectId: projectId,
|
|
1527
|
+
token: token,
|
|
1528
|
+
amount: amount,
|
|
1529
|
+
beneficiary: beneficiary,
|
|
1530
|
+
minReturnedTokens: 0,
|
|
1531
|
+
memo: "",
|
|
1532
|
+
metadata: bytes(abi.encodePacked(metadataProjectId))
|
|
1533
|
+
}) {
|
|
1534
|
+
success = true;
|
|
1535
|
+
} catch (bytes memory) {
|
|
1536
|
+
if (token != JBConstants.NATIVE_TOKEN) {
|
|
1537
|
+
IERC20(token).safeDecreaseAllowance({spender: address(terminal), requestedDecrease: amount});
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1549
1542
|
fallback() external payable {}
|
|
1550
1543
|
receive() external payable {}
|
|
1551
1544
|
}
|