@rev-net/core-v6 0.0.42 → 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 +2 -2
- package/src/REVDeployer.sol +46 -45
- package/src/REVHiddenTokens.sol +1 -3
- package/src/REVLoans.sol +92 -64
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rev-net/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@bananapus/721-hook-v6": "0.0.43",
|
|
30
30
|
"@bananapus/buyback-hook-v6": "0.0.37",
|
|
31
|
-
"@bananapus/core-v6": "0.0.
|
|
31
|
+
"@bananapus/core-v6": "0.0.42",
|
|
32
32
|
"@bananapus/ownable-v6": "0.0.24",
|
|
33
33
|
"@bananapus/permission-ids-v6": "0.0.22",
|
|
34
34
|
"@bananapus/router-terminal-v6": "0.0.36",
|
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,16 +464,16 @@ 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
|
-
// Get a reference to the amount prepaid for the full loan.
|
|
469
|
-
|
|
470
|
-
uint256 prepaid = JBFees.feeAmountFromFloor({amountBeforeFee: loan.amount, feePercent: loan.prepaidFeePercent});
|
|
472
|
+
// Get a reference to the amount prepaid for the full loan.
|
|
473
|
+
uint256 prepaid = JBFees.feeAmountFrom({amountBeforeFee: loan.amount, feePercent: loan.prepaidFeePercent});
|
|
471
474
|
|
|
472
|
-
// This source fee ramps with elapsed time.
|
|
473
|
-
|
|
474
|
-
uint256 fullSourceFeeAmount = JBFees.feeAmountFromFloor({
|
|
475
|
+
// This source fee ramps with elapsed time.
|
|
476
|
+
uint256 fullSourceFeeAmount = JBFees.feeAmountFrom({
|
|
475
477
|
amountBeforeFee: loan.amount - prepaid,
|
|
476
478
|
feePercent: mulDiv({
|
|
477
479
|
x: timeSinceLoanCreated - loan.prepaidDuration,
|
|
@@ -550,7 +552,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
550
552
|
if (tokensLoaned == 0) continue;
|
|
551
553
|
|
|
552
554
|
// Get a reference to the accounting context for the source.
|
|
553
|
-
// slither-disable-next-line calls-loop
|
|
554
555
|
JBAccountingContext memory accountingContext =
|
|
555
556
|
source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
|
|
556
557
|
|
|
@@ -569,7 +570,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
569
570
|
borrowedAmount += normalizedTokens;
|
|
570
571
|
} else {
|
|
571
572
|
// Otherwise, convert via the price feed.
|
|
572
|
-
// slither-disable-next-line calls-loop
|
|
573
573
|
uint256 pricePerUnit = PRICES.pricePerUnitOf({
|
|
574
574
|
projectId: revnetId,
|
|
575
575
|
pricingCurrency: accountingContext.currency,
|
|
@@ -651,7 +651,12 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
651
651
|
/// @param count The number of loans to iterate over.
|
|
652
652
|
function liquidateExpiredLoansFrom(uint256 revnetId, uint256 startingLoanId, uint256 count) external override {
|
|
653
653
|
// Prevent cross-revnet accounting corruption: loan numbers must stay within the revnet's ID namespace.
|
|
654
|
-
|
|
654
|
+
uint256 endLoanNumber = startingLoanId + count;
|
|
655
|
+
if (endLoanNumber > _ONE_TRILLION) {
|
|
656
|
+
revert REVLoans_LoanIdOverflow({
|
|
657
|
+
revnetId: revnetId, loanNumber: endLoanNumber, maxLoanNumber: _ONE_TRILLION
|
|
658
|
+
});
|
|
659
|
+
}
|
|
655
660
|
|
|
656
661
|
// Cache the sender to avoid repeated ERC2771 context reads inside the loop.
|
|
657
662
|
address sender = _msgSender();
|
|
@@ -662,7 +667,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
662
667
|
uint256 loanId = _generateLoanId({revnetId: revnetId, loanNumber: startingLoanId + i});
|
|
663
668
|
|
|
664
669
|
// Check createdAt via storage ref first to avoid loading the full struct for empty slots.
|
|
665
|
-
// slither-disable-next-line incorrect-equality
|
|
666
670
|
if (_loanOf[loanId].createdAt == 0) continue;
|
|
667
671
|
|
|
668
672
|
// Get a reference to the loan being iterated on.
|
|
@@ -672,6 +676,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
672
676
|
address owner = _ownerOf(loanId);
|
|
673
677
|
|
|
674
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)
|
|
675
680
|
if (owner == address(0) || (block.timestamp <= loan.createdAt + LOAN_LIQUIDATION_DURATION)) continue;
|
|
676
681
|
|
|
677
682
|
// Burn the loan.
|
|
@@ -737,15 +742,24 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
737
742
|
_requirePermissionFrom({account: loanOwner, projectId: revnetId, permissionId: JBPermissionIds.REALLOCATE_LOAN});
|
|
738
743
|
|
|
739
744
|
// Make sure the loan hasn't expired.
|
|
745
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
740
746
|
if (block.timestamp - _loanOf[loanId].createdAt > LOAN_LIQUIDATION_DURATION) {
|
|
741
|
-
revert REVLoans_LoanExpired(
|
|
747
|
+
revert REVLoans_LoanExpired({
|
|
748
|
+
timeSinceLoanCreated: block.timestamp - _loanOf[loanId].createdAt,
|
|
749
|
+
loanLiquidationDuration: LOAN_LIQUIDATION_DURATION
|
|
750
|
+
});
|
|
742
751
|
}
|
|
743
752
|
|
|
744
753
|
// Make sure the new loan's source matches the existing loan's source to prevent cross-source value extraction.
|
|
745
754
|
{
|
|
746
755
|
REVLoanSource storage existingSource = _loanOf[loanId].source;
|
|
747
756
|
if (source.token != existingSource.token || source.terminal != existingSource.terminal) {
|
|
748
|
-
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
|
+
});
|
|
749
763
|
}
|
|
750
764
|
}
|
|
751
765
|
|
|
@@ -808,7 +822,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
808
822
|
REVLoan storage loan = _loanOf[loanId];
|
|
809
823
|
|
|
810
824
|
if (collateralCountToReturn > loan.collateral) {
|
|
811
|
-
revert REVLoans_CollateralExceedsLoan(
|
|
825
|
+
revert REVLoans_CollateralExceedsLoan({
|
|
826
|
+
collateralToReturn: collateralCountToReturn, loanCollateral: loan.collateral
|
|
827
|
+
});
|
|
812
828
|
}
|
|
813
829
|
|
|
814
830
|
// Get a reference to the revnet ID of the loan being repaid.
|
|
@@ -835,7 +851,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
835
851
|
|
|
836
852
|
// Make sure the new borrow amount is less than the loan's amount.
|
|
837
853
|
if (newBorrowAmount > loan.amount) {
|
|
838
|
-
revert REVLoans_NewBorrowAmountGreaterThanLoanAmount(
|
|
854
|
+
revert REVLoans_NewBorrowAmountGreaterThanLoanAmount({
|
|
855
|
+
newBorrowAmount: newBorrowAmount, loanAmount: loan.amount
|
|
856
|
+
});
|
|
839
857
|
}
|
|
840
858
|
|
|
841
859
|
// Get the amount of the loan being repaid.
|
|
@@ -845,7 +863,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
845
863
|
// Revert if this repayment would do nothing — no borrow amount repaid and no collateral returned.
|
|
846
864
|
// Without this check, a zero-amount repayment would burn the old loan NFT and mint a new one,
|
|
847
865
|
// incrementing totalLoansBorrowedFor without limit.
|
|
848
|
-
if (repayBorrowAmount == 0 && collateralCountToReturn == 0)
|
|
866
|
+
if (repayBorrowAmount == 0 && collateralCountToReturn == 0) {
|
|
867
|
+
revert REVLoans_NothingToRepay({
|
|
868
|
+
repayBorrowAmount: repayBorrowAmount, collateralCountToReturn: collateralCountToReturn
|
|
869
|
+
});
|
|
870
|
+
}
|
|
849
871
|
|
|
850
872
|
// Keep a reference to the fee that'll be taken.
|
|
851
873
|
uint256 sourceFeeAmount = _determineSourceFeeAmount({loan: loan, amount: repayBorrowAmount});
|
|
@@ -859,7 +881,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
859
881
|
|
|
860
882
|
// Make sure the minimum borrow amount is met.
|
|
861
883
|
if (repayBorrowAmount > maxRepayBorrowAmount) {
|
|
862
|
-
revert REVLoans_OverMaxRepayBorrowAmount(
|
|
884
|
+
revert REVLoans_OverMaxRepayBorrowAmount({
|
|
885
|
+
maxRepayBorrowAmount: maxRepayBorrowAmount, repayBorrowAmount: repayBorrowAmount
|
|
886
|
+
});
|
|
863
887
|
}
|
|
864
888
|
|
|
865
889
|
// Cache the source token before _repayLoan deletes the loan storage.
|
|
@@ -917,13 +941,13 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
917
941
|
if (token == JBConstants.NATIVE_TOKEN) return msg.value;
|
|
918
942
|
|
|
919
943
|
// If the token is not native, revert if there is a non-zero `msg.value`.
|
|
920
|
-
if (msg.value != 0) revert REVLoans_NoMsgValueAllowed();
|
|
944
|
+
if (msg.value != 0) revert REVLoans_NoMsgValueAllowed({msgValue: msg.value, token: token});
|
|
921
945
|
|
|
922
946
|
// Check if the metadata contains permit data.
|
|
923
947
|
if (allowance.amount != 0) {
|
|
924
948
|
// Make sure the permit allowance is enough for this payment. If not we revert early.
|
|
925
949
|
if (allowance.amount < amount) {
|
|
926
|
-
revert REVLoans_PermitAllowanceNotEnough(allowance.amount, amount);
|
|
950
|
+
revert REVLoans_PermitAllowanceNotEnough({allowanceAmount: allowance.amount, requiredAmount: amount});
|
|
927
951
|
}
|
|
928
952
|
|
|
929
953
|
// Keep a reference to the permit rules.
|
|
@@ -997,7 +1021,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
997
1021
|
loan.source.terminal.accountingContextForTokenOf({projectId: revnetId, token: loan.source.token});
|
|
998
1022
|
|
|
999
1023
|
// Pull the amount to be loaned out of the revnet. This will incure the protocol fee.
|
|
1000
|
-
// slither-disable-next-line unused-return
|
|
1001
1024
|
netAmountPaidOut = loan.source.terminal
|
|
1002
1025
|
.useAllowanceOf({
|
|
1003
1026
|
projectId: revnetId,
|
|
@@ -1014,11 +1037,10 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1014
1037
|
// Keep a reference to the fee terminal.
|
|
1015
1038
|
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: REV_ID, token: loan.source.token});
|
|
1016
1039
|
|
|
1017
|
-
// Get the amount of additional fee to take for REV.
|
|
1018
|
-
// protocol fee, so keep it floor-rounded instead of applying the protocol fee helper's dust minimum.
|
|
1040
|
+
// Get the amount of additional fee to take for REV.
|
|
1019
1041
|
uint256 revFeeAmount = address(feeTerminal) == address(0)
|
|
1020
1042
|
? 0
|
|
1021
|
-
: JBFees.
|
|
1043
|
+
: JBFees.feeAmountFrom({amountBeforeFee: addedBorrowAmount, feePercent: REV_PREPAID_FEE_PERCENT});
|
|
1022
1044
|
|
|
1023
1045
|
// Try to pay the REV fee. If it fails, revFeeAmount is zeroed so the borrower receives it instead.
|
|
1024
1046
|
if (revFeeAmount > 0) {
|
|
@@ -1086,7 +1108,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1086
1108
|
// Any reentrant call will see the updated loan values, reverting on overflow.
|
|
1087
1109
|
if (newBorrowAmount > type(uint112).max) revert REVLoans_OverflowAlert(newBorrowAmount, type(uint112).max);
|
|
1088
1110
|
if (newCollateralCount > type(uint112).max) {
|
|
1089
|
-
revert REVLoans_OverflowAlert(newCollateralCount, type(uint112).max);
|
|
1111
|
+
revert REVLoans_OverflowAlert({value: newCollateralCount, limit: type(uint112).max});
|
|
1090
1112
|
}
|
|
1091
1113
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1092
1114
|
loan.amount = uint112(newBorrowAmount);
|
|
@@ -1180,20 +1202,20 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1180
1202
|
returns (uint256 loanId, REVLoan memory)
|
|
1181
1203
|
{
|
|
1182
1204
|
// A loan needs to have collateral.
|
|
1183
|
-
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
|
|
1205
|
+
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid({collateralCount: collateralCount});
|
|
1184
1206
|
|
|
1185
1207
|
// Make sure the source terminal is registered in the directory for this revnet.
|
|
1186
1208
|
if (!DIRECTORY.isTerminalOf({projectId: revnetId, terminal: IJBTerminal(address(source.terminal))})) {
|
|
1187
|
-
revert REVLoans_InvalidTerminal(address(source.terminal), revnetId);
|
|
1209
|
+
revert REVLoans_InvalidTerminal({terminal: address(source.terminal), revnetId: revnetId});
|
|
1188
1210
|
}
|
|
1189
1211
|
|
|
1190
1212
|
// Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
|
|
1191
1213
|
// an 16 year loan can be paid upfront with a
|
|
1192
1214
|
// payment of 50% of the borrowed assets, the cheapest possible rate.
|
|
1193
1215
|
if (prepaidFeePercent < MIN_PREPAID_FEE_PERCENT || prepaidFeePercent > MAX_PREPAID_FEE_PERCENT) {
|
|
1194
|
-
revert REVLoans_InvalidPrepaidFeePercent(
|
|
1195
|
-
prepaidFeePercent, MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT
|
|
1196
|
-
);
|
|
1216
|
+
revert REVLoans_InvalidPrepaidFeePercent({
|
|
1217
|
+
prepaidFeePercent: prepaidFeePercent, min: MIN_PREPAID_FEE_PERCENT, max: MAX_PREPAID_FEE_PERCENT
|
|
1218
|
+
});
|
|
1197
1219
|
}
|
|
1198
1220
|
|
|
1199
1221
|
// Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
|
|
@@ -1202,16 +1224,14 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1202
1224
|
// Enforce the cash out delay.
|
|
1203
1225
|
{
|
|
1204
1226
|
uint256 cashOutDelay = _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
|
|
1227
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
1205
1228
|
if (cashOutDelay > block.timestamp) {
|
|
1206
|
-
revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
1229
|
+
revert REVLoans_CashOutDelayNotFinished({cashOutDelay: cashOutDelay, blockTimestamp: block.timestamp});
|
|
1207
1230
|
}
|
|
1208
1231
|
}
|
|
1209
1232
|
|
|
1210
|
-
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1211
|
-
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1212
|
-
|
|
1213
1233
|
// Get a reference to the loan ID.
|
|
1214
|
-
loanId =
|
|
1234
|
+
loanId = _nextLoanIdFor(revnetId);
|
|
1215
1235
|
|
|
1216
1236
|
// Get a reference to the loan being created.
|
|
1217
1237
|
REVLoan storage loan = _loanOf[loanId];
|
|
@@ -1230,16 +1250,15 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1230
1250
|
});
|
|
1231
1251
|
|
|
1232
1252
|
// Revert if the bonding curve returns zero to prevent creating zero-amount loans.
|
|
1233
|
-
if (borrowAmount == 0)
|
|
1253
|
+
if (borrowAmount == 0) {
|
|
1254
|
+
revert REVLoans_ZeroBorrowAmount({revnetId: revnetId, collateralCount: collateralCount});
|
|
1255
|
+
}
|
|
1234
1256
|
|
|
1235
1257
|
// Make sure the minimum borrow amount is met.
|
|
1236
1258
|
if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
|
|
1237
1259
|
|
|
1238
|
-
// Get the amount of additional fee to take for the revnet issuing the loan.
|
|
1239
|
-
|
|
1240
|
-
// minimum.
|
|
1241
|
-
uint256 sourceFeeAmount =
|
|
1242
|
-
JBFees.feeAmountFromFloor({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
|
|
1260
|
+
// Get the amount of additional fee to take for the revnet issuing the loan.
|
|
1261
|
+
uint256 sourceFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
|
|
1243
1262
|
|
|
1244
1263
|
// Borrow the amount.
|
|
1245
1264
|
_adjust({
|
|
@@ -1270,6 +1289,18 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1270
1289
|
return (loanId, loan);
|
|
1271
1290
|
}
|
|
1272
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
|
+
|
|
1273
1304
|
/// @notice Reallocate collateral from a loan by making a new loan based on the original, with reduced collateral.
|
|
1274
1305
|
/// @param loanId The ID of the loan to reallocate collateral from.
|
|
1275
1306
|
/// @param revnetId The ID of the revnet the loan is from.
|
|
@@ -1292,7 +1323,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1292
1323
|
REVLoan storage loan = _loanOf[loanId];
|
|
1293
1324
|
|
|
1294
1325
|
// Make sure there is enough collateral to transfer.
|
|
1295
|
-
if (collateralCountToRemove > loan.collateral)
|
|
1326
|
+
if (collateralCountToRemove > loan.collateral) {
|
|
1327
|
+
revert REVLoans_NotEnoughCollateral({
|
|
1328
|
+
collateralCountToRemove: collateralCountToRemove, loanCollateral: loan.collateral
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1296
1331
|
|
|
1297
1332
|
// Keep a reference to the new collateral amount.
|
|
1298
1333
|
uint256 newCollateralCount = loan.collateral - collateralCountToRemove;
|
|
@@ -1307,14 +1342,13 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1307
1342
|
|
|
1308
1343
|
// Make sure the borrow amount is not less than the original loan's amount.
|
|
1309
1344
|
if (borrowAmount < loan.amount) {
|
|
1310
|
-
revert REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(
|
|
1345
|
+
revert REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows({
|
|
1346
|
+
newBorrowAmount: borrowAmount, loanAmount: loan.amount
|
|
1347
|
+
});
|
|
1311
1348
|
}
|
|
1312
1349
|
|
|
1313
|
-
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1314
|
-
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1315
|
-
|
|
1316
1350
|
// Get a reference to the replacement loan ID.
|
|
1317
|
-
reallocatedLoanId =
|
|
1351
|
+
reallocatedLoanId = _nextLoanIdFor(revnetId);
|
|
1318
1352
|
|
|
1319
1353
|
// Get a reference to the loan being created.
|
|
1320
1354
|
reallocatedLoan = _loanOf[reallocatedLoanId];
|
|
@@ -1370,7 +1404,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1370
1404
|
});
|
|
1371
1405
|
|
|
1372
1406
|
// Add the loaned amount back to the revnet.
|
|
1373
|
-
// slither-disable-next-line arbitrary-send-eth
|
|
1374
1407
|
loan.source.terminal.addToBalanceOf{value: payValue}({
|
|
1375
1408
|
projectId: revnetId,
|
|
1376
1409
|
token: loan.source.token,
|
|
@@ -1391,7 +1424,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1391
1424
|
/// @param collateralCountToReturn The amount of collateral to return that the loan no longer requires.
|
|
1392
1425
|
/// @param beneficiary The address to receive the returned collateral and any tokens resulting from paying fees.
|
|
1393
1426
|
/// @param loanOwner The owner of the loan NFT (receives replacement loan if partial repay).
|
|
1394
|
-
// slither-disable-next-line reentrancy-eth,reentrancy-events
|
|
1395
1427
|
function _repayLoan(
|
|
1396
1428
|
uint256 loanId,
|
|
1397
1429
|
REVLoan storage loan,
|
|
@@ -1409,7 +1441,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1409
1441
|
_burn(loanId);
|
|
1410
1442
|
|
|
1411
1443
|
// If the loan will carry no more amount or collateral, store its changes directly.
|
|
1412
|
-
// slither-disable-next-line incorrect-equality
|
|
1413
1444
|
if (collateralCountToReturn == loan.collateral) {
|
|
1414
1445
|
// Snapshot the loan to memory BEFORE _adjust zeroes the storage pointer.
|
|
1415
1446
|
REVLoan memory loanSnapshot = loan;
|
|
@@ -1446,12 +1477,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1446
1477
|
|
|
1447
1478
|
return (loanId, paidOffSnapshot);
|
|
1448
1479
|
} else {
|
|
1449
|
-
// Make a new loan with the remaining amount and collateral.
|
|
1450
|
-
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1451
|
-
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1452
|
-
|
|
1453
1480
|
// Get a reference to the replacement loan ID.
|
|
1454
|
-
uint256 paidOffLoanId =
|
|
1481
|
+
uint256 paidOffLoanId = _nextLoanIdFor(revnetId);
|
|
1455
1482
|
|
|
1456
1483
|
// Get a reference to the loan being paid off.
|
|
1457
1484
|
REVLoan storage paidOffLoan = _loanOf[paidOffLoanId];
|
|
@@ -1508,7 +1535,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1508
1535
|
totalCollateralOf[revnetId] -= collateralCount;
|
|
1509
1536
|
|
|
1510
1537
|
// Mint the collateral tokens back to the loan payer.
|
|
1511
|
-
// slither-disable-next-line unused-return,calls-loop
|
|
1512
1538
|
CONTROLLER.mintTokensOf({
|
|
1513
1539
|
projectId: revnetId,
|
|
1514
1540
|
tokenCount: collateralCount,
|
|
@@ -1567,7 +1593,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1567
1593
|
{
|
|
1568
1594
|
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1569
1595
|
|
|
1570
|
-
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
1571
1596
|
try terminal.pay{value: payValue}({
|
|
1572
1597
|
projectId: projectId,
|
|
1573
1598
|
token: token,
|
|
@@ -1586,6 +1611,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1586
1611
|
}
|
|
1587
1612
|
}
|
|
1588
1613
|
|
|
1614
|
+
/// @notice Accepts calldata sent with native tokens so repayment helpers can refund or settle value.
|
|
1589
1615
|
fallback() external payable {}
|
|
1616
|
+
|
|
1617
|
+
/// @notice Accepts native tokens sent directly to the loan contract.
|
|
1590
1618
|
receive() external payable {}
|
|
1591
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,
|