@ballkidz/defifa 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/.gas-snapshot +2 -0
- package/CRYPTO_ECON.md +955 -0
- package/CRYPTO_ECON.pdf +0 -0
- package/CRYPTO_ECON.tex +800 -0
- package/README.md +119 -0
- package/SKILLS.md +177 -0
- package/deployments/defifa-v5/arbitrum_sepolia/DefifaDelegate.json +4867 -0
- package/deployments/defifa-v5/arbitrum_sepolia/DefifaDeployer.json +1719 -0
- package/deployments/defifa-v5/arbitrum_sepolia/DefifaGovernor.json +1535 -0
- package/deployments/defifa-v5/arbitrum_sepolia/DefifaTokenUriResolver.json +295 -0
- package/deployments/defifa-v5/base_sepolia/DefifaDelegate.json +4875 -0
- package/deployments/defifa-v5/base_sepolia/DefifaDeployer.json +1725 -0
- package/deployments/defifa-v5/base_sepolia/DefifaGovernor.json +1543 -0
- package/deployments/defifa-v5/base_sepolia/DefifaTokenUriResolver.json +301 -0
- package/deployments/defifa-v5/optimism_sepolia/DefifaDelegate.json +4875 -0
- package/deployments/defifa-v5/optimism_sepolia/DefifaDeployer.json +1725 -0
- package/deployments/defifa-v5/optimism_sepolia/DefifaGovernor.json +1543 -0
- package/deployments/defifa-v5/optimism_sepolia/DefifaTokenUriResolver.json +301 -0
- package/deployments/defifa-v5/sepolia/DefifaDelegate.json +4875 -0
- package/deployments/defifa-v5/sepolia/DefifaDeployer.json +1725 -0
- package/deployments/defifa-v5/sepolia/DefifaGovernor.json +1543 -0
- package/deployments/defifa-v5/sepolia/DefifaTokenUriResolver.json +301 -0
- package/foundry.lock +17 -0
- package/foundry.toml +35 -0
- package/package.json +33 -0
- package/remappings.txt +6 -0
- package/script/Deploy.s.sol +109 -0
- package/script/helpers/DefifaDeploymentLib.sol +83 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +521 -0
- package/src/DefifaDeployer.sol +894 -0
- package/src/DefifaGovernor.sol +490 -0
- package/src/DefifaHook.sol +1056 -0
- package/src/DefifaProjectOwner.sol +63 -0
- package/src/DefifaTokenUriResolver.sol +312 -0
- package/src/enums/DefifaGamePhase.sol +11 -0
- package/src/enums/DefifaScorecardState.sol +10 -0
- package/src/interfaces/IDefifaDeployer.sol +108 -0
- package/src/interfaces/IDefifaGamePhaseReporter.sol +8 -0
- package/src/interfaces/IDefifaGamePotReporter.sol +8 -0
- package/src/interfaces/IDefifaGovernor.sol +132 -0
- package/src/interfaces/IDefifaHook.sol +228 -0
- package/src/interfaces/IDefifaTokenUriResolver.sol +10 -0
- package/src/libraries/DefifaFontImporter.sol +19 -0
- package/src/libraries/DefifaHookLib.sol +358 -0
- package/src/structs/DefifaAttestations.sol +9 -0
- package/src/structs/DefifaDelegation.sol +9 -0
- package/src/structs/DefifaLaunchProjectData.sol +59 -0
- package/src/structs/DefifaOpsData.sol +20 -0
- package/src/structs/DefifaScorecard.sol +9 -0
- package/src/structs/DefifaTierCashOutWeight.sol +9 -0
- package/src/structs/DefifaTierParams.sol +16 -0
- package/test/DefifaFeeAccounting.t.sol +559 -0
- package/test/DefifaGovernor.t.sol +1333 -0
- package/test/DefifaMintCostInvariant.t.sol +299 -0
- package/test/DefifaNoContest.t.sol +922 -0
- package/test/DefifaSecurity.t.sol +717 -0
- package/test/SVG.t.sol +164 -0
- package/test/deployScript.t.sol +144 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
5
|
+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
|
|
6
|
+
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
|
|
7
|
+
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
8
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
9
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
10
|
+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
|
|
11
|
+
import {DefifaGamePhase} from "./enums/DefifaGamePhase.sol";
|
|
12
|
+
import {IDefifaDeployer} from "./interfaces/IDefifaDeployer.sol";
|
|
13
|
+
import {IDefifaHook} from "./interfaces/IDefifaHook.sol";
|
|
14
|
+
import {IDefifaGamePhaseReporter} from "./interfaces/IDefifaGamePhaseReporter.sol";
|
|
15
|
+
import {IDefifaGamePotReporter} from "./interfaces/IDefifaGamePotReporter.sol";
|
|
16
|
+
import {IDefifaGovernor} from "./interfaces/IDefifaGovernor.sol";
|
|
17
|
+
import {DefifaLaunchProjectData} from "./structs/DefifaLaunchProjectData.sol";
|
|
18
|
+
import {DefifaTierParams} from "./structs/DefifaTierParams.sol";
|
|
19
|
+
import {DefifaOpsData} from "./structs/DefifaOpsData.sol";
|
|
20
|
+
import {DefifaHook} from "./DefifaHook.sol";
|
|
21
|
+
import {DefifaTokenUriResolver} from "./DefifaTokenUriResolver.sol";
|
|
22
|
+
|
|
23
|
+
import {IJBToken} from "@bananapus/core-v6/src/interfaces/IJBToken.sol";
|
|
24
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
25
|
+
import {IJBController, JBRulesetConfig, JBTerminalConfig} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
26
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
27
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
28
|
+
import {IJBMultiTerminal} from "@bananapus/core-v6/src/interfaces/IJBMultiTerminal.sol";
|
|
29
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
30
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
31
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
32
|
+
import {JBCurrencyAmount} from "@bananapus/core-v6/src/structs/JBCurrencyAmount.sol";
|
|
33
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
34
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
35
|
+
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
36
|
+
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
37
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
38
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
39
|
+
import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
|
|
40
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
41
|
+
import {
|
|
42
|
+
JB721TiersRulesetMetadata,
|
|
43
|
+
JB721TiersRulesetMetadataResolver
|
|
44
|
+
} from "@bananapus/721-hook-v6/src/libraries/JB721TiersRulesetMetadataResolver.sol";
|
|
45
|
+
import {IJBRulesets, IJBRulesetApprovalHook, JBRuleset} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
46
|
+
|
|
47
|
+
/// @title DefifaDeployer
|
|
48
|
+
/// @notice Deploys and manages Defifa games.
|
|
49
|
+
contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGamePotReporter, IERC721Receiver {
|
|
50
|
+
using Strings for uint256;
|
|
51
|
+
using SafeERC20 for IERC20;
|
|
52
|
+
|
|
53
|
+
//*********************************************************************//
|
|
54
|
+
// --------------------------- custom errors ------------------------- //
|
|
55
|
+
//*********************************************************************//
|
|
56
|
+
|
|
57
|
+
error DefifaDeployer_CantFulfillYet();
|
|
58
|
+
error DefifaDeployer_NothingToFulfill();
|
|
59
|
+
error DefifaDeployer_GameOver();
|
|
60
|
+
error DefifaDeployer_InvalidFeePercent();
|
|
61
|
+
error DefifaDeployer_InvalidGameConfiguration();
|
|
62
|
+
error DefifaDeployer_IncorrectDecimalAmount();
|
|
63
|
+
error DefifaDeployer_NotNoContest();
|
|
64
|
+
error DefifaDeployer_NoContestAlreadyTriggered();
|
|
65
|
+
error DefifaDeployer_TerminalNotFound();
|
|
66
|
+
error DefifaDeployer_PhaseAlreadyQueued();
|
|
67
|
+
error DefifaDeployer_SplitsDontAddUp();
|
|
68
|
+
error DefifaDeployer_UnexpectedTerminalCurrency();
|
|
69
|
+
|
|
70
|
+
//*********************************************************************//
|
|
71
|
+
// ----------------------- internal properties ----------------------- //
|
|
72
|
+
//*********************************************************************//
|
|
73
|
+
|
|
74
|
+
/// @notice The game's ops.
|
|
75
|
+
mapping(uint256 => DefifaOpsData) internal _opsOf;
|
|
76
|
+
|
|
77
|
+
/// @notice This contract current nonce, used for the registry initialized at 1 since the first contract deployed is
|
|
78
|
+
/// the hook
|
|
79
|
+
uint256 internal _nonce;
|
|
80
|
+
|
|
81
|
+
//*********************************************************************//
|
|
82
|
+
// ------------------ public immutable properties -------------------- //
|
|
83
|
+
//*********************************************************************//
|
|
84
|
+
|
|
85
|
+
/// @notice The group relative to which splits are stored.
|
|
86
|
+
/// @dev This could be any fixed number.
|
|
87
|
+
uint256 public immutable override splitGroup;
|
|
88
|
+
|
|
89
|
+
/// @notice The project ID that'll receive game fees, and relative to which splits are stored.
|
|
90
|
+
/// @dev The owner of this project ID must give this contract operator permissions over the SET_SPLITS operation.
|
|
91
|
+
uint256 public immutable override defifaProjectId;
|
|
92
|
+
|
|
93
|
+
/// @notice The project ID that'll receive protocol fees as commitments are fulfilled.
|
|
94
|
+
uint256 public immutable override baseProtocolProjectId;
|
|
95
|
+
|
|
96
|
+
/// @notice The original code for the Defifa hook to base subsequent instances on.
|
|
97
|
+
address public immutable override hookCodeOrigin;
|
|
98
|
+
|
|
99
|
+
/// @notice The default Defifa token URI resolver.
|
|
100
|
+
IJB721TokenUriResolver public immutable override tokenUriResolver;
|
|
101
|
+
|
|
102
|
+
/// @notice The Defifa governor.
|
|
103
|
+
IDefifaGovernor public immutable override governor;
|
|
104
|
+
|
|
105
|
+
/// @notice The controller with which new projects should be deployed.
|
|
106
|
+
IJBController public immutable override controller;
|
|
107
|
+
|
|
108
|
+
/// @notice The hooks registry.
|
|
109
|
+
IJBAddressRegistry public immutable registry;
|
|
110
|
+
|
|
111
|
+
/// @notice The divisor that describes the protocol fee that should be taken.
|
|
112
|
+
/// @dev This is equal to 100 divided by the fee percent (e.g. 40 = 2.5% fee).
|
|
113
|
+
uint256 public constant override BASE_PROTOCOL_FEE_DIVISOR = 40;
|
|
114
|
+
|
|
115
|
+
/// @notice The divisor that describes the Defifa fee that should be taken.
|
|
116
|
+
/// @dev This is equal to 100 divided by the fee percent (e.g. 20 = 5% fee).
|
|
117
|
+
uint256 public constant override DEFIFA_FEE_DIVISOR = 20;
|
|
118
|
+
|
|
119
|
+
//*********************************************************************//
|
|
120
|
+
// --------------------- public stored properties -------------------- //
|
|
121
|
+
//*********************************************************************//
|
|
122
|
+
|
|
123
|
+
/// @notice The amount of commitments a game has fulfilled.
|
|
124
|
+
/// @dev The ID of the game to check.
|
|
125
|
+
mapping(uint256 => uint256) public override fulfilledCommitmentsOf;
|
|
126
|
+
|
|
127
|
+
/// @notice The total absolute split percent for each game (out of SPLITS_TOTAL_PERCENT).
|
|
128
|
+
mapping(uint256 => uint256) internal _commitmentPercentOf;
|
|
129
|
+
|
|
130
|
+
/// @notice Whether the no-contest refund ruleset has been triggered for a game.
|
|
131
|
+
/// @dev Once triggered, the game stays in NO_CONTEST and refunds are enabled.
|
|
132
|
+
mapping(uint256 => bool) public noContestTriggeredFor;
|
|
133
|
+
|
|
134
|
+
//*********************************************************************//
|
|
135
|
+
// ------------------------- external views -------------------------- //
|
|
136
|
+
//*********************************************************************//
|
|
137
|
+
|
|
138
|
+
/// @notice The game times.
|
|
139
|
+
/// @param gameId The ID of the game for which the game times apply.
|
|
140
|
+
/// @return The game's start time, as a unix timestamp.
|
|
141
|
+
/// @return The game's minting period duration, in seconds.
|
|
142
|
+
/// @return The game's refund period duration, in seconds.
|
|
143
|
+
function timesFor(uint256 gameId) external view override returns (uint48, uint24, uint24) {
|
|
144
|
+
DefifaOpsData memory _ops = _opsOf[gameId];
|
|
145
|
+
return (_ops.start, _ops.mintPeriodDuration, _ops.refundPeriodDuration);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// @notice The token of a game.
|
|
149
|
+
/// @param gameId The ID of the game to get the token of.
|
|
150
|
+
/// @return The game's token.
|
|
151
|
+
function tokenOf(uint256 gameId) external view override returns (address) {
|
|
152
|
+
return _opsOf[gameId].token;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// @notice The safety mechanism parameters of a game.
|
|
156
|
+
/// @param gameId The ID of the game to get the safety params of.
|
|
157
|
+
/// @return minParticipation The minimum treasury balance for the game to proceed to scoring.
|
|
158
|
+
/// @return scorecardTimeout The maximum time after scoring begins for a scorecard to be ratified.
|
|
159
|
+
function safetyParamsOf(uint256 gameId)
|
|
160
|
+
external
|
|
161
|
+
view
|
|
162
|
+
override
|
|
163
|
+
returns (uint256 minParticipation, uint32 scorecardTimeout)
|
|
164
|
+
{
|
|
165
|
+
DefifaOpsData memory _ops = _opsOf[gameId];
|
|
166
|
+
return (_ops.minParticipation, _ops.scorecardTimeout);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// @notice The current pot the game is being played with.
|
|
170
|
+
/// @param gameId The ID of the game for which the pot applies.
|
|
171
|
+
/// @param includeCommitments A flag indicating if the portion of the pot committed to fulfill preprogrammed
|
|
172
|
+
/// obligations should be included.
|
|
173
|
+
/// @return The game's pot amount, as a fixed point number.
|
|
174
|
+
/// @return The token address the game's pot is measured in.
|
|
175
|
+
/// @return The number of decimals included in the amount.
|
|
176
|
+
function currentGamePotOf(
|
|
177
|
+
uint256 gameId,
|
|
178
|
+
bool includeCommitments
|
|
179
|
+
)
|
|
180
|
+
external
|
|
181
|
+
view
|
|
182
|
+
returns (uint256, address, uint256)
|
|
183
|
+
{
|
|
184
|
+
// Get a reference to the token being used by the project.
|
|
185
|
+
address _token = _opsOf[gameId].token;
|
|
186
|
+
|
|
187
|
+
// Get a reference to the terminal.
|
|
188
|
+
IJBTerminal _terminal = controller.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _token});
|
|
189
|
+
|
|
190
|
+
// Get the accounting context for the project.
|
|
191
|
+
JBAccountingContext memory _context = _terminal.accountingContextForTokenOf({projectId: gameId, token: _token});
|
|
192
|
+
|
|
193
|
+
// Get the current balance.
|
|
194
|
+
uint256 _pot = IJBMultiTerminal(address(_terminal)).STORE()
|
|
195
|
+
.balanceOf({terminal: address(_terminal), projectId: gameId, token: _token});
|
|
196
|
+
|
|
197
|
+
// Add any fulfilled commitments.
|
|
198
|
+
if (includeCommitments) _pot += fulfilledCommitmentsOf[gameId];
|
|
199
|
+
|
|
200
|
+
return (_pot, _token, _context.decimals);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// @notice Whether or not the next phase still needs queuing.
|
|
204
|
+
/// @param gameId The ID of the game to get the queue status of.
|
|
205
|
+
/// @return Whether or not the next phase still needs queuing.
|
|
206
|
+
function nextPhaseNeedsQueueing(uint256 gameId) external view override returns (bool) {
|
|
207
|
+
// Get the game's current funding cycle along with its metadata.
|
|
208
|
+
JBRuleset memory _currentRuleset = controller.RULESETS().currentOf(gameId);
|
|
209
|
+
// Get the game's queued funding cycle along with its metadata.
|
|
210
|
+
// slither-disable-next-line unused-return
|
|
211
|
+
(JBRuleset memory _queuedRuleset,) = controller.RULESETS().latestQueuedOf(gameId);
|
|
212
|
+
|
|
213
|
+
// If the configurations are the same and the game hasn't ended, queueing is still needed.
|
|
214
|
+
return _currentRuleset.duration != 0 && _currentRuleset.id == _queuedRuleset.id;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//*********************************************************************//
|
|
218
|
+
// -------------------------- public views --------------------------- //
|
|
219
|
+
//*********************************************************************//
|
|
220
|
+
|
|
221
|
+
/// @notice Returns the number of the game phase.
|
|
222
|
+
/// @dev The game phase corresponds to the game's current funding cycle number.
|
|
223
|
+
/// @dev NO_CONTEST is returned if the minimum participation threshold is not met, or if the scorecard timeout has
|
|
224
|
+
/// elapsed without ratification.
|
|
225
|
+
/// @param gameId The ID of the game to get the phase number of.
|
|
226
|
+
/// @return The game phase.
|
|
227
|
+
function currentGamePhaseOf(uint256 gameId) public view override returns (DefifaGamePhase) {
|
|
228
|
+
// Get the game's current funding cycle along with its metadata.
|
|
229
|
+
(JBRuleset memory _currentRuleset, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
|
|
230
|
+
|
|
231
|
+
if (_currentRuleset.cycleNumber == 0) return DefifaGamePhase.COUNTDOWN;
|
|
232
|
+
if (_currentRuleset.cycleNumber == 1) return DefifaGamePhase.MINT;
|
|
233
|
+
if (_currentRuleset.cycleNumber == 2 && _opsOf[gameId].refundPeriodDuration != 0) {
|
|
234
|
+
return DefifaGamePhase.REFUND;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check if the scorecard has been ratified (game is COMPLETE).
|
|
238
|
+
// This takes priority over all NO_CONTEST checks — a ratified scorecard is final.
|
|
239
|
+
if (IDefifaHook(_metadata.dataHook).cashOutWeightIsSet()) return DefifaGamePhase.COMPLETE;
|
|
240
|
+
|
|
241
|
+
// If no-contest has already been triggered, stay in NO_CONTEST.
|
|
242
|
+
if (noContestTriggeredFor[gameId]) return DefifaGamePhase.NO_CONTEST;
|
|
243
|
+
|
|
244
|
+
// Get the game's ops data for the safety mechanism checks.
|
|
245
|
+
DefifaOpsData memory _ops = _opsOf[gameId];
|
|
246
|
+
|
|
247
|
+
// Check minimum participation threshold: if the treasury balance is below the threshold, the game is
|
|
248
|
+
// NO_CONTEST.
|
|
249
|
+
if (_ops.minParticipation > 0) {
|
|
250
|
+
IJBTerminal _terminal = controller.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _ops.token});
|
|
251
|
+
uint256 _balance = IJBMultiTerminal(address(_terminal)).STORE()
|
|
252
|
+
.balanceOf({terminal: address(_terminal), projectId: gameId, token: _ops.token});
|
|
253
|
+
if (_balance < _ops.minParticipation) return DefifaGamePhase.NO_CONTEST;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check scorecard ratification timeout: if enough time has passed without a ratified scorecard, the game is
|
|
257
|
+
// NO_CONTEST.
|
|
258
|
+
if (_ops.scorecardTimeout > 0 && block.timestamp > _currentRuleset.start + _ops.scorecardTimeout) {
|
|
259
|
+
return DefifaGamePhase.NO_CONTEST;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return DefifaGamePhase.SCORING;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
//*********************************************************************//
|
|
266
|
+
// -------------------------- constructor ---------------------------- //
|
|
267
|
+
//*********************************************************************//
|
|
268
|
+
|
|
269
|
+
/// @param _hookCodeOrigin The code of the Defifa hook.
|
|
270
|
+
/// @param _tokenUriResolver The standard default token URI resolver.
|
|
271
|
+
/// @param _governor The Defifa governor.
|
|
272
|
+
/// @param _controller The controller to use to launch the game from.
|
|
273
|
+
/// @param _registry The contract storing references to the deployer of each hook.
|
|
274
|
+
/// @param _defifaProjectId The ID of the project that should take the fee from the games.
|
|
275
|
+
/// @param _baseProtocolProjectId The ID of the protocol project that'll receive fees from fulfilling commitments.
|
|
276
|
+
constructor(
|
|
277
|
+
address _hookCodeOrigin,
|
|
278
|
+
IJB721TokenUriResolver _tokenUriResolver,
|
|
279
|
+
IDefifaGovernor _governor,
|
|
280
|
+
IJBController _controller,
|
|
281
|
+
IJBAddressRegistry _registry,
|
|
282
|
+
uint256 _defifaProjectId,
|
|
283
|
+
uint256 _baseProtocolProjectId
|
|
284
|
+
) {
|
|
285
|
+
hookCodeOrigin = _hookCodeOrigin;
|
|
286
|
+
tokenUriResolver = _tokenUriResolver;
|
|
287
|
+
governor = _governor;
|
|
288
|
+
controller = _controller;
|
|
289
|
+
registry = _registry;
|
|
290
|
+
defifaProjectId = _defifaProjectId;
|
|
291
|
+
baseProtocolProjectId = _baseProtocolProjectId;
|
|
292
|
+
/// @dev Uses the deployer address as group ID. Game scoring rulesets use uint160(token) as group ID.
|
|
293
|
+
splitGroup = uint256(uint160(address(this)));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
//*********************************************************************//
|
|
297
|
+
// ---------------------- external transactions ---------------------- //
|
|
298
|
+
//*********************************************************************//
|
|
299
|
+
|
|
300
|
+
/// @notice Launches a new game owned by this contract with a DefifaHook attached.
|
|
301
|
+
/// @param launchProjectData Data necessary to fulfill the transaction to launch a game.
|
|
302
|
+
/// @return gameId The ID of the newly configured game.
|
|
303
|
+
function launchGameWith(DefifaLaunchProjectData memory launchProjectData)
|
|
304
|
+
external
|
|
305
|
+
override
|
|
306
|
+
returns (uint256 gameId)
|
|
307
|
+
{
|
|
308
|
+
// Start the game right after the mint and refund durations if it isnt provided.
|
|
309
|
+
if (launchProjectData.start == 0) {
|
|
310
|
+
launchProjectData.start =
|
|
311
|
+
uint48(block.timestamp + launchProjectData.mintPeriodDuration + launchProjectData.refundPeriodDuration);
|
|
312
|
+
}
|
|
313
|
+
// Start minting right away if a start time isn't provided.
|
|
314
|
+
else if (
|
|
315
|
+
launchProjectData.mintPeriodDuration == 0
|
|
316
|
+
&& launchProjectData.start > block.timestamp + launchProjectData.refundPeriodDuration
|
|
317
|
+
) {
|
|
318
|
+
launchProjectData.mintPeriodDuration =
|
|
319
|
+
uint24(launchProjectData.start - (block.timestamp + launchProjectData.refundPeriodDuration));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Make sure the provided gameplay timestamps are sequential and that there is a mint duration.
|
|
323
|
+
// slither-disable-next-line incorrect-equality
|
|
324
|
+
if (
|
|
325
|
+
launchProjectData.mintPeriodDuration == 0
|
|
326
|
+
|| launchProjectData.start
|
|
327
|
+
< block.timestamp + launchProjectData.refundPeriodDuration + launchProjectData.mintPeriodDuration
|
|
328
|
+
) revert DefifaDeployer_InvalidGameConfiguration();
|
|
329
|
+
|
|
330
|
+
// Get the game ID, optimistically knowing it will be one greater than the current count.
|
|
331
|
+
gameId = controller.PROJECTS().count() + 1;
|
|
332
|
+
|
|
333
|
+
{
|
|
334
|
+
// Store the timestamps that'll define the game phases.
|
|
335
|
+
_opsOf[gameId] = DefifaOpsData({
|
|
336
|
+
token: launchProjectData.token.token,
|
|
337
|
+
mintPeriodDuration: launchProjectData.mintPeriodDuration,
|
|
338
|
+
refundPeriodDuration: launchProjectData.refundPeriodDuration,
|
|
339
|
+
start: launchProjectData.start,
|
|
340
|
+
minParticipation: launchProjectData.minParticipation,
|
|
341
|
+
scorecardTimeout: launchProjectData.scorecardTimeout
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Keep a reference to the number of splits.
|
|
345
|
+
uint256 _numberOfSplits = launchProjectData.splits.length;
|
|
346
|
+
|
|
347
|
+
// If there are splits being added, store the fee alongside. The fee will otherwise be added later.
|
|
348
|
+
if (_numberOfSplits != 0) {
|
|
349
|
+
// Make a new splits where fees will be added to.
|
|
350
|
+
JBSplit[] memory _splits = new JBSplit[](launchProjectData.splits.length + 1);
|
|
351
|
+
|
|
352
|
+
// Copy the splits over.
|
|
353
|
+
for (uint256 _i; _i < _numberOfSplits;) {
|
|
354
|
+
// Copy the split over.
|
|
355
|
+
_splits[_i] = launchProjectData.splits[_i];
|
|
356
|
+
unchecked {
|
|
357
|
+
++_i;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Add a split for the fee.
|
|
362
|
+
_splits[_numberOfSplits] = JBSplit({
|
|
363
|
+
preferAddToBalance: false,
|
|
364
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / DEFIFA_FEE_DIVISOR),
|
|
365
|
+
projectId: uint64(defifaProjectId),
|
|
366
|
+
beneficiary: payable(address(this)),
|
|
367
|
+
lockedUntil: 0,
|
|
368
|
+
hook: IJBSplitHook(address(0))
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Store the splits.
|
|
372
|
+
JBSplitGroup[] memory _groupedSplits = new JBSplitGroup[](1);
|
|
373
|
+
_groupedSplits[0] = JBSplitGroup({groupId: splitGroup, splits: _splits});
|
|
374
|
+
|
|
375
|
+
// This contract must have SET_SPLIT_GROUPS permission from the defifa project owner.
|
|
376
|
+
controller.setSplitGroupsOf({
|
|
377
|
+
projectId: defifaProjectId, rulesetId: gameId, splitGroups: _groupedSplits
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Keep track of the number of tiers.
|
|
383
|
+
uint256 _numberOfTiers = launchProjectData.tiers.length;
|
|
384
|
+
|
|
385
|
+
// Create the standard tiers struct that will be populated from the defifa tiers.
|
|
386
|
+
JB721TierConfig[] memory _hookTiers = new JB721TierConfig[](launchProjectData.tiers.length);
|
|
387
|
+
|
|
388
|
+
// Group all the tier names together.
|
|
389
|
+
string[] memory _tierNames = new string[](launchProjectData.tiers.length);
|
|
390
|
+
|
|
391
|
+
// Keep a reference to the tier being iterated on.
|
|
392
|
+
DefifaTierParams memory _defifaTier;
|
|
393
|
+
|
|
394
|
+
// Create the hook tiers from the Defifa tiers.
|
|
395
|
+
for (uint256 _i; _i < _numberOfTiers;) {
|
|
396
|
+
_defifaTier = launchProjectData.tiers[_i];
|
|
397
|
+
|
|
398
|
+
// Set the tier. All tiers use the same price so that price-based voting power is equal.
|
|
399
|
+
_hookTiers[_i] = JB721TierConfig({
|
|
400
|
+
price: launchProjectData.tierPrice,
|
|
401
|
+
initialSupply: 999_999_999, // Uncapped minting — max value allowed by the 721 store.
|
|
402
|
+
votingUnits: 0,
|
|
403
|
+
reserveFrequency: _defifaTier.reservedRate,
|
|
404
|
+
reserveBeneficiary: _defifaTier.reservedTokenBeneficiary,
|
|
405
|
+
encodedIPFSUri: _defifaTier.encodedIPFSUri,
|
|
406
|
+
category: 0,
|
|
407
|
+
discountPercent: 0,
|
|
408
|
+
allowOwnerMint: false,
|
|
409
|
+
useReserveBeneficiaryAsDefault: _defifaTier.shouldUseReservedTokenBeneficiaryAsDefault,
|
|
410
|
+
transfersPausable: false,
|
|
411
|
+
useVotingUnits: false,
|
|
412
|
+
cannotBeRemoved: true,
|
|
413
|
+
cannotIncreaseDiscountPercent: true,
|
|
414
|
+
splitPercent: 0,
|
|
415
|
+
splits: new JBSplit[](0)
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Set the name.
|
|
419
|
+
_tierNames[_i] = _defifaTier.name;
|
|
420
|
+
|
|
421
|
+
unchecked {
|
|
422
|
+
++_i;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Increment the nonce for this deployment.
|
|
427
|
+
uint256 _currentNonce = ++_nonce;
|
|
428
|
+
|
|
429
|
+
// Clone deterministically using sender and nonce to prevent front-running.
|
|
430
|
+
// Clones.clone() creates the proxy before initialize() is called, allowing an
|
|
431
|
+
// attacker to front-run initialization and DOS the game deployment. Using
|
|
432
|
+
// cloneDeterministic with msg.sender in the salt prevents this since a different
|
|
433
|
+
// caller produces a different address.
|
|
434
|
+
DefifaHook _hook = DefifaHook(
|
|
435
|
+
Clones.cloneDeterministic(hookCodeOrigin, keccak256(abi.encodePacked(msg.sender, _currentNonce)))
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Use the default uri resolver if provided, else use the hardcoded generic default.
|
|
439
|
+
IJB721TokenUriResolver _uriResolver = launchProjectData.defaultTokenUriResolver
|
|
440
|
+
!= IJB721TokenUriResolver(address(0))
|
|
441
|
+
? launchProjectData.defaultTokenUriResolver
|
|
442
|
+
: tokenUriResolver;
|
|
443
|
+
|
|
444
|
+
_hook.initialize({
|
|
445
|
+
_gameId: gameId,
|
|
446
|
+
_name: launchProjectData.name,
|
|
447
|
+
_symbol: string.concat("DEFIFA #", gameId.toString()),
|
|
448
|
+
_rulesets: controller.RULESETS(),
|
|
449
|
+
_baseUri: launchProjectData.baseUri,
|
|
450
|
+
_tokenUriResolver: _uriResolver,
|
|
451
|
+
_contractUri: launchProjectData.contractUri,
|
|
452
|
+
_tiers: _hookTiers,
|
|
453
|
+
_currency: launchProjectData.token.currency,
|
|
454
|
+
_store: launchProjectData.store,
|
|
455
|
+
_gamePhaseReporter: this,
|
|
456
|
+
_gamePotReporter: this,
|
|
457
|
+
_defaultAttestationDelegate: launchProjectData.defaultAttestationDelegate,
|
|
458
|
+
_tierNames: _tierNames
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Launch the Juicebox project.
|
|
462
|
+
uint256 _actualGameId =
|
|
463
|
+
_launchGame({launchProjectData: launchProjectData, _gameId: gameId, _dataHook: address(_hook)});
|
|
464
|
+
|
|
465
|
+
// Revert if the game ID does not match (e.g. front-run by another project creation).
|
|
466
|
+
if (gameId != _actualGameId) revert DefifaDeployer_InvalidGameConfiguration();
|
|
467
|
+
|
|
468
|
+
// Clone and initialize the new governor.
|
|
469
|
+
governor.initializeGame({
|
|
470
|
+
gameId: gameId,
|
|
471
|
+
attestationStartTime: uint256(launchProjectData.attestationStartTime),
|
|
472
|
+
attestationGracePeriod: uint256(launchProjectData.attestationGracePeriod)
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Transfer ownership to the specified owner.
|
|
476
|
+
_hook.transferOwnership(address(governor));
|
|
477
|
+
|
|
478
|
+
// Add the hook to the registry, contract nonce starts at 1
|
|
479
|
+
registry.registerAddress({deployer: address(this), nonce: _currentNonce});
|
|
480
|
+
|
|
481
|
+
emit LaunchGame(gameId, _hook, governor, _uriResolver, msg.sender);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/// @notice Fulfill split amounts between all splits for a game.
|
|
485
|
+
/// @param gameId The ID of the game to fulfill splits for.
|
|
486
|
+
function fulfillCommitmentsOf(uint256 gameId) external virtual override {
|
|
487
|
+
// Make sure commitments haven't already been fulfilled.
|
|
488
|
+
if (fulfilledCommitmentsOf[gameId] != 0) return;
|
|
489
|
+
|
|
490
|
+
// Get the game's current funding cycle along with its metadata.
|
|
491
|
+
// slither-disable-next-line unused-return
|
|
492
|
+
(, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
|
|
493
|
+
|
|
494
|
+
// Make sure the game's commitments can be fulfilled.
|
|
495
|
+
if (!IDefifaHook(_metadata.dataHook).cashOutWeightIsSet()) {
|
|
496
|
+
revert DefifaDeployer_CantFulfillYet();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Get the game token and the terminal.
|
|
500
|
+
address _token = _opsOf[gameId].token;
|
|
501
|
+
IJBMultiTerminal _terminal =
|
|
502
|
+
IJBMultiTerminal(address(controller.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _token})));
|
|
503
|
+
|
|
504
|
+
// Get the current pot and store it. This also prevents re-entrance since the check above will return early.
|
|
505
|
+
uint256 _pot = _terminal.STORE().balanceOf({terminal: address(_terminal), projectId: gameId, token: _token});
|
|
506
|
+
// slither-disable-next-line incorrect-equality
|
|
507
|
+
if (_pot == 0) revert DefifaDeployer_NothingToFulfill();
|
|
508
|
+
|
|
509
|
+
// Compute the fee amount based on the total absolute split percent stored at game creation.
|
|
510
|
+
uint256 _feeAmount = mulDiv(_pot, _commitmentPercentOf[gameId], JBConstants.SPLITS_TOTAL_PERCENT);
|
|
511
|
+
|
|
512
|
+
// Store the actual fee amount for accurate currentGamePotOf reporting.
|
|
513
|
+
// Use max(feeAmount, 1) to preserve the reentrancy guard when pot is 0.
|
|
514
|
+
fulfilledCommitmentsOf[gameId] = _feeAmount > 0 ? _feeAmount : 1;
|
|
515
|
+
|
|
516
|
+
// Send only the fee portion as payouts. The remaining balance stays as surplus for cash-outs.
|
|
517
|
+
// slither-disable-next-line unused-return
|
|
518
|
+
_terminal.sendPayoutsOf({
|
|
519
|
+
projectId: gameId,
|
|
520
|
+
token: _token,
|
|
521
|
+
amount: _feeAmount,
|
|
522
|
+
currency: _token == JBConstants.NATIVE_TOKEN ? _metadata.baseCurrency : uint32(uint160(_token)),
|
|
523
|
+
minTokensPaidOut: _feeAmount
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Queue the final ruleset.
|
|
527
|
+
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
528
|
+
rulesetConfigs[0] = JBRulesetConfig({
|
|
529
|
+
mustStartAtOrAfter: 0,
|
|
530
|
+
duration: 0,
|
|
531
|
+
weight: 0,
|
|
532
|
+
weightCutPercent: 0,
|
|
533
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
534
|
+
metadata: JBRulesetMetadata({
|
|
535
|
+
reservedPercent: 0,
|
|
536
|
+
cashOutTaxRate: 0,
|
|
537
|
+
baseCurrency: _metadata.baseCurrency,
|
|
538
|
+
pausePay: true,
|
|
539
|
+
pauseCreditTransfers: false,
|
|
540
|
+
allowOwnerMinting: false,
|
|
541
|
+
allowSetCustomToken: false,
|
|
542
|
+
allowTerminalMigration: false,
|
|
543
|
+
allowSetTerminals: false,
|
|
544
|
+
allowSetController: false,
|
|
545
|
+
allowAddAccountingContext: false,
|
|
546
|
+
allowAddPriceFeed: false,
|
|
547
|
+
// Set this to true so only the deployer can fulfill the commitments.
|
|
548
|
+
ownerMustSendPayouts: true,
|
|
549
|
+
holdFees: false,
|
|
550
|
+
useTotalSurplusForCashOuts: false,
|
|
551
|
+
useDataHookForPay: true,
|
|
552
|
+
useDataHookForCashOut: true,
|
|
553
|
+
dataHook: _metadata.dataHook,
|
|
554
|
+
metadata: uint16(
|
|
555
|
+
JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(
|
|
556
|
+
JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: false})
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
}),
|
|
560
|
+
// No more payouts.
|
|
561
|
+
splitGroups: new JBSplitGroup[](0),
|
|
562
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Update the ruleset to the final one.
|
|
566
|
+
// slither-disable-next-line unused-return
|
|
567
|
+
controller.queueRulesetsOf({
|
|
568
|
+
projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game has finished."
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
emit FulfilledCommitments({gameId: gameId, pot: _pot, caller: msg.sender});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/// @notice Triggers the no-contest refund mechanism for a game.
|
|
575
|
+
/// @dev Anyone can call this once the game is in the NO_CONTEST phase. This queues a new ruleset without
|
|
576
|
+
/// payout limits, making the surplus equal to the balance so users can cash out at their mint price.
|
|
577
|
+
/// @dev Analogous to fulfillCommitmentsOf for COMPLETE — must be called before NO_CONTEST cash-outs work.
|
|
578
|
+
/// @param gameId The ID of the game to trigger no-contest for.
|
|
579
|
+
function triggerNoContestFor(uint256 gameId) external override {
|
|
580
|
+
// Make sure the game is currently in NO_CONTEST phase.
|
|
581
|
+
if (currentGamePhaseOf(gameId) != DefifaGamePhase.NO_CONTEST) {
|
|
582
|
+
revert DefifaDeployer_NotNoContest();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Make sure no-contest hasn't already been triggered.
|
|
586
|
+
if (noContestTriggeredFor[gameId]) revert DefifaDeployer_NoContestAlreadyTriggered();
|
|
587
|
+
|
|
588
|
+
// Mark as triggered.
|
|
589
|
+
noContestTriggeredFor[gameId] = true;
|
|
590
|
+
|
|
591
|
+
// Get the game's current ruleset metadata for the data hook address.
|
|
592
|
+
// slither-disable-next-line unused-return
|
|
593
|
+
(, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
|
|
594
|
+
|
|
595
|
+
// Queue a new ruleset without payout limits so surplus = balance, enabling refunds.
|
|
596
|
+
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
597
|
+
rulesetConfigs[0] = JBRulesetConfig({
|
|
598
|
+
mustStartAtOrAfter: 0,
|
|
599
|
+
duration: 0,
|
|
600
|
+
weight: 0,
|
|
601
|
+
weightCutPercent: 0,
|
|
602
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
603
|
+
metadata: JBRulesetMetadata({
|
|
604
|
+
reservedPercent: 0,
|
|
605
|
+
cashOutTaxRate: 0,
|
|
606
|
+
baseCurrency: _metadata.baseCurrency,
|
|
607
|
+
pausePay: true,
|
|
608
|
+
pauseCreditTransfers: false,
|
|
609
|
+
allowOwnerMinting: false,
|
|
610
|
+
allowSetCustomToken: false,
|
|
611
|
+
allowTerminalMigration: false,
|
|
612
|
+
allowSetTerminals: false,
|
|
613
|
+
allowSetController: false,
|
|
614
|
+
allowAddAccountingContext: false,
|
|
615
|
+
allowAddPriceFeed: false,
|
|
616
|
+
ownerMustSendPayouts: true,
|
|
617
|
+
holdFees: false,
|
|
618
|
+
useTotalSurplusForCashOuts: false,
|
|
619
|
+
useDataHookForPay: true,
|
|
620
|
+
useDataHookForCashOut: true,
|
|
621
|
+
dataHook: _metadata.dataHook,
|
|
622
|
+
metadata: uint16(
|
|
623
|
+
JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(
|
|
624
|
+
JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: false})
|
|
625
|
+
)
|
|
626
|
+
)
|
|
627
|
+
}),
|
|
628
|
+
splitGroups: new JBSplitGroup[](0),
|
|
629
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Queue the no-contest refund ruleset.
|
|
633
|
+
// slither-disable-next-line unused-return
|
|
634
|
+
controller.queueRulesetsOf({
|
|
635
|
+
projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game: no contest."
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
emit QueuedNoContest(gameId, msg.sender);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/// @notice Allows this contract to receive 721s.
|
|
642
|
+
function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
|
|
643
|
+
return IERC721Receiver.onERC721Received.selector;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
//*********************************************************************//
|
|
647
|
+
// ------------------------ internal functions ----------------------- //
|
|
648
|
+
//*********************************************************************//
|
|
649
|
+
|
|
650
|
+
function _launchGame(
|
|
651
|
+
DefifaLaunchProjectData memory launchProjectData,
|
|
652
|
+
uint256 _gameId,
|
|
653
|
+
address _dataHook
|
|
654
|
+
)
|
|
655
|
+
internal
|
|
656
|
+
returns (uint256 projectId)
|
|
657
|
+
{
|
|
658
|
+
//
|
|
659
|
+
JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
|
|
660
|
+
accountingContexts[0] = launchProjectData.token;
|
|
661
|
+
|
|
662
|
+
// Build the terminal configuration for the Defifa project.
|
|
663
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
664
|
+
terminalConfigurations[0] =
|
|
665
|
+
JBTerminalConfig({terminal: launchProjectData.terminal, accountingContextsToAccept: accountingContexts});
|
|
666
|
+
|
|
667
|
+
// Build the rulesets that this Defifa game will go through.
|
|
668
|
+
bool hasRefundPhase = launchProjectData.refundPeriodDuration != 0;
|
|
669
|
+
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](hasRefundPhase ? 3 : 2);
|
|
670
|
+
|
|
671
|
+
// `MINT` cycle.
|
|
672
|
+
rulesetConfigs[0] = JBRulesetConfig({
|
|
673
|
+
mustStartAtOrAfter: launchProjectData.start - launchProjectData.mintPeriodDuration
|
|
674
|
+
- launchProjectData.refundPeriodDuration,
|
|
675
|
+
duration: launchProjectData.mintPeriodDuration,
|
|
676
|
+
weight: 0,
|
|
677
|
+
weightCutPercent: 0,
|
|
678
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
679
|
+
metadata: JBRulesetMetadata({
|
|
680
|
+
reservedPercent: 0,
|
|
681
|
+
cashOutTaxRate: 0,
|
|
682
|
+
baseCurrency: launchProjectData.token.currency,
|
|
683
|
+
pausePay: false,
|
|
684
|
+
pauseCreditTransfers: false,
|
|
685
|
+
allowOwnerMinting: false,
|
|
686
|
+
allowSetCustomToken: false,
|
|
687
|
+
allowTerminalMigration: false,
|
|
688
|
+
allowSetTerminals: false,
|
|
689
|
+
allowSetController: false,
|
|
690
|
+
allowAddAccountingContext: false,
|
|
691
|
+
allowAddPriceFeed: false,
|
|
692
|
+
ownerMustSendPayouts: false,
|
|
693
|
+
holdFees: false,
|
|
694
|
+
useTotalSurplusForCashOuts: false,
|
|
695
|
+
useDataHookForPay: true,
|
|
696
|
+
useDataHookForCashOut: true,
|
|
697
|
+
dataHook: _dataHook,
|
|
698
|
+
metadata: uint16(
|
|
699
|
+
JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(
|
|
700
|
+
JB721TiersRulesetMetadata({
|
|
701
|
+
pauseTransfers: false,
|
|
702
|
+
// Reserved tokens can't be minted during this funding cycle.
|
|
703
|
+
pauseMintPendingReserves: true
|
|
704
|
+
})
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
}),
|
|
708
|
+
splitGroups: new JBSplitGroup[](0),
|
|
709
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
uint256 cycleNumber = 1;
|
|
713
|
+
if (hasRefundPhase) {
|
|
714
|
+
// `REFUND` cycle.
|
|
715
|
+
rulesetConfigs[cycleNumber++] = JBRulesetConfig({
|
|
716
|
+
mustStartAtOrAfter: launchProjectData.start - launchProjectData.refundPeriodDuration,
|
|
717
|
+
duration: launchProjectData.refundPeriodDuration,
|
|
718
|
+
weight: 0,
|
|
719
|
+
weightCutPercent: 0,
|
|
720
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
721
|
+
metadata: JBRulesetMetadata({
|
|
722
|
+
reservedPercent: 0,
|
|
723
|
+
cashOutTaxRate: 0,
|
|
724
|
+
baseCurrency: launchProjectData.token.currency,
|
|
725
|
+
// Refund phase does not allow new payments.
|
|
726
|
+
pausePay: true,
|
|
727
|
+
pauseCreditTransfers: false,
|
|
728
|
+
allowOwnerMinting: false,
|
|
729
|
+
allowSetCustomToken: false,
|
|
730
|
+
allowTerminalMigration: false,
|
|
731
|
+
allowSetTerminals: false,
|
|
732
|
+
allowSetController: false,
|
|
733
|
+
allowAddAccountingContext: false,
|
|
734
|
+
allowAddPriceFeed: false,
|
|
735
|
+
ownerMustSendPayouts: false,
|
|
736
|
+
holdFees: false,
|
|
737
|
+
useTotalSurplusForCashOuts: false,
|
|
738
|
+
useDataHookForPay: true,
|
|
739
|
+
useDataHookForCashOut: true,
|
|
740
|
+
dataHook: _dataHook,
|
|
741
|
+
metadata: uint16(
|
|
742
|
+
JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(
|
|
743
|
+
JB721TiersRulesetMetadata({
|
|
744
|
+
pauseTransfers: false,
|
|
745
|
+
// Reserved tokens can't be minted during this funding cycle.
|
|
746
|
+
pauseMintPendingReserves: true
|
|
747
|
+
})
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
}),
|
|
751
|
+
splitGroups: new JBSplitGroup[](0),
|
|
752
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Set fund access constraints.
|
|
757
|
+
JBCurrencyAmount[] memory payoutAmounts = new JBCurrencyAmount[](1);
|
|
758
|
+
payoutAmounts[0] = JBCurrencyAmount({
|
|
759
|
+
// We allow a payout of the full amount, this will then mostly be added back to the balance of the project.
|
|
760
|
+
amount: type(uint224).max,
|
|
761
|
+
currency: launchProjectData.token.currency
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
JBFundAccessLimitGroup[] memory fundAccessConstraints = new JBFundAccessLimitGroup[](1);
|
|
765
|
+
fundAccessConstraints[0] = JBFundAccessLimitGroup({
|
|
766
|
+
terminal: address(launchProjectData.terminal),
|
|
767
|
+
token: launchProjectData.token.token,
|
|
768
|
+
payoutLimits: payoutAmounts,
|
|
769
|
+
surplusAllowances: new JBCurrencyAmount[](0)
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// `SCORING` cycle.
|
|
773
|
+
rulesetConfigs[cycleNumber++] = JBRulesetConfig({
|
|
774
|
+
mustStartAtOrAfter: launchProjectData.start,
|
|
775
|
+
duration: 0,
|
|
776
|
+
weight: 0,
|
|
777
|
+
weightCutPercent: 0,
|
|
778
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
779
|
+
metadata: JBRulesetMetadata({
|
|
780
|
+
reservedPercent: 0,
|
|
781
|
+
cashOutTaxRate: 0,
|
|
782
|
+
baseCurrency: launchProjectData.token.currency,
|
|
783
|
+
pausePay: true,
|
|
784
|
+
pauseCreditTransfers: false,
|
|
785
|
+
allowOwnerMinting: false,
|
|
786
|
+
allowSetCustomToken: false,
|
|
787
|
+
allowTerminalMigration: false,
|
|
788
|
+
allowSetTerminals: false,
|
|
789
|
+
allowSetController: false,
|
|
790
|
+
allowAddAccountingContext: false,
|
|
791
|
+
allowAddPriceFeed: false,
|
|
792
|
+
// Set this to true so only the deployer can fulfill the commitments.
|
|
793
|
+
ownerMustSendPayouts: true,
|
|
794
|
+
holdFees: false,
|
|
795
|
+
useTotalSurplusForCashOuts: false,
|
|
796
|
+
useDataHookForPay: true,
|
|
797
|
+
useDataHookForCashOut: true,
|
|
798
|
+
dataHook: _dataHook,
|
|
799
|
+
metadata: uint16(
|
|
800
|
+
JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(
|
|
801
|
+
JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: false})
|
|
802
|
+
)
|
|
803
|
+
)
|
|
804
|
+
}),
|
|
805
|
+
splitGroups: _buildSplits({
|
|
806
|
+
_gameId: _gameId,
|
|
807
|
+
_dataHook: _dataHook,
|
|
808
|
+
_token: launchProjectData.token.token,
|
|
809
|
+
_initialSplits: launchProjectData.splits
|
|
810
|
+
}),
|
|
811
|
+
fundAccessLimitGroups: fundAccessConstraints
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// launch the project.
|
|
815
|
+
return controller.launchProjectFor({
|
|
816
|
+
owner: address(this),
|
|
817
|
+
projectUri: launchProjectData.projectUri,
|
|
818
|
+
rulesetConfigurations: rulesetConfigs,
|
|
819
|
+
terminalConfigurations: terminalConfigurations,
|
|
820
|
+
memo: "Launching Defifa game."
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function _buildSplits(
|
|
825
|
+
uint256 _gameId,
|
|
826
|
+
address _dataHook,
|
|
827
|
+
address _token,
|
|
828
|
+
JBSplit[] memory _initialSplits
|
|
829
|
+
)
|
|
830
|
+
internal
|
|
831
|
+
returns (JBSplitGroup[] memory)
|
|
832
|
+
{
|
|
833
|
+
uint256 _numberOfUserSplits = _initialSplits.length;
|
|
834
|
+
|
|
835
|
+
// Compute absolute percents for protocol fees.
|
|
836
|
+
uint256 _nanaAbsolutePercent = JBConstants.SPLITS_TOTAL_PERCENT / BASE_PROTOCOL_FEE_DIVISOR;
|
|
837
|
+
uint256 _defifaAbsolutePercent = JBConstants.SPLITS_TOTAL_PERCENT / DEFIFA_FEE_DIVISOR;
|
|
838
|
+
|
|
839
|
+
// Sum all absolute percents.
|
|
840
|
+
uint256 _totalAbsolutePercent = _nanaAbsolutePercent + _defifaAbsolutePercent;
|
|
841
|
+
for (uint256 _i; _i < _numberOfUserSplits; _i++) {
|
|
842
|
+
_totalAbsolutePercent += _initialSplits[_i].percent;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Validate that total fee splits don't exceed 100%.
|
|
846
|
+
if (_totalAbsolutePercent > JBConstants.SPLITS_TOTAL_PERCENT) revert DefifaDeployer_SplitsDontAddUp();
|
|
847
|
+
|
|
848
|
+
// Store the total absolute percent for use in fulfillCommitmentsOf.
|
|
849
|
+
_commitmentPercentOf[_gameId] = _totalAbsolutePercent;
|
|
850
|
+
|
|
851
|
+
// Build the splits array: user splits + Defifa + NANA (NANA last to absorb rounding).
|
|
852
|
+
uint256 _splitCount = _numberOfUserSplits + 2;
|
|
853
|
+
JBSplit[] memory _splits = new JBSplit[](_splitCount);
|
|
854
|
+
|
|
855
|
+
// Normalize user splits and copy them over.
|
|
856
|
+
uint256 _normalizedTotal;
|
|
857
|
+
for (uint256 _i; _i < _numberOfUserSplits; _i++) {
|
|
858
|
+
_splits[_i] = _initialSplits[_i];
|
|
859
|
+
_splits[_i].percent =
|
|
860
|
+
uint32(mulDiv(_initialSplits[_i].percent, JBConstants.SPLITS_TOTAL_PERCENT, _totalAbsolutePercent));
|
|
861
|
+
_normalizedTotal += _splits[_i].percent;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Add Defifa fee split (normalized).
|
|
865
|
+
uint256 _defifaNormalized =
|
|
866
|
+
mulDiv(_defifaAbsolutePercent, JBConstants.SPLITS_TOTAL_PERCENT, _totalAbsolutePercent);
|
|
867
|
+
_splits[_numberOfUserSplits] = JBSplit({
|
|
868
|
+
preferAddToBalance: false,
|
|
869
|
+
percent: uint32(_defifaNormalized),
|
|
870
|
+
projectId: uint64(defifaProjectId),
|
|
871
|
+
beneficiary: payable(address(_dataHook)),
|
|
872
|
+
lockedUntil: 0,
|
|
873
|
+
hook: IJBSplitHook(address(0))
|
|
874
|
+
});
|
|
875
|
+
_normalizedTotal += _defifaNormalized;
|
|
876
|
+
|
|
877
|
+
// Add NANA protocol fee split last — absorbs rounding remainder.
|
|
878
|
+
// Beneficiary is the data hook so the hook receives NANA tokens for distribution during cash-outs.
|
|
879
|
+
_splits[_splitCount - 1] = JBSplit({
|
|
880
|
+
preferAddToBalance: false,
|
|
881
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT - _normalizedTotal),
|
|
882
|
+
projectId: uint64(baseProtocolProjectId),
|
|
883
|
+
beneficiary: payable(address(_dataHook)),
|
|
884
|
+
lockedUntil: 0,
|
|
885
|
+
hook: IJBSplitHook(address(0))
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// Build the grouped split for the payment of the game token.
|
|
889
|
+
JBSplitGroup[] memory _groupedSplits = new JBSplitGroup[](1);
|
|
890
|
+
_groupedSplits[0] = JBSplitGroup({groupId: uint256(uint160(_token)), splits: _splits});
|
|
891
|
+
|
|
892
|
+
return _groupedSplits;
|
|
893
|
+
}
|
|
894
|
+
}
|