@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 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/audit/PhantomSurplusTerminal.t.sol`
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, audit, and regression coverage
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.43",
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.42",
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",
@@ -266,6 +266,7 @@ REVConfig memory config = REVConfig({
266
266
  }),
267
267
  baseCurrency: 1, // ETH
268
268
  splitOperator: msg.sender,
269
+ scopeCashOutsToLocalBalances: false,
269
270
  stageConfigurations: stages
270
271
  });
271
272
 
@@ -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.
@@ -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.useTotalSurplusForCashOuts = true; // Use surplus from all terminals for cash outs.
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(uint256 revnetId, address terminalToken, uint112 initialIssuance) internal {
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 / 1e18
417
- sqrtPriceX96 = uint160(sqrt(mulDiv(uint256(initialIssuance), 1 << 192, 1e18)));
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 = 1e18 / issuance
420
- sqrtPriceX96 = uint160(sqrt(mulDiv(1e18, 1 << 192, uint256(initialIssuance))));
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) revert REVDeployer_NothingToAutoIssue();
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
- // slither-disable-next-line reentrancy-events
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) revert REVDeployer_StagesRequired();
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, JBConstants.MAX_CASH_OUT_TAX_RATE
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)) revert REVDeployer_AutoIssuanceBeneficiaryZeroAddress();
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()});
@@ -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
- // Add cross-chain remote values for proportional reclaim.
373
- uint256 omnichainSurplus = localSurplus
374
- + SUCKER_REGISTRY.remoteSurplusOf({projectId: revnetId, decimals: decimals, currency: currency});
375
- uint256 omnichainSupply = localSupply + SUCKER_REGISTRY.remoteTotalSupplyOf(revnetId);
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: omnichainSurplus,
385
+ surplus: effectiveSurplus,
378
386
  cashOutCount: collateralCount,
379
- totalSupply: omnichainSupply,
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(timeSinceLoanCreated, LOAN_LIQUIDATION_DURATION);
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
- if (startingLoanId + count > _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
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(block.timestamp - _loanOf[loanId].createdAt, LOAN_LIQUIDATION_DURATION);
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(collateralCountToReturn, loan.collateral);
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(newBorrowAmount, loan.amount);
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) revert REVLoans_NothingToRepay();
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(maxRepayBorrowAmount, repayBorrowAmount);
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 = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
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) revert REVLoans_ZeroBorrowAmount();
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) revert REVLoans_NotEnoughCollateral();
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(borrowAmount, loan.amount);
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 = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
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 = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
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 = msg.sender;
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
- // Compute the cross-chain total supply (local + remote peer chain supplies) for cross-chain-aware bonding
194
- // curve.
195
- totalSupply = context.totalSupply + totalCollateral + SUCKER_REGISTRY.remoteTotalSupplyOf(context.projectId);
196
- effectiveSurplusValue = context.surplus.value + totalBorrowed
197
- + SUCKER_REGISTRY.remoteSurplusOf({
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) revert REVOwner_Unauthorized();
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)) revert REVOwner_Unauthorized();
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)) revert REVOwner_Unauthorized();
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
  }