@rev-net/core-v6 0.0.1
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/LICENSE +21 -0
- package/README.md +65 -0
- package/REVNET_SECURITY_CHECKLIST.md +164 -0
- package/SECURITY.md +68 -0
- package/SKILLS.md +166 -0
- package/deployments/revnet-core-v5/arbitrum/REVDeployer.json +2821 -0
- package/deployments/revnet-core-v5/arbitrum/REVLoans.json +2260 -0
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVDeployer.json +2821 -0
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVLoans.json +2260 -0
- package/deployments/revnet-core-v5/base/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/base/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/base_sepolia/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/base_sepolia/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/ethereum/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/ethereum/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/optimism/REVDeployer.json +2821 -0
- package/deployments/revnet-core-v5/optimism/REVLoans.json +2260 -0
- package/deployments/revnet-core-v5/optimism_sepolia/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/optimism_sepolia/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/sepolia/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/sepolia/REVLoans.json +2264 -0
- package/docs/book.css +13 -0
- package/docs/book.toml +13 -0
- package/docs/solidity.min.js +74 -0
- package/docs/src/README.md +88 -0
- package/docs/src/SUMMARY.md +20 -0
- package/docs/src/src/README.md +7 -0
- package/docs/src/src/REVDeployer.sol/contract.REVDeployer.md +968 -0
- package/docs/src/src/REVLoans.sol/contract.REVLoans.md +1047 -0
- package/docs/src/src/interfaces/IREVDeployer.sol/interface.IREVDeployer.md +243 -0
- package/docs/src/src/interfaces/IREVLoans.sol/interface.IREVLoans.md +296 -0
- package/docs/src/src/interfaces/README.md +5 -0
- package/docs/src/src/structs/README.md +14 -0
- package/docs/src/src/structs/REVAutoIssuance.sol/struct.REVAutoIssuance.md +19 -0
- package/docs/src/src/structs/REVBuybackHookConfig.sol/struct.REVBuybackHookConfig.md +19 -0
- package/docs/src/src/structs/REVBuybackPoolConfig.sol/struct.REVBuybackPoolConfig.md +21 -0
- package/docs/src/src/structs/REVConfig.sol/struct.REVConfig.md +35 -0
- package/docs/src/src/structs/REVCroptopAllowedPost.sol/struct.REVCroptopAllowedPost.md +28 -0
- package/docs/src/src/structs/REVDeploy721TiersHookConfig.sol/struct.REVDeploy721TiersHookConfig.md +34 -0
- package/docs/src/src/structs/REVDescription.sol/struct.REVDescription.md +23 -0
- package/docs/src/src/structs/REVLoan.sol/struct.REVLoan.md +28 -0
- package/docs/src/src/structs/REVLoanSource.sol/struct.REVLoanSource.md +16 -0
- package/docs/src/src/structs/REVStageConfig.sol/struct.REVStageConfig.md +44 -0
- package/docs/src/src/structs/REVSuckerDeploymentConfig.sol/struct.REVSuckerDeploymentConfig.md +16 -0
- package/foundry.lock +11 -0
- package/foundry.toml +23 -0
- package/package.json +31 -0
- package/remappings.txt +1 -0
- package/script/Deploy.s.sol +350 -0
- package/script/helpers/RevnetCoreDeploymentLib.sol +72 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +507 -0
- package/src/REVDeployer.sol +1257 -0
- package/src/REVLoans.sol +1333 -0
- package/src/interfaces/IREVDeployer.sol +198 -0
- package/src/interfaces/IREVLoans.sol +241 -0
- package/src/structs/REVAutoIssuance.sol +11 -0
- package/src/structs/REVConfig.sol +17 -0
- package/src/structs/REVCroptopAllowedPost.sol +20 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +25 -0
- package/src/structs/REVDescription.sol +14 -0
- package/src/structs/REVLoan.sol +19 -0
- package/src/structs/REVLoanSource.sol +11 -0
- package/src/structs/REVStageConfig.sol +34 -0
- package/src/structs/REVSuckerDeploymentConfig.sol +11 -0
- package/test/REV.integrations.t.sol +420 -0
- package/test/REVAutoIssuanceFuzz.t.sol +276 -0
- package/test/REVDeployerAuditRegressions.t.sol +328 -0
- package/test/REVInvincibility.t.sol +1275 -0
- package/test/REVInvincibilityHandler.sol +357 -0
- package/test/REVLifecycle.t.sol +364 -0
- package/test/REVLoans.invariants.t.sol +642 -0
- package/test/REVLoansAttacks.t.sol +739 -0
- package/test/REVLoansAuditRegressions.t.sol +314 -0
- package/test/REVLoansFeeRecovery.t.sol +704 -0
- package/test/REVLoansSourced.t.sol +1732 -0
- package/test/REVLoansUnSourced.t.sol +331 -0
- package/test/TestPR09_ConversionDocumentation.t.sol +304 -0
- package/test/TestPR10_LiquidationBehavior.t.sol +340 -0
- package/test/TestPR11_LowFindings.t.sol +571 -0
- package/test/TestPR12_FlashLoanSurplus.t.sol +305 -0
- package/test/TestPR13_CrossSourceReallocation.t.sol +302 -0
- package/test/TestPR15_CashOutCallerValidation.t.sol +320 -0
- package/test/TestPR16_ZeroRepayment.t.sol +297 -0
- package/test/TestPR21_Uint112Overflow.t.sol +251 -0
- package/test/TestPR22_HookArrayOOB.t.sol +221 -0
- package/test/TestPR26_BurnHeldTokens.t.sol +331 -0
- package/test/TestPR27_CEIPattern.t.sol +448 -0
- package/test/TestPR29_SwapTerminalPermission.t.sol +206 -0
- package/test/TestPR32_MixedFixes.t.sol +529 -0
- package/test/helpers/MaliciousContracts.sol +233 -0
- package/test/mock/MockBuybackDataHook.sol +61 -0
package/src/REVLoans.sol
ADDED
|
@@ -0,0 +1,1333 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
5
|
+
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
6
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
7
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
8
|
+
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
|
9
|
+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
|
|
10
|
+
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
11
|
+
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
12
|
+
import {IAllowanceTransfer} from "@uniswap/permit2/src/interfaces/IAllowanceTransfer.sol";
|
|
13
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
14
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
15
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
16
|
+
import {IJBPayoutTerminal} from "@bananapus/core-v6/src/interfaces/IJBPayoutTerminal.sol";
|
|
17
|
+
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
18
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
19
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
20
|
+
import {IJBTokenUriResolver} from "@bananapus/core-v6/src/interfaces/IJBTokenUriResolver.sol";
|
|
21
|
+
import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
|
|
22
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
23
|
+
import {JBFees} from "@bananapus/core-v6/src/libraries/JBFees.sol";
|
|
24
|
+
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
25
|
+
import {JBSurplus} from "@bananapus/core-v6/src/libraries/JBSurplus.sol";
|
|
26
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
27
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
28
|
+
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
29
|
+
|
|
30
|
+
import {IREVLoans} from "./interfaces/IREVLoans.sol";
|
|
31
|
+
import {REVLoan} from "./structs/REVLoan.sol";
|
|
32
|
+
import {REVLoanSource} from "./structs/REVLoanSource.sol";
|
|
33
|
+
|
|
34
|
+
/// @notice A contract for borrowing from revnets.
|
|
35
|
+
/// @dev Tokens used as collateral are burned, and reminted when the loan is paid off. This keeps the revnet's token
|
|
36
|
+
/// structure orderly.
|
|
37
|
+
/// @dev The borrowable amount is the same as the cash out amount.
|
|
38
|
+
/// @dev An upfront fee is taken when a loan is created. 2.5% is charged by the underlying protocol, 2.5% is charged
|
|
39
|
+
/// by the
|
|
40
|
+
/// revnet issuing the loan, and a variable amount charged by the revnet that receives the fees. This variable amount is
|
|
41
|
+
/// chosen by the borrower, the more paid upfront, the longer the prepaid duration. The loan can be repaid anytime
|
|
42
|
+
/// within the prepaid duration without additional fees.
|
|
43
|
+
/// After the prepaid duration, the loan will increasingly cost more to pay off. After 10 years, the loan collateral
|
|
44
|
+
/// cannot be
|
|
45
|
+
/// recouped.
|
|
46
|
+
/// @dev The loaned amounts include the fees taken, meaning the amount paid back is the amount borrowed plus the fees.
|
|
47
|
+
contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
48
|
+
// A library that parses the packed ruleset metadata into a friendlier format.
|
|
49
|
+
using JBRulesetMetadataResolver for JBRuleset;
|
|
50
|
+
|
|
51
|
+
// A library that adds default safety checks to ERC20 functionality.
|
|
52
|
+
using SafeERC20 for IERC20;
|
|
53
|
+
|
|
54
|
+
//*********************************************************************//
|
|
55
|
+
// --------------------------- custom errors ------------------------- //
|
|
56
|
+
//*********************************************************************//
|
|
57
|
+
|
|
58
|
+
error REVLoans_CollateralExceedsLoan(uint256 collateralToReturn, uint256 loanCollateral);
|
|
59
|
+
error REVLoans_InvalidPrepaidFeePercent(uint256 prepaidFeePercent, uint256 min, uint256 max);
|
|
60
|
+
error REVLoans_NotEnoughCollateral();
|
|
61
|
+
error REVLoans_OverflowAlert(uint256 value, uint256 limit);
|
|
62
|
+
error REVLoans_OverMaxRepayBorrowAmount(uint256 maxRepayBorrowAmount, uint256 repayBorrowAmount);
|
|
63
|
+
error REVLoans_PermitAllowanceNotEnough(uint256 allowanceAmount, uint256 requiredAmount);
|
|
64
|
+
error REVLoans_NewBorrowAmountGreaterThanLoanAmount(uint256 newBorrowAmount, uint256 loanAmount);
|
|
65
|
+
error REVLoans_NoMsgValueAllowed();
|
|
66
|
+
error REVLoans_NothingToRepay();
|
|
67
|
+
error REVLoans_LoanExpired(uint256 timeSinceLoanCreated, uint256 loanLiquidationDuration);
|
|
68
|
+
error REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(uint256 newBorrowAmount, uint256 loanAmount);
|
|
69
|
+
error REVLoans_SourceMismatch();
|
|
70
|
+
error REVLoans_Unauthorized(address caller, address owner);
|
|
71
|
+
error REVLoans_UnderMinBorrowAmount(uint256 minBorrowAmount, uint256 borrowAmount);
|
|
72
|
+
error REVLoans_ZeroCollateralLoanIsInvalid();
|
|
73
|
+
|
|
74
|
+
//*********************************************************************//
|
|
75
|
+
// ------------------------- public constants ------------------------ //
|
|
76
|
+
//*********************************************************************//
|
|
77
|
+
|
|
78
|
+
/// @dev After the prepaid duration, the loan will cost more to pay off. After 10 years, the loan
|
|
79
|
+
/// collateral cannot be recouped. This means paying 50% of the loan amount upfront will pay for having access to
|
|
80
|
+
/// the remaining 50% for 10 years,
|
|
81
|
+
/// whereas paying 0% of the loan upfront will cost 100% of the loan amount to be paid off after 10 years. After 10
|
|
82
|
+
/// years with repayment, both loans cost 100% and are liquidated.
|
|
83
|
+
uint256 public constant override LOAN_LIQUIDATION_DURATION = 3650 days;
|
|
84
|
+
|
|
85
|
+
/// @dev The maximum amount of a loan that can be prepaid at the time of borrowing, in terms of JBConstants.MAX_FEE.
|
|
86
|
+
uint256 public constant override MAX_PREPAID_FEE_PERCENT = 500;
|
|
87
|
+
|
|
88
|
+
/// @dev A fee of 1% is charged by the $REV revnet.
|
|
89
|
+
uint256 public constant override REV_PREPAID_FEE_PERCENT = 10; // 1%
|
|
90
|
+
|
|
91
|
+
/// @dev A fee of 2.5% is charged by the loan's source upfront.
|
|
92
|
+
uint256 public constant override MIN_PREPAID_FEE_PERCENT = 25; // 2.5%
|
|
93
|
+
|
|
94
|
+
//*********************************************************************//
|
|
95
|
+
// -------------------- private constant properties ------------------ //
|
|
96
|
+
//*********************************************************************//
|
|
97
|
+
|
|
98
|
+
/// @notice Just a kind reminder to our readers.
|
|
99
|
+
/// @dev Used in loan token ID generation.
|
|
100
|
+
uint256 private constant _ONE_TRILLION = 1_000_000_000_000;
|
|
101
|
+
|
|
102
|
+
//*********************************************************************//
|
|
103
|
+
// --------------- public immutable stored properties ---------------- //
|
|
104
|
+
//*********************************************************************//
|
|
105
|
+
|
|
106
|
+
/// @notice The permit2 utility.
|
|
107
|
+
IPermit2 public immutable override PERMIT2;
|
|
108
|
+
|
|
109
|
+
/// @notice The controller of revnets that use this loans contract.
|
|
110
|
+
IJBController public immutable override CONTROLLER;
|
|
111
|
+
|
|
112
|
+
/// @notice The directory of terminals and controllers for revnets.
|
|
113
|
+
IJBDirectory public immutable override DIRECTORY;
|
|
114
|
+
|
|
115
|
+
/// @notice A contract that stores prices for each revnet.
|
|
116
|
+
IJBPrices public immutable override PRICES;
|
|
117
|
+
|
|
118
|
+
/// @notice Mints ERC-721s that represent revnet ownership and transfers.
|
|
119
|
+
IJBProjects public immutable override PROJECTS;
|
|
120
|
+
|
|
121
|
+
/// @notice The ID of the REV revnet that will receive the fees.
|
|
122
|
+
uint256 public immutable override REV_ID;
|
|
123
|
+
|
|
124
|
+
//*********************************************************************//
|
|
125
|
+
// --------------------- public stored properties -------------------- //
|
|
126
|
+
//*********************************************************************//
|
|
127
|
+
|
|
128
|
+
/// @notice An indication if a revnet currently has outstanding loans from the specified terminal in the specified
|
|
129
|
+
/// token.
|
|
130
|
+
/// @custom:param revnetId The ID of the revnet issuing the loan.
|
|
131
|
+
/// @custom:param terminal The terminal that the loan is issued from.
|
|
132
|
+
/// @custom:param token The token being loaned.
|
|
133
|
+
mapping(uint256 revnetId => mapping(IJBPayoutTerminal terminal => mapping(address token => bool)))
|
|
134
|
+
public
|
|
135
|
+
override isLoanSourceOf;
|
|
136
|
+
|
|
137
|
+
/// @notice The amount of loans that have been created.
|
|
138
|
+
/// @custom:param revnetId The ID of the revnet to get the number of loans from.
|
|
139
|
+
mapping(uint256 revnetId => uint256) public override numberOfLoansFor;
|
|
140
|
+
|
|
141
|
+
/// @notice The contract resolving each project ID to its ERC721 URI.
|
|
142
|
+
IJBTokenUriResolver public override tokenUriResolver;
|
|
143
|
+
|
|
144
|
+
/// @notice The total amount loaned out by a revnet from a specified terminal in a specified token.
|
|
145
|
+
/// @custom:param revnetId The ID of the revnet issuing the loan.
|
|
146
|
+
/// @custom:param terminal The terminal that the loan is issued from.
|
|
147
|
+
/// @custom:param token The token being loaned.
|
|
148
|
+
mapping(uint256 revnetId => mapping(IJBPayoutTerminal terminal => mapping(address token => uint256)))
|
|
149
|
+
public
|
|
150
|
+
override totalBorrowedFrom;
|
|
151
|
+
|
|
152
|
+
/// @notice The total amount of collateral supporting a revnet's loans.
|
|
153
|
+
/// @custom:param revnetId The ID of the revnet issuing the loan.
|
|
154
|
+
mapping(uint256 revnetId => uint256) public override totalCollateralOf;
|
|
155
|
+
|
|
156
|
+
//*********************************************************************//
|
|
157
|
+
// --------------------- internal stored properties ------------------ //
|
|
158
|
+
//*********************************************************************//
|
|
159
|
+
|
|
160
|
+
/// @notice The sources of each revnet's loan.
|
|
161
|
+
/// @custom:member revnetId The ID of the revnet issuing the loan.
|
|
162
|
+
mapping(uint256 revnetId => REVLoanSource[]) internal _loanSourcesOf;
|
|
163
|
+
|
|
164
|
+
/// @notice The loans.
|
|
165
|
+
/// @custom:member The ID of the loan.
|
|
166
|
+
mapping(uint256 loanId => REVLoan) internal _loanOf;
|
|
167
|
+
|
|
168
|
+
//*********************************************************************//
|
|
169
|
+
// -------------------------- constructor ---------------------------- //
|
|
170
|
+
//*********************************************************************//
|
|
171
|
+
|
|
172
|
+
/// @param controller The controller that manages revnets using this loans contract.
|
|
173
|
+
/// @param projects The contract that mints ERC-721s representing project ownership.
|
|
174
|
+
/// @param revId The ID of the REV revnet that will receive the fees.
|
|
175
|
+
/// @param owner The owner of the contract that can set the URI resolver.
|
|
176
|
+
/// @param permit2 A permit2 utility.
|
|
177
|
+
/// @param trustedForwarder A trusted forwarder of transactions to this contract.
|
|
178
|
+
constructor(
|
|
179
|
+
IJBController controller,
|
|
180
|
+
IJBProjects projects,
|
|
181
|
+
uint256 revId,
|
|
182
|
+
address owner,
|
|
183
|
+
IPermit2 permit2,
|
|
184
|
+
address trustedForwarder
|
|
185
|
+
)
|
|
186
|
+
ERC721("REV Loans", "$REVLOAN")
|
|
187
|
+
ERC2771Context(trustedForwarder)
|
|
188
|
+
Ownable(owner)
|
|
189
|
+
{
|
|
190
|
+
CONTROLLER = controller;
|
|
191
|
+
DIRECTORY = controller.DIRECTORY();
|
|
192
|
+
PRICES = controller.PRICES();
|
|
193
|
+
PROJECTS = projects;
|
|
194
|
+
REV_ID = revId;
|
|
195
|
+
PERMIT2 = permit2;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
//*********************************************************************//
|
|
199
|
+
// ------------------------- external views -------------------------- //
|
|
200
|
+
//*********************************************************************//
|
|
201
|
+
|
|
202
|
+
/// @notice The amount that can be borrowed from a revnet.
|
|
203
|
+
/// @param revnetId The ID of the revnet to check for borrowable assets from.
|
|
204
|
+
/// @param collateralCount The amount of collateral used to secure the loan.
|
|
205
|
+
/// @param decimals The decimals the resulting fixed point value will include.
|
|
206
|
+
/// @param currency The currency that the resulting amount should be in terms of.
|
|
207
|
+
/// @return borrowableAmount The amount that can be borrowed from the revnet.
|
|
208
|
+
function borrowableAmountFrom(
|
|
209
|
+
uint256 revnetId,
|
|
210
|
+
uint256 collateralCount,
|
|
211
|
+
uint256 decimals,
|
|
212
|
+
uint256 currency
|
|
213
|
+
)
|
|
214
|
+
external
|
|
215
|
+
view
|
|
216
|
+
returns (uint256)
|
|
217
|
+
{
|
|
218
|
+
return _borrowableAmountFrom({
|
|
219
|
+
revnetId: revnetId,
|
|
220
|
+
collateralCount: collateralCount,
|
|
221
|
+
decimals: decimals,
|
|
222
|
+
currency: currency,
|
|
223
|
+
terminals: DIRECTORY.terminalsOf(revnetId)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// @notice Get a loan.
|
|
228
|
+
/// @custom:member The ID of the loan.
|
|
229
|
+
function loanOf(uint256 loanId) external view override returns (REVLoan memory) {
|
|
230
|
+
return _loanOf[loanId];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// @notice The sources of each revnet's loan.
|
|
234
|
+
/// @custom:member revnetId The ID of the revnet issuing the loan.
|
|
235
|
+
function loanSourcesOf(uint256 revnetId) external view override returns (REVLoanSource[] memory) {
|
|
236
|
+
return _loanSourcesOf[revnetId];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
//*********************************************************************//
|
|
240
|
+
// -------------------------- public views --------------------------- //
|
|
241
|
+
//*********************************************************************//
|
|
242
|
+
|
|
243
|
+
/// @notice Determines the source fee amount for a loan being paid off a certain amount.
|
|
244
|
+
/// @param loan The loan having its source fee amount determined.
|
|
245
|
+
/// @param amount The amount being paid off.
|
|
246
|
+
/// @return sourceFeeAmount The source fee amount for the loan.
|
|
247
|
+
function determineSourceFeeAmount(REVLoan memory loan, uint256 amount) public view returns (uint256) {
|
|
248
|
+
return _determineSourceFeeAmount(loan, amount);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// @notice Returns the URI where the ERC-721 standard JSON of a loan is hosted.
|
|
252
|
+
/// @param loanId The ID of the loan to get a URI of.
|
|
253
|
+
/// @return The token URI to use for the provided `loanId`.
|
|
254
|
+
function tokenURI(uint256 loanId) public view override returns (string memory) {
|
|
255
|
+
// Keep a reference to the resolver.
|
|
256
|
+
IJBTokenUriResolver resolver = tokenUriResolver;
|
|
257
|
+
|
|
258
|
+
// If there's no resolver, there's no URI.
|
|
259
|
+
if (resolver == IJBTokenUriResolver(address(0))) return "";
|
|
260
|
+
|
|
261
|
+
// Return the resolved URI.
|
|
262
|
+
return resolver.getUri(loanId);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// @notice The revnet ID for the loan with the provided loan ID.
|
|
266
|
+
/// @param loanId The loan ID of the loan to get the revnet ID of.
|
|
267
|
+
/// @return The ID of the revnet.
|
|
268
|
+
function revnetIdOfLoanWith(uint256 loanId) public pure override returns (uint256) {
|
|
269
|
+
return loanId / _ONE_TRILLION;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
//*********************************************************************//
|
|
273
|
+
// -------------------------- internal views ------------------------- //
|
|
274
|
+
//*********************************************************************//
|
|
275
|
+
|
|
276
|
+
/// @notice Checks this contract's balance of a specific token.
|
|
277
|
+
/// @param token The address of the token to get this contract's balance of.
|
|
278
|
+
/// @return This contract's balance.
|
|
279
|
+
function _balanceOf(address token) internal view returns (uint256) {
|
|
280
|
+
// If the `token` is native, get the native token balance.
|
|
281
|
+
return token == JBConstants.NATIVE_TOKEN ? address(this).balance : IERC20(token).balanceOf(address(this));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// @dev This function reads live surplus from the revnet's terminals. A potential concern is flash loan
|
|
285
|
+
/// manipulation: an attacker could temporarily inflate surplus via `addToBalanceOf` or `pay`, borrow at the
|
|
286
|
+
/// inflated rate, then repay the flash loan. However, this attack is economically irrational:
|
|
287
|
+
///
|
|
288
|
+
/// - `addToBalanceOf` permanently donates funds to the project (no recovery mechanism). The attacker's extra
|
|
289
|
+
/// borrowable amount equals `donation * (collateralCount / totalSupply)`, which is always less than the
|
|
290
|
+
/// donation since `collateralCount < totalSupply`. The attacker loses more than they gain.
|
|
291
|
+
/// - `pay` increases both surplus AND totalSupply (via newly minted tokens), so the net effect on the
|
|
292
|
+
/// borrowable-amount-per-token ratio is neutral — the increased surplus is offset by supply dilution.
|
|
293
|
+
/// - With non-zero `cashOutTaxRate`, the bonding curve is concave, making the attack even less profitable.
|
|
294
|
+
/// - Refinancing during inflated surplus (`reallocateCollateralFromLoan`) does not help either: the freed
|
|
295
|
+
/// collateral can only borrow a fraction of the donated amount, keeping the attack net-negative.
|
|
296
|
+
///
|
|
297
|
+
/// In summary, any attempt to inflate surplus to increase borrowing power costs the attacker more than it yields,
|
|
298
|
+
/// because the bonding curve ensures no individual can extract more than their proportional share of surplus.
|
|
299
|
+
/// @dev The amount that can be borrowed from a revnet given a certain amount of collateral.
|
|
300
|
+
/// @dev The system intentionally allows up to 100% LTV (loan-to-value) by design. The borrowable amount equals
|
|
301
|
+
/// what the collateral tokens would receive if cashed out, computed via the bonding curve formula in
|
|
302
|
+
/// `JBCashOuts.cashOutFrom`. The `cashOutTaxRate` configured for the current stage serves as an implicit margin
|
|
303
|
+
/// buffer: a non-zero tax rate reduces the cash-out value below the pro-rata share of surplus, creating an
|
|
304
|
+
/// effective collateralization margin. For example, a 20% `cashOutTaxRate` means borrowers can only extract ~80%
|
|
305
|
+
/// of their pro-rata surplus, providing a ~20% buffer against collateral depreciation before liquidation.
|
|
306
|
+
/// A `cashOutTaxRate` of 0 means the full pro-rata amount is borrowable (true 100% LTV with no margin).
|
|
307
|
+
/// @param revnetId The ID of the revnet to check for borrowable assets from.
|
|
308
|
+
/// @param collateralCount The amount of collateral that the loan will be collateralized with.
|
|
309
|
+
/// @param decimals The decimals the resulting fixed point value will include.
|
|
310
|
+
/// @param currency The currency that the resulting amount should be in terms of.
|
|
311
|
+
/// @param terminals The terminals that the funds are being borrowed from.
|
|
312
|
+
/// @return borrowableAmount The amount that can be borrowed from the revnet.
|
|
313
|
+
function _borrowableAmountFrom(
|
|
314
|
+
uint256 revnetId,
|
|
315
|
+
uint256 collateralCount,
|
|
316
|
+
uint256 decimals,
|
|
317
|
+
uint256 currency,
|
|
318
|
+
IJBTerminal[] memory terminals
|
|
319
|
+
)
|
|
320
|
+
internal
|
|
321
|
+
view
|
|
322
|
+
returns (uint256)
|
|
323
|
+
{
|
|
324
|
+
// Keep a reference to the current stage.
|
|
325
|
+
// slither-disable-next-line unused-return
|
|
326
|
+
(JBRuleset memory currentStage,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
327
|
+
|
|
328
|
+
// Get the surplus of all the revnet's terminals in terms of the native currency.
|
|
329
|
+
uint256 totalSurplus = JBSurplus.currentSurplusOf({
|
|
330
|
+
projectId: revnetId,
|
|
331
|
+
terminals: terminals,
|
|
332
|
+
accountingContexts: new JBAccountingContext[](0),
|
|
333
|
+
decimals: decimals,
|
|
334
|
+
currency: currency
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Get the total amount the revnet currently has loaned out, in terms of the native currency with 18
|
|
338
|
+
// decimals.
|
|
339
|
+
uint256 totalBorrowed = _totalBorrowedFrom({revnetId: revnetId, decimals: decimals, currency: currency});
|
|
340
|
+
|
|
341
|
+
// Get the total amount of tokens in circulation.
|
|
342
|
+
uint256 totalSupply = CONTROLLER.totalTokenSupplyWithReservedTokensOf(revnetId);
|
|
343
|
+
|
|
344
|
+
// Get a refeerence to the collateral being used to secure loans.
|
|
345
|
+
uint256 totalCollateral = totalCollateralOf[revnetId];
|
|
346
|
+
|
|
347
|
+
// Proportional.
|
|
348
|
+
return JBCashOuts.cashOutFrom({
|
|
349
|
+
surplus: totalSurplus + totalBorrowed,
|
|
350
|
+
cashOutCount: collateralCount,
|
|
351
|
+
totalSupply: totalSupply + totalCollateral,
|
|
352
|
+
cashOutTaxRate: currentStage.cashOutTaxRate()
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/// @notice The amount of the loan that should be borrowed for the given collateral amount.
|
|
357
|
+
/// @param loan The loan having its borrow amount determined.
|
|
358
|
+
/// @param revnetId The ID of the revnet to check for borrowable assets from.
|
|
359
|
+
/// @param collateralCount The amount of collateral that the loan will be collateralized with.
|
|
360
|
+
/// @return borrowAmount The amount of the loan that should be borrowed.
|
|
361
|
+
function _borrowAmountFrom(
|
|
362
|
+
REVLoan storage loan,
|
|
363
|
+
uint256 revnetId,
|
|
364
|
+
uint256 collateralCount
|
|
365
|
+
)
|
|
366
|
+
internal
|
|
367
|
+
view
|
|
368
|
+
returns (uint256)
|
|
369
|
+
{
|
|
370
|
+
// If there's no collateral, there's no loan.
|
|
371
|
+
if (collateralCount == 0) return 0;
|
|
372
|
+
|
|
373
|
+
// Get a reference to the accounting context for the source.
|
|
374
|
+
JBAccountingContext memory accountingContext =
|
|
375
|
+
loan.source.terminal.accountingContextForTokenOf({projectId: revnetId, token: loan.source.token});
|
|
376
|
+
|
|
377
|
+
// Keep a reference to the revnet's terminals.
|
|
378
|
+
IJBTerminal[] memory terminals = DIRECTORY.terminalsOf(revnetId);
|
|
379
|
+
|
|
380
|
+
return _borrowableAmountFrom({
|
|
381
|
+
revnetId: revnetId,
|
|
382
|
+
collateralCount: collateralCount,
|
|
383
|
+
decimals: accountingContext.decimals,
|
|
384
|
+
currency: accountingContext.currency,
|
|
385
|
+
terminals: terminals
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// @dev `ERC-2771` specifies the context as being a single address (20 bytes).
|
|
390
|
+
function _contextSuffixLength() internal view override(ERC2771Context, Context) returns (uint256) {
|
|
391
|
+
return super._contextSuffixLength();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// @notice Determines the source fee amount for a loan being paid off a certain amount.
|
|
395
|
+
/// @param loan The loan having its source fee amount determined.
|
|
396
|
+
/// @param amount The amount being paid off.
|
|
397
|
+
/// @return The source fee amount for the loan.
|
|
398
|
+
function _determineSourceFeeAmount(REVLoan memory loan, uint256 amount) internal view returns (uint256) {
|
|
399
|
+
// Keep a reference to the time since the loan was created.
|
|
400
|
+
uint256 timeSinceLoanCreated = block.timestamp - loan.createdAt;
|
|
401
|
+
|
|
402
|
+
// If the loan period has passed the prepaid time frame, take a fee.
|
|
403
|
+
if (timeSinceLoanCreated <= loan.prepaidDuration) return 0;
|
|
404
|
+
|
|
405
|
+
// If the loan period has reached or passed the liquidation time frame, do not allow loan management.
|
|
406
|
+
if (timeSinceLoanCreated >= LOAN_LIQUIDATION_DURATION) {
|
|
407
|
+
revert REVLoans_LoanExpired(timeSinceLoanCreated, LOAN_LIQUIDATION_DURATION);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Get a reference to the amount prepaid for the full loan.
|
|
411
|
+
uint256 prepaid = JBFees.feeAmountFrom({amountBeforeFee: loan.amount, feePercent: loan.prepaidFeePercent});
|
|
412
|
+
|
|
413
|
+
uint256 fullSourceFeeAmount = JBFees.feeAmountFrom({
|
|
414
|
+
amountBeforeFee: loan.amount - prepaid,
|
|
415
|
+
feePercent: mulDiv(
|
|
416
|
+
timeSinceLoanCreated - loan.prepaidDuration,
|
|
417
|
+
JBConstants.MAX_FEE,
|
|
418
|
+
LOAN_LIQUIDATION_DURATION - loan.prepaidDuration
|
|
419
|
+
)
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Calculate the source fee amount for the amount being paid off.
|
|
423
|
+
return mulDiv(fullSourceFeeAmount, amount, loan.amount);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// @notice Generate a ID for a loan given a revnet ID and a loan number within that revnet.
|
|
427
|
+
/// @param revnetId The ID of the revnet to generate a loan ID for.
|
|
428
|
+
/// @param loanNumber The loan number of the loan within the revnet.
|
|
429
|
+
/// @return The token ID of the 721.
|
|
430
|
+
function _generateLoanId(uint256 revnetId, uint256 loanNumber) internal pure returns (uint256) {
|
|
431
|
+
return (revnetId * _ONE_TRILLION) + loanNumber;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/// @notice The calldata. Preferred to use over `msg.data`.
|
|
435
|
+
/// @return calldata The `msg.data` of this call.
|
|
436
|
+
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
437
|
+
return ERC2771Context._msgData();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/// @notice The message's sender. Preferred to use over `msg.sender`.
|
|
441
|
+
/// @return sender The address which sent this call.
|
|
442
|
+
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
443
|
+
return ERC2771Context._msgSender();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/// @notice The total borrowed amount from a revnet, aggregated across all loan sources.
|
|
447
|
+
/// @dev Each source's `totalBorrowedFrom` is stored in the source token's native decimals (e.g. 6 for USDC,
|
|
448
|
+
/// 18 for ETH). Before aggregation, each amount is normalized to the target `decimals` to prevent mixed-decimal
|
|
449
|
+
/// arithmetic errors. For cross-currency sources, the normalized amount is then converted via the price feed.
|
|
450
|
+
/// @dev Callers should ensure the price feed has sufficient precision for the target `decimals`. Inverse price
|
|
451
|
+
/// feeds may truncate to zero at low decimal counts (e.g. a feed returning 1e21 at 6 decimals inverts to
|
|
452
|
+
/// mulDiv(1e6, 1e6, 1e21) = 0), which would cause a division-by-zero in the price conversion.
|
|
453
|
+
/// @param revnetId The ID of the revnet to check for borrowed assets from.
|
|
454
|
+
/// @param decimals The decimals the resulting fixed point value will include.
|
|
455
|
+
/// @param currency The currency the resulting value will be in terms of.
|
|
456
|
+
/// @return borrowedAmount The total amount borrowed.
|
|
457
|
+
function _totalBorrowedFrom(
|
|
458
|
+
uint256 revnetId,
|
|
459
|
+
uint256 decimals,
|
|
460
|
+
uint256 currency
|
|
461
|
+
)
|
|
462
|
+
internal
|
|
463
|
+
view
|
|
464
|
+
returns (uint256 borrowedAmount)
|
|
465
|
+
{
|
|
466
|
+
// Keep a reference to all sources being used to loaned out from this revnet.
|
|
467
|
+
REVLoanSource[] memory sources = _loanSourcesOf[revnetId];
|
|
468
|
+
|
|
469
|
+
// Iterate over all sources being used to loaned out.
|
|
470
|
+
for (uint256 i; i < sources.length; i++) {
|
|
471
|
+
// Get a reference to the token being iterated on.
|
|
472
|
+
REVLoanSource memory source = sources[i];
|
|
473
|
+
|
|
474
|
+
// Get a reference to the accounting context for the source.
|
|
475
|
+
// slither-disable-next-line calls-loop
|
|
476
|
+
JBAccountingContext memory accountingContext =
|
|
477
|
+
source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
|
|
478
|
+
|
|
479
|
+
// Get a reference to the amount of tokens loaned out.
|
|
480
|
+
uint256 tokensLoaned = totalBorrowedFrom[revnetId][source.terminal][source.token];
|
|
481
|
+
|
|
482
|
+
// Skip if no tokens are loaned from this source.
|
|
483
|
+
if (tokensLoaned == 0) continue;
|
|
484
|
+
|
|
485
|
+
// Normalize the token amount from the source's decimals to the target decimals.
|
|
486
|
+
uint256 normalizedTokens;
|
|
487
|
+
if (accountingContext.decimals > decimals) {
|
|
488
|
+
normalizedTokens = tokensLoaned / (10 ** (accountingContext.decimals - decimals));
|
|
489
|
+
} else if (accountingContext.decimals < decimals) {
|
|
490
|
+
normalizedTokens = tokensLoaned * (10 ** (decimals - accountingContext.decimals));
|
|
491
|
+
} else {
|
|
492
|
+
normalizedTokens = tokensLoaned;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// If the currency matches, add the normalized amount directly.
|
|
496
|
+
if (accountingContext.currency == currency) {
|
|
497
|
+
borrowedAmount += normalizedTokens;
|
|
498
|
+
} else {
|
|
499
|
+
// Otherwise, convert via the price feed.
|
|
500
|
+
// slither-disable-next-line calls-loop
|
|
501
|
+
uint256 pricePerUnit = PRICES.pricePerUnitOf({
|
|
502
|
+
projectId: revnetId,
|
|
503
|
+
pricingCurrency: accountingContext.currency,
|
|
504
|
+
unitCurrency: currency,
|
|
505
|
+
decimals: decimals
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
borrowedAmount += mulDiv(normalizedTokens, 10 ** decimals, pricePerUnit);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
//*********************************************************************//
|
|
514
|
+
// ---------------------- external transactions ---------------------- //
|
|
515
|
+
//*********************************************************************//
|
|
516
|
+
|
|
517
|
+
/// @notice Open a loan by borrowing from a revnet.
|
|
518
|
+
/// @dev Collateral tokens are permanently burned when the loan is created. They are re-minted to the borrower
|
|
519
|
+
/// only upon repayment. If the loan expires (after LOAN_LIQUIDATION_DURATION), the collateral is permanently
|
|
520
|
+
/// lost and cannot be recovered.
|
|
521
|
+
/// @param revnetId The ID of the revnet being borrowed from.
|
|
522
|
+
/// @param source The source of the loan being borrowed.
|
|
523
|
+
/// @param minBorrowAmount The minimum amount being borrowed, denominated in the token of the source's accounting
|
|
524
|
+
/// context.
|
|
525
|
+
/// @param collateralCount The amount of tokens to use as collateral for the loan.
|
|
526
|
+
/// @param beneficiary The address that'll receive the borrowed funds and the tokens resulting from fee payments.
|
|
527
|
+
/// @param prepaidFeePercent The fee percent that will be charged upfront from the revnet being borrowed from.
|
|
528
|
+
/// Prepaying a fee is cheaper than paying later.
|
|
529
|
+
/// @return loanId The ID of the loan created from borrowing.
|
|
530
|
+
/// @return loan The loan created from borrowing.
|
|
531
|
+
function borrowFrom(
|
|
532
|
+
uint256 revnetId,
|
|
533
|
+
REVLoanSource calldata source,
|
|
534
|
+
uint256 minBorrowAmount,
|
|
535
|
+
uint256 collateralCount,
|
|
536
|
+
address payable beneficiary,
|
|
537
|
+
uint256 prepaidFeePercent
|
|
538
|
+
)
|
|
539
|
+
public
|
|
540
|
+
override
|
|
541
|
+
returns (uint256 loanId, REVLoan memory)
|
|
542
|
+
{
|
|
543
|
+
// A loan needs to have collateral.
|
|
544
|
+
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
|
|
545
|
+
|
|
546
|
+
// Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
|
|
547
|
+
// an 16 year loan can be paid upfront with a
|
|
548
|
+
// payment of 50% of the borrowed assets, the cheapest possible rate.
|
|
549
|
+
if (prepaidFeePercent < MIN_PREPAID_FEE_PERCENT || prepaidFeePercent > MAX_PREPAID_FEE_PERCENT) {
|
|
550
|
+
revert REVLoans_InvalidPrepaidFeePercent(
|
|
551
|
+
prepaidFeePercent, MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Get a reference to the loan ID.
|
|
556
|
+
loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++numberOfLoansFor[revnetId]});
|
|
557
|
+
|
|
558
|
+
// Get a reference to the loan being created.
|
|
559
|
+
REVLoan storage loan = _loanOf[loanId];
|
|
560
|
+
|
|
561
|
+
// Set the loan's values.
|
|
562
|
+
loan.source = source;
|
|
563
|
+
loan.createdAt = uint40(block.timestamp);
|
|
564
|
+
loan.prepaidFeePercent = uint16(prepaidFeePercent);
|
|
565
|
+
loan.prepaidDuration = uint32(mulDiv(prepaidFeePercent, LOAN_LIQUIDATION_DURATION, MAX_PREPAID_FEE_PERCENT));
|
|
566
|
+
|
|
567
|
+
// Get the amount of the loan.
|
|
568
|
+
uint256 borrowAmount = _borrowAmountFrom({loan: loan, revnetId: revnetId, collateralCount: collateralCount});
|
|
569
|
+
|
|
570
|
+
// Make sure the minimum borrow amount is met.
|
|
571
|
+
if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
|
|
572
|
+
|
|
573
|
+
// Get the amount of additional fee to take for the revnet issuing the loan.
|
|
574
|
+
uint256 sourceFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
|
|
575
|
+
|
|
576
|
+
// Borrow the amount.
|
|
577
|
+
_adjust({
|
|
578
|
+
loan: loan,
|
|
579
|
+
revnetId: revnetId,
|
|
580
|
+
newBorrowAmount: borrowAmount,
|
|
581
|
+
newCollateralCount: collateralCount,
|
|
582
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
583
|
+
beneficiary: beneficiary
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Mint the loan.
|
|
587
|
+
_mint({to: _msgSender(), tokenId: loanId});
|
|
588
|
+
|
|
589
|
+
emit Borrow({
|
|
590
|
+
loanId: loanId,
|
|
591
|
+
revnetId: revnetId,
|
|
592
|
+
loan: loan,
|
|
593
|
+
source: source,
|
|
594
|
+
borrowAmount: borrowAmount,
|
|
595
|
+
collateralCount: collateralCount,
|
|
596
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
597
|
+
beneficiary: beneficiary,
|
|
598
|
+
caller: _msgSender()
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
return (loanId, loan);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/// @notice Liquidates loans that have exceeded the 10-year liquidation duration.
|
|
605
|
+
/// @dev Liquidation permanently destroys the collateral backing expired loans. Since collateral tokens were burned
|
|
606
|
+
/// at deposit time (not held in escrow), there is nothing to return upon liquidation -- the collateral count is
|
|
607
|
+
/// simply removed from tracking. The borrower retains whatever funds they received from the loan, but the
|
|
608
|
+
/// collateral tokens that were burned to secure the loan are permanently lost.
|
|
609
|
+
/// @dev This is an intentional design choice to keep the protocol simple and to incentivize timely repayment or
|
|
610
|
+
/// refinancing. Borrowers have the full LOAN_LIQUIDATION_DURATION (10 years) to repay their loan and recover
|
|
611
|
+
/// their collateral via re-minting.
|
|
612
|
+
/// @dev Since some loans may be reallocated or paid off, loans within startingLoanId and startingLoanId + count
|
|
613
|
+
/// may be skipped, so choose these parameters carefully to avoid extra gas usage.
|
|
614
|
+
/// @param revnetId The ID of the revnet to liquidate loans from.
|
|
615
|
+
/// @param startingLoanId The ID of the loan to start iterating from.
|
|
616
|
+
/// @param count The amount of loans iterate over since the last liquidated loan.
|
|
617
|
+
function liquidateExpiredLoansFrom(uint256 revnetId, uint256 startingLoanId, uint256 count) external override {
|
|
618
|
+
// Iterate over the desired number of loans to check for liquidation.
|
|
619
|
+
for (uint256 i; i < count; i++) {
|
|
620
|
+
// Get a reference to the next loan ID.
|
|
621
|
+
uint256 loanId = _generateLoanId({revnetId: revnetId, loanNumber: startingLoanId + i});
|
|
622
|
+
|
|
623
|
+
// Get a reference to the loan being iterated on.
|
|
624
|
+
REVLoan memory loan = _loanOf[loanId];
|
|
625
|
+
|
|
626
|
+
// If the loan doesn't exist, there's nothing left to liquidate.
|
|
627
|
+
// slither-disable-next-line incorrect-equality
|
|
628
|
+
if (loan.createdAt == 0) break;
|
|
629
|
+
|
|
630
|
+
// Keep a reference to the loan's owner.
|
|
631
|
+
address owner = _ownerOf(loanId);
|
|
632
|
+
|
|
633
|
+
// If the loan is already burned, or if it hasn't passed its liquidation duration, continue.
|
|
634
|
+
if (owner == address(0) || (block.timestamp <= loan.createdAt + LOAN_LIQUIDATION_DURATION)) continue;
|
|
635
|
+
|
|
636
|
+
// Burn the loan.
|
|
637
|
+
_burn(loanId);
|
|
638
|
+
|
|
639
|
+
// Clear stale loan data for gas refund.
|
|
640
|
+
delete _loanOf[loanId];
|
|
641
|
+
|
|
642
|
+
if (loan.collateral > 0) {
|
|
643
|
+
// The collateral was burned at deposit time -- there is nothing to return. This bookkeeping
|
|
644
|
+
// removal means the collateral tokens are permanently lost.
|
|
645
|
+
// Decrement the total amount of collateral tokens supporting loans from this revnet.
|
|
646
|
+
totalCollateralOf[revnetId] -= loan.collateral;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (loan.amount > 0) {
|
|
650
|
+
// Decrement the amount loaned.
|
|
651
|
+
totalBorrowedFrom[revnetId][loan.source.terminal][loan.source.token] -= loan.amount;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
emit Liquidate({loanId: loanId, revnetId: revnetId, loan: loan, caller: _msgSender()});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/// @notice Refinances a loan by transferring extra collateral from an existing loan to a new loan.
|
|
659
|
+
/// @dev Useful if a loan's collateral has gone up in value since the loan was created.
|
|
660
|
+
/// @dev Refinancing a loan will burn the original and create two new loans.
|
|
661
|
+
/// @dev This function is intentionally not payable — it only moves existing collateral between loans and does
|
|
662
|
+
/// not accept new funds. Any ETH sent with the call will be rejected by the EVM.
|
|
663
|
+
/// @param loanId The ID of the loan to reallocate collateral from.
|
|
664
|
+
/// @param collateralCountToTransfer The amount of collateral to transfer from the original loan.
|
|
665
|
+
/// @param source The source of the loan to create.
|
|
666
|
+
/// @param minBorrowAmount The minimum amount being borrowed, denominated in the token of the source's accounting
|
|
667
|
+
/// context.
|
|
668
|
+
/// @param collateralCountToAdd The amount of collateral to add to the loan.
|
|
669
|
+
/// @param beneficiary The address that'll receive the borrowed funds and the tokens resulting from fee payments.
|
|
670
|
+
/// @param prepaidFeePercent The fee percent that will be charged upfront from the revnet being borrowed from.
|
|
671
|
+
/// @return reallocatedLoanId The ID of the loan being reallocated.
|
|
672
|
+
/// @return newLoanId The ID of the new loan.
|
|
673
|
+
/// @return reallocatedLoan The loan being reallocated.
|
|
674
|
+
/// @return newLoan The new loan created from reallocating collateral.
|
|
675
|
+
function reallocateCollateralFromLoan(
|
|
676
|
+
uint256 loanId,
|
|
677
|
+
uint256 collateralCountToTransfer,
|
|
678
|
+
REVLoanSource calldata source,
|
|
679
|
+
uint256 minBorrowAmount,
|
|
680
|
+
uint256 collateralCountToAdd,
|
|
681
|
+
address payable beneficiary,
|
|
682
|
+
uint256 prepaidFeePercent
|
|
683
|
+
)
|
|
684
|
+
external
|
|
685
|
+
override
|
|
686
|
+
returns (uint256 reallocatedLoanId, uint256 newLoanId, REVLoan memory reallocatedLoan, REVLoan memory newLoan)
|
|
687
|
+
{
|
|
688
|
+
// Make sure only the loan's owner can manage it.
|
|
689
|
+
if (_ownerOf(loanId) != _msgSender()) revert REVLoans_Unauthorized(_msgSender(), _ownerOf(loanId));
|
|
690
|
+
|
|
691
|
+
// Make sure the new loan's source matches the existing loan's source to prevent cross-source value extraction.
|
|
692
|
+
{
|
|
693
|
+
REVLoanSource storage existingSource = _loanOf[loanId].source;
|
|
694
|
+
if (source.token != existingSource.token || source.terminal != existingSource.terminal) {
|
|
695
|
+
revert REVLoans_SourceMismatch();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Note: this function is not payable, so the EVM prevents sending ETH at the call level.
|
|
700
|
+
|
|
701
|
+
// Keep a reference to the revnet ID of the loan being reallocated.
|
|
702
|
+
uint256 revnetId = revnetIdOfLoanWith(loanId);
|
|
703
|
+
|
|
704
|
+
// Refinance the loan.
|
|
705
|
+
(reallocatedLoanId, reallocatedLoan) = _reallocateCollateralFromLoan({
|
|
706
|
+
loanId: loanId, revnetId: revnetId, collateralCountToRemove: collateralCountToTransfer
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Make a new loan with the leftover collateral from reallocating.
|
|
710
|
+
(newLoanId, newLoan) = borrowFrom({
|
|
711
|
+
revnetId: revnetId,
|
|
712
|
+
source: source,
|
|
713
|
+
minBorrowAmount: minBorrowAmount,
|
|
714
|
+
collateralCount: collateralCountToTransfer + collateralCountToAdd,
|
|
715
|
+
beneficiary: beneficiary,
|
|
716
|
+
prepaidFeePercent: prepaidFeePercent
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/// @notice Allows the owner of a loan to pay it back or receive returned collateral no longer necessary to support
|
|
721
|
+
/// the loan.
|
|
722
|
+
/// @param loanId The ID of the loan being adjusted.
|
|
723
|
+
/// @param maxRepayBorrowAmount The maximum amount being paid off, denominated in the token of the source's
|
|
724
|
+
/// accounting context.
|
|
725
|
+
/// @param collateralCountToReturn The amount of collateral being returned from the loan.
|
|
726
|
+
/// @param beneficiary The address receiving the returned collateral and any tokens resulting from paying fees.
|
|
727
|
+
/// @param allowance An allowance to faciliate permit2 interactions.
|
|
728
|
+
/// @return paidOffLoanId The ID of the loan after it's been paid off.
|
|
729
|
+
/// @return paidOffloan The loan after it's been paid off.
|
|
730
|
+
function repayLoan(
|
|
731
|
+
uint256 loanId,
|
|
732
|
+
uint256 maxRepayBorrowAmount,
|
|
733
|
+
uint256 collateralCountToReturn,
|
|
734
|
+
address payable beneficiary,
|
|
735
|
+
JBSingleAllowance calldata allowance
|
|
736
|
+
)
|
|
737
|
+
external
|
|
738
|
+
payable
|
|
739
|
+
override
|
|
740
|
+
returns (uint256 paidOffLoanId, REVLoan memory paidOffloan)
|
|
741
|
+
{
|
|
742
|
+
// Make sure only the loan's owner can manage it.
|
|
743
|
+
if (_ownerOf(loanId) != _msgSender()) revert REVLoans_Unauthorized(_msgSender(), _ownerOf(loanId));
|
|
744
|
+
|
|
745
|
+
// Keep a reference to the fee being iterated on.
|
|
746
|
+
REVLoan storage loan = _loanOf[loanId];
|
|
747
|
+
|
|
748
|
+
if (collateralCountToReturn > loan.collateral) {
|
|
749
|
+
revert REVLoans_CollateralExceedsLoan(collateralCountToReturn, loan.collateral);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Get a reference to the revnet ID of the loan being repaid.
|
|
753
|
+
uint256 revnetId = revnetIdOfLoanWith(loanId);
|
|
754
|
+
|
|
755
|
+
// Scope to limit newBorrowAmount's stack lifetime.
|
|
756
|
+
uint256 repayBorrowAmount;
|
|
757
|
+
{
|
|
758
|
+
// Get the new borrow amount.
|
|
759
|
+
uint256 newBorrowAmount = _borrowAmountFrom({
|
|
760
|
+
loan: loan, revnetId: revnetId, collateralCount: loan.collateral - collateralCountToReturn
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// Make sure the new borrow amount is less than the loan's amount.
|
|
764
|
+
if (newBorrowAmount > loan.amount) {
|
|
765
|
+
revert REVLoans_NewBorrowAmountGreaterThanLoanAmount(newBorrowAmount, loan.amount);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Get the amount of the loan being repaid.
|
|
769
|
+
repayBorrowAmount = loan.amount - newBorrowAmount;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Revert if this repayment would do nothing — no borrow amount repaid and no collateral returned.
|
|
773
|
+
// Without this check, a zero-amount repayment would burn the old loan NFT and mint a new one,
|
|
774
|
+
// incrementing numberOfLoansFor without limit.
|
|
775
|
+
if (repayBorrowAmount == 0 && collateralCountToReturn == 0) revert REVLoans_NothingToRepay();
|
|
776
|
+
|
|
777
|
+
// Keep a reference to the fee that'll be taken.
|
|
778
|
+
uint256 sourceFeeAmount = _determineSourceFeeAmount(loan, repayBorrowAmount);
|
|
779
|
+
|
|
780
|
+
// Add the fee to the repay amount.
|
|
781
|
+
repayBorrowAmount += sourceFeeAmount;
|
|
782
|
+
|
|
783
|
+
// Accept the funds that'll be used to pay off loans.
|
|
784
|
+
maxRepayBorrowAmount =
|
|
785
|
+
_acceptFundsFor({token: loan.source.token, amount: maxRepayBorrowAmount, allowance: allowance});
|
|
786
|
+
|
|
787
|
+
// Make sure the minimum borrow amount is met.
|
|
788
|
+
if (repayBorrowAmount > maxRepayBorrowAmount) {
|
|
789
|
+
revert REVLoans_OverMaxRepayBorrowAmount(maxRepayBorrowAmount, repayBorrowAmount);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Cache the source token before _repayLoan deletes the loan storage.
|
|
793
|
+
address sourceToken = loan.source.token;
|
|
794
|
+
|
|
795
|
+
(paidOffLoanId, paidOffloan) = _repayLoan({
|
|
796
|
+
loanId: loanId,
|
|
797
|
+
loan: loan,
|
|
798
|
+
revnetId: revnetId,
|
|
799
|
+
repayBorrowAmount: repayBorrowAmount,
|
|
800
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
801
|
+
collateralCountToReturn: collateralCountToReturn,
|
|
802
|
+
beneficiary: beneficiary
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// If the max repay amount is greater than the repay amount, return the difference back to the payer.
|
|
806
|
+
if (maxRepayBorrowAmount > repayBorrowAmount) {
|
|
807
|
+
_transferFrom({
|
|
808
|
+
from: address(this),
|
|
809
|
+
to: payable(_msgSender()),
|
|
810
|
+
token: sourceToken,
|
|
811
|
+
amount: maxRepayBorrowAmount - repayBorrowAmount
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/// @notice Sets the address of the resolver used to retrieve the tokenURI of loans.
|
|
817
|
+
/// @param resolver The address of the new resolver.
|
|
818
|
+
function setTokenUriResolver(IJBTokenUriResolver resolver) external override onlyOwner {
|
|
819
|
+
// Store the new resolver.
|
|
820
|
+
tokenUriResolver = resolver;
|
|
821
|
+
|
|
822
|
+
emit SetTokenUriResolver({resolver: resolver, caller: _msgSender()});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
//*********************************************************************//
|
|
826
|
+
// --------------------- internal transactions ----------------------- //
|
|
827
|
+
//*********************************************************************//
|
|
828
|
+
|
|
829
|
+
/// @notice Adds collateral to a loan by burning the collateral tokens permanently.
|
|
830
|
+
/// @dev The collateral tokens are burned via the controller, not held in escrow. They are only re-minted if the
|
|
831
|
+
/// loan is repaid. If the loan expires and is liquidated, the burned collateral is permanently lost.
|
|
832
|
+
/// @param revnetId The ID of the revnet the loan is being added in.
|
|
833
|
+
/// @param amount The new amount of collateral being added to the loan.
|
|
834
|
+
function _addCollateralTo(uint256 revnetId, uint256 amount) internal {
|
|
835
|
+
// Increment the total amount of collateral tokens.
|
|
836
|
+
totalCollateralOf[revnetId] += amount;
|
|
837
|
+
|
|
838
|
+
// Permanently burn the tokens that are tracked as collateral. These are only re-minted upon repayment.
|
|
839
|
+
CONTROLLER.burnTokensOf({
|
|
840
|
+
holder: _msgSender(), projectId: revnetId, tokenCount: amount, memo: "Adding collateral to loan"
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/// @notice Add a new amount to the loan that is greater than the previous amount.
|
|
845
|
+
/// @param loan The loan being added to.
|
|
846
|
+
/// @param revnetId The ID of the revnet the loan is being added in.
|
|
847
|
+
/// @param addedBorrowAmount The amount being added to the loan, denominated in the token of the source's
|
|
848
|
+
/// accounting context.
|
|
849
|
+
/// @param sourceFeeAmount The amount of the fee being taken from the revnet acting as the source of the loan.
|
|
850
|
+
/// @param beneficiary The address receiving the returned collateral and any tokens resulting from paying fees.
|
|
851
|
+
function _addTo(
|
|
852
|
+
REVLoan memory loan,
|
|
853
|
+
uint256 revnetId,
|
|
854
|
+
uint256 addedBorrowAmount,
|
|
855
|
+
uint256 sourceFeeAmount,
|
|
856
|
+
address payable beneficiary
|
|
857
|
+
)
|
|
858
|
+
internal
|
|
859
|
+
{
|
|
860
|
+
// Register the source if this is the first time its being used for this revnet.
|
|
861
|
+
if (!isLoanSourceOf[revnetId][loan.source.terminal][loan.source.token]) {
|
|
862
|
+
isLoanSourceOf[revnetId][loan.source.terminal][loan.source.token] = true;
|
|
863
|
+
_loanSourcesOf[revnetId].push(REVLoanSource({token: loan.source.token, terminal: loan.source.terminal}));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Increment the amount of the token borrowed from the revnet from the terminal.
|
|
867
|
+
totalBorrowedFrom[revnetId][loan.source.terminal][loan.source.token] += addedBorrowAmount;
|
|
868
|
+
|
|
869
|
+
uint256 netAmountPaidOut;
|
|
870
|
+
{
|
|
871
|
+
// Get a reference to the accounting context for the source.
|
|
872
|
+
JBAccountingContext memory accountingContext =
|
|
873
|
+
loan.source.terminal.accountingContextForTokenOf({projectId: revnetId, token: loan.source.token});
|
|
874
|
+
|
|
875
|
+
// Pull the amount to be loaned out of the revnet. This will incure the protocol fee.
|
|
876
|
+
// slither-disable-next-line unused-return
|
|
877
|
+
netAmountPaidOut = loan.source.terminal
|
|
878
|
+
.useAllowanceOf({
|
|
879
|
+
projectId: revnetId,
|
|
880
|
+
token: loan.source.token,
|
|
881
|
+
amount: addedBorrowAmount,
|
|
882
|
+
currency: accountingContext.currency,
|
|
883
|
+
minTokensPaidOut: 0,
|
|
884
|
+
beneficiary: payable(address(this)),
|
|
885
|
+
feeBeneficiary: beneficiary,
|
|
886
|
+
memo: "Lending out to a borrower"
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Keep a reference to the fee terminal.
|
|
891
|
+
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: REV_ID, token: loan.source.token});
|
|
892
|
+
|
|
893
|
+
// Get the amount of additional fee to take for REV.
|
|
894
|
+
uint256 revFeeAmount = address(feeTerminal) == address(0)
|
|
895
|
+
? 0
|
|
896
|
+
: JBFees.feeAmountFrom({amountBeforeFee: addedBorrowAmount, feePercent: REV_PREPAID_FEE_PERCENT});
|
|
897
|
+
|
|
898
|
+
if (revFeeAmount > 0) {
|
|
899
|
+
// Increase the allowance for the fee terminal.
|
|
900
|
+
uint256 payValue =
|
|
901
|
+
_beforeTransferTo({to: address(feeTerminal), token: loan.source.token, amount: revFeeAmount});
|
|
902
|
+
|
|
903
|
+
// Pay the fee. Send the REV to the beneficiary. If fee payment fails, give the amount back to the borrower.
|
|
904
|
+
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
905
|
+
try feeTerminal.pay{value: payValue}({
|
|
906
|
+
projectId: REV_ID,
|
|
907
|
+
token: loan.source.token,
|
|
908
|
+
amount: revFeeAmount,
|
|
909
|
+
beneficiary: beneficiary,
|
|
910
|
+
minReturnedTokens: 0,
|
|
911
|
+
memo: "Fee from loan",
|
|
912
|
+
metadata: bytes(abi.encodePacked(revnetId))
|
|
913
|
+
}) {}
|
|
914
|
+
catch (bytes memory) {
|
|
915
|
+
// If the fee can't be processed, decrease the ERC-20 allowance and zero out the fee
|
|
916
|
+
// so the borrower receives it instead.
|
|
917
|
+
if (loan.source.token != JBConstants.NATIVE_TOKEN) {
|
|
918
|
+
IERC20(loan.source.token)
|
|
919
|
+
.safeDecreaseAllowance({spender: address(feeTerminal), requestedDecrease: revFeeAmount});
|
|
920
|
+
}
|
|
921
|
+
revFeeAmount = 0;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Transfer the remaining balance to the borrower.
|
|
926
|
+
_transferFrom({
|
|
927
|
+
from: address(this),
|
|
928
|
+
to: beneficiary,
|
|
929
|
+
token: loan.source.token,
|
|
930
|
+
amount: netAmountPaidOut - revFeeAmount - sourceFeeAmount
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/// @notice Allows the owner of a loan to pay it back, add more, or receive returned collateral no longer necessary
|
|
935
|
+
/// to support the loan.
|
|
936
|
+
/// @param loan The loan being adjusted.
|
|
937
|
+
/// @param revnetId The ID of the revnet the loan is being adjusted in.
|
|
938
|
+
/// @param newBorrowAmount The new amount of the loan, denominated in the token of the source's accounting
|
|
939
|
+
/// context.
|
|
940
|
+
/// @param newCollateralCount The new amount of collateral backing the loan.
|
|
941
|
+
/// @param sourceFeeAmount The amount of the fee being taken from the revnet acting as the source of the loan.
|
|
942
|
+
/// @param beneficiary The address receiving the returned collateral and any tokens resulting from paying fees.
|
|
943
|
+
function _adjust(
|
|
944
|
+
REVLoan storage loan,
|
|
945
|
+
uint256 revnetId,
|
|
946
|
+
uint256 newBorrowAmount,
|
|
947
|
+
uint256 newCollateralCount,
|
|
948
|
+
uint256 sourceFeeAmount,
|
|
949
|
+
address payable beneficiary
|
|
950
|
+
)
|
|
951
|
+
internal
|
|
952
|
+
{
|
|
953
|
+
// Snapshot deltas from current state before writing.
|
|
954
|
+
uint256 addedBorrowAmount = newBorrowAmount > loan.amount ? newBorrowAmount - loan.amount : 0;
|
|
955
|
+
uint256 repaidBorrowAmount = loan.amount > newBorrowAmount ? loan.amount - newBorrowAmount : 0;
|
|
956
|
+
uint256 addedCollateralCount = newCollateralCount > loan.collateral ? newCollateralCount - loan.collateral : 0;
|
|
957
|
+
uint256 returnedCollateralCount =
|
|
958
|
+
loan.collateral > newCollateralCount ? loan.collateral - newCollateralCount : 0;
|
|
959
|
+
|
|
960
|
+
// EFFECTS: Write loan state before any external calls (CEI pattern).
|
|
961
|
+
// Any reentrant call will see the updated loan values, reverting on overflow.
|
|
962
|
+
if (newBorrowAmount > type(uint112).max) revert REVLoans_OverflowAlert(newBorrowAmount, type(uint112).max);
|
|
963
|
+
if (newCollateralCount > type(uint112).max) {
|
|
964
|
+
revert REVLoans_OverflowAlert(newCollateralCount, type(uint112).max);
|
|
965
|
+
}
|
|
966
|
+
loan.amount = uint112(newBorrowAmount);
|
|
967
|
+
loan.collateral = uint112(newCollateralCount);
|
|
968
|
+
|
|
969
|
+
// INTERACTIONS: Execute external calls with pre-computed deltas.
|
|
970
|
+
|
|
971
|
+
// Add to the loan if needed...
|
|
972
|
+
if (addedBorrowAmount > 0) {
|
|
973
|
+
_addTo({
|
|
974
|
+
loan: loan,
|
|
975
|
+
revnetId: revnetId,
|
|
976
|
+
addedBorrowAmount: addedBorrowAmount,
|
|
977
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
978
|
+
beneficiary: beneficiary
|
|
979
|
+
});
|
|
980
|
+
// ... or pay off the loan if needed.
|
|
981
|
+
} else if (repaidBorrowAmount > 0) {
|
|
982
|
+
_removeFrom({loan: loan, revnetId: revnetId, repaidBorrowAmount: repaidBorrowAmount});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Add collateral if needed...
|
|
986
|
+
if (addedCollateralCount > 0) {
|
|
987
|
+
_addCollateralTo({revnetId: revnetId, amount: addedCollateralCount});
|
|
988
|
+
// ... or return collateral if needed.
|
|
989
|
+
} else if (returnedCollateralCount > 0) {
|
|
990
|
+
_returnCollateralFrom({
|
|
991
|
+
revnetId: revnetId, collateralCount: returnedCollateralCount, beneficiary: beneficiary
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// If there is a source fee, pay it.
|
|
996
|
+
if (sourceFeeAmount > 0) {
|
|
997
|
+
// Increase the allowance for the beneficiary.
|
|
998
|
+
uint256 payValue = _beforeTransferTo({
|
|
999
|
+
to: address(loan.source.terminal), token: loan.source.token, amount: sourceFeeAmount
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// Pay the fee.
|
|
1003
|
+
// slither-disable-next-line unused-return
|
|
1004
|
+
loan.source.terminal.pay{value: payValue}({
|
|
1005
|
+
projectId: revnetId,
|
|
1006
|
+
token: loan.source.token,
|
|
1007
|
+
amount: sourceFeeAmount,
|
|
1008
|
+
beneficiary: beneficiary,
|
|
1009
|
+
minReturnedTokens: 0,
|
|
1010
|
+
memo: "Fee from loan",
|
|
1011
|
+
metadata: bytes(abi.encodePacked(REV_ID))
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/// @notice Accepts an incoming token.
|
|
1017
|
+
/// @param token The token being accepted.
|
|
1018
|
+
/// @param amount The number of tokens being accepted.
|
|
1019
|
+
/// @param allowance The permit2 context.
|
|
1020
|
+
/// @return amount The number of tokens which have been accepted.
|
|
1021
|
+
function _acceptFundsFor(
|
|
1022
|
+
address token,
|
|
1023
|
+
uint256 amount,
|
|
1024
|
+
JBSingleAllowance memory allowance
|
|
1025
|
+
)
|
|
1026
|
+
internal
|
|
1027
|
+
returns (uint256)
|
|
1028
|
+
{
|
|
1029
|
+
// If the token is the native token, override `amount` with `msg.value`.
|
|
1030
|
+
if (token == JBConstants.NATIVE_TOKEN) return msg.value;
|
|
1031
|
+
|
|
1032
|
+
// If the token is not native, revert if there is a non-zero `msg.value`.
|
|
1033
|
+
if (msg.value != 0) revert REVLoans_NoMsgValueAllowed();
|
|
1034
|
+
|
|
1035
|
+
// Check if the metadata contains permit data.
|
|
1036
|
+
if (allowance.amount != 0) {
|
|
1037
|
+
// Make sure the permit allowance is enough for this payment. If not we revert early.
|
|
1038
|
+
if (allowance.amount < amount) {
|
|
1039
|
+
revert REVLoans_PermitAllowanceNotEnough(allowance.amount, amount);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Keep a reference to the permit rules.
|
|
1043
|
+
IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({
|
|
1044
|
+
details: IAllowanceTransfer.PermitDetails({
|
|
1045
|
+
token: token, amount: allowance.amount, expiration: allowance.expiration, nonce: allowance.nonce
|
|
1046
|
+
}),
|
|
1047
|
+
spender: address(this),
|
|
1048
|
+
sigDeadline: allowance.sigDeadline
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
// Set the allowance to `spend` tokens for the user.
|
|
1052
|
+
try PERMIT2.permit({owner: _msgSender(), permitSingle: permitSingle, signature: allowance.signature}) {}
|
|
1053
|
+
catch (bytes memory) {}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Get a reference to the balance before receiving tokens.
|
|
1057
|
+
uint256 balanceBefore = _balanceOf(token);
|
|
1058
|
+
|
|
1059
|
+
// Transfer tokens to this terminal from the msg sender.
|
|
1060
|
+
_transferFrom({from: _msgSender(), to: payable(address(this)), token: token, amount: amount});
|
|
1061
|
+
|
|
1062
|
+
// The amount should reflect the change in balance.
|
|
1063
|
+
return _balanceOf(token) - balanceBefore;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/// @notice Logic to be triggered before transferring tokens from this contract.
|
|
1067
|
+
/// @param to The address the transfer is going to.
|
|
1068
|
+
/// @param token The token being transferred.
|
|
1069
|
+
/// @param amount The number of tokens being transferred, as a fixed point number with the same number of decimals
|
|
1070
|
+
/// as the token specifies.
|
|
1071
|
+
/// @return payValue The value to attach to the transaction being sent.
|
|
1072
|
+
function _beforeTransferTo(address to, address token, uint256 amount) internal returns (uint256) {
|
|
1073
|
+
// If the token is the native token, no allowance needed.
|
|
1074
|
+
if (token == JBConstants.NATIVE_TOKEN) return amount;
|
|
1075
|
+
IERC20(token).safeIncreaseAllowance(to, amount);
|
|
1076
|
+
return 0;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/// @notice Pays down a loan.
|
|
1080
|
+
/// @param loanId The ID of the loan being paid down.
|
|
1081
|
+
/// @param loan The loan being paid down.
|
|
1082
|
+
/// @param repayBorrowAmount The amount being paid down from the loan, denominated in the token of the source's
|
|
1083
|
+
/// accounting context.
|
|
1084
|
+
/// @param sourceFeeAmount The amount of the fee being taken from the revnet acting as the source of the loan.
|
|
1085
|
+
/// @param collateralCountToReturn The amount of collateral being returned that the loan no longer requires.
|
|
1086
|
+
/// @param beneficiary The address receiving the returned collateral and any tokens resulting from paying fees.
|
|
1087
|
+
function _repayLoan(
|
|
1088
|
+
uint256 loanId,
|
|
1089
|
+
REVLoan storage loan,
|
|
1090
|
+
uint256 revnetId,
|
|
1091
|
+
uint256 repayBorrowAmount,
|
|
1092
|
+
uint256 sourceFeeAmount,
|
|
1093
|
+
uint256 collateralCountToReturn,
|
|
1094
|
+
address payable beneficiary
|
|
1095
|
+
)
|
|
1096
|
+
internal
|
|
1097
|
+
returns (uint256, REVLoan memory)
|
|
1098
|
+
{
|
|
1099
|
+
// Burn the original loan.
|
|
1100
|
+
_burn(loanId);
|
|
1101
|
+
|
|
1102
|
+
// If the loan will carry no more amount or collateral, store its changes directly.
|
|
1103
|
+
// slither-disable-next-line incorrect-equality
|
|
1104
|
+
if (collateralCountToReturn == loan.collateral) {
|
|
1105
|
+
// Borrow in.
|
|
1106
|
+
_adjust({
|
|
1107
|
+
loan: loan,
|
|
1108
|
+
revnetId: revnetId,
|
|
1109
|
+
newBorrowAmount: 0,
|
|
1110
|
+
newCollateralCount: 0,
|
|
1111
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
1112
|
+
beneficiary: beneficiary
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// Snapshot the loan to memory before deleting storage.
|
|
1116
|
+
REVLoan memory loanSnapshot = loan;
|
|
1117
|
+
|
|
1118
|
+
emit RepayLoan({
|
|
1119
|
+
loanId: loanId,
|
|
1120
|
+
revnetId: revnetId,
|
|
1121
|
+
paidOffLoanId: loanId,
|
|
1122
|
+
loan: loanSnapshot,
|
|
1123
|
+
paidOffLoan: loanSnapshot,
|
|
1124
|
+
repayBorrowAmount: repayBorrowAmount,
|
|
1125
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
1126
|
+
collateralCountToReturn: collateralCountToReturn,
|
|
1127
|
+
beneficiary: beneficiary,
|
|
1128
|
+
caller: _msgSender()
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
// Clear stale loan data for gas refund.
|
|
1132
|
+
delete _loanOf[loanId];
|
|
1133
|
+
|
|
1134
|
+
return (loanId, loanSnapshot);
|
|
1135
|
+
} else {
|
|
1136
|
+
// Make a new loan with the remaining amount and collateral.
|
|
1137
|
+
// Get a reference to the replacement loan ID.
|
|
1138
|
+
uint256 paidOffLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++numberOfLoansFor[revnetId]});
|
|
1139
|
+
|
|
1140
|
+
// Get a reference to the loan being paid off.
|
|
1141
|
+
REVLoan storage paidOffLoan = _loanOf[paidOffLoanId];
|
|
1142
|
+
|
|
1143
|
+
// Set the paid off loan's values the same as the original loan.
|
|
1144
|
+
paidOffLoan.amount = loan.amount;
|
|
1145
|
+
paidOffLoan.collateral = loan.collateral;
|
|
1146
|
+
paidOffLoan.createdAt = loan.createdAt;
|
|
1147
|
+
paidOffLoan.prepaidFeePercent = loan.prepaidFeePercent;
|
|
1148
|
+
paidOffLoan.prepaidDuration = loan.prepaidDuration;
|
|
1149
|
+
paidOffLoan.source = loan.source;
|
|
1150
|
+
|
|
1151
|
+
// Borrow in.
|
|
1152
|
+
_adjust({
|
|
1153
|
+
loan: paidOffLoan,
|
|
1154
|
+
revnetId: revnetId,
|
|
1155
|
+
newBorrowAmount: paidOffLoan.amount - (repayBorrowAmount - sourceFeeAmount),
|
|
1156
|
+
newCollateralCount: paidOffLoan.collateral - collateralCountToReturn,
|
|
1157
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
1158
|
+
beneficiary: beneficiary
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
// Mint the replacement loan.
|
|
1162
|
+
_mint({to: _msgSender(), tokenId: paidOffLoanId});
|
|
1163
|
+
|
|
1164
|
+
emit RepayLoan({
|
|
1165
|
+
loanId: loanId,
|
|
1166
|
+
revnetId: revnetId,
|
|
1167
|
+
paidOffLoanId: paidOffLoanId,
|
|
1168
|
+
loan: loan,
|
|
1169
|
+
paidOffLoan: paidOffLoan,
|
|
1170
|
+
repayBorrowAmount: repayBorrowAmount,
|
|
1171
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
1172
|
+
collateralCountToReturn: collateralCountToReturn,
|
|
1173
|
+
beneficiary: beneficiary,
|
|
1174
|
+
caller: _msgSender()
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// Clear stale loan data for gas refund.
|
|
1178
|
+
delete _loanOf[loanId];
|
|
1179
|
+
|
|
1180
|
+
return (paidOffLoanId, paidOffLoan);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/// @notice Reallocates collateral from a loan by making a new loan based on the original, with reduced collateral.
|
|
1185
|
+
/// @param loanId The ID of the loan to reallocate collateral from.
|
|
1186
|
+
/// @param revnetId The ID of the revnet the loan is from.
|
|
1187
|
+
/// @param collateralCountToRemove The amount of collateral to remove from the loan.
|
|
1188
|
+
/// @return reallocatedLoanId The ID of the loan.
|
|
1189
|
+
/// @return reallocatedLoan The reallocated loan.
|
|
1190
|
+
function _reallocateCollateralFromLoan(
|
|
1191
|
+
uint256 loanId,
|
|
1192
|
+
uint256 revnetId,
|
|
1193
|
+
uint256 collateralCountToRemove
|
|
1194
|
+
)
|
|
1195
|
+
internal
|
|
1196
|
+
returns (uint256 reallocatedLoanId, REVLoan storage reallocatedLoan)
|
|
1197
|
+
{
|
|
1198
|
+
// Burn the original loan.
|
|
1199
|
+
_burn(loanId);
|
|
1200
|
+
|
|
1201
|
+
// Keep a reference to loan having its collateral reduced.
|
|
1202
|
+
REVLoan storage loan = _loanOf[loanId];
|
|
1203
|
+
|
|
1204
|
+
// Make sure there is enough collateral to transfer.
|
|
1205
|
+
if (collateralCountToRemove > loan.collateral) revert REVLoans_NotEnoughCollateral();
|
|
1206
|
+
|
|
1207
|
+
// Keep a reference to the new collateral amount.
|
|
1208
|
+
uint256 newCollateralCount = loan.collateral - collateralCountToRemove;
|
|
1209
|
+
|
|
1210
|
+
// Keep a reference to the new borrow amount.
|
|
1211
|
+
uint256 borrowAmount = _borrowAmountFrom({loan: loan, revnetId: revnetId, collateralCount: newCollateralCount});
|
|
1212
|
+
|
|
1213
|
+
// Make sure the borrow amount is not less than the original loan's amount.
|
|
1214
|
+
if (borrowAmount < loan.amount) {
|
|
1215
|
+
revert REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(borrowAmount, loan.amount);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Get a reference to the replacement loan ID.
|
|
1219
|
+
reallocatedLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++numberOfLoansFor[revnetId]});
|
|
1220
|
+
|
|
1221
|
+
// Get a reference to the loan being created.
|
|
1222
|
+
reallocatedLoan = _loanOf[reallocatedLoanId];
|
|
1223
|
+
|
|
1224
|
+
// Set the reallocated loan's values the same as the original loan.
|
|
1225
|
+
reallocatedLoan.amount = loan.amount;
|
|
1226
|
+
reallocatedLoan.collateral = loan.collateral;
|
|
1227
|
+
reallocatedLoan.createdAt = loan.createdAt;
|
|
1228
|
+
reallocatedLoan.prepaidFeePercent = loan.prepaidFeePercent;
|
|
1229
|
+
reallocatedLoan.prepaidDuration = loan.prepaidDuration;
|
|
1230
|
+
reallocatedLoan.source = loan.source;
|
|
1231
|
+
|
|
1232
|
+
// Reduce the collateral of the reallocated loan.
|
|
1233
|
+
_adjust({
|
|
1234
|
+
loan: reallocatedLoan,
|
|
1235
|
+
revnetId: revnetId,
|
|
1236
|
+
newBorrowAmount: reallocatedLoan.amount, // Don't change the borrow amount.
|
|
1237
|
+
newCollateralCount: newCollateralCount,
|
|
1238
|
+
sourceFeeAmount: 0,
|
|
1239
|
+
beneficiary: payable(_msgSender()) // use the msgSender as the beneficiary, who will have the returned
|
|
1240
|
+
// collateral tokens debited from their balance for the new loan.
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
// Mint the replacement loan.
|
|
1244
|
+
_mint({to: _msgSender(), tokenId: reallocatedLoanId});
|
|
1245
|
+
|
|
1246
|
+
// Clear stale loan data for gas refund.
|
|
1247
|
+
delete _loanOf[loanId];
|
|
1248
|
+
|
|
1249
|
+
emit ReallocateCollateral({
|
|
1250
|
+
loanId: loanId,
|
|
1251
|
+
revnetId: revnetId,
|
|
1252
|
+
reallocatedLoanId: reallocatedLoanId,
|
|
1253
|
+
reallocatedLoan: reallocatedLoan,
|
|
1254
|
+
removedCollateralCount: collateralCountToRemove,
|
|
1255
|
+
caller: _msgSender()
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/// @notice Pays off a loan.
|
|
1260
|
+
/// @param loan The loan being paid off.
|
|
1261
|
+
/// @param revnetId The ID of the revnet the loan is being paid off in.
|
|
1262
|
+
/// @param repaidBorrowAmount The amount being paid off, denominated in the token of the source's accounting
|
|
1263
|
+
/// context.
|
|
1264
|
+
function _removeFrom(REVLoan memory loan, uint256 revnetId, uint256 repaidBorrowAmount) internal {
|
|
1265
|
+
// Decrement the total amount of a token being loaned out by the revnet from its terminal.
|
|
1266
|
+
totalBorrowedFrom[revnetId][loan.source.terminal][loan.source.token] -= repaidBorrowAmount;
|
|
1267
|
+
|
|
1268
|
+
// Increase the allowance for the beneficiary.
|
|
1269
|
+
uint256 payValue = _beforeTransferTo({
|
|
1270
|
+
to: address(loan.source.terminal), token: loan.source.token, amount: repaidBorrowAmount
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
// Add the loaned amount back to the revnet.
|
|
1274
|
+
// slither-disable-next-line arbitrary-send-eth
|
|
1275
|
+
loan.source.terminal.addToBalanceOf{value: payValue}({
|
|
1276
|
+
projectId: revnetId,
|
|
1277
|
+
token: loan.source.token,
|
|
1278
|
+
amount: repaidBorrowAmount,
|
|
1279
|
+
shouldReturnHeldFees: false,
|
|
1280
|
+
memo: "Paying off loan",
|
|
1281
|
+
metadata: bytes(abi.encodePacked(REV_ID))
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/// @notice Returns collateral from a loan.
|
|
1286
|
+
/// @param revnetId The ID of the revnet the loan is being returned in.
|
|
1287
|
+
/// @param collateralCount The amount of collateral being returned from the loan.
|
|
1288
|
+
/// @param beneficiary The address receiving the returned collateral.
|
|
1289
|
+
function _returnCollateralFrom(uint256 revnetId, uint256 collateralCount, address payable beneficiary) internal {
|
|
1290
|
+
// Decrement the total amount of collateral tokens.
|
|
1291
|
+
totalCollateralOf[revnetId] -= collateralCount;
|
|
1292
|
+
|
|
1293
|
+
// Mint the collateral tokens back to the loan payer.
|
|
1294
|
+
// slither-disable-next-line unused-return,calls-loop
|
|
1295
|
+
CONTROLLER.mintTokensOf({
|
|
1296
|
+
projectId: revnetId,
|
|
1297
|
+
tokenCount: collateralCount,
|
|
1298
|
+
beneficiary: beneficiary,
|
|
1299
|
+
memo: "Removing collateral from loan",
|
|
1300
|
+
useReservedPercent: false
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/// @notice Transfers tokens.
|
|
1305
|
+
/// @param from The address to transfer tokens from.
|
|
1306
|
+
/// @param to The address to transfer tokens to.
|
|
1307
|
+
/// @param token The address of the token being transfered.
|
|
1308
|
+
/// @param amount The amount of tokens to transfer, as a fixed point number with the same number of decimals as the
|
|
1309
|
+
/// token.
|
|
1310
|
+
function _transferFrom(address from, address payable to, address token, uint256 amount) internal virtual {
|
|
1311
|
+
if (from == address(this)) {
|
|
1312
|
+
// If the token is native token, assume the `sendValue` standard.
|
|
1313
|
+
if (token == JBConstants.NATIVE_TOKEN) return Address.sendValue({recipient: to, amount: amount});
|
|
1314
|
+
|
|
1315
|
+
// If the transfer is from this contract, use `safeTransfer`.
|
|
1316
|
+
return IERC20(token).safeTransfer({to: to, value: amount});
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// If there's sufficient approval, transfer normally.
|
|
1320
|
+
if (IERC20(token).allowance(address(from), address(this)) >= amount) {
|
|
1321
|
+
return IERC20(token).safeTransferFrom({from: from, to: to, value: amount});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Make sure the amount being paid is less than the maximum permit2 allowance.
|
|
1325
|
+
if (amount > type(uint160).max) revert REVLoans_OverflowAlert(amount, type(uint160).max);
|
|
1326
|
+
|
|
1327
|
+
// Otherwise, attempt to use the `permit2` method.
|
|
1328
|
+
PERMIT2.transferFrom({from: from, to: to, amount: uint160(amount), token: token});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
fallback() external payable {}
|
|
1332
|
+
receive() external payable {}
|
|
1333
|
+
}
|