@rev-net/core-v6 0.0.65 → 0.0.67

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/src/REVOwner.sol CHANGED
@@ -4,7 +4,10 @@ pragma solidity 0.8.28;
4
4
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
5
5
  import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
6
6
  import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
7
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
7
8
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
9
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
10
+ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
8
11
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
9
12
  import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
10
13
  import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
@@ -16,16 +19,24 @@ import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBB
16
19
  import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
17
20
  import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
18
21
  import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
22
+ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
19
23
  import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
24
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
20
25
  import {IJBPeerChainAdjustedAccounts} from "@bananapus/suckers-v6/src/interfaces/IJBPeerChainAdjustedAccounts.sol";
21
26
  import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
22
27
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
23
28
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
29
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
30
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
24
31
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
25
32
  import {mulDiv} from "@prb/math/src/Common.sol";
26
33
 
27
34
  import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
28
35
  import {IREVLoans} from "./interfaces/IREVLoans.sol";
36
+ import {IREVOwner} from "./interfaces/IREVOwner.sol";
37
+ import {REVOwnerAutoIssuance} from "./structs/REVOwnerAutoIssuance.sol";
38
+ import {REVOwnerExtraGrant} from "./structs/REVOwnerExtraGrant.sol";
39
+ import {REVOwnerRevnetInit} from "./structs/REVOwnerRevnetInit.sol";
29
40
 
30
41
  /// @notice The runtime hook for all revnets — set as every revnet's `dataHook` in ruleset metadata. At pay time, it
31
42
  /// coordinates the 721 hook (NFT tier minting) with the buyback hook (secondary market swap routing) and scales weight
@@ -34,7 +45,7 @@ import {IREVLoans} from "./interfaces/IREVLoans.sol";
34
45
  /// proceeds to the fee revnet via `afterCashOutRecordedWith`.
35
46
  /// @dev Separated from `REVDeployer` to stay within the EIP-170 contract size limit. Also implements
36
47
  /// `IJBPeerChainAdjustedAccounts` to expose loan state to peer-chain supply/surplus snapshots.
37
- contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAccounts {
48
+ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAccounts, IERC721Receiver {
38
49
  // A library that adds default safety checks to ERC20 functionality.
39
50
  using SafeERC20 for IERC20;
40
51
 
@@ -46,26 +57,30 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
46
57
  error REVOwner_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
47
58
  error REVOwner_InvalidLoanSourceToken(uint256 revnetId, address token);
48
59
  error REVOwner_NativeFeeValueMismatch(uint256 expected, uint256 actual);
60
+ error REVOwner_NothingToAutoIssue(uint256 revnetId, uint256 stageId, address beneficiary);
61
+ error REVOwner_NothingToBurn(uint256 revnetId, address holder);
62
+ error REVOwner_StageNotStarted(uint256 stageId);
49
63
  error REVOwner_Unauthorized(address caller, address expectedCaller);
64
+ error REVOwner_UnauthorizedOperator(uint256 revnetId, address caller);
50
65
 
51
66
  //*********************************************************************//
52
67
  // --------------- public immutable stored properties ---------------- //
53
68
  //*********************************************************************//
54
69
 
55
70
  /// @notice The buyback hook used as a data hook to route payments through buyback pools.
56
- IJBBuybackHookRegistry public immutable BUYBACK_HOOK;
71
+ IJBBuybackHookRegistry public immutable override BUYBACK_HOOK;
57
72
 
58
73
  /// @notice The directory of terminals and controllers for Juicebox projects.
59
- IJBDirectory public immutable DIRECTORY;
74
+ IJBDirectory public immutable override DIRECTORY;
60
75
 
61
76
  /// @notice The Juicebox project ID of the revnet that receives cash out fees.
62
- uint256 public immutable FEE_REVNET_ID;
77
+ uint256 public immutable override FEE_REVNET_ID;
63
78
 
64
79
  /// @notice The loan contract used by all revnets.
65
- IREVLoans public immutable LOANS;
80
+ IREVLoans public immutable override LOANS;
66
81
 
67
82
  /// @notice Deploys and tracks suckers for revnets.
68
- IJBSuckerRegistry public immutable SUCKER_REGISTRY;
83
+ IJBSuckerRegistry public immutable override SUCKER_REGISTRY;
69
84
 
70
85
  //*********************************************************************//
71
86
  // --------------------- public stored properties -------------------- //
@@ -74,15 +89,43 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
74
89
  /// @notice The timestamp of when cashouts will become available to a specific revnet's participants.
75
90
  /// @dev Only applies to existing revnets deploying onto a new network.
76
91
  /// @custom:param revnetId The ID of the revnet to check the cash out delay for.
77
- mapping(uint256 revnetId => uint256 cashOutDelay) public cashOutDelayOf;
92
+ mapping(uint256 revnetId => uint256 cashOutDelay) public override cashOutDelayOf;
78
93
 
79
94
  /// @notice Each revnet's tiered ERC-721 hook.
80
95
  /// @custom:param revnetId The ID of the revnet to look up.
81
- mapping(uint256 revnetId => IJB721TiersHook tiered721Hook) public tiered721HookOf;
96
+ mapping(uint256 revnetId => IJB721TiersHook tiered721Hook) public override tiered721HookOf;
97
+
98
+ /// @notice The amount of project tokens to auto-issue at a given stage to a given beneficiary.
99
+ /// @custom:param revnetId The ID of the revnet to look up.
100
+ /// @custom:param stageId The ID of the ruleset stage to look up.
101
+ /// @custom:param beneficiary The address that will receive the auto-issued tokens.
102
+ mapping(uint256 revnetId => mapping(uint256 stageId => mapping(address beneficiary => uint256 count)))
103
+ public
104
+ override amountToAutoIssue;
82
105
 
83
106
  /// @notice The deployer that manages revnet state.
84
107
  /// @dev Set once via `setDeployer()` using the precomputed canonical REVDeployer address.
85
- IREVDeployer public deployer;
108
+ IREVDeployer public override deployer;
109
+
110
+ /// @notice The controller used by all revnets to manage their projects.
111
+ /// @dev Cached from the deployer at `setDeployer()` time.
112
+ IJBController public override CONTROLLER;
113
+
114
+ /// @notice The permissions registry used to grant operator authority over revnets.
115
+ /// @dev Cached from the deployer at `setDeployer()` time.
116
+ IJBPermissions public override PERMISSIONS;
117
+
118
+ /// @notice The Juicebox project NFT contract.
119
+ /// @dev Cached from the deployer at `setDeployer()` time. Required for `onERC721Received` authentication.
120
+ IJBProjects public override PROJECTS;
121
+
122
+ //*********************************************************************//
123
+ // -------------------- internal stored properties ------------------- //
124
+ //*********************************************************************//
125
+
126
+ /// @notice Additional operator permissions configured per revnet on top of the protocol-default set.
127
+ /// @custom:param revnetId The ID of the revnet to look up.
128
+ mapping(uint256 revnetId => uint256[] permissionIds) internal _extraOperatorPermissions;
86
129
 
87
130
  //*********************************************************************//
88
131
  // -------------------- private stored properties -------------------- //
@@ -509,15 +552,80 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
509
552
  }
510
553
  }
511
554
 
512
- /// @notice Store the cash out delay for a revnet.
513
- /// @dev Only callable by the deployer.
514
- /// @param revnetId The ID of the revnet.
515
- /// @param cashOutDelay The timestamp after which cash outs are allowed.
516
- function setCashOutDelayOf(uint256 revnetId, uint256 cashOutDelay) external {
555
+ /// @notice Bind every piece of revnet-scoped state managed by this contract in a single deployer call. Stores
556
+ /// the cash-out delay, the tiered ERC-721 hook, auto-issuance allocations, extra operator permissions, the
557
+ /// initial operator, and any integration-specific permission grants.
558
+ /// @dev Only callable by the canonical deployer during a revnet's initial configuration. Extra operator
559
+ /// permissions are appended before the operator is bootstrapped so the operator receives the merged set in a
560
+ /// single permissions write.
561
+ /// @param revnetId The ID of the revnet being initialized.
562
+ /// @param init The full initialization payload.
563
+ function initializeRevnet(uint256 revnetId, REVOwnerRevnetInit calldata init) external override {
564
+ // Only the canonical deployer may bind a revnet's runtime state. Any other caller would let an outsider
565
+ // overwrite cash-out delays, hook addresses, or operator permissions for an existing revnet.
517
566
  if (msg.sender != address(deployer)) {
518
567
  revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(deployer)});
519
568
  }
520
- cashOutDelayOf[revnetId] = cashOutDelay;
569
+
570
+ // Store the cash-out delay if the deployer computed one (existing revnet landing on a new chain). A zero
571
+ // delay means cash outs are unlocked immediately, so skip the storage write to save gas in the common case.
572
+ if (init.cashOutDelay != 0) {
573
+ cashOutDelayOf[revnetId] = init.cashOutDelay;
574
+ }
575
+
576
+ // Bind the tiered ERC-721 hook the deployer just created for this revnet so `beforePayRecordedWith` can
577
+ // route NFT-tier mints through it. The deployer always deploys a hook (empty or pre-tiered), so the zero
578
+ // guard is defensive.
579
+ if (address(init.tiered721Hook) != address(0)) {
580
+ tiered721HookOf[revnetId] = init.tiered721Hook;
581
+ }
582
+
583
+ // Record every auto-issuance allocation that lands on this chain. The deployer pre-filtered by `chainId`
584
+ // and resolved each stage ID to its canonical ruleset ID, so each entry is ready to be claimed later via
585
+ // `autoIssueFor`. `+=` (instead of `=`) lets the deployer split the same beneficiary across multiple stage
586
+ // entries without one overwriting another.
587
+ for (uint256 i; i < init.autoIssuances.length;) {
588
+ REVOwnerAutoIssuance calldata autoIssuance = init.autoIssuances[i];
589
+ amountToAutoIssue[revnetId][autoIssuance.stageId][autoIssuance.beneficiary] += autoIssuance.count;
590
+ unchecked {
591
+ ++i;
592
+ }
593
+ }
594
+
595
+ // Append any deployer-supplied extra permissions (e.g. 721 hook admin permissions) to this revnet's
596
+ // operator set BEFORE bootstrapping the operator below — `_setOperatorOf` reads the merged set, so the
597
+ // operator must see these extras on its first permissions write.
598
+ for (uint256 i; i < init.extraOperatorPermissionIds.length;) {
599
+ _extraOperatorPermissions[revnetId].push(init.extraOperatorPermissionIds[i]);
600
+ unchecked {
601
+ ++i;
602
+ }
603
+ }
604
+
605
+ // Grant the operator the merged default + extra permission set in a single permissions write. Passing
606
+ // `address(0)` here is the explicit way to launch a revnet with no operator — no permissions get written
607
+ // because `_setPermissionsFor` is called with the zero address as the holder.
608
+ _setOperatorOf({revnetId: revnetId, operator: init.operator});
609
+
610
+ // Apply per-revnet permission grants for integrations that aren't the operator (e.g. the Croptop
611
+ // publisher needs `ADJUST_721_TIERS` to post NFTs on the revnet's behalf). Each grant is scoped to a
612
+ // specific (revnet, operator, permissionId) triple on this contract's account so it does not leak into
613
+ // the operator's general authority.
614
+ for (uint256 i; i < init.extraGrants.length;) {
615
+ REVOwnerExtraGrant calldata grant = init.extraGrants[i];
616
+ uint8[] memory permissionIds = new uint8[](1);
617
+ permissionIds[0] = grant.permissionId;
618
+ _setPermissionsFor({
619
+ account: address(this), operator: grant.operator, revnetId: revnetId, permissionIds: permissionIds
620
+ });
621
+ unchecked {
622
+ ++i;
623
+ }
624
+ }
625
+
626
+ // Emit a single marker event for off-chain indexers — the deployer side already emits granular events
627
+ // (`DeployRevnet`, `StoreAutoIssuanceAmount`, `SetCashOutDelay`) with the underlying data.
628
+ emit InitializeRevnet({revnetId: revnetId, caller: msg.sender});
521
629
  }
522
630
 
523
631
  /// @notice Bind the canonical deployer address exactly once.
@@ -525,30 +633,145 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
525
633
  /// Only that deploy-time binder may call this, which avoids an ambient public initializer where any first caller
526
634
  /// could seize the deployer role before the deterministic REVDeployer is actually deployed.
527
635
  /// @param newDeployer The canonical REVDeployer instance that will manage revnet runtime state.
528
- function setDeployer(IREVDeployer newDeployer) external {
636
+ function setDeployer(IREVDeployer newDeployer) external override {
529
637
  // Only the account that deployed this REVOwner may complete the one-time deployer binding.
530
638
  if (msg.sender != _DEPLOYER) revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: _DEPLOYER});
531
639
  // Prevent the deployer binding from being overwritten after initialization.
532
640
  if (address(deployer) != address(0)) revert REVOwner_AlreadyInitialized({deployer: address(deployer)});
533
641
  // Store the canonical REVDeployer that is authorized to manage runtime hook state.
534
642
  deployer = newDeployer;
643
+ // Cache references read from the deployer so on-chain ownership operations don't need to re-traverse the
644
+ // deployer for each call. These are immutable on the deployer side so a single snapshot is sufficient.
645
+ CONTROLLER = newDeployer.CONTROLLER();
646
+ PERMISSIONS = newDeployer.PERMISSIONS();
647
+ PROJECTS = newDeployer.PROJECTS();
648
+
649
+ // Grant the wildcard operator permissions used by every revnet, scoped on this contract's account.
650
+ // The loan contract is the singleton shared by all revnets and uses the surplus allowance of each
651
+ // revnet's terminal. The buyback hook registry configures pools on every revnet.
652
+ uint8[] memory loanPermissionIds = new uint8[](1);
653
+ loanPermissionIds[0] = JBPermissionIds.USE_ALLOWANCE;
654
+ _setPermissionsFor({
655
+ account: address(this), operator: address(LOANS), revnetId: 0, permissionIds: loanPermissionIds
656
+ });
657
+
658
+ uint8[] memory buybackPermissionIds = new uint8[](1);
659
+ buybackPermissionIds[0] = JBPermissionIds.SET_BUYBACK_POOL;
660
+ _setPermissionsFor({
661
+ account: address(this), operator: address(BUYBACK_HOOK), revnetId: 0, permissionIds: buybackPermissionIds
662
+ });
663
+
664
+ // The deployer drives sucker setup for every revnet (initial deploy + post-deploy via
665
+ // `REVDeployer.deploySuckersFor`). Grant it `DEPLOY_SUCKERS` and `MAP_SUCKER_TOKEN` against this contract's
666
+ // account so the sucker registry sees an authorized caller acting for the project owner (this contract).
667
+ uint8[] memory deployerPermissionIds = new uint8[](2);
668
+ deployerPermissionIds[0] = JBPermissionIds.DEPLOY_SUCKERS;
669
+ deployerPermissionIds[1] = JBPermissionIds.MAP_SUCKER_TOKEN;
670
+ _setPermissionsFor({
671
+ account: address(this), operator: address(newDeployer), revnetId: 0, permissionIds: deployerPermissionIds
672
+ });
535
673
  }
536
674
 
537
- /// @notice Store the tiered ERC-721 hook for a revnet.
538
- /// @dev Only callable by the deployer.
539
- /// @param revnetId The ID of the revnet.
540
- /// @param hook The tiered ERC-721 hook.
541
- function setTiered721HookOf(uint256 revnetId, IJB721TiersHook hook) external {
542
- if (msg.sender != address(deployer)) {
543
- revert REVOwner_Unauthorized({caller: msg.sender, expectedCaller: address(deployer)});
675
+ /// @notice Auto-mint a revnet's tokens from a stage for a beneficiary.
676
+ /// @dev Permissionless: anyone can trigger the mint once the stage has started. The recorded amount is consumed
677
+ /// (reset to zero) after the mint executes.
678
+ /// @param revnetId The ID of the revnet to auto-mint tokens for.
679
+ /// @param stageId The ID of the ruleset stage to auto-mint tokens from.
680
+ /// @param beneficiary The address to send auto-minted tokens to.
681
+ function autoIssueFor(uint256 revnetId, uint256 stageId, address beneficiary) external override {
682
+ // Get the ruleset for the stage to check if it has started.
683
+ (JBRuleset memory ruleset,) = CONTROLLER.getRulesetOf({projectId: revnetId, rulesetId: stageId});
684
+
685
+ // Make sure the stage has started.
686
+ // forge-lint: disable-next-line(block-timestamp)
687
+ if (ruleset.start > block.timestamp) {
688
+ revert REVOwner_StageNotStarted({stageId: stageId});
689
+ }
690
+
691
+ uint256 count = amountToAutoIssue[revnetId][stageId][beneficiary];
692
+ if (count == 0) {
693
+ revert REVOwner_NothingToAutoIssue({revnetId: revnetId, stageId: stageId, beneficiary: beneficiary});
544
694
  }
545
- tiered721HookOf[revnetId] = hook;
695
+
696
+ // Reset before the external call to prevent re-entry from re-claiming.
697
+ amountToAutoIssue[revnetId][stageId][beneficiary] = 0;
698
+
699
+ emit AutoIssue({
700
+ revnetId: revnetId, stageId: stageId, beneficiary: beneficiary, count: count, caller: msg.sender
701
+ });
702
+
703
+ CONTROLLER.mintTokensOf({
704
+ projectId: revnetId, tokenCount: count, beneficiary: beneficiary, memo: "", useReservedPercent: false
705
+ });
706
+ }
707
+
708
+ /// @notice Burn any of a revnet's project tokens that have accumulated on this contract.
709
+ /// @dev Tokens accrue here from reserved-token distributions when splits don't sum to 100% (the JBController
710
+ /// mints the leftover to the project owner — which is this contract).
711
+ /// @param revnetId The ID of the revnet to burn tokens for.
712
+ function burnHeldTokensOf(uint256 revnetId) external override {
713
+ // Ask the controller's token registry for this contract's combined credit + ERC-20 balance for the revnet.
714
+ // This is the residue from reserved-token distributions where the splits don't sum to 100% — the
715
+ // controller mints the leftover to the project owner, which is this contract.
716
+ uint256 balance = CONTROLLER.TOKENS().totalBalanceOf({holder: address(this), projectId: revnetId});
717
+
718
+ // If there's nothing held, fail early — burning zero would silently no-op and waste the caller's gas.
719
+ if (balance == 0) revert REVOwner_NothingToBurn({revnetId: revnetId, holder: address(this)});
720
+
721
+ // Burn the full held balance through the controller. The controller drains credits first, then any
722
+ // ERC-20 supply on this contract, so a single call always clears the residue regardless of how it accrued.
723
+ CONTROLLER.burnTokensOf({holder: address(this), projectId: revnetId, tokenCount: balance, memo: ""});
724
+
725
+ // Record the burn for off-chain accounting. `caller` is the address that paid gas, not necessarily a
726
+ // privileged operator — this entrypoint is intentionally permissionless.
727
+ emit BurnHeldTokens({revnetId: revnetId, count: balance, caller: msg.sender});
728
+ }
729
+
730
+ /// @notice Change a revnet's operator.
731
+ /// @dev Only a revnet's current operator can rotate the operator. Passing `address(0)` relinquishes operator
732
+ /// powers permanently — the permissions move to the zero address which cannot execute transactions.
733
+ /// @param revnetId The ID of the revnet to change the operator for.
734
+ /// @param newOperator The new operator's address. Use `address(0)` to relinquish operator powers.
735
+ function setOperatorOf(uint256 revnetId, address newOperator) external override {
736
+ _checkIfIsOperatorOf({revnetId: revnetId, operator: msg.sender});
737
+
738
+ emit ReplaceOperator({revnetId: revnetId, newOperator: newOperator, caller: msg.sender});
739
+
740
+ // Remove operator permissions from the old operator.
741
+ _setPermissionsFor({
742
+ account: address(this), operator: msg.sender, revnetId: revnetId, permissionIds: new uint8[](0)
743
+ });
744
+
745
+ // Grant the default operator permissions to the new operator (no-op if `newOperator == address(0)`).
746
+ _setOperatorOf({revnetId: revnetId, operator: newOperator});
747
+ }
748
+
749
+ /// @notice Required to receive the JBProjects NFT for each revnet that this contract owns.
750
+ /// @dev Only accepts NFTs from the canonical `JBProjects` contract.
751
+ function onERC721Received(address, address, uint256, bytes calldata) external view returns (bytes4) {
752
+ if (msg.sender != address(PROJECTS)) revert();
753
+ return IERC721Receiver.onERC721Received.selector;
546
754
  }
547
755
 
548
756
  //*********************************************************************//
549
757
  // -------------------------- public views --------------------------- //
550
758
  //*********************************************************************//
551
759
 
760
+ /// @notice Check whether an address is a revnet's operator.
761
+ /// @param revnetId The ID of the revnet to check.
762
+ /// @param addr The address to check.
763
+ /// @return flag A flag indicating whether the address holds the revnet's operator permissions.
764
+ function isOperatorOf(uint256 revnetId, address addr) public view override returns (bool) {
765
+ return PERMISSIONS.hasPermissions({
766
+ operator: addr,
767
+ account: address(this),
768
+ projectId: revnetId,
769
+ permissionIds: _operatorPermissionIndexesOf(revnetId),
770
+ includeRoot: false,
771
+ includeWildcardProjectId: false
772
+ });
773
+ }
774
+
552
775
  /// @notice Indicates if this contract adheres to the specified interface.
553
776
  /// @dev See `IERC165.supportsInterface`.
554
777
  /// @return A flag indicating if the provided interface ID is supported.
@@ -570,6 +793,44 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
570
793
  return SUCKER_REGISTRY.isSuckerOf({projectId: revnetId, addr: addr});
571
794
  }
572
795
 
796
+ /// @notice The default + custom operator permissions that should be held by a revnet's operator.
797
+ /// @param revnetId The ID of the revnet to look up.
798
+ /// @return allOperatorPermissions The merged permission ID list.
799
+ function _operatorPermissionIndexesOf(uint256 revnetId)
800
+ internal
801
+ view
802
+ returns (uint256[] memory allOperatorPermissions)
803
+ {
804
+ uint256[] memory customOperatorPermissionIndexes = _extraOperatorPermissions[revnetId];
805
+
806
+ allOperatorPermissions = new uint256[](9 + customOperatorPermissionIndexes.length);
807
+ allOperatorPermissions[0] = JBPermissionIds.SET_SPLIT_GROUPS;
808
+ allOperatorPermissions[1] = JBPermissionIds.SET_BUYBACK_POOL;
809
+ allOperatorPermissions[2] = JBPermissionIds.SET_BUYBACK_TWAP;
810
+ allOperatorPermissions[3] = JBPermissionIds.SET_PROJECT_URI;
811
+ allOperatorPermissions[4] = JBPermissionIds.SUCKER_SAFETY;
812
+ allOperatorPermissions[5] = JBPermissionIds.SET_BUYBACK_HOOK;
813
+ allOperatorPermissions[6] = JBPermissionIds.SET_ROUTER_TERMINAL;
814
+ allOperatorPermissions[7] = JBPermissionIds.SET_TOKEN_METADATA;
815
+ allOperatorPermissions[8] = JBPermissionIds.SIGN_FOR_ERC20;
816
+
817
+ for (uint256 i; i < customOperatorPermissionIndexes.length;) {
818
+ allOperatorPermissions[9 + i] = customOperatorPermissionIndexes[i];
819
+ unchecked {
820
+ ++i;
821
+ }
822
+ }
823
+ }
824
+
825
+ /// @notice Reverts if `operator` is not the revnet's current operator.
826
+ /// @param revnetId The ID of the revnet to check.
827
+ /// @param operator The address to check.
828
+ function _checkIfIsOperatorOf(uint256 revnetId, address operator) internal view {
829
+ if (!isOperatorOf({revnetId: revnetId, addr: operator})) {
830
+ revert REVOwner_UnauthorizedOperator({revnetId: revnetId, caller: operator});
831
+ }
832
+ }
833
+
573
834
  /// @notice Total outstanding local loan debt and collateral for a revnet.
574
835
  /// @dev This is included in cash-out and peer-snapshot math because borrowed funds are still owed to the revnet
575
836
  /// and collateral can re-enter supply when the loan is repaid.
@@ -661,4 +922,44 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
661
922
  IERC20(token).safeIncreaseAllowance({spender: to, value: amount});
662
923
  return 0;
663
924
  }
925
+
926
+ /// @notice Grant the revnet's default operator permission set to `operator`.
927
+ /// @param revnetId The ID of the revnet to scope the permissions for.
928
+ /// @param operator The address to grant operator permissions to.
929
+ function _setOperatorOf(uint256 revnetId, address operator) internal {
930
+ uint256[] memory permissionIndexes = _operatorPermissionIndexesOf(revnetId);
931
+ uint8[] memory permissionIds = new uint8[](permissionIndexes.length);
932
+
933
+ for (uint256 i; i < permissionIndexes.length;) {
934
+ // forge-lint: disable-next-line(unsafe-typecast)
935
+ permissionIds[i] = uint8(permissionIndexes[i]);
936
+ unchecked {
937
+ ++i;
938
+ }
939
+ }
940
+
941
+ _setPermissionsFor({
942
+ account: address(this), operator: operator, revnetId: revnetId, permissionIds: permissionIds
943
+ });
944
+ }
945
+
946
+ /// @notice Set the permissions on the JB permissions registry, scoped to this revnet.
947
+ /// @param account The account whose permission slot is being updated. Always `address(this)`.
948
+ /// @param operator The address whose permissions are being set.
949
+ /// @param revnetId The ID of the revnet to scope the permissions for.
950
+ /// @param permissionIds The permission IDs to grant (empty array revokes).
951
+ function _setPermissionsFor(
952
+ address account,
953
+ address operator,
954
+ uint256 revnetId,
955
+ uint8[] memory permissionIds
956
+ )
957
+ internal
958
+ {
959
+ JBPermissionsData memory permissionData =
960
+ // forge-lint: disable-next-line(unsafe-typecast)
961
+ JBPermissionsData({operator: operator, projectId: uint64(revnetId), permissionIds: permissionIds});
962
+
963
+ PERMISSIONS.setPermissionsFor({account: account, permissionsData: permissionData});
964
+ }
664
965
  }
@@ -22,22 +22,6 @@ import {IREVLoans} from "./IREVLoans.sol";
22
22
 
23
23
  /// @notice Deploys and manages revnets -- Juicebox projects with pre-configured tokenomics.
24
24
  interface IREVDeployer {
25
- /// @notice Emitted when tokens are auto-issued for a beneficiary during a stage.
26
- /// @param revnetId The ID of the revnet.
27
- /// @param stageId The ID of the stage.
28
- /// @param beneficiary The address receiving the auto-issued tokens.
29
- /// @param count The number of tokens auto-issued.
30
- /// @param caller The address that triggered the auto-issuance.
31
- event AutoIssue(
32
- uint256 indexed revnetId, uint256 indexed stageId, address indexed beneficiary, uint256 count, address caller
33
- );
34
-
35
- /// @notice Emitted when held tokens are burned from the deployer contract.
36
- /// @param revnetId The ID of the revnet whose tokens were burned.
37
- /// @param count The number of tokens burned.
38
- /// @param caller The address that triggered the burn.
39
- event BurnHeldTokens(uint256 indexed revnetId, uint256 count, address caller);
40
-
41
25
  /// @notice Emitted when a new revnet is deployed.
42
26
  /// @param revnetId The ID of the deployed revnet.
43
27
  /// @param configuration The revnet's configuration.
@@ -68,12 +52,6 @@ interface IREVDeployer {
68
52
  address caller
69
53
  );
70
54
 
71
- /// @notice Emitted when the operator of a revnet is replaced.
72
- /// @param revnetId The ID of the revnet.
73
- /// @param newOperator The address of the new operator.
74
- /// @param caller The address that replaced the operator.
75
- event ReplaceOperator(uint256 indexed revnetId, address indexed newOperator, address caller);
76
-
77
55
  /// @notice Emitted when the cash out delay is set for a revnet.
78
56
  /// @param revnetId The ID of the revnet.
79
57
  /// @param cashOutDelay The cash out delay in seconds.
@@ -90,13 +68,6 @@ interface IREVDeployer {
90
68
  uint256 indexed revnetId, uint256 indexed stageId, address indexed beneficiary, uint256 count, address caller
91
69
  );
92
70
 
93
- /// @notice The number of revnet tokens that can be auto-minted for a beneficiary during a stage.
94
- /// @param revnetId The ID of the revnet.
95
- /// @param stageId The ID of the stage.
96
- /// @param beneficiary The beneficiary of the auto-mint.
97
- /// @return The number of tokens available to auto-issue.
98
- function amountToAutoIssue(uint256 revnetId, uint256 stageId, address beneficiary) external view returns (uint256);
99
-
100
71
  /// @notice The buyback hook used as a data hook to route payments through buyback pools.
101
72
  /// @return The buyback hook contract.
102
73
  function BUYBACK_HOOK() external view returns (IJBBuybackHookRegistry);
@@ -134,12 +105,6 @@ interface IREVDeployer {
134
105
  /// @return The hook deployer contract.
135
106
  function HOOK_DEPLOYER() external view returns (IJB721TiersHookDeployer);
136
107
 
137
- /// @notice Check whether an address is a revnet's operator.
138
- /// @param revnetId The ID of the revnet to check.
139
- /// @param addr The address to check.
140
- /// @return A flag indicating whether the address is the revnet's operator.
141
- function isOperatorOf(uint256 revnetId, address addr) external view returns (bool);
142
-
143
108
  /// @notice The loan contract used by all revnets.
144
109
  /// @return The loans contract address.
145
110
  function LOANS() external view returns (IREVLoans);
@@ -172,16 +137,6 @@ interface IREVDeployer {
172
137
  /// @return The sucker registry contract.
173
138
  function SUCKER_REGISTRY() external view returns (IJBSuckerRegistry);
174
139
 
175
- /// @notice Auto-mint a revnet's tokens from a stage for a beneficiary.
176
- /// @param revnetId The ID of the revnet to auto-mint tokens for.
177
- /// @param stageId The ID of the stage to auto-mint tokens from.
178
- /// @param beneficiary The address to send auto-minted tokens to.
179
- function autoIssueFor(uint256 revnetId, uint256 stageId, address beneficiary) external;
180
-
181
- /// @notice Burn any of a revnet's tokens held by this contract.
182
- /// @param revnetId The ID of the revnet to burn tokens for.
183
- function burnHeldTokensOf(uint256 revnetId) external;
184
-
185
140
  /// @notice Deploy a revnet with a tiered ERC-721 hook and optional croptop posting support.
186
141
  /// @dev Every revnet gets a 721 hook — pass an empty config if no tiers are needed initially.
187
142
  /// @param revnetId The ID of the Juicebox project to initialize. Send 0 to deploy a new revnet.
@@ -230,9 +185,4 @@ interface IREVDeployer {
230
185
  )
231
186
  external
232
187
  returns (address[] memory suckers);
233
-
234
- /// @notice Change a revnet's operator. Only the current operator can call this.
235
- /// @param revnetId The ID of the revnet.
236
- /// @param newOperator The new operator's address.
237
- function setOperatorOf(uint256 revnetId, address newOperator) external;
238
188
  }