@rev-net/core-v6 0.0.42 → 0.0.44

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