@rev-net/core-v6 0.0.43 → 0.0.44
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/README.md +2 -2
- package/package.json +1 -1
- package/src/REVDeployer.sol +46 -45
- package/src/REVHiddenTokens.sol +1 -3
- package/src/REVLoans.sol +84 -50
- package/src/REVOwner.sol +14 -16
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Most mistakes come from assuming a deploy-time parameter can be changed later or
|
|
|
72
72
|
2. `test/REVLoans.invariants.t.sol`
|
|
73
73
|
3. `test/TestLongTailEconomics.t.sol`
|
|
74
74
|
4. `test/fork/TestLoanBorrowFork.t.sol`
|
|
75
|
-
5. `test/
|
|
75
|
+
5. `test/regression/PhantomSurplusTerminal.t.sol`
|
|
76
76
|
|
|
77
77
|
## Install
|
|
78
78
|
|
|
@@ -108,7 +108,7 @@ src/
|
|
|
108
108
|
interfaces/
|
|
109
109
|
structs/
|
|
110
110
|
test/
|
|
111
|
-
lifecycle, deployment, loan, fork, invariant,
|
|
111
|
+
lifecycle, deployment, loan, fork, invariant, review, and regression coverage
|
|
112
112
|
script/
|
|
113
113
|
Deploy.s.sol
|
|
114
114
|
helpers/
|
package/package.json
CHANGED
package/src/REVDeployer.sol
CHANGED
|
@@ -57,15 +57,15 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
57
57
|
// --------------------------- custom errors ------------------------- //
|
|
58
58
|
//*********************************************************************//
|
|
59
59
|
|
|
60
|
-
error REVDeployer_AutoIssuanceBeneficiaryZeroAddress();
|
|
60
|
+
error REVDeployer_AutoIssuanceBeneficiaryZeroAddress(uint256 stageIndex, uint256 autoIssuanceIndex);
|
|
61
61
|
error REVDeployer_CashOutsCantBeTurnedOffCompletely(uint256 cashOutTaxRate, uint256 maxCashOutTaxRate);
|
|
62
|
-
error REVDeployer_MustHaveSplits();
|
|
63
|
-
error REVDeployer_NothingToAutoIssue();
|
|
64
|
-
error REVDeployer_NothingToBurn();
|
|
65
|
-
error REVDeployer_RulesetDoesNotAllowDeployingSuckers();
|
|
62
|
+
error REVDeployer_MustHaveSplits(uint256 stageIndex, uint256 splitPercent);
|
|
63
|
+
error REVDeployer_NothingToAutoIssue(uint256 revnetId, uint256 stageId, address beneficiary);
|
|
64
|
+
error REVDeployer_NothingToBurn(uint256 revnetId, address holder);
|
|
65
|
+
error REVDeployer_RulesetDoesNotAllowDeployingSuckers(uint256 revnetId);
|
|
66
66
|
error REVDeployer_StageNotStarted(uint256 stageId);
|
|
67
|
-
error REVDeployer_StagesRequired();
|
|
68
|
-
error REVDeployer_StageTimesMustIncrease();
|
|
67
|
+
error REVDeployer_StagesRequired(uint256 stageCount);
|
|
68
|
+
error REVDeployer_StageTimesMustIncrease(uint256 stageIndex, uint256 previousStageStart, uint256 effectiveStart);
|
|
69
69
|
error REVDeployer_Unauthorized(uint256 revnetId, address caller);
|
|
70
70
|
|
|
71
71
|
//*********************************************************************//
|
|
@@ -164,7 +164,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
164
164
|
/// @notice A list of `JBPermissonIds` indices to grant to the split operator of a specific revnet.
|
|
165
165
|
/// @dev These should be set in the revnet's deployment process.
|
|
166
166
|
/// @custom:param revnetId The ID of the revnet to look up.
|
|
167
|
-
// slither-disable-next-line uninitialized-state
|
|
168
167
|
mapping(uint256 revnetId => uint256[]) internal _extraOperatorPermissions;
|
|
169
168
|
|
|
170
169
|
//*********************************************************************//
|
|
@@ -203,7 +202,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
203
202
|
PUBLISHER = publisher;
|
|
204
203
|
BUYBACK_HOOK = buybackHook;
|
|
205
204
|
LOANS = loans;
|
|
206
|
-
// slither-disable-next-line missing-zero-check
|
|
207
205
|
OWNER = owner;
|
|
208
206
|
|
|
209
207
|
// Give the loan contract permission to use the surplus allowance of all revnets.
|
|
@@ -262,7 +260,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
262
260
|
/// @param operator The address to check.
|
|
263
261
|
function _checkIfIsSplitOperatorOf(uint256 revnetId, address operator) internal view {
|
|
264
262
|
if (!isSplitOperatorOf({revnetId: revnetId, addr: operator})) {
|
|
265
|
-
revert REVDeployer_Unauthorized(revnetId, operator);
|
|
263
|
+
revert REVDeployer_Unauthorized({revnetId: revnetId, caller: operator});
|
|
266
264
|
}
|
|
267
265
|
}
|
|
268
266
|
|
|
@@ -399,29 +397,36 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
399
397
|
/// Silently catches failures (e.g., if the pool is already initialized).
|
|
400
398
|
/// @param revnetId The ID of the revnet to initialize a pool for.
|
|
401
399
|
/// @param terminalToken The terminal token to create a buyback pool for.
|
|
400
|
+
/// @param terminalTokenDecimals The number of decimals the terminal token uses.
|
|
402
401
|
/// @param initialIssuance The initial issuance rate (project tokens per terminal token, 18 decimals).
|
|
403
|
-
function _tryInitializeBuybackPoolFor(
|
|
402
|
+
function _tryInitializeBuybackPoolFor(
|
|
403
|
+
uint256 revnetId,
|
|
404
|
+
address terminalToken,
|
|
405
|
+
uint8 terminalTokenDecimals,
|
|
406
|
+
uint112 initialIssuance
|
|
407
|
+
)
|
|
408
|
+
internal
|
|
409
|
+
{
|
|
404
410
|
uint160 sqrtPriceX96;
|
|
411
|
+
uint256 terminalTokenUnit = 10 ** terminalTokenDecimals;
|
|
405
412
|
|
|
406
413
|
if (initialIssuance == 0) {
|
|
407
414
|
sqrtPriceX96 = uint160(1 << 96);
|
|
408
415
|
} else {
|
|
409
416
|
address normalizedTerminalToken = terminalToken == JBConstants.NATIVE_TOKEN ? address(0) : terminalToken;
|
|
410
|
-
// slither-disable-next-line calls-loop
|
|
411
417
|
address projectToken = address(CONTROLLER.TOKENS().tokenOf(revnetId));
|
|
412
418
|
|
|
413
419
|
if (projectToken == address(0) || projectToken == normalizedTerminalToken) {
|
|
414
420
|
sqrtPriceX96 = uint160(1 << 96);
|
|
415
421
|
} else if (normalizedTerminalToken < projectToken) {
|
|
416
|
-
// token0 = terminal, token1 = project → price = issuance /
|
|
417
|
-
sqrtPriceX96 = uint160(sqrt(mulDiv(uint256(initialIssuance), 1 << 192,
|
|
422
|
+
// token0 = terminal, token1 = project → price = issuance / terminalTokenUnit
|
|
423
|
+
sqrtPriceX96 = uint160(sqrt(mulDiv(uint256(initialIssuance), 1 << 192, terminalTokenUnit)));
|
|
418
424
|
} else {
|
|
419
|
-
// token0 = project, token1 = terminal → price =
|
|
420
|
-
sqrtPriceX96 = uint160(sqrt(mulDiv(
|
|
425
|
+
// token0 = project, token1 = terminal → price = terminalTokenUnit / issuance
|
|
426
|
+
sqrtPriceX96 = uint160(sqrt(mulDiv(terminalTokenUnit, 1 << 192, uint256(initialIssuance))));
|
|
421
427
|
}
|
|
422
428
|
}
|
|
423
429
|
|
|
424
|
-
// slither-disable-next-line calls-loop
|
|
425
430
|
try BUYBACK_HOOK.initializePoolFor({
|
|
426
431
|
projectId: revnetId,
|
|
427
432
|
fee: DEFAULT_BUYBACK_POOL_FEE,
|
|
@@ -449,19 +454,21 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
449
454
|
// IDs `block.timestamp`, `block.timestamp + 1`, ... exactly correspond to the JB-assigned ruleset IDs.
|
|
450
455
|
// The returned `ruleset.start` contains the derived start time (from `deriveStartFrom` using the stage's
|
|
451
456
|
// `mustStartAtOrAfter`), NOT the queue timestamp — so the timing guard correctly blocks early claims.
|
|
452
|
-
// slither-disable-next-line unused-return
|
|
453
457
|
(JBRuleset memory ruleset,) = CONTROLLER.getRulesetOf({projectId: revnetId, rulesetId: stageId});
|
|
454
458
|
|
|
455
459
|
// Make sure the stage has started.
|
|
460
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
456
461
|
if (ruleset.start > block.timestamp) {
|
|
457
|
-
revert REVDeployer_StageNotStarted(stageId);
|
|
462
|
+
revert REVDeployer_StageNotStarted({stageId: stageId});
|
|
458
463
|
}
|
|
459
464
|
|
|
460
465
|
// Get a reference to the number of tokens to auto-issue.
|
|
461
466
|
uint256 count = amountToAutoIssue[revnetId][stageId][beneficiary];
|
|
462
467
|
|
|
463
468
|
// If there's nothing to auto-mint, return.
|
|
464
|
-
if (count == 0)
|
|
469
|
+
if (count == 0) {
|
|
470
|
+
revert REVDeployer_NothingToAutoIssue({revnetId: revnetId, stageId: stageId, beneficiary: beneficiary});
|
|
471
|
+
}
|
|
465
472
|
|
|
466
473
|
// Reset the auto-mint amount.
|
|
467
474
|
amountToAutoIssue[revnetId][stageId][beneficiary] = 0;
|
|
@@ -471,7 +478,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
471
478
|
});
|
|
472
479
|
|
|
473
480
|
// Mint the tokens.
|
|
474
|
-
// slither-disable-next-line unused-return
|
|
475
481
|
CONTROLLER.mintTokensOf({
|
|
476
482
|
projectId: revnetId, tokenCount: count, beneficiary: beneficiary, memo: "", useReservedPercent: false
|
|
477
483
|
});
|
|
@@ -482,10 +488,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
482
488
|
/// @param revnetId The ID of the revnet to burn tokens for.
|
|
483
489
|
function burnHeldTokensOf(uint256 revnetId) external override {
|
|
484
490
|
uint256 balance = CONTROLLER.TOKENS().totalBalanceOf({holder: address(this), projectId: revnetId});
|
|
485
|
-
if (balance == 0) revert REVDeployer_NothingToBurn();
|
|
491
|
+
if (balance == 0) revert REVDeployer_NothingToBurn({revnetId: revnetId, holder: address(this)});
|
|
486
492
|
CONTROLLER.burnTokensOf({holder: address(this), projectId: revnetId, tokenCount: balance, memo: ""});
|
|
487
|
-
|
|
488
|
-
emit BurnHeldTokens(revnetId, balance, _msgSender());
|
|
493
|
+
emit BurnHeldTokens({revnetId: revnetId, count: balance, caller: _msgSender()});
|
|
489
494
|
}
|
|
490
495
|
|
|
491
496
|
/// @notice Launch a revnet, or initialize an existing Juicebox project as a revnet.
|
|
@@ -506,7 +511,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
506
511
|
/// @return revnetId The ID of the newly created revnet.
|
|
507
512
|
/// @return hook The address of the tiered ERC-721 hook deployed for the revnet.
|
|
508
513
|
// The deployment flow makes external setup calls, but any observed state is revnet-scoped and reverts atomically.
|
|
509
|
-
// slither-disable-next-line reentrancy-benign
|
|
510
514
|
function deployFor(
|
|
511
515
|
uint256 revnetId,
|
|
512
516
|
REVConfig calldata configuration,
|
|
@@ -541,7 +545,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
541
545
|
|
|
542
546
|
/// @inheritdoc IREVDeployer
|
|
543
547
|
// The deployment flow makes external setup calls, but any observed state is revnet-scoped and reverts atomically.
|
|
544
|
-
// slither-disable-next-line reentrancy-benign
|
|
545
548
|
function deployFor(
|
|
546
549
|
uint256 revnetId,
|
|
547
550
|
REVConfig calldata configuration,
|
|
@@ -610,14 +613,13 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
610
613
|
_checkIfIsSplitOperatorOf({revnetId: revnetId, operator: _msgSender()});
|
|
611
614
|
|
|
612
615
|
// Check if the current ruleset allows deploying new suckers.
|
|
613
|
-
// slither-disable-next-line unused-return
|
|
614
616
|
(, JBRulesetMetadata memory metadata) = CONTROLLER.currentRulesetOf(revnetId);
|
|
615
617
|
|
|
616
618
|
// Check the third bit, it indicates if the ruleset allows new suckers to be deployed.
|
|
617
619
|
bool allowsDeployingSuckers = ((metadata.metadata >> 2) & 1) == 1;
|
|
618
620
|
|
|
619
621
|
if (!allowsDeployingSuckers) {
|
|
620
|
-
revert REVDeployer_RulesetDoesNotAllowDeployingSuckers();
|
|
622
|
+
revert REVDeployer_RulesetDoesNotAllowDeployingSuckers({revnetId: revnetId});
|
|
621
623
|
}
|
|
622
624
|
|
|
623
625
|
// Deploy the suckers.
|
|
@@ -655,7 +657,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
655
657
|
|
|
656
658
|
/// @notice Deploy a revnet which sells tiered ERC-721s and (optionally) allows croptop posts to its ERC-721 tiers.
|
|
657
659
|
// The helper performs external hook/post setup after core revnet setup; any failure reverts the whole deployment.
|
|
658
|
-
// slither-disable-next-line reentrancy-benign
|
|
659
660
|
function _deploy721RevnetFor(
|
|
660
661
|
uint256 revnetId,
|
|
661
662
|
bool shouldDeployNewRevnet,
|
|
@@ -814,12 +815,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
814
815
|
if (!shouldDeployNewRevnet) {
|
|
815
816
|
// Initialize the existing Juicebox project as a revnet by transferring the `JBProjects` NFT to this
|
|
816
817
|
// deployer. This is irreversible.
|
|
817
|
-
// slither-disable-next-line reentrancy-benign
|
|
818
818
|
IERC721(PROJECTS).safeTransferFrom({from: owner, to: address(this), tokenId: revnetId});
|
|
819
819
|
}
|
|
820
820
|
|
|
821
821
|
// Launch the revnet rulesets for the reserved or pre-existing blank project.
|
|
822
|
-
// slither-disable-next-line unused-return
|
|
823
822
|
CONTROLLER.launchRulesetsFor({
|
|
824
823
|
projectId: revnetId,
|
|
825
824
|
projectUri: configuration.description.uri,
|
|
@@ -831,11 +830,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
831
830
|
// Store the cash out delay of the revnet if its stages are already in progress.
|
|
832
831
|
// This prevents cash out liquidity/arbitrage issues for existing revnets which
|
|
833
832
|
// are deploying to a new chain.
|
|
834
|
-
// slither-disable-next-line reentrancy-events
|
|
835
833
|
_setCashOutDelayIfNeeded({revnetId: revnetId, firstStageConfig: configuration.stageConfigurations[0]});
|
|
836
834
|
|
|
837
835
|
// Deploy the revnet's ERC-20 token.
|
|
838
|
-
// slither-disable-next-line unused-return
|
|
839
836
|
CONTROLLER.deployERC20For({
|
|
840
837
|
projectId: revnetId,
|
|
841
838
|
name: configuration.description.name,
|
|
@@ -847,10 +844,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
847
844
|
for (uint256 i; i < terminalConfigurations.length;) {
|
|
848
845
|
JBTerminalConfig calldata terminalConfiguration = terminalConfigurations[i];
|
|
849
846
|
for (uint256 j; j < terminalConfiguration.accountingContextsToAccept.length;) {
|
|
850
|
-
// slither-disable-next-line calls-loop
|
|
851
847
|
_tryInitializeBuybackPoolFor({
|
|
852
848
|
revnetId: revnetId,
|
|
853
849
|
terminalToken: terminalConfiguration.accountingContextsToAccept[j].token,
|
|
850
|
+
terminalTokenDecimals: terminalConfiguration.accountingContextsToAccept[j].decimals,
|
|
854
851
|
initialIssuance: configuration.stageConfigurations[0].initialIssuance
|
|
855
852
|
});
|
|
856
853
|
unchecked {
|
|
@@ -901,7 +898,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
901
898
|
|
|
902
899
|
// Include the caller so two revnets with identical configuration and user salt cannot collide. Same-address
|
|
903
900
|
// cross-chain deployment still works when the same operator calls this helper on each chain.
|
|
904
|
-
// slither-disable-next-line unused-return
|
|
905
901
|
suckers = SUCKER_REGISTRY.deploySuckersFor({
|
|
906
902
|
projectId: revnetId,
|
|
907
903
|
salt: keccak256(abi.encode(encodedConfigurationHash, suckerDeploymentConfiguration.salt, _msgSender())),
|
|
@@ -934,7 +930,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
934
930
|
returns (JBRulesetConfig[] memory rulesetConfigurations, bytes32 encodedConfigurationHash)
|
|
935
931
|
{
|
|
936
932
|
// If there are no stages, revert.
|
|
937
|
-
if (configuration.stageConfigurations.length == 0)
|
|
933
|
+
if (configuration.stageConfigurations.length == 0) {
|
|
934
|
+
revert REVDeployer_StagesRequired({stageCount: configuration.stageConfigurations.length});
|
|
935
|
+
}
|
|
938
936
|
|
|
939
937
|
// Initialize the array of rulesets.
|
|
940
938
|
rulesetConfigurations = new JBRulesetConfig[](configuration.stageConfigurations.length);
|
|
@@ -975,7 +973,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
975
973
|
// Make sure the revnet has at least one split if it has a split percent.
|
|
976
974
|
// Otherwise, the split would go to this contract since its the revnet's owner.
|
|
977
975
|
if (stageConfiguration.splitPercent > 0 && stageConfiguration.splits.length == 0) {
|
|
978
|
-
revert REVDeployer_MustHaveSplits();
|
|
976
|
+
revert REVDeployer_MustHaveSplits({stageIndex: i, splitPercent: stageConfiguration.splitPercent});
|
|
979
977
|
}
|
|
980
978
|
|
|
981
979
|
// Compute the effective start time for this stage.
|
|
@@ -985,7 +983,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
985
983
|
|
|
986
984
|
// If the stage's start time is not after the previous stage's start time, revert.
|
|
987
985
|
if (i > 0 && effectiveStart <= previousStageStart) {
|
|
988
|
-
revert REVDeployer_StageTimesMustIncrease(
|
|
986
|
+
revert REVDeployer_StageTimesMustIncrease({
|
|
987
|
+
stageIndex: i, previousStageStart: previousStageStart, effectiveStart: effectiveStart
|
|
988
|
+
});
|
|
989
989
|
}
|
|
990
990
|
|
|
991
991
|
// Store for the next iteration's ordering check.
|
|
@@ -993,9 +993,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
993
993
|
|
|
994
994
|
// Make sure the revnet doesn't prevent cashouts all together.
|
|
995
995
|
if (stageConfiguration.cashOutTaxRate >= JBConstants.MAX_CASH_OUT_TAX_RATE) {
|
|
996
|
-
revert REVDeployer_CashOutsCantBeTurnedOffCompletely(
|
|
997
|
-
stageConfiguration.cashOutTaxRate,
|
|
998
|
-
|
|
996
|
+
revert REVDeployer_CashOutsCantBeTurnedOffCompletely({
|
|
997
|
+
cashOutTaxRate: stageConfiguration.cashOutTaxRate,
|
|
998
|
+
maxCashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE
|
|
999
|
+
});
|
|
999
1000
|
}
|
|
1000
1001
|
|
|
1001
1002
|
// Set up the ruleset.
|
|
@@ -1027,7 +1028,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1027
1028
|
REVAutoIssuance calldata autoIssuance = stageConfiguration.autoIssuances[j];
|
|
1028
1029
|
|
|
1029
1030
|
// Make sure the beneficiary is not the zero address.
|
|
1030
|
-
if (autoIssuance.beneficiary == address(0))
|
|
1031
|
+
if (autoIssuance.beneficiary == address(0)) {
|
|
1032
|
+
revert REVDeployer_AutoIssuanceBeneficiaryZeroAddress({stageIndex: i, autoIssuanceIndex: j});
|
|
1033
|
+
}
|
|
1031
1034
|
|
|
1032
1035
|
// If there's nothing to auto-mint, continue.
|
|
1033
1036
|
if (autoIssuance.count == 0) continue;
|
|
@@ -1039,7 +1042,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1039
1042
|
// If the issuance config is for another chain, skip it.
|
|
1040
1043
|
if (autoIssuance.chainId != block.chainid) continue;
|
|
1041
1044
|
|
|
1042
|
-
// slither-disable-next-line reentrancy-events
|
|
1043
1045
|
emit StoreAutoIssuanceAmount({
|
|
1044
1046
|
revnetId: revnetId,
|
|
1045
1047
|
stageId: block.timestamp + i,
|
|
@@ -1054,7 +1056,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1054
1056
|
// (JBRulesets.sol L172), producing the same sequential IDs when all stages are queued in one tx.
|
|
1055
1057
|
// `autoIssueFor` later calls `getRulesetOf(revnetId, stageId)` — the returned `ruleset.start`
|
|
1056
1058
|
// is the derived start time (not the queue time), so the timing guard works correctly.
|
|
1057
|
-
// slither-disable-next-line reentrancy-benign
|
|
1058
1059
|
amountToAutoIssue[revnetId][block.timestamp + i][autoIssuance.beneficiary] += autoIssuance.count;
|
|
1059
1060
|
}
|
|
1060
1061
|
unchecked {
|
|
@@ -1073,13 +1074,13 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1073
1074
|
function _setCashOutDelayIfNeeded(uint256 revnetId, REVStageConfig calldata firstStageConfig) internal {
|
|
1074
1075
|
// If this is the first revnet being deployed (with a `startsAtOrAfter` of 0),
|
|
1075
1076
|
// or if the first stage hasn't started yet, we don't need to set a cash out delay.
|
|
1077
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
1076
1078
|
if (firstStageConfig.startsAtOrAfter == 0 || firstStageConfig.startsAtOrAfter >= block.timestamp) return;
|
|
1077
1079
|
|
|
1078
1080
|
// Calculate the timestamp at which the cash out delay ends.
|
|
1079
1081
|
uint256 cashOutDelay = block.timestamp + CASH_OUT_DELAY;
|
|
1080
1082
|
|
|
1081
1083
|
// Store the cash out delay in the owner contract.
|
|
1082
|
-
// slither-disable-next-line reentrancy-events
|
|
1083
1084
|
REVOwner(OWNER).setCashOutDelayOf({revnetId: revnetId, cashOutDelay: cashOutDelay});
|
|
1084
1085
|
|
|
1085
1086
|
emit SetCashOutDelay({revnetId: revnetId, cashOutDelay: cashOutDelay, caller: _msgSender()});
|
package/src/REVHiddenTokens.sol
CHANGED
|
@@ -85,7 +85,7 @@ contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
|
|
|
85
85
|
_hasPermissionFrom(caller, PROJECTS.ownerOf(revnetId), revnetId, JBPermissionIds.HIDE_TOKENS);
|
|
86
86
|
|
|
87
87
|
if (!isAllowlistedHolder && !isPermissionedOperator) {
|
|
88
|
-
revert REVHiddenTokens_Unauthorized(revnetId, caller);
|
|
88
|
+
revert REVHiddenTokens_Unauthorized({revnetId: revnetId, caller: caller});
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Increment the holder's hidden balance.
|
|
@@ -95,7 +95,6 @@ contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
|
|
|
95
95
|
totalHiddenOf[revnetId] += tokenCount;
|
|
96
96
|
|
|
97
97
|
// Burn the tokens from the holder. The holder must have granted BURN_TOKENS permission.
|
|
98
|
-
// slither-disable-next-line reentrancy-events
|
|
99
98
|
CONTROLLER.burnTokensOf({holder: holder, projectId: revnetId, tokenCount: tokenCount, memo: ""});
|
|
100
99
|
|
|
101
100
|
emit HideTokens({revnetId: revnetId, tokenCount: tokenCount, holder: holder, caller: _msgSender()});
|
|
@@ -125,7 +124,6 @@ contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
|
|
|
125
124
|
totalHiddenOf[revnetId] -= tokenCount;
|
|
126
125
|
|
|
127
126
|
// Mint the tokens to the beneficiary without applying the reserved percent.
|
|
128
|
-
// slither-disable-next-line unused-return,reentrancy-events
|
|
129
127
|
CONTROLLER.mintTokensOf({
|
|
130
128
|
projectId: revnetId, tokenCount: tokenCount, beneficiary: holder, memo: "", useReservedPercent: false
|
|
131
129
|
});
|
package/src/REVLoans.sol
CHANGED
|
@@ -61,19 +61,21 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
61
61
|
error REVLoans_InvalidPrepaidFeePercent(uint256 prepaidFeePercent, uint256 min, uint256 max);
|
|
62
62
|
error REVLoans_InvalidTerminal(address terminal, uint256 revnetId);
|
|
63
63
|
error REVLoans_LoanExpired(uint256 timeSinceLoanCreated, uint256 loanLiquidationDuration);
|
|
64
|
-
error REVLoans_LoanIdOverflow();
|
|
64
|
+
error REVLoans_LoanIdOverflow(uint256 revnetId, uint256 loanNumber, uint256 maxLoanNumber);
|
|
65
65
|
error REVLoans_NewBorrowAmountGreaterThanLoanAmount(uint256 newBorrowAmount, uint256 loanAmount);
|
|
66
|
-
error REVLoans_NoMsgValueAllowed();
|
|
67
|
-
error REVLoans_NotEnoughCollateral();
|
|
68
|
-
error REVLoans_NothingToRepay();
|
|
66
|
+
error REVLoans_NoMsgValueAllowed(uint256 msgValue, address token);
|
|
67
|
+
error REVLoans_NotEnoughCollateral(uint256 collateralCountToRemove, uint256 loanCollateral);
|
|
68
|
+
error REVLoans_NothingToRepay(uint256 repayBorrowAmount, uint256 collateralCountToReturn);
|
|
69
69
|
error REVLoans_OverMaxRepayBorrowAmount(uint256 maxRepayBorrowAmount, uint256 repayBorrowAmount);
|
|
70
70
|
error REVLoans_OverflowAlert(uint256 value, uint256 limit);
|
|
71
71
|
error REVLoans_PermitAllowanceNotEnough(uint256 allowanceAmount, uint256 requiredAmount);
|
|
72
72
|
error REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(uint256 newBorrowAmount, uint256 loanAmount);
|
|
73
|
-
error REVLoans_SourceMismatch(
|
|
73
|
+
error REVLoans_SourceMismatch(
|
|
74
|
+
address expectedToken, address actualToken, address expectedTerminal, address actualTerminal
|
|
75
|
+
);
|
|
74
76
|
error REVLoans_UnderMinBorrowAmount(uint256 minBorrowAmount, uint256 borrowAmount);
|
|
75
|
-
error REVLoans_ZeroBorrowAmount();
|
|
76
|
-
error REVLoans_ZeroCollateralLoanIsInvalid();
|
|
77
|
+
error REVLoans_ZeroBorrowAmount(uint256 revnetId, uint256 collateralCount);
|
|
78
|
+
error REVLoans_ZeroCollateralLoanIsInvalid(uint256 collateralCount);
|
|
77
79
|
|
|
78
80
|
//*********************************************************************//
|
|
79
81
|
// ------------------------- public constants ------------------------ //
|
|
@@ -229,6 +231,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
229
231
|
JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
|
|
230
232
|
|
|
231
233
|
// If the cash out delay hasn't passed yet, no amount is borrowable.
|
|
234
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
232
235
|
if (_cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset}) > block.timestamp) return 0;
|
|
233
236
|
|
|
234
237
|
return _borrowableAmountFrom({
|
|
@@ -443,7 +446,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
443
446
|
/// @param revnetId The ID of the revnet.
|
|
444
447
|
/// @return currentRuleset The current ruleset.
|
|
445
448
|
function _currentRulesetOf(uint256 revnetId) internal view returns (JBRuleset memory currentRuleset) {
|
|
446
|
-
// slither-disable-next-line unused-return
|
|
447
449
|
(currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
448
450
|
}
|
|
449
451
|
|
|
@@ -462,7 +464,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
462
464
|
// Uses `>` (not `>=`) so the exact boundary second is still repayable — the liquidation path
|
|
463
465
|
// uses `<=`, and matching `>=` here would create a 1-second window where neither path is available.
|
|
464
466
|
if (timeSinceLoanCreated > LOAN_LIQUIDATION_DURATION) {
|
|
465
|
-
revert REVLoans_LoanExpired(
|
|
467
|
+
revert REVLoans_LoanExpired({
|
|
468
|
+
timeSinceLoanCreated: timeSinceLoanCreated, loanLiquidationDuration: LOAN_LIQUIDATION_DURATION
|
|
469
|
+
});
|
|
466
470
|
}
|
|
467
471
|
|
|
468
472
|
// Get a reference to the amount prepaid for the full loan.
|
|
@@ -548,7 +552,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
548
552
|
if (tokensLoaned == 0) continue;
|
|
549
553
|
|
|
550
554
|
// Get a reference to the accounting context for the source.
|
|
551
|
-
// slither-disable-next-line calls-loop
|
|
552
555
|
JBAccountingContext memory accountingContext =
|
|
553
556
|
source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
|
|
554
557
|
|
|
@@ -567,7 +570,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
567
570
|
borrowedAmount += normalizedTokens;
|
|
568
571
|
} else {
|
|
569
572
|
// Otherwise, convert via the price feed.
|
|
570
|
-
// slither-disable-next-line calls-loop
|
|
571
573
|
uint256 pricePerUnit = PRICES.pricePerUnitOf({
|
|
572
574
|
projectId: revnetId,
|
|
573
575
|
pricingCurrency: accountingContext.currency,
|
|
@@ -649,7 +651,12 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
649
651
|
/// @param count The number of loans to iterate over.
|
|
650
652
|
function liquidateExpiredLoansFrom(uint256 revnetId, uint256 startingLoanId, uint256 count) external override {
|
|
651
653
|
// Prevent cross-revnet accounting corruption: loan numbers must stay within the revnet's ID namespace.
|
|
652
|
-
|
|
654
|
+
uint256 endLoanNumber = startingLoanId + count;
|
|
655
|
+
if (endLoanNumber > _ONE_TRILLION) {
|
|
656
|
+
revert REVLoans_LoanIdOverflow({
|
|
657
|
+
revnetId: revnetId, loanNumber: endLoanNumber, maxLoanNumber: _ONE_TRILLION
|
|
658
|
+
});
|
|
659
|
+
}
|
|
653
660
|
|
|
654
661
|
// Cache the sender to avoid repeated ERC2771 context reads inside the loop.
|
|
655
662
|
address sender = _msgSender();
|
|
@@ -660,7 +667,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
660
667
|
uint256 loanId = _generateLoanId({revnetId: revnetId, loanNumber: startingLoanId + i});
|
|
661
668
|
|
|
662
669
|
// Check createdAt via storage ref first to avoid loading the full struct for empty slots.
|
|
663
|
-
// slither-disable-next-line incorrect-equality
|
|
664
670
|
if (_loanOf[loanId].createdAt == 0) continue;
|
|
665
671
|
|
|
666
672
|
// Get a reference to the loan being iterated on.
|
|
@@ -670,6 +676,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
670
676
|
address owner = _ownerOf(loanId);
|
|
671
677
|
|
|
672
678
|
// If the loan is already burned, or if it hasn't passed its liquidation duration, continue.
|
|
679
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
673
680
|
if (owner == address(0) || (block.timestamp <= loan.createdAt + LOAN_LIQUIDATION_DURATION)) continue;
|
|
674
681
|
|
|
675
682
|
// Burn the loan.
|
|
@@ -735,15 +742,24 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
735
742
|
_requirePermissionFrom({account: loanOwner, projectId: revnetId, permissionId: JBPermissionIds.REALLOCATE_LOAN});
|
|
736
743
|
|
|
737
744
|
// Make sure the loan hasn't expired.
|
|
745
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
738
746
|
if (block.timestamp - _loanOf[loanId].createdAt > LOAN_LIQUIDATION_DURATION) {
|
|
739
|
-
revert REVLoans_LoanExpired(
|
|
747
|
+
revert REVLoans_LoanExpired({
|
|
748
|
+
timeSinceLoanCreated: block.timestamp - _loanOf[loanId].createdAt,
|
|
749
|
+
loanLiquidationDuration: LOAN_LIQUIDATION_DURATION
|
|
750
|
+
});
|
|
740
751
|
}
|
|
741
752
|
|
|
742
753
|
// Make sure the new loan's source matches the existing loan's source to prevent cross-source value extraction.
|
|
743
754
|
{
|
|
744
755
|
REVLoanSource storage existingSource = _loanOf[loanId].source;
|
|
745
756
|
if (source.token != existingSource.token || source.terminal != existingSource.terminal) {
|
|
746
|
-
revert REVLoans_SourceMismatch(
|
|
757
|
+
revert REVLoans_SourceMismatch({
|
|
758
|
+
expectedToken: existingSource.token,
|
|
759
|
+
actualToken: source.token,
|
|
760
|
+
expectedTerminal: address(existingSource.terminal),
|
|
761
|
+
actualTerminal: address(source.terminal)
|
|
762
|
+
});
|
|
747
763
|
}
|
|
748
764
|
}
|
|
749
765
|
|
|
@@ -806,7 +822,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
806
822
|
REVLoan storage loan = _loanOf[loanId];
|
|
807
823
|
|
|
808
824
|
if (collateralCountToReturn > loan.collateral) {
|
|
809
|
-
revert REVLoans_CollateralExceedsLoan(
|
|
825
|
+
revert REVLoans_CollateralExceedsLoan({
|
|
826
|
+
collateralToReturn: collateralCountToReturn, loanCollateral: loan.collateral
|
|
827
|
+
});
|
|
810
828
|
}
|
|
811
829
|
|
|
812
830
|
// Get a reference to the revnet ID of the loan being repaid.
|
|
@@ -833,7 +851,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
833
851
|
|
|
834
852
|
// Make sure the new borrow amount is less than the loan's amount.
|
|
835
853
|
if (newBorrowAmount > loan.amount) {
|
|
836
|
-
revert REVLoans_NewBorrowAmountGreaterThanLoanAmount(
|
|
854
|
+
revert REVLoans_NewBorrowAmountGreaterThanLoanAmount({
|
|
855
|
+
newBorrowAmount: newBorrowAmount, loanAmount: loan.amount
|
|
856
|
+
});
|
|
837
857
|
}
|
|
838
858
|
|
|
839
859
|
// Get the amount of the loan being repaid.
|
|
@@ -843,7 +863,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
843
863
|
// Revert if this repayment would do nothing — no borrow amount repaid and no collateral returned.
|
|
844
864
|
// Without this check, a zero-amount repayment would burn the old loan NFT and mint a new one,
|
|
845
865
|
// incrementing totalLoansBorrowedFor without limit.
|
|
846
|
-
if (repayBorrowAmount == 0 && collateralCountToReturn == 0)
|
|
866
|
+
if (repayBorrowAmount == 0 && collateralCountToReturn == 0) {
|
|
867
|
+
revert REVLoans_NothingToRepay({
|
|
868
|
+
repayBorrowAmount: repayBorrowAmount, collateralCountToReturn: collateralCountToReturn
|
|
869
|
+
});
|
|
870
|
+
}
|
|
847
871
|
|
|
848
872
|
// Keep a reference to the fee that'll be taken.
|
|
849
873
|
uint256 sourceFeeAmount = _determineSourceFeeAmount({loan: loan, amount: repayBorrowAmount});
|
|
@@ -857,7 +881,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
857
881
|
|
|
858
882
|
// Make sure the minimum borrow amount is met.
|
|
859
883
|
if (repayBorrowAmount > maxRepayBorrowAmount) {
|
|
860
|
-
revert REVLoans_OverMaxRepayBorrowAmount(
|
|
884
|
+
revert REVLoans_OverMaxRepayBorrowAmount({
|
|
885
|
+
maxRepayBorrowAmount: maxRepayBorrowAmount, repayBorrowAmount: repayBorrowAmount
|
|
886
|
+
});
|
|
861
887
|
}
|
|
862
888
|
|
|
863
889
|
// Cache the source token before _repayLoan deletes the loan storage.
|
|
@@ -915,13 +941,13 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
915
941
|
if (token == JBConstants.NATIVE_TOKEN) return msg.value;
|
|
916
942
|
|
|
917
943
|
// If the token is not native, revert if there is a non-zero `msg.value`.
|
|
918
|
-
if (msg.value != 0) revert REVLoans_NoMsgValueAllowed();
|
|
944
|
+
if (msg.value != 0) revert REVLoans_NoMsgValueAllowed({msgValue: msg.value, token: token});
|
|
919
945
|
|
|
920
946
|
// Check if the metadata contains permit data.
|
|
921
947
|
if (allowance.amount != 0) {
|
|
922
948
|
// Make sure the permit allowance is enough for this payment. If not we revert early.
|
|
923
949
|
if (allowance.amount < amount) {
|
|
924
|
-
revert REVLoans_PermitAllowanceNotEnough(allowance.amount, amount);
|
|
950
|
+
revert REVLoans_PermitAllowanceNotEnough({allowanceAmount: allowance.amount, requiredAmount: amount});
|
|
925
951
|
}
|
|
926
952
|
|
|
927
953
|
// Keep a reference to the permit rules.
|
|
@@ -995,7 +1021,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
995
1021
|
loan.source.terminal.accountingContextForTokenOf({projectId: revnetId, token: loan.source.token});
|
|
996
1022
|
|
|
997
1023
|
// Pull the amount to be loaned out of the revnet. This will incure the protocol fee.
|
|
998
|
-
// slither-disable-next-line unused-return
|
|
999
1024
|
netAmountPaidOut = loan.source.terminal
|
|
1000
1025
|
.useAllowanceOf({
|
|
1001
1026
|
projectId: revnetId,
|
|
@@ -1083,7 +1108,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1083
1108
|
// Any reentrant call will see the updated loan values, reverting on overflow.
|
|
1084
1109
|
if (newBorrowAmount > type(uint112).max) revert REVLoans_OverflowAlert(newBorrowAmount, type(uint112).max);
|
|
1085
1110
|
if (newCollateralCount > type(uint112).max) {
|
|
1086
|
-
revert REVLoans_OverflowAlert(newCollateralCount, type(uint112).max);
|
|
1111
|
+
revert REVLoans_OverflowAlert({value: newCollateralCount, limit: type(uint112).max});
|
|
1087
1112
|
}
|
|
1088
1113
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1089
1114
|
loan.amount = uint112(newBorrowAmount);
|
|
@@ -1177,20 +1202,20 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1177
1202
|
returns (uint256 loanId, REVLoan memory)
|
|
1178
1203
|
{
|
|
1179
1204
|
// A loan needs to have collateral.
|
|
1180
|
-
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
|
|
1205
|
+
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid({collateralCount: collateralCount});
|
|
1181
1206
|
|
|
1182
1207
|
// Make sure the source terminal is registered in the directory for this revnet.
|
|
1183
1208
|
if (!DIRECTORY.isTerminalOf({projectId: revnetId, terminal: IJBTerminal(address(source.terminal))})) {
|
|
1184
|
-
revert REVLoans_InvalidTerminal(address(source.terminal), revnetId);
|
|
1209
|
+
revert REVLoans_InvalidTerminal({terminal: address(source.terminal), revnetId: revnetId});
|
|
1185
1210
|
}
|
|
1186
1211
|
|
|
1187
1212
|
// Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
|
|
1188
1213
|
// an 16 year loan can be paid upfront with a
|
|
1189
1214
|
// payment of 50% of the borrowed assets, the cheapest possible rate.
|
|
1190
1215
|
if (prepaidFeePercent < MIN_PREPAID_FEE_PERCENT || prepaidFeePercent > MAX_PREPAID_FEE_PERCENT) {
|
|
1191
|
-
revert REVLoans_InvalidPrepaidFeePercent(
|
|
1192
|
-
prepaidFeePercent, MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT
|
|
1193
|
-
);
|
|
1216
|
+
revert REVLoans_InvalidPrepaidFeePercent({
|
|
1217
|
+
prepaidFeePercent: prepaidFeePercent, min: MIN_PREPAID_FEE_PERCENT, max: MAX_PREPAID_FEE_PERCENT
|
|
1218
|
+
});
|
|
1194
1219
|
}
|
|
1195
1220
|
|
|
1196
1221
|
// Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
|
|
@@ -1199,16 +1224,14 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1199
1224
|
// Enforce the cash out delay.
|
|
1200
1225
|
{
|
|
1201
1226
|
uint256 cashOutDelay = _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
|
|
1227
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
1202
1228
|
if (cashOutDelay > block.timestamp) {
|
|
1203
|
-
revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
1229
|
+
revert REVLoans_CashOutDelayNotFinished({cashOutDelay: cashOutDelay, blockTimestamp: block.timestamp});
|
|
1204
1230
|
}
|
|
1205
1231
|
}
|
|
1206
1232
|
|
|
1207
|
-
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1208
|
-
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1209
|
-
|
|
1210
1233
|
// Get a reference to the loan ID.
|
|
1211
|
-
loanId =
|
|
1234
|
+
loanId = _nextLoanIdFor(revnetId);
|
|
1212
1235
|
|
|
1213
1236
|
// Get a reference to the loan being created.
|
|
1214
1237
|
REVLoan storage loan = _loanOf[loanId];
|
|
@@ -1227,7 +1250,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1227
1250
|
});
|
|
1228
1251
|
|
|
1229
1252
|
// Revert if the bonding curve returns zero to prevent creating zero-amount loans.
|
|
1230
|
-
if (borrowAmount == 0)
|
|
1253
|
+
if (borrowAmount == 0) {
|
|
1254
|
+
revert REVLoans_ZeroBorrowAmount({revnetId: revnetId, collateralCount: collateralCount});
|
|
1255
|
+
}
|
|
1231
1256
|
|
|
1232
1257
|
// Make sure the minimum borrow amount is met.
|
|
1233
1258
|
if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
|
|
@@ -1264,6 +1289,18 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1264
1289
|
return (loanId, loan);
|
|
1265
1290
|
}
|
|
1266
1291
|
|
|
1292
|
+
/// @notice Allocate the next loan ID for a revnet.
|
|
1293
|
+
/// @param revnetId The ID of the revnet.
|
|
1294
|
+
/// @return loanId The allocated loan ID.
|
|
1295
|
+
function _nextLoanIdFor(uint256 revnetId) internal returns (uint256 loanId) {
|
|
1296
|
+
uint256 loanNumber = totalLoansBorrowedFor[revnetId] + 1;
|
|
1297
|
+
if (loanNumber > _ONE_TRILLION) {
|
|
1298
|
+
revert REVLoans_LoanIdOverflow({revnetId: revnetId, loanNumber: loanNumber, maxLoanNumber: _ONE_TRILLION});
|
|
1299
|
+
}
|
|
1300
|
+
totalLoansBorrowedFor[revnetId] = loanNumber;
|
|
1301
|
+
return _generateLoanId({revnetId: revnetId, loanNumber: loanNumber});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1267
1304
|
/// @notice Reallocate collateral from a loan by making a new loan based on the original, with reduced collateral.
|
|
1268
1305
|
/// @param loanId The ID of the loan to reallocate collateral from.
|
|
1269
1306
|
/// @param revnetId The ID of the revnet the loan is from.
|
|
@@ -1286,7 +1323,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1286
1323
|
REVLoan storage loan = _loanOf[loanId];
|
|
1287
1324
|
|
|
1288
1325
|
// Make sure there is enough collateral to transfer.
|
|
1289
|
-
if (collateralCountToRemove > loan.collateral)
|
|
1326
|
+
if (collateralCountToRemove > loan.collateral) {
|
|
1327
|
+
revert REVLoans_NotEnoughCollateral({
|
|
1328
|
+
collateralCountToRemove: collateralCountToRemove, loanCollateral: loan.collateral
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1290
1331
|
|
|
1291
1332
|
// Keep a reference to the new collateral amount.
|
|
1292
1333
|
uint256 newCollateralCount = loan.collateral - collateralCountToRemove;
|
|
@@ -1301,14 +1342,13 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1301
1342
|
|
|
1302
1343
|
// Make sure the borrow amount is not less than the original loan's amount.
|
|
1303
1344
|
if (borrowAmount < loan.amount) {
|
|
1304
|
-
revert REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(
|
|
1345
|
+
revert REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows({
|
|
1346
|
+
newBorrowAmount: borrowAmount, loanAmount: loan.amount
|
|
1347
|
+
});
|
|
1305
1348
|
}
|
|
1306
1349
|
|
|
1307
|
-
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1308
|
-
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1309
|
-
|
|
1310
1350
|
// Get a reference to the replacement loan ID.
|
|
1311
|
-
reallocatedLoanId =
|
|
1351
|
+
reallocatedLoanId = _nextLoanIdFor(revnetId);
|
|
1312
1352
|
|
|
1313
1353
|
// Get a reference to the loan being created.
|
|
1314
1354
|
reallocatedLoan = _loanOf[reallocatedLoanId];
|
|
@@ -1364,7 +1404,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1364
1404
|
});
|
|
1365
1405
|
|
|
1366
1406
|
// Add the loaned amount back to the revnet.
|
|
1367
|
-
// slither-disable-next-line arbitrary-send-eth
|
|
1368
1407
|
loan.source.terminal.addToBalanceOf{value: payValue}({
|
|
1369
1408
|
projectId: revnetId,
|
|
1370
1409
|
token: loan.source.token,
|
|
@@ -1385,7 +1424,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1385
1424
|
/// @param collateralCountToReturn The amount of collateral to return that the loan no longer requires.
|
|
1386
1425
|
/// @param beneficiary The address to receive the returned collateral and any tokens resulting from paying fees.
|
|
1387
1426
|
/// @param loanOwner The owner of the loan NFT (receives replacement loan if partial repay).
|
|
1388
|
-
// slither-disable-next-line reentrancy-eth,reentrancy-events
|
|
1389
1427
|
function _repayLoan(
|
|
1390
1428
|
uint256 loanId,
|
|
1391
1429
|
REVLoan storage loan,
|
|
@@ -1403,7 +1441,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1403
1441
|
_burn(loanId);
|
|
1404
1442
|
|
|
1405
1443
|
// If the loan will carry no more amount or collateral, store its changes directly.
|
|
1406
|
-
// slither-disable-next-line incorrect-equality
|
|
1407
1444
|
if (collateralCountToReturn == loan.collateral) {
|
|
1408
1445
|
// Snapshot the loan to memory BEFORE _adjust zeroes the storage pointer.
|
|
1409
1446
|
REVLoan memory loanSnapshot = loan;
|
|
@@ -1440,12 +1477,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1440
1477
|
|
|
1441
1478
|
return (loanId, paidOffSnapshot);
|
|
1442
1479
|
} else {
|
|
1443
|
-
// Make a new loan with the remaining amount and collateral.
|
|
1444
|
-
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1445
|
-
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1446
|
-
|
|
1447
1480
|
// Get a reference to the replacement loan ID.
|
|
1448
|
-
uint256 paidOffLoanId =
|
|
1481
|
+
uint256 paidOffLoanId = _nextLoanIdFor(revnetId);
|
|
1449
1482
|
|
|
1450
1483
|
// Get a reference to the loan being paid off.
|
|
1451
1484
|
REVLoan storage paidOffLoan = _loanOf[paidOffLoanId];
|
|
@@ -1502,7 +1535,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1502
1535
|
totalCollateralOf[revnetId] -= collateralCount;
|
|
1503
1536
|
|
|
1504
1537
|
// Mint the collateral tokens back to the loan payer.
|
|
1505
|
-
// slither-disable-next-line unused-return,calls-loop
|
|
1506
1538
|
CONTROLLER.mintTokensOf({
|
|
1507
1539
|
projectId: revnetId,
|
|
1508
1540
|
tokenCount: collateralCount,
|
|
@@ -1561,7 +1593,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1561
1593
|
{
|
|
1562
1594
|
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1563
1595
|
|
|
1564
|
-
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
1565
1596
|
try terminal.pay{value: payValue}({
|
|
1566
1597
|
projectId: projectId,
|
|
1567
1598
|
token: token,
|
|
@@ -1580,6 +1611,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1580
1611
|
}
|
|
1581
1612
|
}
|
|
1582
1613
|
|
|
1614
|
+
/// @notice Accepts calldata sent with native tokens so repayment helpers can refund or settle value.
|
|
1583
1615
|
fallback() external payable {}
|
|
1616
|
+
|
|
1617
|
+
/// @notice Accepts native tokens sent directly to the loan contract.
|
|
1584
1618
|
receive() external payable {}
|
|
1585
1619
|
}
|
package/src/REVOwner.sol
CHANGED
|
@@ -43,9 +43,9 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
43
43
|
// --------------------------- custom errors ------------------------- //
|
|
44
44
|
//*********************************************************************//
|
|
45
45
|
|
|
46
|
-
error REVOwner_AlreadyInitialized();
|
|
46
|
+
error REVOwner_AlreadyInitialized(address deployer);
|
|
47
47
|
error REVOwner_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
|
|
48
|
-
error REVOwner_Unauthorized();
|
|
48
|
+
error REVOwner_Unauthorized(address caller, address expectedCaller);
|
|
49
49
|
|
|
50
50
|
//*********************************************************************//
|
|
51
51
|
// ------------------------- public constants ------------------------ //
|
|
@@ -89,7 +89,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
89
89
|
|
|
90
90
|
/// @notice Each revnet's tiered ERC-721 hook.
|
|
91
91
|
/// @custom:param revnetId The ID of the revnet to look up.
|
|
92
|
-
// slither-disable-next-line uninitialized-state
|
|
93
92
|
mapping(uint256 revnetId => IJB721TiersHook tiered721Hook) public tiered721HookOf;
|
|
94
93
|
|
|
95
94
|
/// @notice The deployer that manages revnet state.
|
|
@@ -183,8 +182,9 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
183
182
|
uint256 cashOutDelay = cashOutDelayOf[context.projectId];
|
|
184
183
|
|
|
185
184
|
// Enforce the cash out delay.
|
|
185
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
186
186
|
if (cashOutDelay > block.timestamp) {
|
|
187
|
-
revert REVOwner_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
187
|
+
revert REVOwner_CashOutDelayNotFinished({cashOutDelay: cashOutDelay, blockTimestamp: block.timestamp});
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
// Get the terminal that will receive the cash out fee.
|
|
@@ -209,7 +209,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
209
209
|
JBBeforeCashOutRecordedContext memory routedContext = context;
|
|
210
210
|
routedContext.totalSupply = totalSupply;
|
|
211
211
|
routedContext.surplus.value = effectiveSurplusValue;
|
|
212
|
-
// slither-disable-next-line unused-return
|
|
213
212
|
(cashOutTaxRate, cashOutCount,,, hookSpecifications) = BUYBACK_HOOK.beforeCashOutRecordedWith(routedContext);
|
|
214
213
|
return (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
|
|
215
214
|
}
|
|
@@ -261,7 +260,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
261
260
|
|
|
262
261
|
// Let the buyback hook adjust the cash out parameters and optionally return a hook specification.
|
|
263
262
|
JBCashOutHookSpecification[] memory buybackHookSpecifications;
|
|
264
|
-
// slither-disable-next-line unused-return
|
|
265
263
|
(cashOutTaxRate, cashOutCount,,, buybackHookSpecifications) =
|
|
266
264
|
BUYBACK_HOOK.beforeCashOutRecordedWith(buybackHookContext);
|
|
267
265
|
|
|
@@ -316,7 +314,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
316
314
|
bool usesTiered721Hook = address(tiered721Hook) != address(0);
|
|
317
315
|
if (usesTiered721Hook) {
|
|
318
316
|
JBPayHookSpecification[] memory specs;
|
|
319
|
-
// slither-disable-next-line unused-return
|
|
320
317
|
(, specs) = IJBRulesetDataHook(address(tiered721Hook)).beforePayRecordedWith(context);
|
|
321
318
|
// The 721 hook returns a single spec (itself) whose amount is the total split amount.
|
|
322
319
|
if (specs.length > 0) {
|
|
@@ -429,7 +426,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
429
426
|
});
|
|
430
427
|
|
|
431
428
|
// Pay the fee.
|
|
432
|
-
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
433
429
|
try feeTerminal.pay{value: payValue}({
|
|
434
430
|
projectId: FEE_REVNET_ID,
|
|
435
431
|
token: context.forwardedAmount.token,
|
|
@@ -454,7 +450,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
454
450
|
to: msg.sender, token: context.forwardedAmount.token, amount: context.forwardedAmount.value
|
|
455
451
|
});
|
|
456
452
|
|
|
457
|
-
// slither-disable-next-line arbitrary-send-eth
|
|
458
453
|
IJBTerminal(msg.sender).addToBalanceOf{value: payValue}({
|
|
459
454
|
projectId: context.projectId,
|
|
460
455
|
token: context.forwardedAmount.token,
|
|
@@ -474,9 +469,11 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
474
469
|
/// @param deployer The canonical REVDeployer instance that will manage revnet runtime state.
|
|
475
470
|
function setDeployer(IREVDeployer deployer) external {
|
|
476
471
|
// Only the account that deployed this REVOwner may complete the one-time deployer binding.
|
|
477
|
-
if (msg.sender != _DEPLOYER_BINDER)
|
|
472
|
+
if (msg.sender != _DEPLOYER_BINDER) {
|
|
473
|
+
revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: _DEPLOYER_BINDER});
|
|
474
|
+
}
|
|
478
475
|
// Prevent the deployer binding from being overwritten after initialization.
|
|
479
|
-
if (address(DEPLOYER) != address(0)) revert REVOwner_AlreadyInitialized();
|
|
476
|
+
if (address(DEPLOYER) != address(0)) revert REVOwner_AlreadyInitialized({deployer: address(DEPLOYER)});
|
|
480
477
|
// Store the canonical REVDeployer that is authorized to manage runtime hook state.
|
|
481
478
|
DEPLOYER = deployer;
|
|
482
479
|
}
|
|
@@ -486,7 +483,9 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
486
483
|
/// @param revnetId The ID of the revnet.
|
|
487
484
|
/// @param cashOutDelay The timestamp after which cash outs are allowed.
|
|
488
485
|
function setCashOutDelayOf(uint256 revnetId, uint256 cashOutDelay) external {
|
|
489
|
-
if (msg.sender != address(DEPLOYER))
|
|
486
|
+
if (msg.sender != address(DEPLOYER)) {
|
|
487
|
+
revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(DEPLOYER)});
|
|
488
|
+
}
|
|
490
489
|
cashOutDelayOf[revnetId] = cashOutDelay;
|
|
491
490
|
}
|
|
492
491
|
|
|
@@ -495,7 +494,9 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
495
494
|
/// @param revnetId The ID of the revnet.
|
|
496
495
|
/// @param hook The tiered ERC-721 hook.
|
|
497
496
|
function setTiered721HookOf(uint256 revnetId, IJB721TiersHook hook) external {
|
|
498
|
-
if (msg.sender != address(DEPLOYER))
|
|
497
|
+
if (msg.sender != address(DEPLOYER)) {
|
|
498
|
+
revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(DEPLOYER)});
|
|
499
|
+
}
|
|
499
500
|
tiered721HookOf[revnetId] = hook;
|
|
500
501
|
}
|
|
501
502
|
|
|
@@ -552,13 +553,11 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
552
553
|
for (uint256 i; i < sources.length; i++) {
|
|
553
554
|
REVLoanSource memory source = sources[i];
|
|
554
555
|
// Each configured source must be queried live so cash-out math includes current outstanding debt.
|
|
555
|
-
// slither-disable-next-line calls-loop
|
|
556
556
|
uint256 tokensLoaned =
|
|
557
557
|
loans.totalBorrowedFrom({revnetId: revnetId, terminal: source.terminal, token: source.token});
|
|
558
558
|
if (tokensLoaned == 0) continue;
|
|
559
559
|
|
|
560
560
|
// Read the source token's accounting context so debt can be normalized before cross-currency conversion.
|
|
561
|
-
// slither-disable-next-line calls-loop
|
|
562
561
|
JBAccountingContext memory accountingContext =
|
|
563
562
|
source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
|
|
564
563
|
|
|
@@ -576,7 +575,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
576
575
|
borrowedAmount += normalizedTokens;
|
|
577
576
|
} else {
|
|
578
577
|
// Convert source-token debt into the requested currency using the loans contract's shared prices.
|
|
579
|
-
// slither-disable-next-line calls-loop
|
|
580
578
|
uint256 pricePerUnit = loans.PRICES()
|
|
581
579
|
.pricePerUnitOf({
|
|
582
580
|
projectId: revnetId,
|