@rev-net/core-v6 0.0.11 → 0.0.13

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.
Files changed (81) hide show
  1. package/ADMINISTRATION.md +7 -7
  2. package/ARCHITECTURE.md +11 -11
  3. package/AUDIT_INSTRUCTIONS.md +295 -0
  4. package/CHANGE_LOG.md +316 -0
  5. package/README.md +9 -6
  6. package/RISKS.md +180 -35
  7. package/SKILLS.md +9 -11
  8. package/STYLE_GUIDE.md +14 -1
  9. package/USER_JOURNEYS.md +489 -0
  10. package/package.json +9 -9
  11. package/script/Deploy.s.sol +124 -40
  12. package/script/helpers/RevnetCoreDeploymentLib.sol +19 -6
  13. package/src/REVDeployer.sol +183 -175
  14. package/src/REVLoans.sol +65 -28
  15. package/src/interfaces/IREVDeployer.sol +25 -23
  16. package/src/structs/REV721TiersHookFlags.sol +1 -0
  17. package/src/structs/REVAutoIssuance.sol +1 -0
  18. package/src/structs/REVBaseline721HookConfig.sol +1 -0
  19. package/src/structs/REVConfig.sol +1 -0
  20. package/src/structs/REVCroptopAllowedPost.sol +1 -0
  21. package/src/structs/REVDeploy721TiersHookConfig.sol +13 -14
  22. package/src/structs/REVDescription.sol +1 -0
  23. package/src/structs/REVLoan.sol +1 -0
  24. package/src/structs/REVLoanSource.sol +1 -0
  25. package/src/structs/REVStageConfig.sol +1 -0
  26. package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
  27. package/test/REV.integrations.t.sol +148 -19
  28. package/test/REVAutoIssuanceFuzz.t.sol +31 -6
  29. package/test/REVDeployerRegressions.t.sol +47 -9
  30. package/test/REVInvincibility.t.sol +83 -19
  31. package/test/REVInvincibilityHandler.sol +29 -0
  32. package/test/REVLifecycle.t.sol +36 -6
  33. package/test/REVLoans.invariants.t.sol +64 -10
  34. package/test/REVLoansAttacks.t.sol +54 -9
  35. package/test/REVLoansFeeRecovery.t.sol +61 -15
  36. package/test/REVLoansFindings.t.sol +42 -9
  37. package/test/REVLoansRegressions.t.sol +33 -6
  38. package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
  39. package/test/REVLoansSourced.t.sol +79 -17
  40. package/test/REVLoansUnSourced.t.sol +61 -10
  41. package/test/TestBurnHeldTokens.t.sol +47 -11
  42. package/test/TestCEIPattern.t.sol +37 -6
  43. package/test/TestCashOutCallerValidation.t.sol +41 -8
  44. package/test/TestConversionDocumentation.t.sol +50 -13
  45. package/test/TestCrossCurrencyReclaim.t.sol +584 -0
  46. package/test/TestCrossSourceReallocation.t.sol +37 -6
  47. package/test/TestERC2771MetaTx.t.sol +557 -0
  48. package/test/TestEmptyBuybackSpecs.t.sol +45 -10
  49. package/test/TestFlashLoanSurplus.t.sol +39 -7
  50. package/test/TestHookArrayOOB.t.sol +42 -13
  51. package/test/TestLiquidationBehavior.t.sol +37 -7
  52. package/test/TestLoanSourceRotation.t.sol +525 -0
  53. package/test/TestLongTailEconomics.t.sol +651 -0
  54. package/test/TestLowFindings.t.sol +80 -8
  55. package/test/TestMixedFixes.t.sol +43 -9
  56. package/test/TestPermit2Signatures.t.sol +657 -0
  57. package/test/TestReallocationSandwich.t.sol +384 -0
  58. package/test/TestRevnetRegressions.t.sol +324 -0
  59. package/test/TestSplitWeightAdjustment.t.sol +52 -13
  60. package/test/TestSplitWeightE2E.t.sol +53 -18
  61. package/test/TestSplitWeightFork.t.sol +66 -21
  62. package/test/TestStageTransitionBorrowable.t.sol +38 -6
  63. package/test/TestSwapTerminalPermission.t.sol +37 -7
  64. package/test/TestUint112Overflow.t.sol +39 -6
  65. package/test/TestZeroRepayment.t.sol +37 -6
  66. package/test/fork/ForkTestBase.sol +66 -17
  67. package/test/fork/TestCashOutFork.t.sol +9 -3
  68. package/test/fork/TestLoanBorrowFork.t.sol +1 -0
  69. package/test/fork/TestLoanCrossRulesetFork.t.sol +11 -3
  70. package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
  71. package/test/fork/TestLoanReallocateFork.t.sol +1 -0
  72. package/test/fork/TestLoanRepayFork.t.sol +1 -0
  73. package/test/fork/TestLoanTransferFork.t.sol +133 -0
  74. package/test/fork/TestSplitWeightFork.t.sol +3 -0
  75. package/test/helpers/REVEmpty721Config.sol +46 -0
  76. package/test/mock/MockBuybackDataHook.sol +1 -0
  77. package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
  78. package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
  79. package/test/regression/TestCumulativeLoanCounter.t.sol +38 -8
  80. package/test/regression/TestLiquidateGapHandling.t.sol +40 -8
  81. package/test/regression/TestZeroPriceFeed.t.sol +396 -0
@@ -1,12 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
5
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6
- import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7
- import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
8
- import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
9
- import {mulDiv} from "@prb/math/src/Common.sol";
10
4
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
11
5
  import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
12
6
  import {JB721TiersHookFlags} from "@bananapus/721-hook-v6/src/structs/JB721TiersHookFlags.sol";
@@ -15,13 +9,11 @@ import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/
15
9
  import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
16
10
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
17
11
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
18
- import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
19
12
  import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
20
13
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
21
14
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
22
15
  import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
23
16
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
24
- import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
25
17
  import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
26
18
  import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
27
19
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
@@ -38,14 +30,18 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
38
30
  import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
39
31
  import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
40
32
  import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
41
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
42
- import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
43
33
  import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
44
34
  import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
45
35
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
46
36
  import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
47
37
  import {CTPublisher} from "@croptop/core-v6/src/CTPublisher.sol";
48
38
  import {CTAllowedPost} from "@croptop/core-v6/src/structs/CTAllowedPost.sol";
39
+ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
40
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
41
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
42
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
43
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
44
+ import {mulDiv} from "@prb/math/src/Common.sol";
49
45
 
50
46
  import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
51
47
  import {REVAutoIssuance} from "./structs/REVAutoIssuance.sol";
@@ -224,6 +220,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
224
220
  });
225
221
 
226
222
  // Give the loan contract permission to use the surplus allowance of all revnets.
223
+ // Uses wildcard revnetId=0 intentionally — the loan contract is a singleton shared by all revnets,
224
+ // and each revnet's surplus allowance limits already constrain how much can be drawn.
227
225
  _setPermission({operator: LOANS, revnetId: 0, permissionId: JBPermissionIds.USE_ALLOWANCE});
228
226
 
229
227
  // Give the buyback hook (registry) permission to configure pools on all revnets.
@@ -255,6 +253,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
255
253
  )
256
254
  {
257
255
  // If the cash out is from a sucker, return the full cash out amount without taxes or fees.
256
+ // This relies on the sucker registry to only contain trusted sucker contracts deployed via
257
+ // the registry's own deploySuckersFor flow — external addresses cannot register as suckers.
258
258
  if (_isSuckerOf({revnetId: context.projectId, addr: context.holder})) {
259
259
  return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
260
260
  }
@@ -270,13 +270,16 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
270
270
  // Get the terminal that will receive the cash out fee.
271
271
  IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
272
272
 
273
- // If there's no cash out tax (100% cash out tax rate), or if there's no fee terminal, do not charge a fee.
274
- if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0)) {
273
+ // If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
274
+ // feeless (e.g. the router terminal routing value between projects), do not charge a fee.
275
+ if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
275
276
  return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, hookSpecifications);
276
277
  }
277
278
 
278
279
  // Get a reference to the number of tokens being used to pay the fee (out of the total being cashed out).
279
- uint256 feeCashOutCount = mulDiv(context.cashOutCount, FEE, JBConstants.MAX_FEE);
280
+ // Micro cash outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero, bypassing the fee.
281
+ // Economically insignificant: the gas cost of the transaction far exceeds the bypassed fee. No fix needed.
282
+ uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
280
283
  uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
281
284
 
282
285
  // Keep a reference to the amount claimable with non-fee tokens.
@@ -353,7 +356,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
353
356
  if (projectAmount == 0) {
354
357
  weight = 0;
355
358
  } else if (projectAmount < context.amount.value) {
356
- weight = mulDiv(weight, projectAmount, context.amount.value);
359
+ weight = mulDiv({x: weight, y: projectAmount, denominator: context.amount.value});
357
360
  }
358
361
 
359
362
  // Merge hook specifications: 721 hook spec first, then buyback hook spec.
@@ -483,26 +486,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
483
486
  }
484
487
  }
485
488
 
486
- /// @notice Try to initialize a Uniswap V4 buyback pool for a terminal token at a generic 1:1 price.
487
- /// @dev Called after the ERC-20 token is deployed so the pool can be initialized in the PoolManager.
488
- /// Silently catches failures (e.g., if the pool is already initialized).
489
- /// @param revnetId The ID of the revnet.
490
- /// @param terminalToken The terminal token to initialize a buyback pool for.
491
- function _tryInitializeBuybackPoolFor(uint256 revnetId, address terminalToken) internal {
492
- // Try to initialize the pool at a generic 1:1 sqrtPriceX96 and configure the buyback hook.
493
- // The buyback hook constructs the PoolKey internally from the project token, terminal token, and pool params.
494
- // slither-disable-next-line calls-loop
495
- try BUYBACK_HOOK.initializePoolFor({
496
- projectId: revnetId,
497
- fee: DEFAULT_BUYBACK_POOL_FEE,
498
- tickSpacing: DEFAULT_BUYBACK_TICK_SPACING,
499
- twapWindow: DEFAULT_BUYBACK_TWAP_WINDOW,
500
- terminalToken: terminalToken,
501
- sqrtPriceX96: uint160(1 << 96)
502
- }) {}
503
- catch {} // Pool may already be initialized — that's OK.
504
- }
505
-
506
489
  /// @notice Make a ruleset configuration for a revnet's stage.
507
490
  /// @param baseCurrency The base currency of the revnet.
508
491
  /// @param stageConfiguration The stage configuration to make a ruleset for.
@@ -565,7 +548,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
565
548
  uint256[] memory customSplitOperatorPermissionIndexes = _extraOperatorPermissions[revnetId];
566
549
 
567
550
  // Make the array that merges the default and custom operator permissions.
568
- allOperatorPermissions = new uint256[](8 + customSplitOperatorPermissionIndexes.length);
551
+ allOperatorPermissions = new uint256[](9 + customSplitOperatorPermissionIndexes.length);
569
552
  allOperatorPermissions[0] = JBPermissionIds.SET_SPLIT_GROUPS;
570
553
  allOperatorPermissions[1] = JBPermissionIds.SET_BUYBACK_POOL;
571
554
  allOperatorPermissions[2] = JBPermissionIds.SET_BUYBACK_TWAP;
@@ -574,13 +557,34 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
574
557
  allOperatorPermissions[5] = JBPermissionIds.SUCKER_SAFETY;
575
558
  allOperatorPermissions[6] = JBPermissionIds.SET_BUYBACK_HOOK;
576
559
  allOperatorPermissions[7] = JBPermissionIds.SET_ROUTER_TERMINAL;
560
+ allOperatorPermissions[8] = JBPermissionIds.SET_TOKEN_METADATA;
577
561
 
578
562
  // Copy the custom permissions into the array.
579
563
  for (uint256 i; i < customSplitOperatorPermissionIndexes.length; i++) {
580
- allOperatorPermissions[8 + i] = customSplitOperatorPermissionIndexes[i];
564
+ allOperatorPermissions[9 + i] = customSplitOperatorPermissionIndexes[i];
581
565
  }
582
566
  }
583
567
 
568
+ /// @notice Try to initialize a Uniswap V4 buyback pool for a terminal token at a generic 1:1 price.
569
+ /// @dev Called after the ERC-20 token is deployed so the pool can be initialized in the PoolManager.
570
+ /// Silently catches failures (e.g., if the pool is already initialized).
571
+ /// @param revnetId The ID of the revnet.
572
+ /// @param terminalToken The terminal token to initialize a buyback pool for.
573
+ function _tryInitializeBuybackPoolFor(uint256 revnetId, address terminalToken) internal {
574
+ // Try to initialize the pool at a generic 1:1 sqrtPriceX96 and configure the buyback hook.
575
+ // The buyback hook constructs the PoolKey internally from the project token, terminal token, and pool params.
576
+ // slither-disable-next-line calls-loop
577
+ try BUYBACK_HOOK.initializePoolFor({
578
+ projectId: revnetId,
579
+ fee: DEFAULT_BUYBACK_POOL_FEE,
580
+ tickSpacing: DEFAULT_BUYBACK_TICK_SPACING,
581
+ twapWindow: DEFAULT_BUYBACK_TWAP_WINDOW,
582
+ terminalToken: terminalToken,
583
+ sqrtPriceX96: uint160(1 << 96)
584
+ }) {}
585
+ catch {} // Pool may already be initialized — that's OK.
586
+ }
587
+
584
588
  //*********************************************************************//
585
589
  // --------------------- external transactions ----------------------- //
586
590
  //*********************************************************************//
@@ -648,7 +652,13 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
648
652
  /// @param stageId The ID of the stage auto-mint tokens are available from.
649
653
  /// @param beneficiary The address to auto-mint tokens to.
650
654
  function autoIssueFor(uint256 revnetId, uint256 stageId, address beneficiary) external override {
651
- // Get a reference to the ruleset for the stage.
655
+ // Get the ruleset for the stage to check if it has started.
656
+ // Stage IDs are `block.timestamp + i` where `i` is the stage index. These match real JB ruleset IDs
657
+ // because JBRulesets assigns IDs the same way: `latestId >= block.timestamp ? latestId + 1 : block.timestamp`
658
+ // (see JBRulesets.sol L172). When all stages are queued in a single deployFor() call, the sequential
659
+ // IDs `block.timestamp`, `block.timestamp + 1`, ... exactly correspond to the JB-assigned ruleset IDs.
660
+ // The returned `ruleset.start` contains the derived start time (from `deriveStartFrom` using the stage's
661
+ // `mustStartAtOrAfter`), NOT the queue timestamp — so the timing guard correctly blocks early claims.
652
662
  // slither-disable-next-line unused-return
653
663
  (JBRuleset memory ruleset,) = CONTROLLER.getRulesetOf({projectId: revnetId, rulesetId: stageId});
654
664
 
@@ -677,6 +687,17 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
677
687
  });
678
688
  }
679
689
 
690
+ /// @notice Burn any of a revnet's tokens held by this contract.
691
+ /// @dev Project tokens can end up here from reserved token distribution when splits don't sum to 100%.
692
+ /// @param revnetId The ID of the revnet whose tokens should be burned.
693
+ function burnHeldTokensOf(uint256 revnetId) external override {
694
+ uint256 balance = CONTROLLER.TOKENS().totalBalanceOf({holder: address(this), projectId: revnetId});
695
+ if (balance == 0) revert REVDeployer_NothingToBurn();
696
+ CONTROLLER.burnTokensOf({holder: address(this), projectId: revnetId, tokenCount: balance, memo: ""});
697
+ // slither-disable-next-line reentrancy-events
698
+ emit BurnHeldTokens(revnetId, balance, _msgSender());
699
+ }
700
+
680
701
  /// @notice Launch a revnet, or initialize an existing Juicebox project as a revnet.
681
702
  /// @dev When initializing an existing project (revnetId != 0):
682
703
  /// - The project must not yet have a controller or rulesets. `JBController.launchRulesetsFor` enforces this —
@@ -691,16 +712,21 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
691
712
  /// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
692
713
  /// @param suckerDeploymentConfiguration The suckers to set up for the revnet. Suckers facilitate cross-chain
693
714
  /// token transfers between peer revnets on different networks.
715
+ /// @param tiered721HookConfiguration How to set up the tiered ERC-721 hook for the revnet.
716
+ /// @param allowedPosts Restrictions on which croptop posts are allowed on the revnet's ERC-721 tiers.
694
717
  /// @return revnetId The ID of the newly created revnet.
718
+ /// @return hook The address of the tiered ERC-721 hook that was deployed for the revnet.
695
719
  function deployFor(
696
720
  uint256 revnetId,
697
721
  REVConfig calldata configuration,
698
722
  JBTerminalConfig[] calldata terminalConfigurations,
699
- REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration
723
+ REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
724
+ REVDeploy721TiersHookConfig calldata tiered721HookConfiguration,
725
+ REVCroptopAllowedPost[] calldata allowedPosts
700
726
  )
701
727
  external
702
728
  override
703
- returns (uint256)
729
+ returns (uint256, IJB721TiersHook hook)
704
730
  {
705
731
  // Keep a reference to the revnet ID which was passed in.
706
732
  bool shouldDeployNewRevnet = revnetId == 0;
@@ -709,34 +735,69 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
709
735
  // (which will be 1 greater than the current count).
710
736
  if (shouldDeployNewRevnet) revnetId = _nextProjectId();
711
737
 
712
- // Normalize and encode the configurations.
713
- (JBRulesetConfig[] memory rulesetConfigurations, bytes32 encodedConfigurationHash) = _makeRulesetConfigurations({
714
- revnetId: revnetId, configuration: configuration, terminalConfigurations: terminalConfigurations
715
- });
716
-
717
- // Deploy the revnet.
718
- _deployRevnetFor({
738
+ // Deploy the revnet with the specified tiered ERC-721 hook and croptop posting criteria.
739
+ hook = _deploy721RevnetFor({
719
740
  revnetId: revnetId,
720
741
  shouldDeployNewRevnet: shouldDeployNewRevnet,
721
742
  configuration: configuration,
722
743
  terminalConfigurations: terminalConfigurations,
723
744
  suckerDeploymentConfiguration: suckerDeploymentConfiguration,
724
- rulesetConfigurations: rulesetConfigurations,
725
- encodedConfigurationHash: encodedConfigurationHash
745
+ tiered721HookConfiguration: tiered721HookConfiguration,
746
+ allowedPosts: allowedPosts
726
747
  });
727
748
 
728
- return revnetId;
749
+ return (revnetId, hook);
729
750
  }
730
751
 
731
- /// @notice Burn any of a revnet's tokens held by this contract.
732
- /// @dev Project tokens can end up here from reserved token distribution when splits don't sum to 100%.
733
- /// @param revnetId The ID of the revnet whose tokens should be burned.
734
- function burnHeldTokensOf(uint256 revnetId) external override {
735
- uint256 balance = CONTROLLER.TOKENS().totalBalanceOf({holder: address(this), projectId: revnetId});
736
- if (balance == 0) revert REVDeployer_NothingToBurn();
737
- CONTROLLER.burnTokensOf({holder: address(this), projectId: revnetId, tokenCount: balance, memo: ""});
738
- // slither-disable-next-line reentrancy-events
739
- emit BurnHeldTokens(revnetId, balance, _msgSender());
752
+ /// @inheritdoc IREVDeployer
753
+ function deployFor(
754
+ uint256 revnetId,
755
+ REVConfig calldata configuration,
756
+ JBTerminalConfig[] calldata terminalConfigurations,
757
+ REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration
758
+ )
759
+ external
760
+ override
761
+ returns (uint256, IJB721TiersHook hook)
762
+ {
763
+ bool shouldDeployNewRevnet = revnetId == 0;
764
+ if (shouldDeployNewRevnet) revnetId = _nextProjectId();
765
+
766
+ // Deploy the revnet (project, rulesets, ERC-20, suckers, etc.).
767
+ bytes32 encodedConfigurationHash = _deployRevnetFor({
768
+ revnetId: revnetId,
769
+ shouldDeployNewRevnet: shouldDeployNewRevnet,
770
+ configuration: configuration,
771
+ terminalConfigurations: terminalConfigurations,
772
+ suckerDeploymentConfiguration: suckerDeploymentConfiguration
773
+ });
774
+
775
+ // Deploy a default empty 721 hook for the revnet.
776
+ {
777
+ JBDeploy721TiersHookConfig memory deployConfig;
778
+ deployConfig.tiersConfig.currency = configuration.baseCurrency;
779
+ deployConfig.tiersConfig.decimals = 18;
780
+
781
+ hook = HOOK_DEPLOYER.deployHookFor({
782
+ projectId: revnetId,
783
+ deployTiersHookConfig: deployConfig,
784
+ salt: keccak256(abi.encode(bytes32(0), encodedConfigurationHash, _msgSender()))
785
+ });
786
+ }
787
+
788
+ // Store the tiered ERC-721 hook.
789
+ tiered721HookOf[revnetId] = hook;
790
+
791
+ // Grant the split operator all 721 permissions (no prevent* flags for default config).
792
+ _extraOperatorPermissions[revnetId].push(JBPermissionIds.ADJUST_721_TIERS);
793
+ _extraOperatorPermissions[revnetId].push(JBPermissionIds.SET_721_METADATA);
794
+ _extraOperatorPermissions[revnetId].push(JBPermissionIds.MINT_721);
795
+ _extraOperatorPermissions[revnetId].push(JBPermissionIds.SET_721_DISCOUNT_PERCENT);
796
+
797
+ // Give the split operator their permissions (base + 721 extras).
798
+ _setSplitOperatorOf({revnetId: revnetId, operator: configuration.splitOperator});
799
+
800
+ return (revnetId, hook);
740
801
  }
741
802
 
742
803
  /// @notice Deploy new suckers for an existing revnet.
@@ -775,51 +836,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
775
836
  });
776
837
  }
777
838
 
778
- /// @notice Launch a revnet which sells tiered ERC-721s and (optionally) allows croptop posts to its ERC-721 tiers.
779
- /// @dev When initializing an existing project (revnetId != 0), the project must be blank (no controller or
780
- /// rulesets). The initialization is irreversible. See `deployFor` documentation for full details.
781
- /// @param revnetId The ID of the Juicebox project to initialize as a revnet. Send 0 to deploy a new revnet.
782
- /// @param configuration Core revnet configuration. See `REVConfig`.
783
- /// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
784
- /// @param suckerDeploymentConfiguration The suckers to set up for the revnet. Suckers facilitate cross-chain
785
- /// token transfers between peer revnets on different networks.
786
- /// @param tiered721HookConfiguration How to set up the tiered ERC-721 hook for the revnet.
787
- /// @param allowedPosts Restrictions on which croptop posts are allowed on the revnet's ERC-721 tiers.
788
- /// @return revnetId The ID of the newly created revnet.
789
- /// @return hook The address of the tiered ERC-721 hook that was deployed for the revnet.
790
- function deployWith721sFor(
791
- uint256 revnetId,
792
- REVConfig calldata configuration,
793
- JBTerminalConfig[] calldata terminalConfigurations,
794
- REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
795
- REVDeploy721TiersHookConfig calldata tiered721HookConfiguration,
796
- REVCroptopAllowedPost[] calldata allowedPosts
797
- )
798
- external
799
- override
800
- returns (uint256, IJB721TiersHook hook)
801
- {
802
- // Keep a reference to the revnet ID which was passed in.
803
- bool shouldDeployNewRevnet = revnetId == 0;
804
-
805
- // If the caller is deploying a new revnet, calculate its ID
806
- // (which will be 1 greater than the current count).
807
- if (shouldDeployNewRevnet) revnetId = _nextProjectId();
808
-
809
- // Deploy the revnet with the specified tiered ERC-721 hook and croptop posting criteria.
810
- hook = _deploy721RevnetFor({
811
- revnetId: revnetId,
812
- shouldDeployNewRevnet: shouldDeployNewRevnet,
813
- configuration: configuration,
814
- terminalConfigurations: terminalConfigurations,
815
- suckerDeploymentConfiguration: suckerDeploymentConfiguration,
816
- tiered721HookConfiguration: tiered721HookConfiguration,
817
- allowedPosts: allowedPosts
818
- });
819
-
820
- return (revnetId, hook);
821
- }
822
-
823
839
  /// @notice Change a revnet's split operator.
824
840
  /// @dev Only a revnet's current split operator can set a new split operator.
825
841
  /// @param revnetId The ID of the revnet to set the split operator of.
@@ -852,91 +868,86 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
852
868
  function _beforeTransferTo(address to, address token, uint256 amount) internal returns (uint256) {
853
869
  // If the token is the native token, no allowance needed.
854
870
  if (token == JBConstants.NATIVE_TOKEN) return amount;
855
- IERC20(token).safeIncreaseAllowance(to, amount);
871
+ IERC20(token).safeIncreaseAllowance({spender: to, value: amount});
856
872
  return 0;
857
873
  }
858
874
 
859
875
  /// @notice Deploy a revnet which sells tiered ERC-721s and (optionally) allows croptop posts to its ERC-721 tiers.
860
- /// @param revnetId The ID of the Juicebox project to turn into a revnet. Send 0 to deploy a new revnet.
861
- /// @param shouldDeployNewRevnet Whether to deploy a new revnet or convert an existing Juicebox project into a
862
- /// revnet.
863
- /// @param configuration Core revnet configuration. See `REVConfig`.
864
- /// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
865
- /// @param suckerDeploymentConfiguration The suckers to set up for the revnet. Suckers facilitate cross-chain
866
- /// token transfers between peer revnets on different networks.
867
- /// @param tiered721HookConfiguration How to set up the tiered ERC-721 hook for the revnet.
868
- /// @param allowedPosts Restrictions on which croptop posts are allowed on the revnet's ERC-721 tiers.
869
- /// @return hook The address of the tiered ERC-721 hook that was deployed for the revnet.
870
876
  function _deploy721RevnetFor(
871
877
  uint256 revnetId,
872
878
  bool shouldDeployNewRevnet,
873
879
  REVConfig calldata configuration,
874
880
  JBTerminalConfig[] calldata terminalConfigurations,
875
881
  REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
876
- REVDeploy721TiersHookConfig calldata tiered721HookConfiguration,
877
- REVCroptopAllowedPost[] calldata allowedPosts
882
+ REVDeploy721TiersHookConfig memory tiered721HookConfiguration,
883
+ REVCroptopAllowedPost[] memory allowedPosts
878
884
  )
879
885
  internal
880
886
  returns (IJB721TiersHook hook)
881
887
  {
882
- // Normalize and encode the configurations.
883
- (JBRulesetConfig[] memory rulesetConfigurations, bytes32 encodedConfigurationHash) = _makeRulesetConfigurations({
884
- revnetId: revnetId, configuration: configuration, terminalConfigurations: terminalConfigurations
888
+ // Deploy the revnet (project, rulesets, ERC-20, suckers, etc.).
889
+ bytes32 encodedConfigurationHash = _deployRevnetFor({
890
+ revnetId: revnetId,
891
+ shouldDeployNewRevnet: shouldDeployNewRevnet,
892
+ configuration: configuration,
893
+ terminalConfigurations: terminalConfigurations,
894
+ suckerDeploymentConfiguration: suckerDeploymentConfiguration
885
895
  });
886
896
 
887
897
  // Convert the REVBaseline721HookConfig to JBDeploy721TiersHookConfig, forcing issueTokensForSplits to false.
888
898
  // Revnets do their own weight adjustment for splits, so the 721 hook must not also adjust.
889
- JBDeploy721TiersHookConfig memory hookConfig = JBDeploy721TiersHookConfig({
890
- name: tiered721HookConfiguration.baseline721HookConfiguration.name,
891
- symbol: tiered721HookConfiguration.baseline721HookConfiguration.symbol,
892
- baseUri: tiered721HookConfiguration.baseline721HookConfiguration.baseUri,
893
- tokenUriResolver: tiered721HookConfiguration.baseline721HookConfiguration.tokenUriResolver,
894
- contractUri: tiered721HookConfiguration.baseline721HookConfiguration.contractUri,
895
- tiersConfig: tiered721HookConfiguration.baseline721HookConfiguration.tiersConfig,
896
- reserveBeneficiary: tiered721HookConfiguration.baseline721HookConfiguration.reserveBeneficiary,
897
- flags: JB721TiersHookFlags({
898
- noNewTiersWithReserves: tiered721HookConfiguration.baseline721HookConfiguration.flags
899
- .noNewTiersWithReserves,
900
- noNewTiersWithVotes: tiered721HookConfiguration.baseline721HookConfiguration.flags.noNewTiersWithVotes,
901
- noNewTiersWithOwnerMinting: tiered721HookConfiguration.baseline721HookConfiguration.flags
902
- .noNewTiersWithOwnerMinting,
903
- preventOverspending: tiered721HookConfiguration.baseline721HookConfiguration.flags.preventOverspending,
904
- issueTokensForSplits: false
905
- })
906
- });
907
-
908
- // Deploy the tiered ERC-721 hook contract.
909
- // slither-disable-next-line reentrancy-benign
910
899
  hook = HOOK_DEPLOYER.deployHookFor({
911
900
  projectId: revnetId,
912
- deployTiersHookConfig: hookConfig,
901
+ deployTiersHookConfig: JBDeploy721TiersHookConfig({
902
+ name: tiered721HookConfiguration.baseline721HookConfiguration.name,
903
+ symbol: tiered721HookConfiguration.baseline721HookConfiguration.symbol,
904
+ baseUri: tiered721HookConfiguration.baseline721HookConfiguration.baseUri,
905
+ tokenUriResolver: tiered721HookConfiguration.baseline721HookConfiguration.tokenUriResolver,
906
+ contractUri: tiered721HookConfiguration.baseline721HookConfiguration.contractUri,
907
+ tiersConfig: tiered721HookConfiguration.baseline721HookConfiguration.tiersConfig,
908
+ reserveBeneficiary: tiered721HookConfiguration.baseline721HookConfiguration.reserveBeneficiary,
909
+ flags: JB721TiersHookFlags({
910
+ noNewTiersWithReserves: tiered721HookConfiguration.baseline721HookConfiguration.flags
911
+ .noNewTiersWithReserves,
912
+ noNewTiersWithVotes: tiered721HookConfiguration.baseline721HookConfiguration.flags
913
+ .noNewTiersWithVotes,
914
+ noNewTiersWithOwnerMinting: tiered721HookConfiguration.baseline721HookConfiguration.flags
915
+ .noNewTiersWithOwnerMinting,
916
+ preventOverspending: tiered721HookConfiguration.baseline721HookConfiguration.flags
917
+ .preventOverspending,
918
+ issueTokensForSplits: false
919
+ })
920
+ }),
913
921
  salt: keccak256(abi.encode(tiered721HookConfiguration.salt, encodedConfigurationHash, _msgSender()))
914
922
  });
915
923
 
916
924
  // Store the tiered ERC-721 hook.
917
925
  tiered721HookOf[revnetId] = hook;
918
926
 
919
- // If specified, give the split operator permission to add and remove tiers.
920
- if (tiered721HookConfiguration.splitOperatorCanAdjustTiers) {
927
+ // Give the split operator permission to add and remove tiers unless prevented.
928
+ if (!tiered721HookConfiguration.preventSplitOperatorAdjustingTiers) {
921
929
  _extraOperatorPermissions[revnetId].push(JBPermissionIds.ADJUST_721_TIERS);
922
930
  }
923
931
 
924
- // If specified, give the split operator permission to set ERC-721 tier metadata.
925
- if (tiered721HookConfiguration.splitOperatorCanUpdateMetadata) {
932
+ // Give the split operator permission to set ERC-721 tier metadata unless prevented.
933
+ if (!tiered721HookConfiguration.preventSplitOperatorUpdatingMetadata) {
926
934
  _extraOperatorPermissions[revnetId].push(JBPermissionIds.SET_721_METADATA);
927
935
  }
928
936
 
929
- // If specified, give the split operator permission to mint ERC-721s (without a payment)
930
- // from tiers with `allowOwnerMint` set to true.
931
- if (tiered721HookConfiguration.splitOperatorCanMint) {
937
+ // Give the split operator permission to mint ERC-721s (without a payment)
938
+ // from tiers with `allowOwnerMint` set to true, unless prevented.
939
+ if (!tiered721HookConfiguration.preventSplitOperatorMinting) {
932
940
  _extraOperatorPermissions[revnetId].push(JBPermissionIds.MINT_721);
933
941
  }
934
942
 
935
- // If specified, give the split operator permission to increase the discount of a tier.
936
- if (tiered721HookConfiguration.splitOperatorCanIncreaseDiscountPercent) {
943
+ // Give the split operator permission to increase the discount of a tier unless prevented.
944
+ if (!tiered721HookConfiguration.preventSplitOperatorIncreasingDiscountPercent) {
937
945
  _extraOperatorPermissions[revnetId].push(JBPermissionIds.SET_721_DISCOUNT_PERCENT);
938
946
  }
939
947
 
948
+ // Give the split operator their permissions (base + 721 extras).
949
+ _setSplitOperatorOf({revnetId: revnetId, operator: configuration.splitOperator});
950
+
940
951
  // If there are posts to allow, configure them.
941
952
  if (allowedPosts.length != 0) {
942
953
  // Keep a reference to the formatted allowed posts.
@@ -945,7 +956,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
945
956
  // Iterate through each post to add it to the formatted list.
946
957
  for (uint256 i; i < allowedPosts.length; i++) {
947
958
  // Set the post being iterated on.
948
- REVCroptopAllowedPost calldata post = allowedPosts[i];
959
+ REVCroptopAllowedPost memory post = allowedPosts[i];
949
960
 
950
961
  // Set the formatted post.
951
962
  formattedAllowedPosts[i] = CTAllowedPost({
@@ -967,16 +978,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
967
978
  operator: address(PUBLISHER), revnetId: revnetId, permissionId: JBPermissionIds.ADJUST_721_TIERS
968
979
  });
969
980
  }
970
-
971
- _deployRevnetFor({
972
- revnetId: revnetId,
973
- shouldDeployNewRevnet: shouldDeployNewRevnet,
974
- configuration: configuration,
975
- terminalConfigurations: terminalConfigurations,
976
- suckerDeploymentConfiguration: suckerDeploymentConfiguration,
977
- rulesetConfigurations: rulesetConfigurations,
978
- encodedConfigurationHash: encodedConfigurationHash
979
- });
980
981
  }
981
982
 
982
983
  /// @notice Deploy a revnet, or initialize an existing Juicebox project as a revnet.
@@ -993,21 +994,22 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
993
994
  /// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
994
995
  /// @param suckerDeploymentConfiguration The suckers to set up for the revnet. Suckers facilitate cross-chain
995
996
  /// token transfers between peer revnets on different networks.
996
- /// @param rulesetConfigurations The rulesets to set up for the revnet.
997
- /// @param encodedConfigurationHash A hash that represents the revnet's configuration.
998
- /// See `_makeRulesetConfigurations(…)` for encoding details. Clients can read the encoded configuration
999
- /// from the `DeployRevnet` event emitted by this contract.
997
+ /// @return encodedConfigurationHash A hash that represents the revnet's configuration.
1000
998
  function _deployRevnetFor(
1001
999
  uint256 revnetId,
1002
1000
  bool shouldDeployNewRevnet,
1003
1001
  REVConfig calldata configuration,
1004
1002
  JBTerminalConfig[] calldata terminalConfigurations,
1005
- REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
1006
- JBRulesetConfig[] memory rulesetConfigurations,
1007
- bytes32 encodedConfigurationHash
1003
+ REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration
1008
1004
  )
1009
1005
  internal
1006
+ returns (bytes32 encodedConfigurationHash)
1010
1007
  {
1008
+ // Normalize and encode the configurations.
1009
+ JBRulesetConfig[] memory rulesetConfigurations;
1010
+ (rulesetConfigurations, encodedConfigurationHash) = _makeRulesetConfigurations({
1011
+ revnetId: revnetId, configuration: configuration, terminalConfigurations: terminalConfigurations
1012
+ });
1011
1013
  if (shouldDeployNewRevnet) {
1012
1014
  // If we're deploying a new revnet, launch a Juicebox project for it.
1013
1015
  // Sanity check that we deployed the `revnetId` that we expected to deploy.
@@ -1064,13 +1066,12 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
1064
1066
  JBTerminalConfig calldata terminalConfiguration = terminalConfigurations[i];
1065
1067
  for (uint256 j; j < terminalConfiguration.accountingContextsToAccept.length; j++) {
1066
1068
  // slither-disable-next-line calls-loop
1067
- _tryInitializeBuybackPoolFor(revnetId, terminalConfiguration.accountingContextsToAccept[j].token);
1069
+ _tryInitializeBuybackPoolFor({
1070
+ revnetId: revnetId, terminalToken: terminalConfiguration.accountingContextsToAccept[j].token
1071
+ });
1068
1072
  }
1069
1073
  }
1070
1074
 
1071
- // Give the split operator their permissions.
1072
- _setSplitOperatorOf({revnetId: revnetId, operator: configuration.splitOperator});
1073
-
1074
1075
  // Deploy the suckers (if applicable).
1075
1076
  if (suckerDeploymentConfiguration.salt != bytes32(0)) {
1076
1077
  _deploySuckersFor({
@@ -1130,6 +1131,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
1130
1131
  /// of collateral) or lower issuance weight (reducing the surplus-per-token ratio). Borrowers should monitor
1131
1132
  /// upcoming stage transitions and adjust their positions accordingly, as loans that fall below their required
1132
1133
  /// collateralization may become eligible for liquidation.
1134
+ /// @dev `cashOutTaxRate` changes at stage boundaries may allow users to cash out just before a rate increase.
1135
+ /// This is accepted behavior — the arbitrage window is bounded by the ruleset design, and all stages are
1136
+ /// configured immutably at deployment time.
1133
1137
  /// @param revnetId The ID of the revnet to make rulesets for.
1134
1138
  /// @param configuration The configuration containing the revnet's stages.
1135
1139
  /// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
@@ -1237,8 +1241,11 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
1237
1241
  });
1238
1242
 
1239
1243
  // Store the amount of tokens that can be auto-minted on this chain during this stage.
1240
- // The first stage ID is stored at this block's timestamp,
1241
- // and further stage IDs have incrementally increasing IDs
1244
+ // The stage ID is `block.timestamp + i`. This matches the ruleset ID that JBRulesets assigns
1245
+ // because JBRulesets uses `latestId >= block.timestamp ? latestId + 1 : block.timestamp`
1246
+ // (JBRulesets.sol L172), producing the same sequential IDs when all stages are queued in one tx.
1247
+ // `autoIssueFor` later calls `getRulesetOf(revnetId, stageId)` — the returned `ruleset.start`
1248
+ // is the derived start time (not the queue time), so the timing guard works correctly.
1242
1249
  // slither-disable-next-line reentrancy-benign
1243
1250
  amountToAutoIssue[revnetId][block.timestamp + i][autoIssuance.beneficiary] += autoIssuance.count;
1244
1251
  }
@@ -1296,7 +1303,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
1296
1303
  {
1297
1304
  // Set up the permission data.
1298
1305
  JBPermissionsData memory permissionData =
1299
- JBPermissionsData({operator: operator, projectId: uint64(revnetId), permissionIds: permissionIds});
1306
+ // forge-lint: disable-next-line(unsafe-typecast)
1307
+ JBPermissionsData({operator: operator, projectId: uint64(revnetId), permissionIds: permissionIds});
1300
1308
 
1301
1309
  // Set the permissions.
1302
1310
  PERMISSIONS.setPermissionsFor({account: account, permissionsData: permissionData});