@rev-net/core-v6 0.0.1
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/LICENSE +21 -0
- package/README.md +65 -0
- package/REVNET_SECURITY_CHECKLIST.md +164 -0
- package/SECURITY.md +68 -0
- package/SKILLS.md +166 -0
- package/deployments/revnet-core-v5/arbitrum/REVDeployer.json +2821 -0
- package/deployments/revnet-core-v5/arbitrum/REVLoans.json +2260 -0
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVDeployer.json +2821 -0
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVLoans.json +2260 -0
- package/deployments/revnet-core-v5/base/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/base/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/base_sepolia/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/base_sepolia/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/ethereum/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/ethereum/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/optimism/REVDeployer.json +2821 -0
- package/deployments/revnet-core-v5/optimism/REVLoans.json +2260 -0
- package/deployments/revnet-core-v5/optimism_sepolia/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/optimism_sepolia/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/sepolia/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/sepolia/REVLoans.json +2264 -0
- package/docs/book.css +13 -0
- package/docs/book.toml +13 -0
- package/docs/solidity.min.js +74 -0
- package/docs/src/README.md +88 -0
- package/docs/src/SUMMARY.md +20 -0
- package/docs/src/src/README.md +7 -0
- package/docs/src/src/REVDeployer.sol/contract.REVDeployer.md +968 -0
- package/docs/src/src/REVLoans.sol/contract.REVLoans.md +1047 -0
- package/docs/src/src/interfaces/IREVDeployer.sol/interface.IREVDeployer.md +243 -0
- package/docs/src/src/interfaces/IREVLoans.sol/interface.IREVLoans.md +296 -0
- package/docs/src/src/interfaces/README.md +5 -0
- package/docs/src/src/structs/README.md +14 -0
- package/docs/src/src/structs/REVAutoIssuance.sol/struct.REVAutoIssuance.md +19 -0
- package/docs/src/src/structs/REVBuybackHookConfig.sol/struct.REVBuybackHookConfig.md +19 -0
- package/docs/src/src/structs/REVBuybackPoolConfig.sol/struct.REVBuybackPoolConfig.md +21 -0
- package/docs/src/src/structs/REVConfig.sol/struct.REVConfig.md +35 -0
- package/docs/src/src/structs/REVCroptopAllowedPost.sol/struct.REVCroptopAllowedPost.md +28 -0
- package/docs/src/src/structs/REVDeploy721TiersHookConfig.sol/struct.REVDeploy721TiersHookConfig.md +34 -0
- package/docs/src/src/structs/REVDescription.sol/struct.REVDescription.md +23 -0
- package/docs/src/src/structs/REVLoan.sol/struct.REVLoan.md +28 -0
- package/docs/src/src/structs/REVLoanSource.sol/struct.REVLoanSource.md +16 -0
- package/docs/src/src/structs/REVStageConfig.sol/struct.REVStageConfig.md +44 -0
- package/docs/src/src/structs/REVSuckerDeploymentConfig.sol/struct.REVSuckerDeploymentConfig.md +16 -0
- package/foundry.lock +11 -0
- package/foundry.toml +23 -0
- package/package.json +31 -0
- package/remappings.txt +1 -0
- package/script/Deploy.s.sol +350 -0
- package/script/helpers/RevnetCoreDeploymentLib.sol +72 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +507 -0
- package/src/REVDeployer.sol +1257 -0
- package/src/REVLoans.sol +1333 -0
- package/src/interfaces/IREVDeployer.sol +198 -0
- package/src/interfaces/IREVLoans.sol +241 -0
- package/src/structs/REVAutoIssuance.sol +11 -0
- package/src/structs/REVConfig.sol +17 -0
- package/src/structs/REVCroptopAllowedPost.sol +20 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +25 -0
- package/src/structs/REVDescription.sol +14 -0
- package/src/structs/REVLoan.sol +19 -0
- package/src/structs/REVLoanSource.sol +11 -0
- package/src/structs/REVStageConfig.sol +34 -0
- package/src/structs/REVSuckerDeploymentConfig.sol +11 -0
- package/test/REV.integrations.t.sol +420 -0
- package/test/REVAutoIssuanceFuzz.t.sol +276 -0
- package/test/REVDeployerAuditRegressions.t.sol +328 -0
- package/test/REVInvincibility.t.sol +1275 -0
- package/test/REVInvincibilityHandler.sol +357 -0
- package/test/REVLifecycle.t.sol +364 -0
- package/test/REVLoans.invariants.t.sol +642 -0
- package/test/REVLoansAttacks.t.sol +739 -0
- package/test/REVLoansAuditRegressions.t.sol +314 -0
- package/test/REVLoansFeeRecovery.t.sol +704 -0
- package/test/REVLoansSourced.t.sol +1732 -0
- package/test/REVLoansUnSourced.t.sol +331 -0
- package/test/TestPR09_ConversionDocumentation.t.sol +304 -0
- package/test/TestPR10_LiquidationBehavior.t.sol +340 -0
- package/test/TestPR11_LowFindings.t.sol +571 -0
- package/test/TestPR12_FlashLoanSurplus.t.sol +305 -0
- package/test/TestPR13_CrossSourceReallocation.t.sol +302 -0
- package/test/TestPR15_CashOutCallerValidation.t.sol +320 -0
- package/test/TestPR16_ZeroRepayment.t.sol +297 -0
- package/test/TestPR21_Uint112Overflow.t.sol +251 -0
- package/test/TestPR22_HookArrayOOB.t.sol +221 -0
- package/test/TestPR26_BurnHeldTokens.t.sol +331 -0
- package/test/TestPR27_CEIPattern.t.sol +448 -0
- package/test/TestPR29_SwapTerminalPermission.t.sol +206 -0
- package/test/TestPR32_MixedFixes.t.sol +529 -0
- package/test/helpers/MaliciousContracts.sol +233 -0
- package/test/mock/MockBuybackDataHook.sol +61 -0
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
5
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
6
|
+
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
7
|
+
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
8
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
9
|
+
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
10
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
11
|
+
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
12
|
+
import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
|
|
13
|
+
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
14
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
15
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
16
|
+
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
17
|
+
import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
|
|
18
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
19
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
20
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
21
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
22
|
+
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
23
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
24
|
+
import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
|
|
25
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
26
|
+
import {JBSplitGroupIds} from "@bananapus/core-v6/src/libraries/JBSplitGroupIds.sol";
|
|
27
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
28
|
+
import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
|
|
29
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
30
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
31
|
+
import {JBCurrencyAmount} from "@bananapus/core-v6/src/structs/JBCurrencyAmount.sol";
|
|
32
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
33
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
34
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
35
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
36
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
37
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
38
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
39
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
40
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
41
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
42
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
43
|
+
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
44
|
+
import {CTPublisher} from "@croptop/core-v6/src/CTPublisher.sol";
|
|
45
|
+
import {CTAllowedPost} from "@croptop/core-v6/src/structs/CTAllowedPost.sol";
|
|
46
|
+
|
|
47
|
+
import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
|
|
48
|
+
import {REVAutoIssuance} from "./structs/REVAutoIssuance.sol";
|
|
49
|
+
import {REVConfig} from "./structs/REVConfig.sol";
|
|
50
|
+
import {REVCroptopAllowedPost} from "./structs/REVCroptopAllowedPost.sol";
|
|
51
|
+
import {REVDeploy721TiersHookConfig} from "./structs/REVDeploy721TiersHookConfig.sol";
|
|
52
|
+
import {REVStageConfig} from "./structs/REVStageConfig.sol";
|
|
53
|
+
import {REVSuckerDeploymentConfig} from "./structs/REVSuckerDeploymentConfig.sol";
|
|
54
|
+
|
|
55
|
+
/// @notice `REVDeployer` deploys, manages, and operates Revnets.
|
|
56
|
+
/// @dev Revnets are unowned Juicebox projects which operate autonomously after deployment.
|
|
57
|
+
contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCashOutHook, IERC721Receiver {
|
|
58
|
+
// A library that adds default safety checks to ERC20 functionality.
|
|
59
|
+
using SafeERC20 for IERC20;
|
|
60
|
+
|
|
61
|
+
//*********************************************************************//
|
|
62
|
+
// --------------------------- custom errors ------------------------- //
|
|
63
|
+
//*********************************************************************//
|
|
64
|
+
|
|
65
|
+
error REVDeployer_AutoIssuanceBeneficiaryZeroAddress();
|
|
66
|
+
error REVDeployer_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
|
|
67
|
+
error REVDeployer_CashOutsCantBeTurnedOffCompletely(uint256 cashOutTaxRate, uint256 maxCashOutTaxRate);
|
|
68
|
+
error REVDeployer_MustHaveSplits();
|
|
69
|
+
error REVDeployer_NothingToAutoIssue();
|
|
70
|
+
error REVDeployer_RulesetDoesNotAllowDeployingSuckers();
|
|
71
|
+
error REVDeployer_StageNotStarted(uint256 stageId);
|
|
72
|
+
error REVDeployer_StagesRequired();
|
|
73
|
+
error REVDeployer_StageTimesMustIncrease();
|
|
74
|
+
error REVDeployer_NothingToBurn();
|
|
75
|
+
error REVDeployer_Unauthorized(uint256 revnetId, address caller);
|
|
76
|
+
|
|
77
|
+
//*********************************************************************//
|
|
78
|
+
// ------------------------- public constants ------------------------ //
|
|
79
|
+
//*********************************************************************//
|
|
80
|
+
|
|
81
|
+
/// @notice The number of seconds until a revnet's participants can cash out, starting from the time when that
|
|
82
|
+
/// revnet is deployed to a new network.
|
|
83
|
+
/// - Only applies to existing revnets which are deploying onto a new network.
|
|
84
|
+
/// - To prevent liquidity/arbitrage issues which might arise when an existing revnet adds a brand-new treasury.
|
|
85
|
+
/// @dev 30 days, in seconds.
|
|
86
|
+
uint256 public constant override CASH_OUT_DELAY = 2_592_000;
|
|
87
|
+
|
|
88
|
+
/// @notice The cash out fee (as a fraction out of `JBConstants.MAX_FEE`).
|
|
89
|
+
/// Cashout fees are paid to the revnet with the `FEE_REVNET_ID`.
|
|
90
|
+
/// @dev Fees are charged on cashouts if the cash out tax rate is greater than 0%.
|
|
91
|
+
/// @dev When suckers withdraw funds, they do not pay cash out fees.
|
|
92
|
+
uint256 public constant override FEE = 25; // 2.5%
|
|
93
|
+
|
|
94
|
+
/// @notice The default Uniswap pool fee tier used when auto-configuring buyback pools.
|
|
95
|
+
/// @dev 10_000 = 1%. This is the standard fee tier for most project token pairs.
|
|
96
|
+
uint24 public constant DEFAULT_BUYBACK_POOL_FEE = 10_000;
|
|
97
|
+
|
|
98
|
+
/// @notice The default TWAP window used when auto-configuring buyback pools.
|
|
99
|
+
/// @dev 2 days provides robust manipulation resistance.
|
|
100
|
+
uint32 public constant DEFAULT_BUYBACK_TWAP_WINDOW = 2 days;
|
|
101
|
+
|
|
102
|
+
//*********************************************************************//
|
|
103
|
+
// --------------- public immutable stored properties ---------------- //
|
|
104
|
+
//*********************************************************************//
|
|
105
|
+
|
|
106
|
+
/// @notice The buyback hook used as a data hook to route payments through buyback pools.
|
|
107
|
+
IJBRulesetDataHook public immutable override BUYBACK_HOOK;
|
|
108
|
+
|
|
109
|
+
/// @notice The controller used to create and manage Juicebox projects for revnets.
|
|
110
|
+
IJBController public immutable override CONTROLLER;
|
|
111
|
+
|
|
112
|
+
/// @notice The directory of terminals and controllers for Juicebox projects (and revnets).
|
|
113
|
+
IJBDirectory public immutable override DIRECTORY;
|
|
114
|
+
|
|
115
|
+
/// @notice The Juicebox project ID of the revnet that receives cash out fees.
|
|
116
|
+
uint256 public immutable override FEE_REVNET_ID;
|
|
117
|
+
|
|
118
|
+
/// @notice Deploys tiered ERC-721 hooks for revnets.
|
|
119
|
+
IJB721TiersHookDeployer public immutable override HOOK_DEPLOYER;
|
|
120
|
+
|
|
121
|
+
/// @notice The loan contract used by all revnets.
|
|
122
|
+
/// @dev Revnets can offer loans to their participants, collateralized by their tokens.
|
|
123
|
+
/// Participants can borrow up to the current cash out value of their tokens.
|
|
124
|
+
address public immutable override LOANS;
|
|
125
|
+
|
|
126
|
+
/// @notice Stores Juicebox project (and revnet) access permissions.
|
|
127
|
+
IJBPermissions public immutable override PERMISSIONS;
|
|
128
|
+
|
|
129
|
+
/// @notice Mints ERC-721s that represent Juicebox project (and revnet) ownership and transfers.
|
|
130
|
+
IJBProjects public immutable override PROJECTS;
|
|
131
|
+
|
|
132
|
+
/// @notice Manages the publishing of ERC-721 posts to revnet's tiered ERC-721 hooks.
|
|
133
|
+
CTPublisher public immutable override PUBLISHER;
|
|
134
|
+
|
|
135
|
+
/// @notice Deploys and tracks suckers for revnets.
|
|
136
|
+
IJBSuckerRegistry public immutable override SUCKER_REGISTRY;
|
|
137
|
+
|
|
138
|
+
//*********************************************************************//
|
|
139
|
+
// --------------------- public stored properties -------------------- //
|
|
140
|
+
//*********************************************************************//
|
|
141
|
+
|
|
142
|
+
/// @notice The number of revnet tokens which can be "auto-minted" (minted without payments)
|
|
143
|
+
/// for a specific beneficiary during a stage. Think of this as a per-stage premint.
|
|
144
|
+
/// @dev These tokens can be minted with `autoIssueFor(…)`.
|
|
145
|
+
/// @custom:param revnetId The ID of the revnet to get the auto-mint amount for.
|
|
146
|
+
/// @custom:param stageId The ID of the stage to get the auto-mint amount for.
|
|
147
|
+
/// @custom:param beneficiary The beneficiary of the auto-mint.
|
|
148
|
+
mapping(uint256 revnetId => mapping(uint256 stageId => mapping(address beneficiary => uint256)))
|
|
149
|
+
public
|
|
150
|
+
override amountToAutoIssue;
|
|
151
|
+
|
|
152
|
+
/// @notice The timestamp of when cashouts will become available to a specific revnet's participants.
|
|
153
|
+
/// @dev Only applies to existing revnets which are deploying onto a new network.
|
|
154
|
+
/// @custom:param revnetId The ID of the revnet to get the cash out delay for.
|
|
155
|
+
mapping(uint256 revnetId => uint256 cashOutDelay) public override cashOutDelayOf;
|
|
156
|
+
|
|
157
|
+
/// @notice The hashed encoded configuration of each revnet.
|
|
158
|
+
/// @dev This is used to ensure that the encoded configuration of a revnet is the same when deploying suckers for
|
|
159
|
+
/// omnichain operations.
|
|
160
|
+
/// @custom:param revnetId The ID of the revnet to get the hashed encoded configuration for.
|
|
161
|
+
mapping(uint256 revnetId => bytes32 hashedEncodedConfiguration) public override hashedEncodedConfigurationOf;
|
|
162
|
+
|
|
163
|
+
/// @notice Each revnet's tiered ERC-721 hook.
|
|
164
|
+
/// @custom:param revnetId The ID of the revnet to get the tiered ERC-721 hook for.
|
|
165
|
+
// slither-disable-next-line uninitialized-state
|
|
166
|
+
mapping(uint256 revnetId => IJB721TiersHook tiered721Hook) public override tiered721HookOf;
|
|
167
|
+
|
|
168
|
+
//*********************************************************************//
|
|
169
|
+
// ------------------- internal stored properties -------------------- //
|
|
170
|
+
//*********************************************************************//
|
|
171
|
+
|
|
172
|
+
/// @notice A list of `JBPermissonIds` indices to grant to the split operator of a specific revnet.
|
|
173
|
+
/// @dev These should be set in the revnet's deployment process.
|
|
174
|
+
/// @custom:param revnetId The ID of the revnet to get the extra operator permissions for.
|
|
175
|
+
// slither-disable-next-line uninitialized-state
|
|
176
|
+
mapping(uint256 revnetId => uint256[]) internal _extraOperatorPermissions;
|
|
177
|
+
|
|
178
|
+
//*********************************************************************//
|
|
179
|
+
// -------------------------- constructor ---------------------------- //
|
|
180
|
+
//*********************************************************************//
|
|
181
|
+
|
|
182
|
+
/// @param controller The controller to use for launching and operating the Juicebox projects which will be revnets.
|
|
183
|
+
/// @param suckerRegistry The registry to use for deploying and tracking each revnet's suckers.
|
|
184
|
+
/// @param feeRevnetId The Juicebox project ID of the revnet that will receive fees.
|
|
185
|
+
/// @param hookDeployer The deployer to use for revnet's tiered ERC-721 hooks.
|
|
186
|
+
/// @param publisher The croptop publisher revnets can use to publish ERC-721 posts to their tiered ERC-721 hooks.
|
|
187
|
+
/// @param buybackHook The buyback hook used as a data hook to route payments through buyback pools.
|
|
188
|
+
/// @param loans The loan contract used by all revnets.
|
|
189
|
+
/// @param trustedForwarder The trusted forwarder for the ERC2771Context.
|
|
190
|
+
constructor(
|
|
191
|
+
IJBController controller,
|
|
192
|
+
IJBSuckerRegistry suckerRegistry,
|
|
193
|
+
uint256 feeRevnetId,
|
|
194
|
+
IJB721TiersHookDeployer hookDeployer,
|
|
195
|
+
CTPublisher publisher,
|
|
196
|
+
IJBRulesetDataHook buybackHook,
|
|
197
|
+
address loans,
|
|
198
|
+
address trustedForwarder
|
|
199
|
+
)
|
|
200
|
+
ERC2771Context(trustedForwarder)
|
|
201
|
+
{
|
|
202
|
+
CONTROLLER = controller;
|
|
203
|
+
DIRECTORY = controller.DIRECTORY();
|
|
204
|
+
PROJECTS = controller.PROJECTS();
|
|
205
|
+
PERMISSIONS = IJBPermissioned(address(CONTROLLER)).PERMISSIONS();
|
|
206
|
+
SUCKER_REGISTRY = suckerRegistry;
|
|
207
|
+
FEE_REVNET_ID = feeRevnetId;
|
|
208
|
+
HOOK_DEPLOYER = hookDeployer;
|
|
209
|
+
PUBLISHER = publisher;
|
|
210
|
+
BUYBACK_HOOK = buybackHook;
|
|
211
|
+
// slither-disable-next-line missing-zero-check
|
|
212
|
+
LOANS = loans;
|
|
213
|
+
|
|
214
|
+
// Give the sucker registry permission to map tokens for all revnets.
|
|
215
|
+
_setPermission({
|
|
216
|
+
operator: address(SUCKER_REGISTRY), revnetId: 0, permissionId: JBPermissionIds.MAP_SUCKER_TOKEN
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Give the loan contract permission to use the surplus allowance of all revnets.
|
|
220
|
+
_setPermission({operator: LOANS, revnetId: 0, permissionId: JBPermissionIds.USE_ALLOWANCE});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//*********************************************************************//
|
|
224
|
+
// ------------------------- external views -------------------------- //
|
|
225
|
+
//*********************************************************************//
|
|
226
|
+
|
|
227
|
+
/// @notice Before a revnet processes an incoming payment, determine the weight and pay hooks to use.
|
|
228
|
+
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
|
|
229
|
+
/// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
|
|
230
|
+
/// @return weight The weight which revnet tokens are minted relative to. This can be used to customize how many
|
|
231
|
+
/// tokens get minted by a payment.
|
|
232
|
+
/// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
|
|
233
|
+
/// into the revnet. Useful for automatically routing funds from a treasury as payments come in.
|
|
234
|
+
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
235
|
+
external
|
|
236
|
+
view
|
|
237
|
+
override
|
|
238
|
+
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
239
|
+
{
|
|
240
|
+
// Keep a reference to the specifications provided by the buyback hook.
|
|
241
|
+
JBPayHookSpecification[] memory buybackHookSpecifications;
|
|
242
|
+
|
|
243
|
+
// Read the weight and specifications from the buyback hook.
|
|
244
|
+
(weight, buybackHookSpecifications) = BUYBACK_HOOK.beforePayRecordedWith(context);
|
|
245
|
+
|
|
246
|
+
// Keep a reference to the revnet's tiered ERC-721 hook.
|
|
247
|
+
IJB721TiersHook tiered721Hook = tiered721HookOf[context.projectId];
|
|
248
|
+
|
|
249
|
+
// Is there a tiered ERC-721 hook?
|
|
250
|
+
bool usesTiered721Hook = address(tiered721Hook) != address(0);
|
|
251
|
+
|
|
252
|
+
// Initialize the returned specification array.
|
|
253
|
+
hookSpecifications = new JBPayHookSpecification[]((usesTiered721Hook ? 1 : 0) + 1);
|
|
254
|
+
|
|
255
|
+
// If we have a tiered ERC-721 hook, add it to the array.
|
|
256
|
+
if (usesTiered721Hook) {
|
|
257
|
+
hookSpecifications[0] =
|
|
258
|
+
JBPayHookSpecification({hook: IJBPayHook(address(tiered721Hook)), amount: 0, metadata: bytes("")});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Add the buyback hook specification.
|
|
262
|
+
hookSpecifications[usesTiered721Hook ? 1 : 0] = buybackHookSpecifications[0];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// @notice Determine how a cash out from a revnet should be processed.
|
|
266
|
+
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
|
|
267
|
+
/// @dev If a sucker is cashing out, no taxes or fees are imposed.
|
|
268
|
+
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
269
|
+
/// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
|
|
270
|
+
/// out.
|
|
271
|
+
/// @return cashOutCount The number of revnet tokens that are cashed out.
|
|
272
|
+
/// @return totalSupply The total revnet token supply.
|
|
273
|
+
/// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
|
|
274
|
+
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
275
|
+
external
|
|
276
|
+
view
|
|
277
|
+
override
|
|
278
|
+
returns (
|
|
279
|
+
uint256 cashOutTaxRate,
|
|
280
|
+
uint256 cashOutCount,
|
|
281
|
+
uint256 totalSupply,
|
|
282
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
283
|
+
)
|
|
284
|
+
{
|
|
285
|
+
// If the cash out is from a sucker, return the full cash out amount without taxes or fees.
|
|
286
|
+
if (_isSuckerOf({revnetId: context.projectId, addr: context.holder})) {
|
|
287
|
+
return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Keep a reference to the cash out delay of the revnet.
|
|
291
|
+
uint256 cashOutDelay = cashOutDelayOf[context.projectId];
|
|
292
|
+
|
|
293
|
+
// Enforce the cash out delay.
|
|
294
|
+
if (cashOutDelay > block.timestamp) {
|
|
295
|
+
revert REVDeployer_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Get the terminal that will receive the cash out fee.
|
|
299
|
+
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
|
|
300
|
+
|
|
301
|
+
// If there's no cash out tax (100% cash out tax rate), or if there's no fee terminal, do not charge a fee.
|
|
302
|
+
if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0)) {
|
|
303
|
+
return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, hookSpecifications);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Get a reference to the number of tokens being used to pay the fee (out of the total being cashed out).
|
|
307
|
+
uint256 feeCashOutCount = mulDiv(context.cashOutCount, FEE, JBConstants.MAX_FEE);
|
|
308
|
+
uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
|
|
309
|
+
|
|
310
|
+
// Keep a reference to the amount claimable with non-fee tokens.
|
|
311
|
+
uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
|
|
312
|
+
surplus: context.surplus.value,
|
|
313
|
+
cashOutCount: nonFeeCashOutCount,
|
|
314
|
+
totalSupply: context.totalSupply,
|
|
315
|
+
cashOutTaxRate: context.cashOutTaxRate
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Keep a reference to the fee amount after the reclaimed amount is subtracted.
|
|
319
|
+
uint256 feeAmount = JBCashOuts.cashOutFrom({
|
|
320
|
+
surplus: context.surplus.value - postFeeReclaimedAmount,
|
|
321
|
+
cashOutCount: feeCashOutCount,
|
|
322
|
+
totalSupply: context.totalSupply - nonFeeCashOutCount,
|
|
323
|
+
cashOutTaxRate: context.cashOutTaxRate
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Assemble a cash out hook specification to invoke `afterCashOutRecordedWith(…)` with, to process the fee.
|
|
327
|
+
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
328
|
+
hookSpecifications[0] = JBCashOutHookSpecification({
|
|
329
|
+
hook: IJBCashOutHook(address(this)), amount: feeAmount, metadata: abi.encode(feeTerminal)
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Return the cash out rate and the number of revnet tokens to cash out, minus the tokens being used to pay the
|
|
333
|
+
// fee.
|
|
334
|
+
return (context.cashOutTaxRate, nonFeeCashOutCount, context.totalSupply, hookSpecifications);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// @notice A flag indicating whether an address has permission to mint a revnet's tokens on-demand.
|
|
338
|
+
/// @dev Required by the `IJBRulesetDataHook` interface.
|
|
339
|
+
/// @param revnetId The ID of the revnet to check permissions for.
|
|
340
|
+
/// @param ruleset The ruleset to check the mint permission for.
|
|
341
|
+
/// @param addr The address to check the mint permission of.
|
|
342
|
+
/// @return flag A flag indicating whether the address has permission to mint the revnet's tokens on-demand.
|
|
343
|
+
function hasMintPermissionFor(
|
|
344
|
+
uint256 revnetId,
|
|
345
|
+
JBRuleset calldata ruleset,
|
|
346
|
+
address addr
|
|
347
|
+
)
|
|
348
|
+
external
|
|
349
|
+
view
|
|
350
|
+
override
|
|
351
|
+
returns (bool)
|
|
352
|
+
{
|
|
353
|
+
// The loans contract, buyback hook (and its delegates), and suckers are allowed to mint the revnet's tokens.
|
|
354
|
+
return addr == LOANS || addr == address(BUYBACK_HOOK)
|
|
355
|
+
|| BUYBACK_HOOK.hasMintPermissionFor({projectId: revnetId, ruleset: ruleset, addr: addr})
|
|
356
|
+
|| _isSuckerOf({revnetId: revnetId, addr: addr});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// @dev Make sure this contract can only receive project NFTs from `JBProjects`.
|
|
360
|
+
function onERC721Received(address, address, uint256, bytes calldata) external view returns (bytes4) {
|
|
361
|
+
// Make sure the 721 received is from the `JBProjects` contract.
|
|
362
|
+
if (msg.sender != address(PROJECTS)) revert();
|
|
363
|
+
|
|
364
|
+
return IERC721Receiver.onERC721Received.selector;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
//*********************************************************************//
|
|
368
|
+
// -------------------------- public views --------------------------- //
|
|
369
|
+
//*********************************************************************//
|
|
370
|
+
|
|
371
|
+
/// @notice A flag indicating whether an address is a revnet's split operator.
|
|
372
|
+
/// @param revnetId The ID of the revnet.
|
|
373
|
+
/// @param addr The address to check.
|
|
374
|
+
/// @return flag A flag indicating whether the address is the revnet's split operator.
|
|
375
|
+
function isSplitOperatorOf(uint256 revnetId, address addr) public view override returns (bool) {
|
|
376
|
+
return PERMISSIONS.hasPermissions({
|
|
377
|
+
operator: addr,
|
|
378
|
+
account: address(this),
|
|
379
|
+
projectId: revnetId,
|
|
380
|
+
permissionIds: _splitOperatorPermissionIndexesOf(revnetId),
|
|
381
|
+
includeRoot: false,
|
|
382
|
+
includeWildcardProjectId: false
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// @notice Indicates if this contract adheres to the specified interface.
|
|
387
|
+
/// @dev See `IERC165.supportsInterface`.
|
|
388
|
+
/// @return A flag indicating if the provided interface ID is supported.
|
|
389
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
390
|
+
return interfaceId == type(IREVDeployer).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
|
|
391
|
+
|| interfaceId == type(IJBCashOutHook).interfaceId || interfaceId == type(IERC721Receiver).interfaceId;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
//*********************************************************************//
|
|
395
|
+
// -------------------------- internal views ------------------------- //
|
|
396
|
+
//*********************************************************************//
|
|
397
|
+
|
|
398
|
+
/// @notice If the specified address is not the revnet's current split operator, revert.
|
|
399
|
+
/// @param revnetId The ID of the revnet to check split operator status for.
|
|
400
|
+
/// @param operator The address being checked.
|
|
401
|
+
function _checkIfIsSplitOperatorOf(uint256 revnetId, address operator) internal view {
|
|
402
|
+
if (!isSplitOperatorOf({revnetId: revnetId, addr: operator})) {
|
|
403
|
+
revert REVDeployer_Unauthorized(revnetId, operator);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// @notice A flag indicating whether an address is a revnet's sucker.
|
|
408
|
+
/// @param revnetId The ID of the revnet to check sucker status for.
|
|
409
|
+
/// @param addr The address being checked.
|
|
410
|
+
/// @return isSucker A flag indicating whether the address is one of the revnet's suckers.
|
|
411
|
+
function _isSuckerOf(uint256 revnetId, address addr) internal view returns (bool) {
|
|
412
|
+
return SUCKER_REGISTRY.isSuckerOf({projectId: revnetId, addr: addr});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/// @notice Initialize fund access limits for the loan contract and configure buyback pools for each terminal token.
|
|
416
|
+
/// @dev Returns an unlimited surplus allowance for each terminal+token pair derived from the terminal
|
|
417
|
+
/// configurations. Also auto-configures a buyback pool for each token with sensible defaults (1% fee, 2-day TWAP).
|
|
418
|
+
/// @param revnetId The ID of the revnet to configure buyback pools for.
|
|
419
|
+
/// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
|
|
420
|
+
/// @return fundAccessLimitGroups The fund access limit groups for the loans.
|
|
421
|
+
function _makeLoanFundAccessLimitsAndBuybackPools(
|
|
422
|
+
uint256 revnetId,
|
|
423
|
+
JBTerminalConfig[] calldata terminalConfigurations
|
|
424
|
+
)
|
|
425
|
+
internal
|
|
426
|
+
returns (JBFundAccessLimitGroup[] memory fundAccessLimitGroups)
|
|
427
|
+
{
|
|
428
|
+
// Count the total number of accounting contexts across all terminals.
|
|
429
|
+
uint256 count;
|
|
430
|
+
for (uint256 i; i < terminalConfigurations.length; i++) {
|
|
431
|
+
count += terminalConfigurations[i].accountingContextsToAccept.length;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Initialize the fund access limit groups.
|
|
435
|
+
fundAccessLimitGroups = new JBFundAccessLimitGroup[](count);
|
|
436
|
+
|
|
437
|
+
// Set up the fund access limits and buyback pools.
|
|
438
|
+
uint256 index;
|
|
439
|
+
for (uint256 i; i < terminalConfigurations.length; i++) {
|
|
440
|
+
JBTerminalConfig calldata terminalConfiguration = terminalConfigurations[i];
|
|
441
|
+
for (uint256 j; j < terminalConfiguration.accountingContextsToAccept.length; j++) {
|
|
442
|
+
JBAccountingContext calldata accountingContext = terminalConfiguration.accountingContextsToAccept[j];
|
|
443
|
+
|
|
444
|
+
// Set up an unlimited allowance for the loan contract to use.
|
|
445
|
+
JBCurrencyAmount[] memory loanAllowances = new JBCurrencyAmount[](1);
|
|
446
|
+
loanAllowances[0] = JBCurrencyAmount({currency: accountingContext.currency, amount: type(uint224).max});
|
|
447
|
+
|
|
448
|
+
// Set up the fund access limits for the loans.
|
|
449
|
+
fundAccessLimitGroups[index++] = JBFundAccessLimitGroup({
|
|
450
|
+
terminal: address(terminalConfiguration.terminal),
|
|
451
|
+
token: accountingContext.token,
|
|
452
|
+
payoutLimits: new JBCurrencyAmount[](0),
|
|
453
|
+
surplusAllowances: loanAllowances
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Configure a buyback pool for this terminal token with default fee and TWAP window.
|
|
457
|
+
// slither-disable-next-line unused-return,calls-loop
|
|
458
|
+
IJBBuybackHook(address(BUYBACK_HOOK))
|
|
459
|
+
.setPoolFor({
|
|
460
|
+
projectId: revnetId,
|
|
461
|
+
fee: DEFAULT_BUYBACK_POOL_FEE,
|
|
462
|
+
twapWindow: DEFAULT_BUYBACK_TWAP_WINDOW,
|
|
463
|
+
terminalToken: accountingContext.token
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/// @notice Make a ruleset configuration for a revnet's stage.
|
|
470
|
+
/// @param baseCurrency The base currency of the revnet.
|
|
471
|
+
/// @param stageConfiguration The stage configuration to make a ruleset for.
|
|
472
|
+
/// @param fundAccessLimitGroups The fund access limit groups to set up for the ruleset.
|
|
473
|
+
/// @return rulesetConfiguration The ruleset configuration.
|
|
474
|
+
function _makeRulesetConfiguration(
|
|
475
|
+
uint32 baseCurrency,
|
|
476
|
+
REVStageConfig calldata stageConfiguration,
|
|
477
|
+
JBFundAccessLimitGroup[] memory fundAccessLimitGroups
|
|
478
|
+
)
|
|
479
|
+
internal
|
|
480
|
+
view
|
|
481
|
+
returns (JBRulesetConfig memory)
|
|
482
|
+
{
|
|
483
|
+
// Set up the ruleset's metadata.
|
|
484
|
+
JBRulesetMetadata memory metadata;
|
|
485
|
+
metadata.reservedPercent = stageConfiguration.splitPercent;
|
|
486
|
+
metadata.cashOutTaxRate = stageConfiguration.cashOutTaxRate;
|
|
487
|
+
metadata.baseCurrency = baseCurrency;
|
|
488
|
+
metadata.useTotalSurplusForCashOuts = true; // Use surplus from all terminals for cash outs.
|
|
489
|
+
metadata.allowOwnerMinting = true; // Allow this contract to auto-mint tokens as the revnet's owner.
|
|
490
|
+
metadata.useDataHookForPay = true; // Call this contract's `beforePayRecordedWith(…)` callback on payments.
|
|
491
|
+
metadata.useDataHookForCashOut = true; // Call this contract's `beforeCashOutRecordedWith(…)` callback on cash
|
|
492
|
+
// outs.
|
|
493
|
+
metadata.dataHook = address(this); // This contract is the data hook.
|
|
494
|
+
metadata.metadata = stageConfiguration.extraMetadata;
|
|
495
|
+
|
|
496
|
+
// Package the reserved token splits.
|
|
497
|
+
JBSplitGroup[] memory splitGroups = new JBSplitGroup[](1);
|
|
498
|
+
splitGroups[0] = JBSplitGroup({groupId: JBSplitGroupIds.RESERVED_TOKENS, splits: stageConfiguration.splits});
|
|
499
|
+
|
|
500
|
+
return JBRulesetConfig({
|
|
501
|
+
mustStartAtOrAfter: stageConfiguration.startsAtOrAfter,
|
|
502
|
+
duration: stageConfiguration.issuanceCutFrequency,
|
|
503
|
+
weight: stageConfiguration.initialIssuance,
|
|
504
|
+
weightCutPercent: stageConfiguration.issuanceCutPercent,
|
|
505
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
506
|
+
metadata: metadata,
|
|
507
|
+
splitGroups: splitGroups,
|
|
508
|
+
fundAccessLimitGroups: fundAccessLimitGroups
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/// @notice Returns the next project ID.
|
|
513
|
+
/// @return nextProjectId The next project ID.
|
|
514
|
+
function _nextProjectId() internal view returns (uint256) {
|
|
515
|
+
return PROJECTS.count() + 1;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/// @notice Returns the permissions that the split operator should be granted for a revnet.
|
|
519
|
+
/// @param revnetId The ID of the revnet to get split operator permissions for.
|
|
520
|
+
/// @return allOperatorPermissions The permissions that the split operator should be granted for the revnet,
|
|
521
|
+
/// including both default and custom permissions.
|
|
522
|
+
function _splitOperatorPermissionIndexesOf(uint256 revnetId)
|
|
523
|
+
internal
|
|
524
|
+
view
|
|
525
|
+
returns (uint256[] memory allOperatorPermissions)
|
|
526
|
+
{
|
|
527
|
+
// Keep a reference to the custom split operator permissions.
|
|
528
|
+
uint256[] memory customSplitOperatorPermissionIndexes = _extraOperatorPermissions[revnetId];
|
|
529
|
+
|
|
530
|
+
// Make the array that merges the default and custom operator permissions.
|
|
531
|
+
allOperatorPermissions = new uint256[](9 + customSplitOperatorPermissionIndexes.length);
|
|
532
|
+
allOperatorPermissions[0] = JBPermissionIds.SET_SPLIT_GROUPS;
|
|
533
|
+
allOperatorPermissions[1] = JBPermissionIds.SET_BUYBACK_POOL;
|
|
534
|
+
allOperatorPermissions[2] = JBPermissionIds.SET_BUYBACK_TWAP;
|
|
535
|
+
allOperatorPermissions[3] = JBPermissionIds.SET_PROJECT_URI;
|
|
536
|
+
allOperatorPermissions[4] = JBPermissionIds.ADD_PRICE_FEED;
|
|
537
|
+
allOperatorPermissions[5] = JBPermissionIds.SUCKER_SAFETY;
|
|
538
|
+
allOperatorPermissions[6] = JBPermissionIds.ADD_SWAP_TERMINAL_POOL;
|
|
539
|
+
allOperatorPermissions[7] = JBPermissionIds.SET_BUYBACK_HOOK;
|
|
540
|
+
allOperatorPermissions[8] = JBPermissionIds.SET_SWAP_TERMINAL;
|
|
541
|
+
|
|
542
|
+
// Copy the custom permissions into the array.
|
|
543
|
+
for (uint256 i; i < customSplitOperatorPermissionIndexes.length; i++) {
|
|
544
|
+
allOperatorPermissions[9 + i] = customSplitOperatorPermissionIndexes[i];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
//*********************************************************************//
|
|
549
|
+
// --------------------- external transactions ----------------------- //
|
|
550
|
+
//*********************************************************************//
|
|
551
|
+
|
|
552
|
+
/// @notice Processes the fee from a cash out.
|
|
553
|
+
/// @param context Cash out context passed in by the terminal.
|
|
554
|
+
function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context) external payable {
|
|
555
|
+
// No caller validation needed — this hook only pays fees to the fee project using funds forwarded by the
|
|
556
|
+
// caller. A non-terminal caller would just be donating their own funds as fees. There's nothing to exploit.
|
|
557
|
+
|
|
558
|
+
// If there's sufficient approval, transfer normally.
|
|
559
|
+
if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
|
|
560
|
+
IERC20(context.forwardedAmount.token)
|
|
561
|
+
.safeTransferFrom({from: msg.sender, to: address(this), value: context.forwardedAmount.value});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Parse the metadata forwarded from the data hook to get the fee terminal.
|
|
565
|
+
// See `beforeCashOutRecordedWith(…)`.
|
|
566
|
+
(IJBTerminal feeTerminal) = abi.decode(context.hookMetadata, (IJBTerminal));
|
|
567
|
+
|
|
568
|
+
// Determine how much to pay in `msg.value` (in the native currency).
|
|
569
|
+
uint256 payValue = _beforeTransferTo({
|
|
570
|
+
to: address(feeTerminal), token: context.forwardedAmount.token, amount: context.forwardedAmount.value
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Pay the fee.
|
|
574
|
+
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
575
|
+
try feeTerminal.pay{value: payValue}({
|
|
576
|
+
projectId: FEE_REVNET_ID,
|
|
577
|
+
token: context.forwardedAmount.token,
|
|
578
|
+
amount: context.forwardedAmount.value,
|
|
579
|
+
beneficiary: context.holder,
|
|
580
|
+
minReturnedTokens: 0,
|
|
581
|
+
memo: "",
|
|
582
|
+
metadata: bytes(abi.encodePacked(context.projectId))
|
|
583
|
+
}) {}
|
|
584
|
+
catch (bytes memory) {
|
|
585
|
+
// Decrease the allowance for the fee terminal if the token is not the native token.
|
|
586
|
+
if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
|
|
587
|
+
IERC20(context.forwardedAmount.token)
|
|
588
|
+
.safeDecreaseAllowance({
|
|
589
|
+
spender: address(feeTerminal), requestedDecrease: context.forwardedAmount.value
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// If the fee can't be processed, return the funds to the project.
|
|
594
|
+
payValue = _beforeTransferTo({
|
|
595
|
+
to: msg.sender, token: context.forwardedAmount.token, amount: context.forwardedAmount.value
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// slither-disable-next-line arbitrary-send-eth
|
|
599
|
+
IJBTerminal(msg.sender).addToBalanceOf{value: payValue}({
|
|
600
|
+
projectId: context.projectId,
|
|
601
|
+
token: context.forwardedAmount.token,
|
|
602
|
+
amount: context.forwardedAmount.value,
|
|
603
|
+
shouldReturnHeldFees: false,
|
|
604
|
+
memo: "",
|
|
605
|
+
metadata: bytes(abi.encodePacked(FEE_REVNET_ID))
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/// @notice Auto-mint a revnet's tokens from a stage for a beneficiary.
|
|
611
|
+
/// @param revnetId The ID of the revnet to auto-mint tokens from.
|
|
612
|
+
/// @param stageId The ID of the stage auto-mint tokens are available from.
|
|
613
|
+
/// @param beneficiary The address to auto-mint tokens to.
|
|
614
|
+
function autoIssueFor(uint256 revnetId, uint256 stageId, address beneficiary) external override {
|
|
615
|
+
// Get a reference to the ruleset for the stage.
|
|
616
|
+
// slither-disable-next-line unused-return
|
|
617
|
+
(JBRuleset memory ruleset,) = CONTROLLER.getRulesetOf({projectId: revnetId, rulesetId: stageId});
|
|
618
|
+
|
|
619
|
+
// Make sure the stage has started.
|
|
620
|
+
if (ruleset.start > block.timestamp) {
|
|
621
|
+
revert REVDeployer_StageNotStarted(stageId);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Get a reference to the number of tokens to auto-issue.
|
|
625
|
+
uint256 count = amountToAutoIssue[revnetId][stageId][beneficiary];
|
|
626
|
+
|
|
627
|
+
// If there's nothing to auto-mint, return.
|
|
628
|
+
if (count == 0) revert REVDeployer_NothingToAutoIssue();
|
|
629
|
+
|
|
630
|
+
// Reset the auto-mint amount.
|
|
631
|
+
amountToAutoIssue[revnetId][stageId][beneficiary] = 0;
|
|
632
|
+
|
|
633
|
+
emit AutoIssue({
|
|
634
|
+
revnetId: revnetId, stageId: stageId, beneficiary: beneficiary, count: count, caller: _msgSender()
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Mint the tokens.
|
|
638
|
+
// slither-disable-next-line unused-return
|
|
639
|
+
CONTROLLER.mintTokensOf({
|
|
640
|
+
projectId: revnetId, tokenCount: count, beneficiary: beneficiary, memo: "", useReservedPercent: false
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/// @notice Launch a revnet, or initialize an existing Juicebox project as a revnet.
|
|
645
|
+
/// @dev When initializing an existing project (revnetId != 0):
|
|
646
|
+
/// - The project must not yet have a controller or rulesets. `JBController.launchRulesetsFor` enforces this —
|
|
647
|
+
/// it reverts if rulesets have already been launched, and `JBDirectory.setControllerOf` only allows setting the
|
|
648
|
+
/// first controller. This means conversion only works for blank projects (just an ID with no on-chain state).
|
|
649
|
+
/// - This is useful in deploy scripts where the project ID is needed before configuration (e.g. for cross-chain
|
|
650
|
+
/// sucker peer mappings): create the project first, then initialize it as a revnet here.
|
|
651
|
+
/// - Initialization is a one-way operation: the project's ownership NFT is permanently transferred to this
|
|
652
|
+
/// REVDeployer, and the project becomes subject to immutable revnet rules. This cannot be undone.
|
|
653
|
+
/// @param revnetId The ID of the Juicebox project to initialize as a revnet. Send 0 to deploy a new revnet.
|
|
654
|
+
/// @param configuration Core revnet configuration. See `REVConfig`.
|
|
655
|
+
/// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
|
|
656
|
+
/// @param suckerDeploymentConfiguration The suckers to set up for the revnet. Suckers facilitate cross-chain
|
|
657
|
+
/// token transfers between peer revnets on different networks.
|
|
658
|
+
/// @return revnetId The ID of the newly created revnet.
|
|
659
|
+
function deployFor(
|
|
660
|
+
uint256 revnetId,
|
|
661
|
+
REVConfig calldata configuration,
|
|
662
|
+
JBTerminalConfig[] calldata terminalConfigurations,
|
|
663
|
+
REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration
|
|
664
|
+
)
|
|
665
|
+
external
|
|
666
|
+
override
|
|
667
|
+
returns (uint256)
|
|
668
|
+
{
|
|
669
|
+
// Keep a reference to the revnet ID which was passed in.
|
|
670
|
+
bool shouldDeployNewRevnet = revnetId == 0;
|
|
671
|
+
|
|
672
|
+
// If the caller is deploying a new revnet, calculate its ID
|
|
673
|
+
// (which will be 1 greater than the current count).
|
|
674
|
+
if (shouldDeployNewRevnet) revnetId = _nextProjectId();
|
|
675
|
+
|
|
676
|
+
// Normalize and encode the configurations.
|
|
677
|
+
(JBRulesetConfig[] memory rulesetConfigurations, bytes32 encodedConfigurationHash) = _makeRulesetConfigurations({
|
|
678
|
+
revnetId: revnetId, configuration: configuration, terminalConfigurations: terminalConfigurations
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// Deploy the revnet.
|
|
682
|
+
_deployRevnetFor({
|
|
683
|
+
revnetId: revnetId,
|
|
684
|
+
shouldDeployNewRevnet: shouldDeployNewRevnet,
|
|
685
|
+
configuration: configuration,
|
|
686
|
+
terminalConfigurations: terminalConfigurations,
|
|
687
|
+
suckerDeploymentConfiguration: suckerDeploymentConfiguration,
|
|
688
|
+
rulesetConfigurations: rulesetConfigurations,
|
|
689
|
+
encodedConfigurationHash: encodedConfigurationHash
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
return revnetId;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/// @notice Deploy new suckers for an existing revnet.
|
|
696
|
+
/// @dev Only the revnet's split operator can deploy new suckers.
|
|
697
|
+
/// @param revnetId The ID of the revnet to deploy suckers for.
|
|
698
|
+
/// See `_makeRulesetConfigurations(…)` for encoding details. Clients can read the encoded configuration
|
|
699
|
+
/// from the `DeployRevnet` event emitted by this contract.
|
|
700
|
+
/// @param suckerDeploymentConfiguration The suckers to set up for the revnet.
|
|
701
|
+
function deploySuckersFor(
|
|
702
|
+
uint256 revnetId,
|
|
703
|
+
REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration
|
|
704
|
+
)
|
|
705
|
+
external
|
|
706
|
+
override
|
|
707
|
+
returns (address[] memory suckers)
|
|
708
|
+
{
|
|
709
|
+
// Make sure the caller is the revnet's split operator.
|
|
710
|
+
_checkIfIsSplitOperatorOf({revnetId: revnetId, operator: _msgSender()});
|
|
711
|
+
|
|
712
|
+
// Check if the current ruleset allows deploying new suckers.
|
|
713
|
+
// slither-disable-next-line unused-return
|
|
714
|
+
(, JBRulesetMetadata memory metadata) = CONTROLLER.currentRulesetOf(revnetId);
|
|
715
|
+
|
|
716
|
+
// Check the third bit, it indicates if the ruleset allows new suckers to be deployed.
|
|
717
|
+
bool allowsDeployingSuckers = ((metadata.metadata >> 2) & 1) == 1;
|
|
718
|
+
|
|
719
|
+
if (!allowsDeployingSuckers) {
|
|
720
|
+
revert REVDeployer_RulesetDoesNotAllowDeployingSuckers();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Deploy the suckers.
|
|
724
|
+
suckers = _deploySuckersFor({
|
|
725
|
+
revnetId: revnetId,
|
|
726
|
+
encodedConfigurationHash: hashedEncodedConfigurationOf[revnetId],
|
|
727
|
+
suckerDeploymentConfiguration: suckerDeploymentConfiguration
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/// @notice Launch a revnet which sells tiered ERC-721s and (optionally) allows croptop posts to its ERC-721 tiers.
|
|
732
|
+
/// @dev When initializing an existing project (revnetId != 0), the project must be blank (no controller or
|
|
733
|
+
/// rulesets). The initialization is irreversible. See `deployFor` documentation for full details.
|
|
734
|
+
/// @param revnetId The ID of the Juicebox project to initialize as a revnet. Send 0 to deploy a new revnet.
|
|
735
|
+
/// @param configuration Core revnet configuration. See `REVConfig`.
|
|
736
|
+
/// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
|
|
737
|
+
/// @param suckerDeploymentConfiguration The suckers to set up for the revnet. Suckers facilitate cross-chain
|
|
738
|
+
/// token transfers between peer revnets on different networks.
|
|
739
|
+
/// @param tiered721HookConfiguration How to set up the tiered ERC-721 hook for the revnet.
|
|
740
|
+
/// @param allowedPosts Restrictions on which croptop posts are allowed on the revnet's ERC-721 tiers.
|
|
741
|
+
/// @return revnetId The ID of the newly created revnet.
|
|
742
|
+
/// @return hook The address of the tiered ERC-721 hook that was deployed for the revnet.
|
|
743
|
+
function deployWith721sFor(
|
|
744
|
+
uint256 revnetId,
|
|
745
|
+
REVConfig calldata configuration,
|
|
746
|
+
JBTerminalConfig[] calldata terminalConfigurations,
|
|
747
|
+
REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
|
|
748
|
+
REVDeploy721TiersHookConfig calldata tiered721HookConfiguration,
|
|
749
|
+
REVCroptopAllowedPost[] calldata allowedPosts
|
|
750
|
+
)
|
|
751
|
+
external
|
|
752
|
+
override
|
|
753
|
+
returns (uint256, IJB721TiersHook hook)
|
|
754
|
+
{
|
|
755
|
+
// Keep a reference to the revnet ID which was passed in.
|
|
756
|
+
bool shouldDeployNewRevnet = revnetId == 0;
|
|
757
|
+
|
|
758
|
+
// If the caller is deploying a new revnet, calculate its ID
|
|
759
|
+
// (which will be 1 greater than the current count).
|
|
760
|
+
if (shouldDeployNewRevnet) revnetId = _nextProjectId();
|
|
761
|
+
|
|
762
|
+
// Deploy the revnet with the specified tiered ERC-721 hook and croptop posting criteria.
|
|
763
|
+
hook = _deploy721RevnetFor({
|
|
764
|
+
revnetId: revnetId,
|
|
765
|
+
shouldDeployNewRevnet: shouldDeployNewRevnet,
|
|
766
|
+
configuration: configuration,
|
|
767
|
+
terminalConfigurations: terminalConfigurations,
|
|
768
|
+
suckerDeploymentConfiguration: suckerDeploymentConfiguration,
|
|
769
|
+
tiered721HookConfiguration: tiered721HookConfiguration,
|
|
770
|
+
allowedPosts: allowedPosts
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
return (revnetId, hook);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/// @notice Burn any of a revnet's tokens held by this contract.
|
|
777
|
+
/// @dev Project tokens can end up here from reserved token distribution when splits don't sum to 100%.
|
|
778
|
+
/// @param revnetId The ID of the revnet whose tokens should be burned.
|
|
779
|
+
function burnHeldTokensOf(uint256 revnetId) external override {
|
|
780
|
+
uint256 balance = CONTROLLER.TOKENS().totalBalanceOf({holder: address(this), projectId: revnetId});
|
|
781
|
+
if (balance == 0) revert REVDeployer_NothingToBurn();
|
|
782
|
+
CONTROLLER.burnTokensOf({holder: address(this), projectId: revnetId, tokenCount: balance, memo: ""});
|
|
783
|
+
// slither-disable-next-line reentrancy-events
|
|
784
|
+
emit BurnHeldTokens(revnetId, balance, _msgSender());
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/// @notice Change a revnet's split operator.
|
|
788
|
+
/// @dev Only a revnet's current split operator can set a new split operator.
|
|
789
|
+
/// @param revnetId The ID of the revnet to set the split operator of.
|
|
790
|
+
/// @param newSplitOperator The new split operator's address.
|
|
791
|
+
function setSplitOperatorOf(uint256 revnetId, address newSplitOperator) external override {
|
|
792
|
+
// Enforce permissions.
|
|
793
|
+
_checkIfIsSplitOperatorOf({revnetId: revnetId, operator: _msgSender()});
|
|
794
|
+
|
|
795
|
+
emit ReplaceSplitOperator({revnetId: revnetId, newSplitOperator: newSplitOperator, caller: _msgSender()});
|
|
796
|
+
|
|
797
|
+
// Remove operator permissions from the old split operator.
|
|
798
|
+
_setPermissionsFor({
|
|
799
|
+
account: address(this), operator: _msgSender(), revnetId: revnetId, permissionIds: new uint8[](0)
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// Set the new split operator.
|
|
803
|
+
_setSplitOperatorOf({revnetId: revnetId, operator: newSplitOperator});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
//*********************************************************************//
|
|
807
|
+
// --------------------- internal transactions ----------------------- //
|
|
808
|
+
//*********************************************************************//
|
|
809
|
+
|
|
810
|
+
/// @notice Logic to be triggered before transferring tokens from this contract.
|
|
811
|
+
/// @param to The address the transfer is going to.
|
|
812
|
+
/// @param token The token being transferred.
|
|
813
|
+
/// @param amount The number of tokens being transferred, as a fixed point number with the same number of decimals
|
|
814
|
+
/// as the token specifies.
|
|
815
|
+
/// @return payValue The value to attach to the transaction being sent.
|
|
816
|
+
function _beforeTransferTo(address to, address token, uint256 amount) internal returns (uint256) {
|
|
817
|
+
// If the token is the native token, no allowance needed.
|
|
818
|
+
if (token == JBConstants.NATIVE_TOKEN) return amount;
|
|
819
|
+
IERC20(token).safeIncreaseAllowance(to, amount);
|
|
820
|
+
return 0;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/// @notice Deploy a revnet which sells tiered ERC-721s and (optionally) allows croptop posts to its ERC-721 tiers.
|
|
824
|
+
/// @param revnetId The ID of the Juicebox project to turn into a revnet. Send 0 to deploy a new revnet.
|
|
825
|
+
/// @param shouldDeployNewRevnet Whether to deploy a new revnet or convert an existing Juicebox project into a
|
|
826
|
+
/// revnet.
|
|
827
|
+
/// @param configuration Core revnet configuration. See `REVConfig`.
|
|
828
|
+
/// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
|
|
829
|
+
/// @param suckerDeploymentConfiguration The suckers to set up for the revnet. Suckers facilitate cross-chain
|
|
830
|
+
/// token transfers between peer revnets on different networks.
|
|
831
|
+
/// @param tiered721HookConfiguration How to set up the tiered ERC-721 hook for the revnet.
|
|
832
|
+
/// @param allowedPosts Restrictions on which croptop posts are allowed on the revnet's ERC-721 tiers.
|
|
833
|
+
/// @return hook The address of the tiered ERC-721 hook that was deployed for the revnet.
|
|
834
|
+
function _deploy721RevnetFor(
|
|
835
|
+
uint256 revnetId,
|
|
836
|
+
bool shouldDeployNewRevnet,
|
|
837
|
+
REVConfig calldata configuration,
|
|
838
|
+
JBTerminalConfig[] calldata terminalConfigurations,
|
|
839
|
+
REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
|
|
840
|
+
REVDeploy721TiersHookConfig calldata tiered721HookConfiguration,
|
|
841
|
+
REVCroptopAllowedPost[] calldata allowedPosts
|
|
842
|
+
)
|
|
843
|
+
internal
|
|
844
|
+
returns (IJB721TiersHook hook)
|
|
845
|
+
{
|
|
846
|
+
// Normalize and encode the configurations.
|
|
847
|
+
(JBRulesetConfig[] memory rulesetConfigurations, bytes32 encodedConfigurationHash) = _makeRulesetConfigurations({
|
|
848
|
+
revnetId: revnetId, configuration: configuration, terminalConfigurations: terminalConfigurations
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// Deploy the tiered ERC-721 hook contract.
|
|
852
|
+
// slither-disable-next-line reentrancy-benign
|
|
853
|
+
hook = HOOK_DEPLOYER.deployHookFor({
|
|
854
|
+
projectId: revnetId,
|
|
855
|
+
deployTiersHookConfig: tiered721HookConfiguration.baseline721HookConfiguration,
|
|
856
|
+
salt: keccak256(abi.encode(tiered721HookConfiguration.salt, encodedConfigurationHash, _msgSender()))
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// Store the tiered ERC-721 hook.
|
|
860
|
+
tiered721HookOf[revnetId] = hook;
|
|
861
|
+
|
|
862
|
+
// If specified, give the split operator permission to add and remove tiers.
|
|
863
|
+
if (tiered721HookConfiguration.splitOperatorCanAdjustTiers) {
|
|
864
|
+
_extraOperatorPermissions[revnetId].push(JBPermissionIds.ADJUST_721_TIERS);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// If specified, give the split operator permission to set ERC-721 tier metadata.
|
|
868
|
+
if (tiered721HookConfiguration.splitOperatorCanUpdateMetadata) {
|
|
869
|
+
_extraOperatorPermissions[revnetId].push(JBPermissionIds.SET_721_METADATA);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// If specified, give the split operator permission to mint ERC-721s (without a payment)
|
|
873
|
+
// from tiers with `allowOwnerMint` set to true.
|
|
874
|
+
if (tiered721HookConfiguration.splitOperatorCanMint) {
|
|
875
|
+
_extraOperatorPermissions[revnetId].push(JBPermissionIds.MINT_721);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// If specified, give the split operator permission to increase the discount of a tier.
|
|
879
|
+
if (tiered721HookConfiguration.splitOperatorCanIncreaseDiscountPercent) {
|
|
880
|
+
_extraOperatorPermissions[revnetId].push(JBPermissionIds.SET_721_DISCOUNT_PERCENT);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// If there are posts to allow, configure them.
|
|
884
|
+
if (allowedPosts.length != 0) {
|
|
885
|
+
// Keep a reference to the formatted allowed posts.
|
|
886
|
+
CTAllowedPost[] memory formattedAllowedPosts = new CTAllowedPost[](allowedPosts.length);
|
|
887
|
+
|
|
888
|
+
// Iterate through each post to add it to the formatted list.
|
|
889
|
+
for (uint256 i; i < allowedPosts.length; i++) {
|
|
890
|
+
// Set the post being iterated on.
|
|
891
|
+
REVCroptopAllowedPost calldata post = allowedPosts[i];
|
|
892
|
+
|
|
893
|
+
// Set the formatted post.
|
|
894
|
+
formattedAllowedPosts[i] = CTAllowedPost({
|
|
895
|
+
hook: address(hook),
|
|
896
|
+
category: post.category,
|
|
897
|
+
minimumPrice: post.minimumPrice,
|
|
898
|
+
minimumTotalSupply: post.minimumTotalSupply,
|
|
899
|
+
maximumTotalSupply: post.maximumTotalSupply,
|
|
900
|
+
maximumSplitPercent: post.maximumSplitPercent,
|
|
901
|
+
allowedAddresses: post.allowedAddresses
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Set up the allowed posts in the publisher.
|
|
906
|
+
PUBLISHER.configurePostingCriteriaFor({allowedPosts: formattedAllowedPosts});
|
|
907
|
+
|
|
908
|
+
// Give the croptop publisher permission to post new ERC-721 tiers on this contract's behalf.
|
|
909
|
+
_setPermission({
|
|
910
|
+
operator: address(PUBLISHER), revnetId: revnetId, permissionId: JBPermissionIds.ADJUST_721_TIERS
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
_deployRevnetFor({
|
|
915
|
+
revnetId: revnetId,
|
|
916
|
+
shouldDeployNewRevnet: shouldDeployNewRevnet,
|
|
917
|
+
configuration: configuration,
|
|
918
|
+
terminalConfigurations: terminalConfigurations,
|
|
919
|
+
suckerDeploymentConfiguration: suckerDeploymentConfiguration,
|
|
920
|
+
rulesetConfigurations: rulesetConfigurations,
|
|
921
|
+
encodedConfigurationHash: encodedConfigurationHash
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/// @notice Deploy a revnet, or initialize an existing Juicebox project as a revnet.
|
|
926
|
+
/// @dev When initializing an existing project (`shouldDeployNewRevnet == false`):
|
|
927
|
+
/// - The project must be blank — no controller or rulesets. This is enforced by `JBController.launchRulesetsFor`,
|
|
928
|
+
/// which reverts if rulesets exist, and by `JBDirectory.setControllerOf`, which only allows setting the first
|
|
929
|
+
/// controller. Without a controller, no tokens or terminals can exist, so the project is guaranteed to be
|
|
930
|
+
/// uninitialized.
|
|
931
|
+
/// - The project's JBProjects NFT is permanently transferred to this contract. This is irreversible.
|
|
932
|
+
/// @param revnetId The ID of the Juicebox project to initialize as a revnet. Send 0 to deploy a new revnet.
|
|
933
|
+
/// @param shouldDeployNewRevnet Whether to deploy a new revnet or convert an existing Juicebox project into a
|
|
934
|
+
/// revnet.
|
|
935
|
+
/// @param configuration Core revnet configuration. See `REVConfig`.
|
|
936
|
+
/// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
|
|
937
|
+
/// @param suckerDeploymentConfiguration The suckers to set up for the revnet. Suckers facilitate cross-chain
|
|
938
|
+
/// token transfers between peer revnets on different networks.
|
|
939
|
+
/// @param rulesetConfigurations The rulesets to set up for the revnet.
|
|
940
|
+
/// @param encodedConfigurationHash A hash that represents the revnet's configuration.
|
|
941
|
+
/// See `_makeRulesetConfigurations(…)` for encoding details. Clients can read the encoded configuration
|
|
942
|
+
/// from the `DeployRevnet` event emitted by this contract.
|
|
943
|
+
function _deployRevnetFor(
|
|
944
|
+
uint256 revnetId,
|
|
945
|
+
bool shouldDeployNewRevnet,
|
|
946
|
+
REVConfig calldata configuration,
|
|
947
|
+
JBTerminalConfig[] calldata terminalConfigurations,
|
|
948
|
+
REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
|
|
949
|
+
JBRulesetConfig[] memory rulesetConfigurations,
|
|
950
|
+
bytes32 encodedConfigurationHash
|
|
951
|
+
)
|
|
952
|
+
internal
|
|
953
|
+
{
|
|
954
|
+
if (shouldDeployNewRevnet) {
|
|
955
|
+
// If we're deploying a new revnet, launch a Juicebox project for it.
|
|
956
|
+
// Sanity check that we deployed the `revnetId` that we expected to deploy.
|
|
957
|
+
// slither-disable-next-line reentrancy-benign,reentrancy-events
|
|
958
|
+
assert(
|
|
959
|
+
CONTROLLER.launchProjectFor({
|
|
960
|
+
owner: address(this),
|
|
961
|
+
projectUri: configuration.description.uri,
|
|
962
|
+
rulesetConfigurations: rulesetConfigurations,
|
|
963
|
+
terminalConfigurations: terminalConfigurations,
|
|
964
|
+
memo: ""
|
|
965
|
+
}) == revnetId
|
|
966
|
+
);
|
|
967
|
+
} else {
|
|
968
|
+
// Keep a reference to the Juicebox project's owner.
|
|
969
|
+
address owner = PROJECTS.ownerOf(revnetId);
|
|
970
|
+
|
|
971
|
+
// Make sure the caller is the owner of the Juicebox project.
|
|
972
|
+
if (_msgSender() != owner) revert REVDeployer_Unauthorized(revnetId, _msgSender());
|
|
973
|
+
|
|
974
|
+
// Initialize the existing Juicebox project as a revnet by
|
|
975
|
+
// transferring the `JBProjects` NFT to this deployer. This is irreversible.
|
|
976
|
+
IERC721(PROJECTS).safeTransferFrom({from: owner, to: address(this), tokenId: revnetId});
|
|
977
|
+
|
|
978
|
+
// Launch the revnet rulesets for the pre-existing project.
|
|
979
|
+
// slither-disable-next-line unused-return
|
|
980
|
+
CONTROLLER.launchRulesetsFor({
|
|
981
|
+
projectId: revnetId,
|
|
982
|
+
rulesetConfigurations: rulesetConfigurations,
|
|
983
|
+
terminalConfigurations: terminalConfigurations,
|
|
984
|
+
memo: ""
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// Set the revnet's URI.
|
|
988
|
+
CONTROLLER.setUriOf({projectId: revnetId, uri: configuration.description.uri});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Store the cash out delay of the revnet if its stages are already in progress.
|
|
992
|
+
// This prevents cash out liquidity/arbitrage issues for existing revnets which
|
|
993
|
+
// are deploying to a new chain.
|
|
994
|
+
_setCashOutDelayIfNeeded({revnetId: revnetId, firstStageConfig: configuration.stageConfigurations[0]});
|
|
995
|
+
|
|
996
|
+
// Deploy the revnet's ERC-20 token.
|
|
997
|
+
// slither-disable-next-line unused-return
|
|
998
|
+
CONTROLLER.deployERC20For({
|
|
999
|
+
projectId: revnetId,
|
|
1000
|
+
name: configuration.description.name,
|
|
1001
|
+
symbol: configuration.description.ticker,
|
|
1002
|
+
salt: keccak256(abi.encode(configuration.description.salt, encodedConfigurationHash, _msgSender()))
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// Give the split operator their permissions.
|
|
1006
|
+
_setSplitOperatorOf({revnetId: revnetId, operator: configuration.splitOperator});
|
|
1007
|
+
|
|
1008
|
+
// Deploy the suckers (if applicable).
|
|
1009
|
+
if (suckerDeploymentConfiguration.salt != bytes32(0)) {
|
|
1010
|
+
_deploySuckersFor({
|
|
1011
|
+
revnetId: revnetId,
|
|
1012
|
+
encodedConfigurationHash: encodedConfigurationHash,
|
|
1013
|
+
suckerDeploymentConfiguration: suckerDeploymentConfiguration
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Store the hashed encoded configuration.
|
|
1018
|
+
hashedEncodedConfigurationOf[revnetId] = encodedConfigurationHash;
|
|
1019
|
+
|
|
1020
|
+
emit DeployRevnet({
|
|
1021
|
+
revnetId: revnetId,
|
|
1022
|
+
configuration: configuration,
|
|
1023
|
+
terminalConfigurations: terminalConfigurations,
|
|
1024
|
+
suckerDeploymentConfiguration: suckerDeploymentConfiguration,
|
|
1025
|
+
rulesetConfigurations: rulesetConfigurations,
|
|
1026
|
+
encodedConfigurationHash: encodedConfigurationHash,
|
|
1027
|
+
caller: _msgSender()
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/// @param encodedConfigurationHash A hash that represents the revnet's configuration.
|
|
1032
|
+
/// See `_makeRulesetConfigurations(…)` for encoding details. Clients can read the encoded configuration
|
|
1033
|
+
/// from the `DeployRevnet` event emitted by this contract.
|
|
1034
|
+
/// @param suckerDeploymentConfiguration The suckers to set up for the revnet.
|
|
1035
|
+
function _deploySuckersFor(
|
|
1036
|
+
uint256 revnetId,
|
|
1037
|
+
bytes32 encodedConfigurationHash,
|
|
1038
|
+
REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration
|
|
1039
|
+
)
|
|
1040
|
+
internal
|
|
1041
|
+
returns (address[] memory suckers)
|
|
1042
|
+
{
|
|
1043
|
+
emit DeploySuckers({
|
|
1044
|
+
revnetId: revnetId,
|
|
1045
|
+
encodedConfigurationHash: encodedConfigurationHash,
|
|
1046
|
+
suckerDeploymentConfiguration: suckerDeploymentConfiguration,
|
|
1047
|
+
caller: _msgSender()
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// Deploy the suckers.
|
|
1051
|
+
// slither-disable-next-line unused-return
|
|
1052
|
+
suckers = SUCKER_REGISTRY.deploySuckersFor({
|
|
1053
|
+
projectId: revnetId,
|
|
1054
|
+
salt: keccak256(abi.encode(encodedConfigurationHash, suckerDeploymentConfiguration.salt, _msgSender())),
|
|
1055
|
+
configurations: suckerDeploymentConfiguration.deployerConfigurations
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/// @notice Convert a revnet's stages into a series of Juicebox project rulesets.
|
|
1060
|
+
/// @dev Stage transitions affect outstanding loan health. When a new stage activates, parameters such as
|
|
1061
|
+
/// `cashOutTaxRate` and `weight` change, which directly impact the borrowable amount calculated by
|
|
1062
|
+
/// `REVLoans._borrowableAmountFrom`. Loans originated under a previous stage's parameters may become
|
|
1063
|
+
/// under-collateralized if the new stage has a higher `cashOutTaxRate` (reducing the borrowable amount per unit
|
|
1064
|
+
/// of collateral) or lower issuance weight (reducing the surplus-per-token ratio). Borrowers should monitor
|
|
1065
|
+
/// upcoming stage transitions and adjust their positions accordingly, as loans that fall below their required
|
|
1066
|
+
/// collateralization may become eligible for liquidation.
|
|
1067
|
+
/// @param revnetId The ID of the revnet to make rulesets for.
|
|
1068
|
+
/// @param configuration The configuration containing the revnet's stages.
|
|
1069
|
+
/// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
|
|
1070
|
+
/// @return rulesetConfigurations A list of ruleset configurations defined by the stages.
|
|
1071
|
+
/// @return encodedConfigurationHash A hash that represents the revnet's configuration. Used for sucker
|
|
1072
|
+
/// deployment salts.
|
|
1073
|
+
function _makeRulesetConfigurations(
|
|
1074
|
+
uint256 revnetId,
|
|
1075
|
+
REVConfig calldata configuration,
|
|
1076
|
+
JBTerminalConfig[] calldata terminalConfigurations
|
|
1077
|
+
)
|
|
1078
|
+
internal
|
|
1079
|
+
returns (JBRulesetConfig[] memory rulesetConfigurations, bytes32 encodedConfigurationHash)
|
|
1080
|
+
{
|
|
1081
|
+
// If there are no stages, revert.
|
|
1082
|
+
if (configuration.stageConfigurations.length == 0) revert REVDeployer_StagesRequired();
|
|
1083
|
+
|
|
1084
|
+
// Initialize the array of rulesets.
|
|
1085
|
+
rulesetConfigurations = new JBRulesetConfig[](configuration.stageConfigurations.length);
|
|
1086
|
+
|
|
1087
|
+
// Add the base configuration to the byte-encoded configuration.
|
|
1088
|
+
bytes memory encodedConfiguration = abi.encode(
|
|
1089
|
+
configuration.baseCurrency,
|
|
1090
|
+
configuration.description.name,
|
|
1091
|
+
configuration.description.ticker,
|
|
1092
|
+
configuration.description.salt
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
// Initialize fund access limit groups for the loan contract and configure buyback pools.
|
|
1096
|
+
JBFundAccessLimitGroup[] memory fundAccessLimitGroups = _makeLoanFundAccessLimitsAndBuybackPools({
|
|
1097
|
+
revnetId: revnetId, terminalConfigurations: terminalConfigurations
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
// Iterate through each stage to set up its ruleset.
|
|
1101
|
+
for (uint256 i; i < configuration.stageConfigurations.length; i++) {
|
|
1102
|
+
// Set the stage being iterated on.
|
|
1103
|
+
REVStageConfig calldata stageConfiguration = configuration.stageConfigurations[i];
|
|
1104
|
+
|
|
1105
|
+
// Make sure the revnet has at least one split if it has a split percent.
|
|
1106
|
+
// Otherwise, the split would go to this contract since its the revnet's owner.
|
|
1107
|
+
if (stageConfiguration.splitPercent > 0 && stageConfiguration.splits.length == 0) {
|
|
1108
|
+
revert REVDeployer_MustHaveSplits();
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// If the stage's start time is not after the previous stage's start time, revert.
|
|
1112
|
+
if (i > 0 && stageConfiguration.startsAtOrAfter <= configuration.stageConfigurations[i - 1].startsAtOrAfter)
|
|
1113
|
+
{
|
|
1114
|
+
revert REVDeployer_StageTimesMustIncrease();
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Make sure the revnet doesn't prevent cashouts all together.
|
|
1118
|
+
if (stageConfiguration.cashOutTaxRate >= JBConstants.MAX_CASH_OUT_TAX_RATE) {
|
|
1119
|
+
revert REVDeployer_CashOutsCantBeTurnedOffCompletely(
|
|
1120
|
+
stageConfiguration.cashOutTaxRate, JBConstants.MAX_CASH_OUT_TAX_RATE
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Set up the ruleset.
|
|
1125
|
+
rulesetConfigurations[i] = _makeRulesetConfiguration({
|
|
1126
|
+
baseCurrency: configuration.baseCurrency,
|
|
1127
|
+
stageConfiguration: stageConfiguration,
|
|
1128
|
+
fundAccessLimitGroups: fundAccessLimitGroups
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
// Add the stage's properties to the byte-encoded configuration.
|
|
1132
|
+
encodedConfiguration = abi.encode(
|
|
1133
|
+
encodedConfiguration,
|
|
1134
|
+
// If no start time is provided for the first stage, use the current block's timestamp.
|
|
1135
|
+
// In the future, revnets deployed on other networks can match this revnet's encoded stage by specifying
|
|
1136
|
+
// the
|
|
1137
|
+
// same start time.
|
|
1138
|
+
(i == 0 && stageConfiguration.startsAtOrAfter == 0)
|
|
1139
|
+
? block.timestamp
|
|
1140
|
+
: stageConfiguration.startsAtOrAfter,
|
|
1141
|
+
stageConfiguration.splitPercent,
|
|
1142
|
+
stageConfiguration.initialIssuance,
|
|
1143
|
+
stageConfiguration.issuanceCutFrequency,
|
|
1144
|
+
stageConfiguration.issuanceCutPercent,
|
|
1145
|
+
stageConfiguration.cashOutTaxRate
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
// Add each auto-mint to the byte-encoded representation.
|
|
1149
|
+
for (uint256 j; j < stageConfiguration.autoIssuances.length; j++) {
|
|
1150
|
+
REVAutoIssuance calldata autoIssuance = stageConfiguration.autoIssuances[j];
|
|
1151
|
+
|
|
1152
|
+
// Make sure the beneficiary is not the zero address.
|
|
1153
|
+
if (autoIssuance.beneficiary == address(0)) revert REVDeployer_AutoIssuanceBeneficiaryZeroAddress();
|
|
1154
|
+
|
|
1155
|
+
// If there's nothing to auto-mint, continue.
|
|
1156
|
+
if (autoIssuance.count == 0) continue;
|
|
1157
|
+
|
|
1158
|
+
encodedConfiguration = abi.encode(
|
|
1159
|
+
encodedConfiguration, autoIssuance.chainId, autoIssuance.beneficiary, autoIssuance.count
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
// If the issuance config is for another chain, skip it.
|
|
1163
|
+
if (autoIssuance.chainId != block.chainid) continue;
|
|
1164
|
+
|
|
1165
|
+
// slither-disable-next-line reentrancy-events
|
|
1166
|
+
emit StoreAutoIssuanceAmount({
|
|
1167
|
+
revnetId: revnetId,
|
|
1168
|
+
stageId: block.timestamp + i,
|
|
1169
|
+
beneficiary: autoIssuance.beneficiary,
|
|
1170
|
+
count: autoIssuance.count,
|
|
1171
|
+
caller: _msgSender()
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
// Store the amount of tokens that can be auto-minted on this chain during this stage.
|
|
1175
|
+
// The first stage ID is stored at this block's timestamp,
|
|
1176
|
+
// and further stage IDs have incrementally increasing IDs
|
|
1177
|
+
// slither-disable-next-line reentrancy-benign
|
|
1178
|
+
amountToAutoIssue[revnetId][block.timestamp + i][autoIssuance.beneficiary] += autoIssuance.count;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Hash the encoded configuration.
|
|
1183
|
+
encodedConfigurationHash = keccak256(encodedConfiguration);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/// @notice Sets the cash out delay if the revnet's stages are already in progress.
|
|
1187
|
+
/// @dev This prevents cash out liquidity/arbitrage issues for existing revnets which
|
|
1188
|
+
/// are deploying to a new chain.
|
|
1189
|
+
/// @param revnetId The ID of the revnet to set the cash out delay for.
|
|
1190
|
+
/// @param firstStageConfig The revnet's first stage.
|
|
1191
|
+
function _setCashOutDelayIfNeeded(uint256 revnetId, REVStageConfig calldata firstStageConfig) internal {
|
|
1192
|
+
// If this is the first revnet being deployed (with a `startsAtOrAfter` of 0),
|
|
1193
|
+
// or if the first stage hasn't started yet, we don't need to set a cash out delay.
|
|
1194
|
+
if (firstStageConfig.startsAtOrAfter == 0 || firstStageConfig.startsAtOrAfter >= block.timestamp) return;
|
|
1195
|
+
|
|
1196
|
+
// Calculate the timestamp at which the cash out delay ends.
|
|
1197
|
+
uint256 cashOutDelay = block.timestamp + CASH_OUT_DELAY;
|
|
1198
|
+
|
|
1199
|
+
// Store the cash out delay.
|
|
1200
|
+
cashOutDelayOf[revnetId] = cashOutDelay;
|
|
1201
|
+
|
|
1202
|
+
emit SetCashOutDelay({revnetId: revnetId, cashOutDelay: cashOutDelay, caller: _msgSender()});
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/// @notice Grants a permission to an address (an "operator").
|
|
1206
|
+
/// @param operator The address to give the permission to.
|
|
1207
|
+
/// @param revnetId The ID of the revnet to scope the permission for.
|
|
1208
|
+
/// @param permissionId The ID of the permission to set. See `JBPermissionIds`.
|
|
1209
|
+
function _setPermission(address operator, uint256 revnetId, uint8 permissionId) internal {
|
|
1210
|
+
uint8[] memory permissionsIds = new uint8[](1);
|
|
1211
|
+
permissionsIds[0] = permissionId;
|
|
1212
|
+
|
|
1213
|
+
// Give the operator the permission.
|
|
1214
|
+
_setPermissionsFor({
|
|
1215
|
+
account: address(this), operator: operator, revnetId: revnetId, permissionIds: permissionsIds
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/// @notice Grants a permission to an address (an "operator").
|
|
1220
|
+
/// @param account The account granting the permission.
|
|
1221
|
+
/// @param operator The address to give the permission to.
|
|
1222
|
+
/// @param revnetId The ID of the revnet to scope the permission for.
|
|
1223
|
+
/// @param permissionIds An array of permission IDs to set. See `JBPermissionIds`.
|
|
1224
|
+
function _setPermissionsFor(
|
|
1225
|
+
address account,
|
|
1226
|
+
address operator,
|
|
1227
|
+
uint256 revnetId,
|
|
1228
|
+
uint8[] memory permissionIds
|
|
1229
|
+
)
|
|
1230
|
+
internal
|
|
1231
|
+
{
|
|
1232
|
+
// Set up the permission data.
|
|
1233
|
+
JBPermissionsData memory permissionData =
|
|
1234
|
+
JBPermissionsData({operator: operator, projectId: uint64(revnetId), permissionIds: permissionIds});
|
|
1235
|
+
|
|
1236
|
+
// Set the permissions.
|
|
1237
|
+
PERMISSIONS.setPermissionsFor({account: account, permissionsData: permissionData});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/// @notice Give a split operator their permissions.
|
|
1241
|
+
/// @dev Only a revnet's current split operator can set a new split operator, by calling `setSplitOperatorOf(…)`.
|
|
1242
|
+
/// @param revnetId The ID of the revnet to set the split operator of.
|
|
1243
|
+
/// @param operator The new split operator's address.
|
|
1244
|
+
function _setSplitOperatorOf(uint256 revnetId, address operator) internal {
|
|
1245
|
+
// Get the permission indexes for the split operator.
|
|
1246
|
+
uint256[] memory permissionIndexes = _splitOperatorPermissionIndexesOf(revnetId);
|
|
1247
|
+
uint8[] memory permissionIds = new uint8[](permissionIndexes.length);
|
|
1248
|
+
|
|
1249
|
+
for (uint256 i; i < permissionIndexes.length; i++) {
|
|
1250
|
+
permissionIds[i] = uint8(permissionIndexes[i]);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
_setPermissionsFor({
|
|
1254
|
+
account: address(this), operator: operator, revnetId: revnetId, permissionIds: permissionIds
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
}
|