@rev-net/core-v6 0.0.43 → 0.0.45
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/references/operations.md +1 -0
- package/script/Deploy.s.sol +3 -1
- package/src/REVDeployer.sol +51 -46
- package/src/REVHiddenTokens.sol +1 -3
- package/src/REVLoans.sol +95 -56
- package/src/REVOwner.sol +28 -23
- package/src/structs/REVConfig.sol +3 -0
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.45",
|
|
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.44",
|
|
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/references/operations.md
CHANGED
package/script/Deploy.s.sol
CHANGED
|
@@ -271,6 +271,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
271
271
|
description: REVDescription({name: NAME, ticker: SYMBOL, uri: PROJECT_URI, salt: ERC20_SALT}),
|
|
272
272
|
baseCurrency: ETH_CURRENCY,
|
|
273
273
|
splitOperator: OPERATOR,
|
|
274
|
+
scopeCashOutsToLocalBalances: false,
|
|
274
275
|
stageConfigurations: stageConfigurations
|
|
275
276
|
});
|
|
276
277
|
|
|
@@ -497,7 +498,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
497
498
|
feeRevnetId: FEE_PROJECT_ID,
|
|
498
499
|
suckerRegistry: suckers.registry,
|
|
499
500
|
loans: revloans,
|
|
500
|
-
hiddenTokens: revHiddenTokens
|
|
501
|
+
hiddenTokens: revHiddenTokens,
|
|
502
|
+
deployerBinder: msg.sender
|
|
501
503
|
});
|
|
502
504
|
|
|
503
505
|
// Deploy REVDeployer with the REVLoans, buyback hook, and REVOwner addresses.
|
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
|
|
|
@@ -318,11 +316,13 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
318
316
|
|
|
319
317
|
/// @notice Make a ruleset configuration for a revnet's stage.
|
|
320
318
|
/// @param baseCurrency The base currency of the revnet.
|
|
319
|
+
/// @param scopeCashOutsToLocalBalances If true, cash-out calculations use only the local terminal's surplus.
|
|
321
320
|
/// @param stageConfiguration The stage configuration to build a ruleset from.
|
|
322
321
|
/// @param fundAccessLimitGroups The fund access limit groups to include in the ruleset.
|
|
323
322
|
/// @return rulesetConfiguration The ruleset configuration.
|
|
324
323
|
function _makeRulesetConfiguration(
|
|
325
324
|
uint32 baseCurrency,
|
|
325
|
+
bool scopeCashOutsToLocalBalances,
|
|
326
326
|
REVStageConfig calldata stageConfiguration,
|
|
327
327
|
JBFundAccessLimitGroup[] memory fundAccessLimitGroups
|
|
328
328
|
)
|
|
@@ -335,7 +335,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
335
335
|
metadata.reservedPercent = stageConfiguration.splitPercent;
|
|
336
336
|
metadata.cashOutTaxRate = stageConfiguration.cashOutTaxRate;
|
|
337
337
|
metadata.baseCurrency = baseCurrency;
|
|
338
|
-
metadata.
|
|
338
|
+
metadata.scopeCashOutsToLocalBalances = scopeCashOutsToLocalBalances;
|
|
339
339
|
metadata.allowOwnerMinting = true; // Allow this contract to auto-mint tokens as the revnet's owner.
|
|
340
340
|
metadata.useDataHookForPay = true; // Call this contract's `beforePayRecordedWith(…)` callback on payments.
|
|
341
341
|
metadata.useDataHookForCashOut = true; // Call this contract's `beforeCashOutRecordedWith(…)` callback on cash
|
|
@@ -399,29 +399,36 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
399
399
|
/// Silently catches failures (e.g., if the pool is already initialized).
|
|
400
400
|
/// @param revnetId The ID of the revnet to initialize a pool for.
|
|
401
401
|
/// @param terminalToken The terminal token to create a buyback pool for.
|
|
402
|
+
/// @param terminalTokenDecimals The number of decimals the terminal token uses.
|
|
402
403
|
/// @param initialIssuance The initial issuance rate (project tokens per terminal token, 18 decimals).
|
|
403
|
-
function _tryInitializeBuybackPoolFor(
|
|
404
|
+
function _tryInitializeBuybackPoolFor(
|
|
405
|
+
uint256 revnetId,
|
|
406
|
+
address terminalToken,
|
|
407
|
+
uint8 terminalTokenDecimals,
|
|
408
|
+
uint112 initialIssuance
|
|
409
|
+
)
|
|
410
|
+
internal
|
|
411
|
+
{
|
|
404
412
|
uint160 sqrtPriceX96;
|
|
413
|
+
uint256 terminalTokenUnit = 10 ** terminalTokenDecimals;
|
|
405
414
|
|
|
406
415
|
if (initialIssuance == 0) {
|
|
407
416
|
sqrtPriceX96 = uint160(1 << 96);
|
|
408
417
|
} else {
|
|
409
418
|
address normalizedTerminalToken = terminalToken == JBConstants.NATIVE_TOKEN ? address(0) : terminalToken;
|
|
410
|
-
// slither-disable-next-line calls-loop
|
|
411
419
|
address projectToken = address(CONTROLLER.TOKENS().tokenOf(revnetId));
|
|
412
420
|
|
|
413
421
|
if (projectToken == address(0) || projectToken == normalizedTerminalToken) {
|
|
414
422
|
sqrtPriceX96 = uint160(1 << 96);
|
|
415
423
|
} else if (normalizedTerminalToken < projectToken) {
|
|
416
|
-
// token0 = terminal, token1 = project → price = issuance /
|
|
417
|
-
sqrtPriceX96 = uint160(sqrt(mulDiv(uint256(initialIssuance), 1 << 192,
|
|
424
|
+
// token0 = terminal, token1 = project → price = issuance / terminalTokenUnit
|
|
425
|
+
sqrtPriceX96 = uint160(sqrt(mulDiv(uint256(initialIssuance), 1 << 192, terminalTokenUnit)));
|
|
418
426
|
} else {
|
|
419
|
-
// token0 = project, token1 = terminal → price =
|
|
420
|
-
sqrtPriceX96 = uint160(sqrt(mulDiv(
|
|
427
|
+
// token0 = project, token1 = terminal → price = terminalTokenUnit / issuance
|
|
428
|
+
sqrtPriceX96 = uint160(sqrt(mulDiv(terminalTokenUnit, 1 << 192, uint256(initialIssuance))));
|
|
421
429
|
}
|
|
422
430
|
}
|
|
423
431
|
|
|
424
|
-
// slither-disable-next-line calls-loop
|
|
425
432
|
try BUYBACK_HOOK.initializePoolFor({
|
|
426
433
|
projectId: revnetId,
|
|
427
434
|
fee: DEFAULT_BUYBACK_POOL_FEE,
|
|
@@ -449,19 +456,21 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
449
456
|
// IDs `block.timestamp`, `block.timestamp + 1`, ... exactly correspond to the JB-assigned ruleset IDs.
|
|
450
457
|
// The returned `ruleset.start` contains the derived start time (from `deriveStartFrom` using the stage's
|
|
451
458
|
// `mustStartAtOrAfter`), NOT the queue timestamp — so the timing guard correctly blocks early claims.
|
|
452
|
-
// slither-disable-next-line unused-return
|
|
453
459
|
(JBRuleset memory ruleset,) = CONTROLLER.getRulesetOf({projectId: revnetId, rulesetId: stageId});
|
|
454
460
|
|
|
455
461
|
// Make sure the stage has started.
|
|
462
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
456
463
|
if (ruleset.start > block.timestamp) {
|
|
457
|
-
revert REVDeployer_StageNotStarted(stageId);
|
|
464
|
+
revert REVDeployer_StageNotStarted({stageId: stageId});
|
|
458
465
|
}
|
|
459
466
|
|
|
460
467
|
// Get a reference to the number of tokens to auto-issue.
|
|
461
468
|
uint256 count = amountToAutoIssue[revnetId][stageId][beneficiary];
|
|
462
469
|
|
|
463
470
|
// If there's nothing to auto-mint, return.
|
|
464
|
-
if (count == 0)
|
|
471
|
+
if (count == 0) {
|
|
472
|
+
revert REVDeployer_NothingToAutoIssue({revnetId: revnetId, stageId: stageId, beneficiary: beneficiary});
|
|
473
|
+
}
|
|
465
474
|
|
|
466
475
|
// Reset the auto-mint amount.
|
|
467
476
|
amountToAutoIssue[revnetId][stageId][beneficiary] = 0;
|
|
@@ -471,7 +480,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
471
480
|
});
|
|
472
481
|
|
|
473
482
|
// Mint the tokens.
|
|
474
|
-
// slither-disable-next-line unused-return
|
|
475
483
|
CONTROLLER.mintTokensOf({
|
|
476
484
|
projectId: revnetId, tokenCount: count, beneficiary: beneficiary, memo: "", useReservedPercent: false
|
|
477
485
|
});
|
|
@@ -482,10 +490,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
482
490
|
/// @param revnetId The ID of the revnet to burn tokens for.
|
|
483
491
|
function burnHeldTokensOf(uint256 revnetId) external override {
|
|
484
492
|
uint256 balance = CONTROLLER.TOKENS().totalBalanceOf({holder: address(this), projectId: revnetId});
|
|
485
|
-
if (balance == 0) revert REVDeployer_NothingToBurn();
|
|
493
|
+
if (balance == 0) revert REVDeployer_NothingToBurn({revnetId: revnetId, holder: address(this)});
|
|
486
494
|
CONTROLLER.burnTokensOf({holder: address(this), projectId: revnetId, tokenCount: balance, memo: ""});
|
|
487
|
-
|
|
488
|
-
emit BurnHeldTokens(revnetId, balance, _msgSender());
|
|
495
|
+
emit BurnHeldTokens({revnetId: revnetId, count: balance, caller: _msgSender()});
|
|
489
496
|
}
|
|
490
497
|
|
|
491
498
|
/// @notice Launch a revnet, or initialize an existing Juicebox project as a revnet.
|
|
@@ -506,7 +513,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
506
513
|
/// @return revnetId The ID of the newly created revnet.
|
|
507
514
|
/// @return hook The address of the tiered ERC-721 hook deployed for the revnet.
|
|
508
515
|
// 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
516
|
function deployFor(
|
|
511
517
|
uint256 revnetId,
|
|
512
518
|
REVConfig calldata configuration,
|
|
@@ -541,7 +547,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
541
547
|
|
|
542
548
|
/// @inheritdoc IREVDeployer
|
|
543
549
|
// 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
550
|
function deployFor(
|
|
546
551
|
uint256 revnetId,
|
|
547
552
|
REVConfig calldata configuration,
|
|
@@ -610,14 +615,13 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
610
615
|
_checkIfIsSplitOperatorOf({revnetId: revnetId, operator: _msgSender()});
|
|
611
616
|
|
|
612
617
|
// Check if the current ruleset allows deploying new suckers.
|
|
613
|
-
// slither-disable-next-line unused-return
|
|
614
618
|
(, JBRulesetMetadata memory metadata) = CONTROLLER.currentRulesetOf(revnetId);
|
|
615
619
|
|
|
616
620
|
// Check the third bit, it indicates if the ruleset allows new suckers to be deployed.
|
|
617
621
|
bool allowsDeployingSuckers = ((metadata.metadata >> 2) & 1) == 1;
|
|
618
622
|
|
|
619
623
|
if (!allowsDeployingSuckers) {
|
|
620
|
-
revert REVDeployer_RulesetDoesNotAllowDeployingSuckers();
|
|
624
|
+
revert REVDeployer_RulesetDoesNotAllowDeployingSuckers({revnetId: revnetId});
|
|
621
625
|
}
|
|
622
626
|
|
|
623
627
|
// Deploy the suckers.
|
|
@@ -655,7 +659,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
655
659
|
|
|
656
660
|
/// @notice Deploy a revnet which sells tiered ERC-721s and (optionally) allows croptop posts to its ERC-721 tiers.
|
|
657
661
|
// 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
662
|
function _deploy721RevnetFor(
|
|
660
663
|
uint256 revnetId,
|
|
661
664
|
bool shouldDeployNewRevnet,
|
|
@@ -814,12 +817,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
814
817
|
if (!shouldDeployNewRevnet) {
|
|
815
818
|
// Initialize the existing Juicebox project as a revnet by transferring the `JBProjects` NFT to this
|
|
816
819
|
// deployer. This is irreversible.
|
|
817
|
-
// slither-disable-next-line reentrancy-benign
|
|
818
820
|
IERC721(PROJECTS).safeTransferFrom({from: owner, to: address(this), tokenId: revnetId});
|
|
819
821
|
}
|
|
820
822
|
|
|
821
823
|
// Launch the revnet rulesets for the reserved or pre-existing blank project.
|
|
822
|
-
// slither-disable-next-line unused-return
|
|
823
824
|
CONTROLLER.launchRulesetsFor({
|
|
824
825
|
projectId: revnetId,
|
|
825
826
|
projectUri: configuration.description.uri,
|
|
@@ -831,11 +832,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
831
832
|
// Store the cash out delay of the revnet if its stages are already in progress.
|
|
832
833
|
// This prevents cash out liquidity/arbitrage issues for existing revnets which
|
|
833
834
|
// are deploying to a new chain.
|
|
834
|
-
// slither-disable-next-line reentrancy-events
|
|
835
835
|
_setCashOutDelayIfNeeded({revnetId: revnetId, firstStageConfig: configuration.stageConfigurations[0]});
|
|
836
836
|
|
|
837
837
|
// Deploy the revnet's ERC-20 token.
|
|
838
|
-
// slither-disable-next-line unused-return
|
|
839
838
|
CONTROLLER.deployERC20For({
|
|
840
839
|
projectId: revnetId,
|
|
841
840
|
name: configuration.description.name,
|
|
@@ -847,10 +846,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
847
846
|
for (uint256 i; i < terminalConfigurations.length;) {
|
|
848
847
|
JBTerminalConfig calldata terminalConfiguration = terminalConfigurations[i];
|
|
849
848
|
for (uint256 j; j < terminalConfiguration.accountingContextsToAccept.length;) {
|
|
850
|
-
// slither-disable-next-line calls-loop
|
|
851
849
|
_tryInitializeBuybackPoolFor({
|
|
852
850
|
revnetId: revnetId,
|
|
853
851
|
terminalToken: terminalConfiguration.accountingContextsToAccept[j].token,
|
|
852
|
+
terminalTokenDecimals: terminalConfiguration.accountingContextsToAccept[j].decimals,
|
|
854
853
|
initialIssuance: configuration.stageConfigurations[0].initialIssuance
|
|
855
854
|
});
|
|
856
855
|
unchecked {
|
|
@@ -901,7 +900,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
901
900
|
|
|
902
901
|
// Include the caller so two revnets with identical configuration and user salt cannot collide. Same-address
|
|
903
902
|
// cross-chain deployment still works when the same operator calls this helper on each chain.
|
|
904
|
-
// slither-disable-next-line unused-return
|
|
905
903
|
suckers = SUCKER_REGISTRY.deploySuckersFor({
|
|
906
904
|
projectId: revnetId,
|
|
907
905
|
salt: keccak256(abi.encode(encodedConfigurationHash, suckerDeploymentConfiguration.salt, _msgSender())),
|
|
@@ -934,7 +932,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
934
932
|
returns (JBRulesetConfig[] memory rulesetConfigurations, bytes32 encodedConfigurationHash)
|
|
935
933
|
{
|
|
936
934
|
// If there are no stages, revert.
|
|
937
|
-
if (configuration.stageConfigurations.length == 0)
|
|
935
|
+
if (configuration.stageConfigurations.length == 0) {
|
|
936
|
+
revert REVDeployer_StagesRequired({stageCount: configuration.stageConfigurations.length});
|
|
937
|
+
}
|
|
938
938
|
|
|
939
939
|
// Initialize the array of rulesets.
|
|
940
940
|
rulesetConfigurations = new JBRulesetConfig[](configuration.stageConfigurations.length);
|
|
@@ -942,6 +942,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
942
942
|
// Add the base configuration to the byte-encoded configuration.
|
|
943
943
|
bytes memory encodedConfiguration = abi.encode(
|
|
944
944
|
configuration.baseCurrency,
|
|
945
|
+
configuration.scopeCashOutsToLocalBalances,
|
|
945
946
|
configuration.description.name,
|
|
946
947
|
configuration.description.ticker,
|
|
947
948
|
configuration.description.salt
|
|
@@ -975,7 +976,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
975
976
|
// Make sure the revnet has at least one split if it has a split percent.
|
|
976
977
|
// Otherwise, the split would go to this contract since its the revnet's owner.
|
|
977
978
|
if (stageConfiguration.splitPercent > 0 && stageConfiguration.splits.length == 0) {
|
|
978
|
-
revert REVDeployer_MustHaveSplits();
|
|
979
|
+
revert REVDeployer_MustHaveSplits({stageIndex: i, splitPercent: stageConfiguration.splitPercent});
|
|
979
980
|
}
|
|
980
981
|
|
|
981
982
|
// Compute the effective start time for this stage.
|
|
@@ -985,7 +986,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
985
986
|
|
|
986
987
|
// If the stage's start time is not after the previous stage's start time, revert.
|
|
987
988
|
if (i > 0 && effectiveStart <= previousStageStart) {
|
|
988
|
-
revert REVDeployer_StageTimesMustIncrease(
|
|
989
|
+
revert REVDeployer_StageTimesMustIncrease({
|
|
990
|
+
stageIndex: i, previousStageStart: previousStageStart, effectiveStart: effectiveStart
|
|
991
|
+
});
|
|
989
992
|
}
|
|
990
993
|
|
|
991
994
|
// Store for the next iteration's ordering check.
|
|
@@ -993,14 +996,16 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
993
996
|
|
|
994
997
|
// Make sure the revnet doesn't prevent cashouts all together.
|
|
995
998
|
if (stageConfiguration.cashOutTaxRate >= JBConstants.MAX_CASH_OUT_TAX_RATE) {
|
|
996
|
-
revert REVDeployer_CashOutsCantBeTurnedOffCompletely(
|
|
997
|
-
stageConfiguration.cashOutTaxRate,
|
|
998
|
-
|
|
999
|
+
revert REVDeployer_CashOutsCantBeTurnedOffCompletely({
|
|
1000
|
+
cashOutTaxRate: stageConfiguration.cashOutTaxRate,
|
|
1001
|
+
maxCashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE
|
|
1002
|
+
});
|
|
999
1003
|
}
|
|
1000
1004
|
|
|
1001
1005
|
// Set up the ruleset.
|
|
1002
1006
|
rulesetConfigurations[i] = _makeRulesetConfiguration({
|
|
1003
1007
|
baseCurrency: configuration.baseCurrency,
|
|
1008
|
+
scopeCashOutsToLocalBalances: configuration.scopeCashOutsToLocalBalances,
|
|
1004
1009
|
stageConfiguration: stageConfiguration,
|
|
1005
1010
|
fundAccessLimitGroups: fundAccessLimitGroups
|
|
1006
1011
|
});
|
|
@@ -1027,7 +1032,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1027
1032
|
REVAutoIssuance calldata autoIssuance = stageConfiguration.autoIssuances[j];
|
|
1028
1033
|
|
|
1029
1034
|
// Make sure the beneficiary is not the zero address.
|
|
1030
|
-
if (autoIssuance.beneficiary == address(0))
|
|
1035
|
+
if (autoIssuance.beneficiary == address(0)) {
|
|
1036
|
+
revert REVDeployer_AutoIssuanceBeneficiaryZeroAddress({stageIndex: i, autoIssuanceIndex: j});
|
|
1037
|
+
}
|
|
1031
1038
|
|
|
1032
1039
|
// If there's nothing to auto-mint, continue.
|
|
1033
1040
|
if (autoIssuance.count == 0) continue;
|
|
@@ -1039,7 +1046,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1039
1046
|
// If the issuance config is for another chain, skip it.
|
|
1040
1047
|
if (autoIssuance.chainId != block.chainid) continue;
|
|
1041
1048
|
|
|
1042
|
-
// slither-disable-next-line reentrancy-events
|
|
1043
1049
|
emit StoreAutoIssuanceAmount({
|
|
1044
1050
|
revnetId: revnetId,
|
|
1045
1051
|
stageId: block.timestamp + i,
|
|
@@ -1054,7 +1060,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1054
1060
|
// (JBRulesets.sol L172), producing the same sequential IDs when all stages are queued in one tx.
|
|
1055
1061
|
// `autoIssueFor` later calls `getRulesetOf(revnetId, stageId)` — the returned `ruleset.start`
|
|
1056
1062
|
// is the derived start time (not the queue time), so the timing guard works correctly.
|
|
1057
|
-
// slither-disable-next-line reentrancy-benign
|
|
1058
1063
|
amountToAutoIssue[revnetId][block.timestamp + i][autoIssuance.beneficiary] += autoIssuance.count;
|
|
1059
1064
|
}
|
|
1060
1065
|
unchecked {
|
|
@@ -1073,13 +1078,13 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1073
1078
|
function _setCashOutDelayIfNeeded(uint256 revnetId, REVStageConfig calldata firstStageConfig) internal {
|
|
1074
1079
|
// If this is the first revnet being deployed (with a `startsAtOrAfter` of 0),
|
|
1075
1080
|
// or if the first stage hasn't started yet, we don't need to set a cash out delay.
|
|
1081
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
1076
1082
|
if (firstStageConfig.startsAtOrAfter == 0 || firstStageConfig.startsAtOrAfter >= block.timestamp) return;
|
|
1077
1083
|
|
|
1078
1084
|
// Calculate the timestamp at which the cash out delay ends.
|
|
1079
1085
|
uint256 cashOutDelay = block.timestamp + CASH_OUT_DELAY;
|
|
1080
1086
|
|
|
1081
1087
|
// Store the cash out delay in the owner contract.
|
|
1082
|
-
// slither-disable-next-line reentrancy-events
|
|
1083
1088
|
REVOwner(OWNER).setCashOutDelayOf({revnetId: revnetId, cashOutDelay: cashOutDelay});
|
|
1084
1089
|
|
|
1085
1090
|
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({
|
|
@@ -369,14 +372,19 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
369
372
|
// for the same collateral changes. A lower cashOutTaxRate in a later stage means more borrowable value per
|
|
370
373
|
// collateral. This is by design: loan value tracks the current bonding curve parameters, just as cash-out
|
|
371
374
|
// value does. Borrowers benefit from decreasing tax rates and are constrained by increasing ones.
|
|
372
|
-
//
|
|
373
|
-
uint256
|
|
374
|
-
|
|
375
|
-
|
|
375
|
+
// Start with local values. If the ruleset aggregates cross-chain state, add remote supply and surplus.
|
|
376
|
+
uint256 effectiveSurplus = localSurplus;
|
|
377
|
+
uint256 effectiveSupply = localSupply;
|
|
378
|
+
if (!currentStage.scopeCashOutsToLocalBalances()) {
|
|
379
|
+
effectiveSurplus += SUCKER_REGISTRY.remoteSurplusOf({
|
|
380
|
+
projectId: revnetId, decimals: decimals, currency: currency
|
|
381
|
+
});
|
|
382
|
+
effectiveSupply += SUCKER_REGISTRY.remoteTotalSupplyOf(revnetId);
|
|
383
|
+
}
|
|
376
384
|
uint256 reclaimable = JBCashOuts.cashOutFrom({
|
|
377
|
-
surplus:
|
|
385
|
+
surplus: effectiveSurplus,
|
|
378
386
|
cashOutCount: collateralCount,
|
|
379
|
-
totalSupply:
|
|
387
|
+
totalSupply: effectiveSupply,
|
|
380
388
|
cashOutTaxRate: currentStage.cashOutTaxRate()
|
|
381
389
|
});
|
|
382
390
|
// Cap at local surplus — can't borrow more than what this chain's terminals actually hold.
|
|
@@ -443,7 +451,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
443
451
|
/// @param revnetId The ID of the revnet.
|
|
444
452
|
/// @return currentRuleset The current ruleset.
|
|
445
453
|
function _currentRulesetOf(uint256 revnetId) internal view returns (JBRuleset memory currentRuleset) {
|
|
446
|
-
// slither-disable-next-line unused-return
|
|
447
454
|
(currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
448
455
|
}
|
|
449
456
|
|
|
@@ -462,7 +469,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
462
469
|
// Uses `>` (not `>=`) so the exact boundary second is still repayable — the liquidation path
|
|
463
470
|
// uses `<=`, and matching `>=` here would create a 1-second window where neither path is available.
|
|
464
471
|
if (timeSinceLoanCreated > LOAN_LIQUIDATION_DURATION) {
|
|
465
|
-
revert REVLoans_LoanExpired(
|
|
472
|
+
revert REVLoans_LoanExpired({
|
|
473
|
+
timeSinceLoanCreated: timeSinceLoanCreated, loanLiquidationDuration: LOAN_LIQUIDATION_DURATION
|
|
474
|
+
});
|
|
466
475
|
}
|
|
467
476
|
|
|
468
477
|
// Get a reference to the amount prepaid for the full loan.
|
|
@@ -548,7 +557,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
548
557
|
if (tokensLoaned == 0) continue;
|
|
549
558
|
|
|
550
559
|
// Get a reference to the accounting context for the source.
|
|
551
|
-
// slither-disable-next-line calls-loop
|
|
552
560
|
JBAccountingContext memory accountingContext =
|
|
553
561
|
source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
|
|
554
562
|
|
|
@@ -567,7 +575,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
567
575
|
borrowedAmount += normalizedTokens;
|
|
568
576
|
} else {
|
|
569
577
|
// Otherwise, convert via the price feed.
|
|
570
|
-
// slither-disable-next-line calls-loop
|
|
571
578
|
uint256 pricePerUnit = PRICES.pricePerUnitOf({
|
|
572
579
|
projectId: revnetId,
|
|
573
580
|
pricingCurrency: accountingContext.currency,
|
|
@@ -649,7 +656,12 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
649
656
|
/// @param count The number of loans to iterate over.
|
|
650
657
|
function liquidateExpiredLoansFrom(uint256 revnetId, uint256 startingLoanId, uint256 count) external override {
|
|
651
658
|
// Prevent cross-revnet accounting corruption: loan numbers must stay within the revnet's ID namespace.
|
|
652
|
-
|
|
659
|
+
uint256 endLoanNumber = startingLoanId + count;
|
|
660
|
+
if (endLoanNumber > _ONE_TRILLION) {
|
|
661
|
+
revert REVLoans_LoanIdOverflow({
|
|
662
|
+
revnetId: revnetId, loanNumber: endLoanNumber, maxLoanNumber: _ONE_TRILLION
|
|
663
|
+
});
|
|
664
|
+
}
|
|
653
665
|
|
|
654
666
|
// Cache the sender to avoid repeated ERC2771 context reads inside the loop.
|
|
655
667
|
address sender = _msgSender();
|
|
@@ -660,7 +672,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
660
672
|
uint256 loanId = _generateLoanId({revnetId: revnetId, loanNumber: startingLoanId + i});
|
|
661
673
|
|
|
662
674
|
// Check createdAt via storage ref first to avoid loading the full struct for empty slots.
|
|
663
|
-
// slither-disable-next-line incorrect-equality
|
|
664
675
|
if (_loanOf[loanId].createdAt == 0) continue;
|
|
665
676
|
|
|
666
677
|
// Get a reference to the loan being iterated on.
|
|
@@ -670,6 +681,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
670
681
|
address owner = _ownerOf(loanId);
|
|
671
682
|
|
|
672
683
|
// If the loan is already burned, or if it hasn't passed its liquidation duration, continue.
|
|
684
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
673
685
|
if (owner == address(0) || (block.timestamp <= loan.createdAt + LOAN_LIQUIDATION_DURATION)) continue;
|
|
674
686
|
|
|
675
687
|
// Burn the loan.
|
|
@@ -735,15 +747,24 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
735
747
|
_requirePermissionFrom({account: loanOwner, projectId: revnetId, permissionId: JBPermissionIds.REALLOCATE_LOAN});
|
|
736
748
|
|
|
737
749
|
// Make sure the loan hasn't expired.
|
|
750
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
738
751
|
if (block.timestamp - _loanOf[loanId].createdAt > LOAN_LIQUIDATION_DURATION) {
|
|
739
|
-
revert REVLoans_LoanExpired(
|
|
752
|
+
revert REVLoans_LoanExpired({
|
|
753
|
+
timeSinceLoanCreated: block.timestamp - _loanOf[loanId].createdAt,
|
|
754
|
+
loanLiquidationDuration: LOAN_LIQUIDATION_DURATION
|
|
755
|
+
});
|
|
740
756
|
}
|
|
741
757
|
|
|
742
758
|
// Make sure the new loan's source matches the existing loan's source to prevent cross-source value extraction.
|
|
743
759
|
{
|
|
744
760
|
REVLoanSource storage existingSource = _loanOf[loanId].source;
|
|
745
761
|
if (source.token != existingSource.token || source.terminal != existingSource.terminal) {
|
|
746
|
-
revert REVLoans_SourceMismatch(
|
|
762
|
+
revert REVLoans_SourceMismatch({
|
|
763
|
+
expectedToken: existingSource.token,
|
|
764
|
+
actualToken: source.token,
|
|
765
|
+
expectedTerminal: address(existingSource.terminal),
|
|
766
|
+
actualTerminal: address(source.terminal)
|
|
767
|
+
});
|
|
747
768
|
}
|
|
748
769
|
}
|
|
749
770
|
|
|
@@ -806,7 +827,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
806
827
|
REVLoan storage loan = _loanOf[loanId];
|
|
807
828
|
|
|
808
829
|
if (collateralCountToReturn > loan.collateral) {
|
|
809
|
-
revert REVLoans_CollateralExceedsLoan(
|
|
830
|
+
revert REVLoans_CollateralExceedsLoan({
|
|
831
|
+
collateralToReturn: collateralCountToReturn, loanCollateral: loan.collateral
|
|
832
|
+
});
|
|
810
833
|
}
|
|
811
834
|
|
|
812
835
|
// Get a reference to the revnet ID of the loan being repaid.
|
|
@@ -833,7 +856,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
833
856
|
|
|
834
857
|
// Make sure the new borrow amount is less than the loan's amount.
|
|
835
858
|
if (newBorrowAmount > loan.amount) {
|
|
836
|
-
revert REVLoans_NewBorrowAmountGreaterThanLoanAmount(
|
|
859
|
+
revert REVLoans_NewBorrowAmountGreaterThanLoanAmount({
|
|
860
|
+
newBorrowAmount: newBorrowAmount, loanAmount: loan.amount
|
|
861
|
+
});
|
|
837
862
|
}
|
|
838
863
|
|
|
839
864
|
// Get the amount of the loan being repaid.
|
|
@@ -843,7 +868,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
843
868
|
// Revert if this repayment would do nothing — no borrow amount repaid and no collateral returned.
|
|
844
869
|
// Without this check, a zero-amount repayment would burn the old loan NFT and mint a new one,
|
|
845
870
|
// incrementing totalLoansBorrowedFor without limit.
|
|
846
|
-
if (repayBorrowAmount == 0 && collateralCountToReturn == 0)
|
|
871
|
+
if (repayBorrowAmount == 0 && collateralCountToReturn == 0) {
|
|
872
|
+
revert REVLoans_NothingToRepay({
|
|
873
|
+
repayBorrowAmount: repayBorrowAmount, collateralCountToReturn: collateralCountToReturn
|
|
874
|
+
});
|
|
875
|
+
}
|
|
847
876
|
|
|
848
877
|
// Keep a reference to the fee that'll be taken.
|
|
849
878
|
uint256 sourceFeeAmount = _determineSourceFeeAmount({loan: loan, amount: repayBorrowAmount});
|
|
@@ -857,7 +886,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
857
886
|
|
|
858
887
|
// Make sure the minimum borrow amount is met.
|
|
859
888
|
if (repayBorrowAmount > maxRepayBorrowAmount) {
|
|
860
|
-
revert REVLoans_OverMaxRepayBorrowAmount(
|
|
889
|
+
revert REVLoans_OverMaxRepayBorrowAmount({
|
|
890
|
+
maxRepayBorrowAmount: maxRepayBorrowAmount, repayBorrowAmount: repayBorrowAmount
|
|
891
|
+
});
|
|
861
892
|
}
|
|
862
893
|
|
|
863
894
|
// Cache the source token before _repayLoan deletes the loan storage.
|
|
@@ -915,13 +946,13 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
915
946
|
if (token == JBConstants.NATIVE_TOKEN) return msg.value;
|
|
916
947
|
|
|
917
948
|
// If the token is not native, revert if there is a non-zero `msg.value`.
|
|
918
|
-
if (msg.value != 0) revert REVLoans_NoMsgValueAllowed();
|
|
949
|
+
if (msg.value != 0) revert REVLoans_NoMsgValueAllowed({msgValue: msg.value, token: token});
|
|
919
950
|
|
|
920
951
|
// Check if the metadata contains permit data.
|
|
921
952
|
if (allowance.amount != 0) {
|
|
922
953
|
// Make sure the permit allowance is enough for this payment. If not we revert early.
|
|
923
954
|
if (allowance.amount < amount) {
|
|
924
|
-
revert REVLoans_PermitAllowanceNotEnough(allowance.amount, amount);
|
|
955
|
+
revert REVLoans_PermitAllowanceNotEnough({allowanceAmount: allowance.amount, requiredAmount: amount});
|
|
925
956
|
}
|
|
926
957
|
|
|
927
958
|
// Keep a reference to the permit rules.
|
|
@@ -995,7 +1026,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
995
1026
|
loan.source.terminal.accountingContextForTokenOf({projectId: revnetId, token: loan.source.token});
|
|
996
1027
|
|
|
997
1028
|
// Pull the amount to be loaned out of the revnet. This will incure the protocol fee.
|
|
998
|
-
// slither-disable-next-line unused-return
|
|
999
1029
|
netAmountPaidOut = loan.source.terminal
|
|
1000
1030
|
.useAllowanceOf({
|
|
1001
1031
|
projectId: revnetId,
|
|
@@ -1083,7 +1113,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1083
1113
|
// Any reentrant call will see the updated loan values, reverting on overflow.
|
|
1084
1114
|
if (newBorrowAmount > type(uint112).max) revert REVLoans_OverflowAlert(newBorrowAmount, type(uint112).max);
|
|
1085
1115
|
if (newCollateralCount > type(uint112).max) {
|
|
1086
|
-
revert REVLoans_OverflowAlert(newCollateralCount, type(uint112).max);
|
|
1116
|
+
revert REVLoans_OverflowAlert({value: newCollateralCount, limit: type(uint112).max});
|
|
1087
1117
|
}
|
|
1088
1118
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1089
1119
|
loan.amount = uint112(newBorrowAmount);
|
|
@@ -1177,20 +1207,20 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1177
1207
|
returns (uint256 loanId, REVLoan memory)
|
|
1178
1208
|
{
|
|
1179
1209
|
// A loan needs to have collateral.
|
|
1180
|
-
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
|
|
1210
|
+
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid({collateralCount: collateralCount});
|
|
1181
1211
|
|
|
1182
1212
|
// Make sure the source terminal is registered in the directory for this revnet.
|
|
1183
1213
|
if (!DIRECTORY.isTerminalOf({projectId: revnetId, terminal: IJBTerminal(address(source.terminal))})) {
|
|
1184
|
-
revert REVLoans_InvalidTerminal(address(source.terminal), revnetId);
|
|
1214
|
+
revert REVLoans_InvalidTerminal({terminal: address(source.terminal), revnetId: revnetId});
|
|
1185
1215
|
}
|
|
1186
1216
|
|
|
1187
1217
|
// Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
|
|
1188
1218
|
// an 16 year loan can be paid upfront with a
|
|
1189
1219
|
// payment of 50% of the borrowed assets, the cheapest possible rate.
|
|
1190
1220
|
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
|
-
);
|
|
1221
|
+
revert REVLoans_InvalidPrepaidFeePercent({
|
|
1222
|
+
prepaidFeePercent: prepaidFeePercent, min: MIN_PREPAID_FEE_PERCENT, max: MAX_PREPAID_FEE_PERCENT
|
|
1223
|
+
});
|
|
1194
1224
|
}
|
|
1195
1225
|
|
|
1196
1226
|
// Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
|
|
@@ -1199,16 +1229,14 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1199
1229
|
// Enforce the cash out delay.
|
|
1200
1230
|
{
|
|
1201
1231
|
uint256 cashOutDelay = _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
|
|
1232
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
1202
1233
|
if (cashOutDelay > block.timestamp) {
|
|
1203
|
-
revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
1234
|
+
revert REVLoans_CashOutDelayNotFinished({cashOutDelay: cashOutDelay, blockTimestamp: block.timestamp});
|
|
1204
1235
|
}
|
|
1205
1236
|
}
|
|
1206
1237
|
|
|
1207
|
-
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1208
|
-
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1209
|
-
|
|
1210
1238
|
// Get a reference to the loan ID.
|
|
1211
|
-
loanId =
|
|
1239
|
+
loanId = _nextLoanIdFor(revnetId);
|
|
1212
1240
|
|
|
1213
1241
|
// Get a reference to the loan being created.
|
|
1214
1242
|
REVLoan storage loan = _loanOf[loanId];
|
|
@@ -1227,7 +1255,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1227
1255
|
});
|
|
1228
1256
|
|
|
1229
1257
|
// Revert if the bonding curve returns zero to prevent creating zero-amount loans.
|
|
1230
|
-
if (borrowAmount == 0)
|
|
1258
|
+
if (borrowAmount == 0) {
|
|
1259
|
+
revert REVLoans_ZeroBorrowAmount({revnetId: revnetId, collateralCount: collateralCount});
|
|
1260
|
+
}
|
|
1231
1261
|
|
|
1232
1262
|
// Make sure the minimum borrow amount is met.
|
|
1233
1263
|
if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
|
|
@@ -1264,6 +1294,18 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1264
1294
|
return (loanId, loan);
|
|
1265
1295
|
}
|
|
1266
1296
|
|
|
1297
|
+
/// @notice Allocate the next loan ID for a revnet.
|
|
1298
|
+
/// @param revnetId The ID of the revnet.
|
|
1299
|
+
/// @return loanId The allocated loan ID.
|
|
1300
|
+
function _nextLoanIdFor(uint256 revnetId) internal returns (uint256 loanId) {
|
|
1301
|
+
uint256 loanNumber = totalLoansBorrowedFor[revnetId] + 1;
|
|
1302
|
+
if (loanNumber > _ONE_TRILLION) {
|
|
1303
|
+
revert REVLoans_LoanIdOverflow({revnetId: revnetId, loanNumber: loanNumber, maxLoanNumber: _ONE_TRILLION});
|
|
1304
|
+
}
|
|
1305
|
+
totalLoansBorrowedFor[revnetId] = loanNumber;
|
|
1306
|
+
return _generateLoanId({revnetId: revnetId, loanNumber: loanNumber});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1267
1309
|
/// @notice Reallocate collateral from a loan by making a new loan based on the original, with reduced collateral.
|
|
1268
1310
|
/// @param loanId The ID of the loan to reallocate collateral from.
|
|
1269
1311
|
/// @param revnetId The ID of the revnet the loan is from.
|
|
@@ -1286,7 +1328,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1286
1328
|
REVLoan storage loan = _loanOf[loanId];
|
|
1287
1329
|
|
|
1288
1330
|
// Make sure there is enough collateral to transfer.
|
|
1289
|
-
if (collateralCountToRemove > loan.collateral)
|
|
1331
|
+
if (collateralCountToRemove > loan.collateral) {
|
|
1332
|
+
revert REVLoans_NotEnoughCollateral({
|
|
1333
|
+
collateralCountToRemove: collateralCountToRemove, loanCollateral: loan.collateral
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1290
1336
|
|
|
1291
1337
|
// Keep a reference to the new collateral amount.
|
|
1292
1338
|
uint256 newCollateralCount = loan.collateral - collateralCountToRemove;
|
|
@@ -1301,14 +1347,13 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1301
1347
|
|
|
1302
1348
|
// Make sure the borrow amount is not less than the original loan's amount.
|
|
1303
1349
|
if (borrowAmount < loan.amount) {
|
|
1304
|
-
revert REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(
|
|
1350
|
+
revert REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows({
|
|
1351
|
+
newBorrowAmount: borrowAmount, loanAmount: loan.amount
|
|
1352
|
+
});
|
|
1305
1353
|
}
|
|
1306
1354
|
|
|
1307
|
-
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1308
|
-
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1309
|
-
|
|
1310
1355
|
// Get a reference to the replacement loan ID.
|
|
1311
|
-
reallocatedLoanId =
|
|
1356
|
+
reallocatedLoanId = _nextLoanIdFor(revnetId);
|
|
1312
1357
|
|
|
1313
1358
|
// Get a reference to the loan being created.
|
|
1314
1359
|
reallocatedLoan = _loanOf[reallocatedLoanId];
|
|
@@ -1364,7 +1409,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1364
1409
|
});
|
|
1365
1410
|
|
|
1366
1411
|
// Add the loaned amount back to the revnet.
|
|
1367
|
-
// slither-disable-next-line arbitrary-send-eth
|
|
1368
1412
|
loan.source.terminal.addToBalanceOf{value: payValue}({
|
|
1369
1413
|
projectId: revnetId,
|
|
1370
1414
|
token: loan.source.token,
|
|
@@ -1385,7 +1429,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1385
1429
|
/// @param collateralCountToReturn The amount of collateral to return that the loan no longer requires.
|
|
1386
1430
|
/// @param beneficiary The address to receive the returned collateral and any tokens resulting from paying fees.
|
|
1387
1431
|
/// @param loanOwner The owner of the loan NFT (receives replacement loan if partial repay).
|
|
1388
|
-
// slither-disable-next-line reentrancy-eth,reentrancy-events
|
|
1389
1432
|
function _repayLoan(
|
|
1390
1433
|
uint256 loanId,
|
|
1391
1434
|
REVLoan storage loan,
|
|
@@ -1403,7 +1446,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1403
1446
|
_burn(loanId);
|
|
1404
1447
|
|
|
1405
1448
|
// If the loan will carry no more amount or collateral, store its changes directly.
|
|
1406
|
-
// slither-disable-next-line incorrect-equality
|
|
1407
1449
|
if (collateralCountToReturn == loan.collateral) {
|
|
1408
1450
|
// Snapshot the loan to memory BEFORE _adjust zeroes the storage pointer.
|
|
1409
1451
|
REVLoan memory loanSnapshot = loan;
|
|
@@ -1440,12 +1482,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1440
1482
|
|
|
1441
1483
|
return (loanId, paidOffSnapshot);
|
|
1442
1484
|
} 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
1485
|
// Get a reference to the replacement loan ID.
|
|
1448
|
-
uint256 paidOffLoanId =
|
|
1486
|
+
uint256 paidOffLoanId = _nextLoanIdFor(revnetId);
|
|
1449
1487
|
|
|
1450
1488
|
// Get a reference to the loan being paid off.
|
|
1451
1489
|
REVLoan storage paidOffLoan = _loanOf[paidOffLoanId];
|
|
@@ -1502,7 +1540,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1502
1540
|
totalCollateralOf[revnetId] -= collateralCount;
|
|
1503
1541
|
|
|
1504
1542
|
// Mint the collateral tokens back to the loan payer.
|
|
1505
|
-
// slither-disable-next-line unused-return,calls-loop
|
|
1506
1543
|
CONTROLLER.mintTokensOf({
|
|
1507
1544
|
projectId: revnetId,
|
|
1508
1545
|
tokenCount: collateralCount,
|
|
@@ -1561,7 +1598,6 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1561
1598
|
{
|
|
1562
1599
|
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1563
1600
|
|
|
1564
|
-
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
1565
1601
|
try terminal.pay{value: payValue}({
|
|
1566
1602
|
projectId: projectId,
|
|
1567
1603
|
token: token,
|
|
@@ -1580,6 +1616,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1580
1616
|
}
|
|
1581
1617
|
}
|
|
1582
1618
|
|
|
1619
|
+
/// @notice Accepts calldata sent with native tokens so repayment helpers can refund or settle value.
|
|
1583
1620
|
fallback() external payable {}
|
|
1621
|
+
|
|
1622
|
+
/// @notice Accepts native tokens sent directly to the loan contract.
|
|
1584
1623
|
receive() external payable {}
|
|
1585
1624
|
}
|
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.
|
|
@@ -113,13 +112,16 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
113
112
|
/// @param suckerRegistry The sucker registry.
|
|
114
113
|
/// @param loans The loan contract.
|
|
115
114
|
/// @param hiddenTokens The hidden tokens contract.
|
|
115
|
+
/// @param deployerBinder The account allowed to bind the canonical deployer via `setDeployer`. Passed explicitly
|
|
116
|
+
/// because CREATE2 deployments set `msg.sender` to the factory, not the intended operator.
|
|
116
117
|
constructor(
|
|
117
118
|
IJBBuybackHookRegistry buybackHook,
|
|
118
119
|
IJBDirectory directory,
|
|
119
120
|
uint256 feeRevnetId,
|
|
120
121
|
IJBSuckerRegistry suckerRegistry,
|
|
121
122
|
IREVLoans loans,
|
|
122
|
-
IREVHiddenTokens hiddenTokens
|
|
123
|
+
IREVHiddenTokens hiddenTokens,
|
|
124
|
+
address deployerBinder
|
|
123
125
|
) {
|
|
124
126
|
BUYBACK_HOOK = buybackHook;
|
|
125
127
|
DIRECTORY = directory;
|
|
@@ -127,7 +129,7 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
127
129
|
SUCKER_REGISTRY = suckerRegistry;
|
|
128
130
|
LOANS = loans;
|
|
129
131
|
HIDDEN_TOKENS = hiddenTokens;
|
|
130
|
-
_DEPLOYER_BINDER =
|
|
132
|
+
_DEPLOYER_BINDER = deployerBinder;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
//*********************************************************************//
|
|
@@ -183,22 +185,27 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
183
185
|
uint256 cashOutDelay = cashOutDelayOf[context.projectId];
|
|
184
186
|
|
|
185
187
|
// Enforce the cash out delay.
|
|
188
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
186
189
|
if (cashOutDelay > block.timestamp) {
|
|
187
|
-
revert REVOwner_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
190
|
+
revert REVOwner_CashOutDelayNotFinished({cashOutDelay: cashOutDelay, blockTimestamp: block.timestamp});
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
// Get the terminal that will receive the cash out fee.
|
|
191
194
|
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
|
|
192
195
|
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
// Start with local supply and surplus (including collateral and borrowed amounts).
|
|
197
|
+
totalSupply = context.totalSupply + totalCollateral;
|
|
198
|
+
effectiveSurplusValue = context.surplus.value + totalBorrowed;
|
|
199
|
+
|
|
200
|
+
// If the ruleset aggregates cross-chain state, add remote supply and surplus.
|
|
201
|
+
if (!context.scopeCashOutsToLocalBalances) {
|
|
202
|
+
totalSupply += SUCKER_REGISTRY.remoteTotalSupplyOf(context.projectId);
|
|
203
|
+
effectiveSurplusValue += SUCKER_REGISTRY.remoteSurplusOf({
|
|
198
204
|
projectId: context.projectId,
|
|
199
205
|
decimals: context.surplus.decimals,
|
|
200
206
|
currency: uint256(context.surplus.currency)
|
|
201
207
|
});
|
|
208
|
+
}
|
|
202
209
|
|
|
203
210
|
// If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
|
|
204
211
|
// feeless (e.g. the router terminal routing value between projects), proxy to the buyback hook with our
|
|
@@ -209,7 +216,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
209
216
|
JBBeforeCashOutRecordedContext memory routedContext = context;
|
|
210
217
|
routedContext.totalSupply = totalSupply;
|
|
211
218
|
routedContext.surplus.value = effectiveSurplusValue;
|
|
212
|
-
// slither-disable-next-line unused-return
|
|
213
219
|
(cashOutTaxRate, cashOutCount,,, hookSpecifications) = BUYBACK_HOOK.beforeCashOutRecordedWith(routedContext);
|
|
214
220
|
return (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
|
|
215
221
|
}
|
|
@@ -261,7 +267,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
261
267
|
|
|
262
268
|
// Let the buyback hook adjust the cash out parameters and optionally return a hook specification.
|
|
263
269
|
JBCashOutHookSpecification[] memory buybackHookSpecifications;
|
|
264
|
-
// slither-disable-next-line unused-return
|
|
265
270
|
(cashOutTaxRate, cashOutCount,,, buybackHookSpecifications) =
|
|
266
271
|
BUYBACK_HOOK.beforeCashOutRecordedWith(buybackHookContext);
|
|
267
272
|
|
|
@@ -316,7 +321,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
316
321
|
bool usesTiered721Hook = address(tiered721Hook) != address(0);
|
|
317
322
|
if (usesTiered721Hook) {
|
|
318
323
|
JBPayHookSpecification[] memory specs;
|
|
319
|
-
// slither-disable-next-line unused-return
|
|
320
324
|
(, specs) = IJBRulesetDataHook(address(tiered721Hook)).beforePayRecordedWith(context);
|
|
321
325
|
// The 721 hook returns a single spec (itself) whose amount is the total split amount.
|
|
322
326
|
if (specs.length > 0) {
|
|
@@ -429,7 +433,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
429
433
|
});
|
|
430
434
|
|
|
431
435
|
// Pay the fee.
|
|
432
|
-
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
433
436
|
try feeTerminal.pay{value: payValue}({
|
|
434
437
|
projectId: FEE_REVNET_ID,
|
|
435
438
|
token: context.forwardedAmount.token,
|
|
@@ -454,7 +457,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
454
457
|
to: msg.sender, token: context.forwardedAmount.token, amount: context.forwardedAmount.value
|
|
455
458
|
});
|
|
456
459
|
|
|
457
|
-
// slither-disable-next-line arbitrary-send-eth
|
|
458
460
|
IJBTerminal(msg.sender).addToBalanceOf{value: payValue}({
|
|
459
461
|
projectId: context.projectId,
|
|
460
462
|
token: context.forwardedAmount.token,
|
|
@@ -474,9 +476,11 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
474
476
|
/// @param deployer The canonical REVDeployer instance that will manage revnet runtime state.
|
|
475
477
|
function setDeployer(IREVDeployer deployer) external {
|
|
476
478
|
// Only the account that deployed this REVOwner may complete the one-time deployer binding.
|
|
477
|
-
if (msg.sender != _DEPLOYER_BINDER)
|
|
479
|
+
if (msg.sender != _DEPLOYER_BINDER) {
|
|
480
|
+
revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: _DEPLOYER_BINDER});
|
|
481
|
+
}
|
|
478
482
|
// Prevent the deployer binding from being overwritten after initialization.
|
|
479
|
-
if (address(DEPLOYER) != address(0)) revert REVOwner_AlreadyInitialized();
|
|
483
|
+
if (address(DEPLOYER) != address(0)) revert REVOwner_AlreadyInitialized({deployer: address(DEPLOYER)});
|
|
480
484
|
// Store the canonical REVDeployer that is authorized to manage runtime hook state.
|
|
481
485
|
DEPLOYER = deployer;
|
|
482
486
|
}
|
|
@@ -486,7 +490,9 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
486
490
|
/// @param revnetId The ID of the revnet.
|
|
487
491
|
/// @param cashOutDelay The timestamp after which cash outs are allowed.
|
|
488
492
|
function setCashOutDelayOf(uint256 revnetId, uint256 cashOutDelay) external {
|
|
489
|
-
if (msg.sender != address(DEPLOYER))
|
|
493
|
+
if (msg.sender != address(DEPLOYER)) {
|
|
494
|
+
revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(DEPLOYER)});
|
|
495
|
+
}
|
|
490
496
|
cashOutDelayOf[revnetId] = cashOutDelay;
|
|
491
497
|
}
|
|
492
498
|
|
|
@@ -495,7 +501,9 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
495
501
|
/// @param revnetId The ID of the revnet.
|
|
496
502
|
/// @param hook The tiered ERC-721 hook.
|
|
497
503
|
function setTiered721HookOf(uint256 revnetId, IJB721TiersHook hook) external {
|
|
498
|
-
if (msg.sender != address(DEPLOYER))
|
|
504
|
+
if (msg.sender != address(DEPLOYER)) {
|
|
505
|
+
revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(DEPLOYER)});
|
|
506
|
+
}
|
|
499
507
|
tiered721HookOf[revnetId] = hook;
|
|
500
508
|
}
|
|
501
509
|
|
|
@@ -552,13 +560,11 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
552
560
|
for (uint256 i; i < sources.length; i++) {
|
|
553
561
|
REVLoanSource memory source = sources[i];
|
|
554
562
|
// Each configured source must be queried live so cash-out math includes current outstanding debt.
|
|
555
|
-
// slither-disable-next-line calls-loop
|
|
556
563
|
uint256 tokensLoaned =
|
|
557
564
|
loans.totalBorrowedFrom({revnetId: revnetId, terminal: source.terminal, token: source.token});
|
|
558
565
|
if (tokensLoaned == 0) continue;
|
|
559
566
|
|
|
560
567
|
// Read the source token's accounting context so debt can be normalized before cross-currency conversion.
|
|
561
|
-
// slither-disable-next-line calls-loop
|
|
562
568
|
JBAccountingContext memory accountingContext =
|
|
563
569
|
source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
|
|
564
570
|
|
|
@@ -576,7 +582,6 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
|
|
|
576
582
|
borrowedAmount += normalizedTokens;
|
|
577
583
|
} else {
|
|
578
584
|
// Convert source-token debt into the requested currency using the loans contract's shared prices.
|
|
579
|
-
// slither-disable-next-line calls-loop
|
|
580
585
|
uint256 pricePerUnit = loans.PRICES()
|
|
581
586
|
.pricePerUnitOf({
|
|
582
587
|
projectId: revnetId,
|
|
@@ -11,10 +11,13 @@ import {REVStageConfig} from "./REVStageConfig.sol";
|
|
|
11
11
|
/// @custom:member baseCurrency The currency that issuance pricing is denominated in (e.g. ETH or USD).
|
|
12
12
|
/// @custom:member splitOperator The address that receives production splits and can reassign the operator role.
|
|
13
13
|
/// Only the current operator can replace itself after deployment.
|
|
14
|
+
/// @custom:member scopeCashOutsToLocalBalances If true, cash-out calculations use only the local terminal's surplus.
|
|
15
|
+
/// When false, the bonding curve considers surplus from every terminal across all chains.
|
|
14
16
|
/// @custom:member stageConfigurations The ordered stages that define how the revnet's tokenomics evolve over time.
|
|
15
17
|
struct REVConfig {
|
|
16
18
|
REVDescription description;
|
|
17
19
|
uint32 baseCurrency;
|
|
18
20
|
address splitOperator;
|
|
21
|
+
bool scopeCashOutsToLocalBalances;
|
|
19
22
|
REVStageConfig[] stageConfigurations;
|
|
20
23
|
}
|