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