@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.
Files changed (59) hide show
  1. package/.gas-snapshot +2 -0
  2. package/CRYPTO_ECON.md +955 -0
  3. package/CRYPTO_ECON.pdf +0 -0
  4. package/CRYPTO_ECON.tex +800 -0
  5. package/README.md +119 -0
  6. package/SKILLS.md +177 -0
  7. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDelegate.json +4867 -0
  8. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDeployer.json +1719 -0
  9. package/deployments/defifa-v5/arbitrum_sepolia/DefifaGovernor.json +1535 -0
  10. package/deployments/defifa-v5/arbitrum_sepolia/DefifaTokenUriResolver.json +295 -0
  11. package/deployments/defifa-v5/base_sepolia/DefifaDelegate.json +4875 -0
  12. package/deployments/defifa-v5/base_sepolia/DefifaDeployer.json +1725 -0
  13. package/deployments/defifa-v5/base_sepolia/DefifaGovernor.json +1543 -0
  14. package/deployments/defifa-v5/base_sepolia/DefifaTokenUriResolver.json +301 -0
  15. package/deployments/defifa-v5/optimism_sepolia/DefifaDelegate.json +4875 -0
  16. package/deployments/defifa-v5/optimism_sepolia/DefifaDeployer.json +1725 -0
  17. package/deployments/defifa-v5/optimism_sepolia/DefifaGovernor.json +1543 -0
  18. package/deployments/defifa-v5/optimism_sepolia/DefifaTokenUriResolver.json +301 -0
  19. package/deployments/defifa-v5/sepolia/DefifaDelegate.json +4875 -0
  20. package/deployments/defifa-v5/sepolia/DefifaDeployer.json +1725 -0
  21. package/deployments/defifa-v5/sepolia/DefifaGovernor.json +1543 -0
  22. package/deployments/defifa-v5/sepolia/DefifaTokenUriResolver.json +301 -0
  23. package/foundry.lock +17 -0
  24. package/foundry.toml +35 -0
  25. package/package.json +33 -0
  26. package/remappings.txt +6 -0
  27. package/script/Deploy.s.sol +109 -0
  28. package/script/helpers/DefifaDeploymentLib.sol +83 -0
  29. package/slither-ci.config.json +10 -0
  30. package/sphinx.lock +521 -0
  31. package/src/DefifaDeployer.sol +894 -0
  32. package/src/DefifaGovernor.sol +490 -0
  33. package/src/DefifaHook.sol +1056 -0
  34. package/src/DefifaProjectOwner.sol +63 -0
  35. package/src/DefifaTokenUriResolver.sol +312 -0
  36. package/src/enums/DefifaGamePhase.sol +11 -0
  37. package/src/enums/DefifaScorecardState.sol +10 -0
  38. package/src/interfaces/IDefifaDeployer.sol +108 -0
  39. package/src/interfaces/IDefifaGamePhaseReporter.sol +8 -0
  40. package/src/interfaces/IDefifaGamePotReporter.sol +8 -0
  41. package/src/interfaces/IDefifaGovernor.sol +132 -0
  42. package/src/interfaces/IDefifaHook.sol +228 -0
  43. package/src/interfaces/IDefifaTokenUriResolver.sol +10 -0
  44. package/src/libraries/DefifaFontImporter.sol +19 -0
  45. package/src/libraries/DefifaHookLib.sol +358 -0
  46. package/src/structs/DefifaAttestations.sol +9 -0
  47. package/src/structs/DefifaDelegation.sol +9 -0
  48. package/src/structs/DefifaLaunchProjectData.sol +59 -0
  49. package/src/structs/DefifaOpsData.sol +20 -0
  50. package/src/structs/DefifaScorecard.sol +9 -0
  51. package/src/structs/DefifaTierCashOutWeight.sol +9 -0
  52. package/src/structs/DefifaTierParams.sol +16 -0
  53. package/test/DefifaFeeAccounting.t.sol +559 -0
  54. package/test/DefifaGovernor.t.sol +1333 -0
  55. package/test/DefifaMintCostInvariant.t.sol +299 -0
  56. package/test/DefifaNoContest.t.sol +922 -0
  57. package/test/DefifaSecurity.t.sol +717 -0
  58. package/test/SVG.t.sol +164 -0
  59. package/test/deployScript.t.sol +144 -0
@@ -0,0 +1,1333 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity 0.8.26;
3
+
4
+ // solhint-disable-next-line no-unused-import
5
+ import "forge-std/Test.sol";
6
+ // solhint-disable-next-line no-unused-import
7
+ import "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
8
+
9
+ import {DefifaGovernor} from "../src/DefifaGovernor.sol";
10
+ import {DefifaDeployer} from "../src/DefifaDeployer.sol";
11
+ import {DefifaHook} from "../src/DefifaHook.sol";
12
+ import {DefifaTokenUriResolver} from "../src/DefifaTokenUriResolver.sol";
13
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
14
+
15
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
16
+ import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
17
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
18
+ import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
19
+ import {
20
+ JB721TiersRulesetMetadataResolver
21
+ } from "@bananapus/721-hook-v6/src/libraries/JB721TiersRulesetMetadataResolver.sol";
22
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
23
+
24
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
25
+ import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
26
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
27
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
28
+ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
29
+ import {DefifaDelegation} from "../src/structs/DefifaDelegation.sol";
30
+ import {DefifaLaunchProjectData} from "../src/structs/DefifaLaunchProjectData.sol";
31
+ import {DefifaTierParams} from "../src/structs/DefifaTierParams.sol";
32
+ import {DefifaTierCashOutWeight} from "../src/structs/DefifaTierCashOutWeight.sol";
33
+
34
+ /// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
35
+ contract TimestampReader {
36
+ function timestamp() external view returns (uint256) {
37
+ return block.timestamp;
38
+ }
39
+ }
40
+
41
+ contract DefifaGovernorTest is JBTest, TestBaseWorkflow {
42
+ using JBRulesetMetadataResolver for JBRuleset;
43
+
44
+ TimestampReader private _tsReader = new TimestampReader();
45
+
46
+ address _protocolFeeProjectTokenAccount;
47
+ address _defifaProjectTokenAccount;
48
+
49
+ uint256 _protocolFeeProjectId;
50
+ uint256 _defifaProjectId;
51
+ address _owner = 0x1000000000000000000000000000000000000000;
52
+ uint256 _gameId = 3;
53
+
54
+ DefifaDeployer deployer;
55
+ DefifaHook hook;
56
+ DefifaGovernor governor;
57
+
58
+ address projectOwner = address(bytes20(keccak256("projectOwner")));
59
+
60
+ function setUp() public virtual override {
61
+ super.setUp();
62
+
63
+ // Terminal configurations.
64
+ JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
65
+ _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
66
+
67
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
68
+ terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
69
+
70
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
71
+ rulesetConfigs[0] = JBRulesetConfig({
72
+ mustStartAtOrAfter: 0,
73
+ duration: 10 days,
74
+ weight: 1e18,
75
+ weightCutPercent: 0,
76
+ approvalHook: IJBRulesetApprovalHook(address(0)),
77
+ metadata: JBRulesetMetadata({
78
+ reservedPercent: 0,
79
+ cashOutTaxRate: 0,
80
+ baseCurrency: JBCurrencyIds.ETH,
81
+ pausePay: false,
82
+ pauseCreditTransfers: false,
83
+ allowOwnerMinting: false,
84
+ allowSetCustomToken: false,
85
+ allowTerminalMigration: false,
86
+ allowSetTerminals: false,
87
+ allowSetController: false,
88
+ allowAddAccountingContext: false,
89
+ allowAddPriceFeed: false,
90
+ ownerMustSendPayouts: false,
91
+ holdFees: false,
92
+ useTotalSurplusForCashOuts: false,
93
+ useDataHookForPay: true,
94
+ useDataHookForCashOut: true,
95
+ dataHook: address(0),
96
+ metadata: 0
97
+ }),
98
+ splitGroups: new JBSplitGroup[](0),
99
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
100
+ });
101
+
102
+ // Launch the NANA fee project.
103
+ _protocolFeeProjectId =
104
+ jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
105
+ vm.prank(projectOwner);
106
+ _protocolFeeProjectTokenAccount =
107
+ address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
108
+
109
+ // Launch the Defifa fee project.
110
+ _defifaProjectId =
111
+ jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
112
+ vm.prank(projectOwner);
113
+ _defifaProjectTokenAccount =
114
+ address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
115
+
116
+ hook = new DefifaHook(
117
+ jbDirectory(), IERC20(address(_defifaProjectTokenAccount)), IERC20(_protocolFeeProjectTokenAccount)
118
+ );
119
+ governor = new DefifaGovernor(jbController(), address(this));
120
+ JBAddressRegistry _registry = new JBAddressRegistry();
121
+ DefifaTokenUriResolver _tokenURIResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
122
+ deployer = new DefifaDeployer(
123
+ address(hook),
124
+ _tokenURIResolver,
125
+ governor,
126
+ jbController(),
127
+ _registry,
128
+ _defifaProjectId,
129
+ _protocolFeeProjectId
130
+ );
131
+
132
+ // Transfer ownership of the hook to the deployer.
133
+ hook.transferOwnership(address(deployer));
134
+ // Transfer ownership of the governor to the deployer.
135
+ governor.transferOwnership(address(deployer));
136
+ }
137
+
138
+ function testReceiveVotingPower(uint8 nTiers, uint8 tier) public {
139
+ vm.assume(nTiers < 100);
140
+ vm.assume(nTiers >= tier);
141
+ vm.assume(tier != 0);
142
+ address _user = address(bytes20(keccak256("user")));
143
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
144
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
145
+
146
+ // Phase 1: Mint
147
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
148
+ //deployer.queueNextPhaseOf(_projectId);
149
+ // User should have no voting power (yet)
150
+ assertEq(_governor.getAttestationWeight(_gameId, _user, uint48(block.timestamp)), 0);
151
+ // fund user
152
+ vm.deal(_user, 1 ether);
153
+ // Build metadata to buy specific NFT
154
+ uint16[] memory rawMetadata = new uint16[](1);
155
+ vm.assume(tier != 0);
156
+ rawMetadata[0] = uint16(tier); // reward tier
157
+
158
+ // Pay to the project and mint an NFT
159
+ vm.prank(_user);
160
+ jbMultiTerminal().pay{value: 1 ether}(
161
+ _projectId,
162
+ JBConstants.NATIVE_TOKEN,
163
+ 1 ether,
164
+ _user,
165
+ 0,
166
+ "",
167
+ _buildPayMetadata(abi.encode(_user, rawMetadata))
168
+ );
169
+
170
+ // The user should now have a balance
171
+ assertEq(_nft.balanceOf(_user), 1);
172
+
173
+ // Forward 1 block, user should receive all the voting power of the tier, as its the only NFT
174
+ vm.warp(block.timestamp + 1);
175
+
176
+ assertEq(_nft.store().tierOf(address(_nft), tier, false).votingUnits, 1 ether);
177
+ assertEq(
178
+ _governor.MAX_ATTESTATION_POWER_TIER(),
179
+ _governor.getAttestationWeight(_gameId, _user, uint48(block.timestamp))
180
+ );
181
+ }
182
+
183
+ // cashOuts can happen after mint phase
184
+ // function testRefund_fails_afterMintPhase() external {
185
+ // uint8 nTiers = 10;
186
+ // address[] memory _users = new address[](nTiers);
187
+ // DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
188
+ // (uint256 _projectId, , ) = createDefifaProject(defifaData);
189
+ // // Phase 1: Mint
190
+ // vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
191
+ // //deployer.queueNextPhaseOf(_projectId);
192
+ // for (uint256 i = 0; i < nTiers; i++) {
193
+ // // Generate a new address for each tier
194
+ // _users[i] = address(bytes20(keccak256(abi.encode('user', Strings.toString(i)))));
195
+ // // fund user
196
+ // vm.deal(_users[i], 1 ether);
197
+ // // Build metadata to buy specific NFT
198
+ // uint16[] memory rawMetadata = new uint16[](1);
199
+ // rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
200
+ // bytes memory metadata = abi.encode(
201
+ // bytes32(0),
202
+ // bytes32(0),
203
+ // type(IDefifaHook).interfaceId,
204
+ // _users[i],
205
+ // rawMetadata
206
+ // );
207
+ // // Pay to the project and mint an NFT
208
+ // vm.prank(_users[i]);
209
+ // jbMultiTerminal().pay{value: 1 ether}(
210
+ // _projectId,
211
+ // 1 ether,
212
+ // address(0),
213
+ // _users[i],
214
+ // 0,
215
+ // true,
216
+ // '',
217
+ // metadata
218
+ // );
219
+ // }
220
+ // // Phase 2: Redeem
221
+ // vm.warp(block.timestamp + defifaData.mintPeriodDuration);
222
+ // //deployer.queueNextPhaseOf(_projectId);
223
+ // // Phase 3: Start
224
+ // vm.warp(defifaData.start + 1);
225
+ // //deployer.queueNextPhaseOf(_projectId);
226
+ // // Make sure this is actually Phase 3
227
+ // assertEq(jbRulesets().currentOf(_projectId).number, 3);
228
+ // for (uint256 i = 0; i < _users.length; i++) {
229
+ // address _user = _users[i];
230
+ // // Craft the metadata: redeem the tokenId
231
+ // bytes memory cashOutMetadata;
232
+ // {
233
+ // uint256[] memory cashOutId = new uint256[](1);
234
+ // cashOutId[0] = _generateTokenId(i + 1, 1);
235
+ // cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutId);
236
+ // }
237
+ // vm.expectRevert(abi.encodeWithSignature('FUNDING_CYCLE_REDEEM_PAUSED()'));
238
+ // vm.prank(_user);
239
+ // JBMultiTerminal(address(jbMultiTerminal())).redeemTokensOf({
240
+ // _holder: _user,
241
+ // _projectId: _projectId,
242
+ // _tokenCount: 0,
243
+ // _token: address(0),
244
+ // _minReturnedTokens: 0,
245
+ // _beneficiary: payable(_user),
246
+ // _memo: 'Refund plz',
247
+ // _metadata: cashOutMetadata
248
+ // });
249
+ // }
250
+ // // // Phase 4: End
251
+ // // vm.warp(deployer.endOf(_projectId));
252
+ // // Forward the amount of blocks needed to reach the end (and round up)
253
+ // // vm.roll(deployer.endOf(_projectId) - block.timestamp / 12 + 1);
254
+ // vm.warp(block.timestamp + 1 weeks);
255
+ // assertEq(jbRulesets().currentOf(_projectId).number, 4);
256
+ // for (uint256 i = 0; i < _users.length; i++) {
257
+ // address _user = _users[i];
258
+ // // Craft the metadata: redeem the tokenId
259
+ // bytes memory cashOutMetadata;
260
+ // {
261
+ // uint256[] memory cashOutId = new uint256[](1);
262
+ // cashOutId[0] = _generateTokenId(i + 1, 1);
263
+ // cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutId);
264
+ // }
265
+ // // Here the refunds are not allowed but cashOuts are,
266
+ // // so it should instead revert with an error showing that there is no cashOut set for our tier
267
+ // vm.expectRevert(abi.encodeWithSignature('NOTHING_TO_CLAIM()'));
268
+ // vm.prank(_user);
269
+ // JBMultiTerminal(address(jbMultiTerminal())).redeemTokensOf({
270
+ // _holder: _user,
271
+ // _projectId: _projectId,
272
+ // _tokenCount: 0,
273
+ // _token: address(0),
274
+ // _minReturnedTokens: 0,
275
+ // _beneficiary: payable(_user),
276
+ // _memo: 'Refund plz',
277
+ // _metadata: cashOutMetadata
278
+ // });
279
+ // }
280
+ // }
281
+
282
+ function testMint_fails_afterMintPhase() external {
283
+ uint8 nTiers = 10;
284
+ address[] memory _users = new address[](nTiers);
285
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
286
+ (uint256 _projectId,,) = createDefifaProject(defifaData);
287
+ // Phase 1: minting
288
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
289
+ //deployer.queueNextPhaseOf(_projectId);
290
+ // Phase 2: Redeem
291
+ vm.warp(block.timestamp + defifaData.mintPeriodDuration);
292
+ //deployer.queueNextPhaseOf(_projectId);
293
+ // Make sure this is actually Phase 2
294
+ assertEq(jbRulesets().currentOf(_projectId).cycleNumber, 2);
295
+
296
+ for (uint256 i = 0; i < nTiers; i++) {
297
+ // Generate a new address for each tier
298
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
299
+ // fund user
300
+ vm.deal(_users[i], 1 ether);
301
+ // Build metadata to buy specific NFT
302
+ uint16[] memory rawMetadata = new uint16[](1);
303
+ rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
304
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
305
+ // Pay to the project and mint an NFT
306
+ vm.expectRevert(JBTerminalStore.JBTerminalStore_RulesetPaymentPaused.selector);
307
+ vm.prank(_users[i]);
308
+ jbMultiTerminal().pay{value: 1 ether}(
309
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
310
+ );
311
+ }
312
+ for (uint256 i = 0; i < nTiers; i++) {
313
+ // Generate a new address for each tier
314
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
315
+ // fund user
316
+ vm.deal(_users[i], 1 ether);
317
+ // Build metadata to buy specific NFT
318
+ uint16[] memory rawMetadata = new uint16[](1);
319
+ rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
320
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
321
+ // Pay to the project and mint an NFT
322
+ vm.expectRevert(JBTerminalStore.JBTerminalStore_RulesetPaymentPaused.selector);
323
+ vm.prank(_users[i]);
324
+ jbMultiTerminal().pay{value: 1 ether}(
325
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
326
+ );
327
+ }
328
+ }
329
+
330
+ // Transfers are no longer disabled
331
+ // function testTransfer_fails_afterTradeDeadline() external {
332
+ // uint8 nTiers = 10;
333
+ // address[] memory _users = new address[](nTiers);
334
+ // DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData();
335
+ // (uint256 _projectId, DefifaHook _nft, ) = createDefifaProject(
336
+ // uint256(nTiers),
337
+ // getBasicDefifaLaunchData()
338
+ // );
339
+ // // Phase 1: minting
340
+ // vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
341
+ // for (uint256 i = 0; i < nTiers; i++) {
342
+ // // Generate a new address for each tier
343
+ // _users[i] = address(bytes20(keccak256(abi.encode('user', Strings.toString(i)))));
344
+ // // fund user
345
+ // vm.deal(_users[i], 1 ether);
346
+ // // Build metadata to buy specific NFT
347
+ // uint16[] memory rawMetadata = new uint16[](1);
348
+ // rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
349
+ // bytes memory metadata = abi.encode(
350
+ // bytes32(0),
351
+ // bytes32(0),
352
+ // type(IDefifaHook).interfaceId,
353
+ // false,
354
+ // false,
355
+ // false,
356
+ // rawMetadata
357
+ // );
358
+ // // Pay to the project and mint an NFT
359
+ // vm.prank(_users[i]);
360
+ // jbMultiTerminal().pay{value: 1 ether}(
361
+ // _projectId,
362
+ // 1 ether,
363
+ // address(0),
364
+ // _users[i],
365
+ // 0,
366
+ // true,
367
+ // '',
368
+ // metadata
369
+ // );
370
+ // }
371
+ // // Phase 2: Redeem
372
+ // vm.warp(block.timestamp + defifaData.mintPeriodDuration);
373
+ // //deployer.queueNextPhaseOf(_projectId);
374
+ // // Make sure this is actually Phase 2
375
+ // assertEq(jbRulesets().currentOf(_projectId).number, 2);
376
+ // // Phase 3: Start
377
+ // vm.warp(defifaData.start + 1);
378
+ // //deployer.queueNextPhaseOf(_projectId);
379
+ // // Make sure this is actually Phase 3
380
+ // assertEq(jbRulesets().currentOf(_projectId).number, 3);
381
+ // uint256 _tokenIdToTransfer = _generateTokenId(1, 1);
382
+ // vm.prank(_users[0]);
383
+ // // trasnfers not possible in phase 3
384
+ // vm.expectRevert(abi.encodeWithSignature('TRANSFERS_PAUSED()'));
385
+ // _nft.transferFrom(_users[0], _users[1], _tokenIdToTransfer);
386
+ // }
387
+ function testSetCashOutRates_fails_unmetQuorum() external {
388
+ uint8 nTiers = 10;
389
+ address[] memory _users = new address[](nTiers);
390
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
391
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
392
+ // Phase 1: minting
393
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
394
+ //deployer.queueNextPhaseOf(_projectId);
395
+ for (uint256 i = 0; i < nTiers; i++) {
396
+ // Generate a new address for each tier
397
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
398
+ // fund user
399
+ vm.deal(_users[i], 1 ether);
400
+ // Build metadata to buy specific NFT
401
+ uint16[] memory rawMetadata = new uint16[](1);
402
+ rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
403
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
404
+ // Pay to the project and mint an NFT
405
+ vm.prank(_users[i]);
406
+ jbMultiTerminal().pay{value: 1 ether}(
407
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
408
+ );
409
+ // Set the delegate as the user themselves
410
+ DefifaDelegation[] memory tiered721SetDelegatesData = new DefifaDelegation[](1);
411
+ tiered721SetDelegatesData[0] = DefifaDelegation({delegatee: _users[i], tierId: uint256(i + 1)});
412
+ vm.prank(_users[i]);
413
+ _nft.setTierDelegatesTo(tiered721SetDelegatesData);
414
+ // Forward 1 block, user should receive all the voting power of the tier, as its the only NFT
415
+ vm.warp(_tsReader.timestamp() + 1);
416
+ assertEq(
417
+ _governor.MAX_ATTESTATION_POWER_TIER(),
418
+ _governor.getAttestationWeight(_gameId, _users[i], uint48(_tsReader.timestamp()))
419
+ );
420
+ }
421
+ // Warp to scoring phase (past start time)
422
+ vm.warp(defifaData.start + 1);
423
+ // Generate the scorecards
424
+ DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
425
+ // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
426
+ for (uint256 i = 0; i < scorecards.length; i++) {
427
+ scorecards[i].id = i + 1;
428
+ scorecards[i].cashOutWeight = i % 2 == 0 ? 1_000_000_000 / (scorecards.length / 2) : 0;
429
+ }
430
+ // Forward time so proposals can be created
431
+ uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
432
+ // Forward time so voting becomes active
433
+ vm.warp(_tsReader.timestamp() + _governor.attestationStartTimeOf(_gameId) + 1);
434
+ // We have only 40% vote on the proposal, making it still be below quorum.
435
+ for (uint256 i = 0; i < _users.length * 4 / 10; i++) {
436
+ vm.prank(_users[i]);
437
+ _governor.attestToScorecardFrom(_gameId, _proposalId);
438
+ }
439
+ // Forward the amount of blocks needed to reach the end (and round up)
440
+ vm.warp(_tsReader.timestamp() + _governor.attestationGracePeriodOf(_gameId) + 1);
441
+ // Execute the proposal
442
+ vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
443
+ _governor.ratifyScorecardFrom(_gameId, scorecards);
444
+ }
445
+
446
+ function testSetCashOutRatesAndRedeem_multipleTiers(uint8 nTiers, uint8[] calldata distribution) public {
447
+ vm.assume(nTiers > 10 && nTiers < 100);
448
+ vm.assume(distribution.length < nTiers);
449
+
450
+ uint256 _sumDistribution;
451
+ for (uint256 i = 0; i < distribution.length; i++) {
452
+ _sumDistribution += distribution[i];
453
+ }
454
+ vm.assume(_sumDistribution > 0);
455
+ address[] memory _users = new address[](nTiers);
456
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
457
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
458
+
459
+ // Phase 1: minting
460
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
461
+ for (uint256 i = 0; i < nTiers; i++) {
462
+ // Generate a new address for each tier
463
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
464
+ // fund user
465
+ vm.deal(_users[i], 1 ether);
466
+
467
+ // Build metadata to buy specific NFT
468
+ bytes memory metadata;
469
+ {
470
+ uint16[] memory rawMetadata = new uint16[](1);
471
+ rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
472
+ metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
473
+ }
474
+
475
+ // Pay to the project and mint an NFT
476
+ vm.prank(_users[i]);
477
+ jbMultiTerminal().pay{value: 1 ether}(
478
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
479
+ );
480
+ // Forward 1 block, user should receive all the voting power of the tier, as its the only NFT
481
+ vm.warp(block.timestamp + 1);
482
+ assertEq(
483
+ _governor.MAX_ATTESTATION_POWER_TIER(),
484
+ _governor.getAttestationWeight(_gameId, _users[i], uint48(block.timestamp))
485
+ );
486
+ // Have a user mint and refund the tier
487
+ mintAndRefund(_nft, _projectId, i + 1);
488
+ }
489
+ // Have a user mint and refund the tier
490
+ mintAndRefund(_nft, _projectId, 1);
491
+
492
+ // Warp to scoring phase (past start time)
493
+ vm.warp(defifaData.start + 1);
494
+ // Generate the scorecards — must sum to exactly TOTAL_CASHOUT_WEIGHT
495
+ DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
496
+ uint256 assignedCashOutWeight;
497
+ // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
498
+ for (uint256 i = 0; i < scorecards.length; i++) {
499
+ scorecards[i].id = i + 1;
500
+ if (distribution.length <= i) continue;
501
+ scorecards[i].cashOutWeight = (uint256(distribution[i]) * _nft.TOTAL_CASHOUT_WEIGHT()) / _sumDistribution;
502
+ assignedCashOutWeight += scorecards[i].cashOutWeight;
503
+ }
504
+ // Absorb rounding remainder into first tier with weight
505
+ if (assignedCashOutWeight < _nft.TOTAL_CASHOUT_WEIGHT()) {
506
+ uint256 remainder = _nft.TOTAL_CASHOUT_WEIGHT() - assignedCashOutWeight;
507
+ for (uint256 i = 0; i < scorecards.length; i++) {
508
+ if (scorecards[i].cashOutWeight > 0) {
509
+ scorecards[i].cashOutWeight += remainder;
510
+ break;
511
+ }
512
+ }
513
+ }
514
+ // Forward time so proposals can be created
515
+ uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
516
+ // Forward time so voting becomes active
517
+ vm.warp(block.timestamp + _governor.attestationStartTimeOf(_gameId) + 1);
518
+ // No voting delay after the initial voting delay has passed in
519
+ //assertEq(_governor.attestationStartTimeOf(_gameId), 0);
520
+ // All the users vote
521
+ // 0 = Against
522
+ // 1 = For
523
+ // 2 = Abstain
524
+ for (uint256 i = 0; i < _users.length; i++) {
525
+ vm.prank(_users[i]);
526
+ _governor.attestToScorecardFrom(_gameId, _proposalId);
527
+ }
528
+ // each block is of 12 secs
529
+ vm.warp(block.timestamp + _governor.attestationGracePeriodOf(_gameId));
530
+
531
+ _governor.ratifyScorecardFrom(_gameId, scorecards);
532
+ // Move forward 1 block to start the new ruleset.
533
+ vm.roll(block.number + 1);
534
+
535
+ _verifyCashOutsAndRedeem(
536
+ _projectId, _nft, scorecards, _users, _sumDistribution, distribution, assignedCashOutWeight
537
+ );
538
+ }
539
+
540
+ function _verifyCashOutsAndRedeem(
541
+ uint256 _projectId,
542
+ DefifaHook _nft,
543
+ DefifaTierCashOutWeight[] memory scorecards,
544
+ address[] memory _users,
545
+ uint256 _sumDistribution,
546
+ uint8[] calldata distribution,
547
+ uint256 assignedCashOutWeight
548
+ )
549
+ internal
550
+ {
551
+ uint256 _pot = jbMultiTerminal()
552
+ .currentSurplusOf(_projectId, jbMultiTerminal().accountingContextsOf(_projectId), 18, JBCurrencyIds.ETH);
553
+ // Assert that the deployer did *NOT* receive any fee tokens.
554
+ assertEq(IERC20(_protocolFeeProjectTokenAccount).balanceOf(address(deployer)), 0);
555
+ assertEq(IERC20(_defifaProjectTokenAccount).balanceOf(address(deployer)), 0);
556
+
557
+ // Verify that the cashOutWeights actually changed
558
+ for (uint256 i = 0; i < scorecards.length; i++) {
559
+ _verifySingleCashOut(_projectId, _nft, scorecards[i], _users[i], _pot, _sumDistribution, distribution, i);
560
+ }
561
+ // All NFTs should have been redeemed, only some dust should be left
562
+ uint256 remainingSurplus = jbMultiTerminal()
563
+ .currentSurplusOf(_projectId, jbMultiTerminal().accountingContextsOf(_projectId), 18, JBCurrencyIds.ETH);
564
+ uint256 _expected = _pot * (_nft.TOTAL_CASHOUT_WEIGHT() - assignedCashOutWeight) / _nft.TOTAL_CASHOUT_WEIGHT();
565
+ assertApproxEqAbs(remainingSurplus, _expected, 10 ** 14);
566
+
567
+ // There should be no fee tokens left in the hook.
568
+ assertEq(IERC20(_protocolFeeProjectTokenAccount).balanceOf(address(_nft)), 0);
569
+ assertEq(IERC20(_defifaProjectTokenAccount).balanceOf(address(_nft)), 0);
570
+ }
571
+
572
+ function _verifySingleCashOut(
573
+ uint256 _projectId,
574
+ DefifaHook _nft,
575
+ DefifaTierCashOutWeight memory scorecard,
576
+ address _user,
577
+ uint256 _pot,
578
+ uint256 _sumDistribution,
579
+ uint8[] calldata distribution,
580
+ uint256 i
581
+ )
582
+ internal
583
+ {
584
+ assertEq(_nft.tierCashOutWeights()[i], scorecard.cashOutWeight);
585
+
586
+ bytes memory cashOutMetadata;
587
+ uint256 _receiveDefifa;
588
+ uint256 _receiveNana;
589
+ {
590
+ uint256[] memory cashOutId = new uint256[](1);
591
+ cashOutId[0] = _generateTokenId(i + 1, 1);
592
+ cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutId));
593
+ (_receiveDefifa, _receiveNana) = _nft.tokensClaimableFor(cashOutId);
594
+ }
595
+ uint256 _nanaBalance = IERC20(_protocolFeeProjectTokenAccount).balanceOf(_user);
596
+ uint256 _defifaBalance = IERC20(_defifaProjectTokenAccount).balanceOf(_user);
597
+
598
+ vm.prank(_user);
599
+ JBMultiTerminal(address(jbMultiTerminal()))
600
+ .cashOutTokensOf({
601
+ holder: _user,
602
+ projectId: _projectId,
603
+ cashOutCount: 0,
604
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
605
+ minTokensReclaimed: 0,
606
+ beneficiary: payable(_user),
607
+ metadata: cashOutMetadata
608
+ });
609
+
610
+ assertEq(IERC20(_protocolFeeProjectTokenAccount).balanceOf(_user), _nanaBalance + _receiveNana);
611
+ assertEq(IERC20(_defifaProjectTokenAccount).balanceOf(_user), _defifaBalance + _receiveDefifa);
612
+
613
+ if (scorecard.cashOutWeight == 0) return;
614
+
615
+ uint256 _expectedTierCashOut = _pot;
616
+ if (distribution.length > i) {
617
+ _expectedTierCashOut = (_expectedTierCashOut * distribution[i]) / _sumDistribution;
618
+ }
619
+ assertApproxEqRel(_expectedTierCashOut, _user.balance, 0.001 ether);
620
+ }
621
+
622
+ function testVotingPowerDecreasesAfterRefund() public {
623
+ uint256 nOfOtherTiers = 31;
624
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(uint8(nOfOtherTiers + 1));
625
+ (uint256 _projectId, DefifaHook _hook, DefifaGovernor _governor) = createDefifaProject(defifaData);
626
+
627
+ // Phase 1: minting
628
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
629
+ //deployer.queueNextPhaseOf(_projectId);
630
+
631
+ JB721Tier memory _tier = _hook.store().tierOf(address(_hook), 1, false);
632
+ uint256 _cost = _tier.price;
633
+
634
+ address _delegateUser = address(bytes20(keccak256("_delegateUser")));
635
+ address _refundUser = address(bytes20(keccak256("refund_user")));
636
+ // The user should have no balance
637
+ assertEq(_hook.balanceOf(_refundUser), 0);
638
+ // Build metadata to buy specific NFT
639
+ uint16[] memory rawMetadata = new uint16[](1);
640
+ rawMetadata[0] = uint16(1); // reward tier, 1 indexed
641
+ bytes memory metadata = _buildPayMetadata(abi.encode(_refundUser, rawMetadata));
642
+ // Pay to the project and mint an NFT
643
+ vm.deal(_refundUser, _cost);
644
+
645
+ vm.prank(_refundUser);
646
+ jbMultiTerminal().pay{value: _cost}(_projectId, JBConstants.NATIVE_TOKEN, _cost, _refundUser, 0, "", metadata);
647
+
648
+ vm.warp(block.timestamp + 1);
649
+
650
+ assertEq(
651
+ _governor.MAX_ATTESTATION_POWER_TIER(),
652
+ _governor.getAttestationWeight(_gameId, _refundUser, uint48(block.timestamp))
653
+ );
654
+
655
+ // User should no longer have any funds
656
+ assertEq(_refundUser.balance, 0);
657
+ // The user should have have a token
658
+ assertEq(_hook.balanceOf(_refundUser), 1);
659
+
660
+ uint256 _numberBurned = _hook.store().numberOfBurnedFor(address(_hook), 1);
661
+ // Craft the metadata: redeem the tokenId
662
+ bytes memory cashOutMetadata;
663
+ {
664
+ uint256[] memory cashOutId = new uint256[](1);
665
+ cashOutId[0] = _generateTokenId(1, _tier.initialSupply - _tier.remainingSupply + 1 + _numberBurned);
666
+ cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutId));
667
+ }
668
+
669
+ vm.prank(_refundUser);
670
+ JBMultiTerminal(address(jbMultiTerminal()))
671
+ .cashOutTokensOf({
672
+ holder: _refundUser,
673
+ projectId: _projectId,
674
+ cashOutCount: 0,
675
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
676
+ minTokensReclaimed: 0,
677
+ beneficiary: payable(_refundUser),
678
+ metadata: cashOutMetadata
679
+ });
680
+ vm.warp(block.timestamp + 1);
681
+
682
+ assertEq(_refundUser.balance, _cost);
683
+ assertEq(_hook.balanceOf(_refundUser), 0);
684
+
685
+ assertEq(0, _governor.getAttestationWeight(_gameId, _refundUser, uint48(block.timestamp)));
686
+ }
687
+
688
+ function testRevertsIfDelegationisDoneAfterMintPhase(
689
+ uint8 nUsersWithWinningTier,
690
+ uint8 winningTierExtraWeight,
691
+ uint8 baseCashOutWeight
692
+ )
693
+ public
694
+ {
695
+ uint256 nOfOtherTiers = 31;
696
+ vm.assume(nUsersWithWinningTier > 1 && nUsersWithWinningTier < 100);
697
+ uint256 totalWeight = baseCashOutWeight * (nOfOtherTiers + 1) + winningTierExtraWeight;
698
+ vm.assume(totalWeight > 1);
699
+
700
+ address[] memory _users = new address[](nOfOtherTiers + nUsersWithWinningTier);
701
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(uint8(nOfOtherTiers + 1));
702
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
703
+ // Phase 1: minting
704
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
705
+ //deployer.queueNextPhaseOf(_projectId);
706
+
707
+ for (uint256 i = 0; i < nOfOtherTiers + nUsersWithWinningTier; i++) {
708
+ // Generate a new address for each tier
709
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
710
+ // fund user
711
+ vm.deal(_users[i], 1 ether);
712
+ if (i < nOfOtherTiers) {
713
+ // Build metadata to buy specific NFT
714
+ uint16[] memory rawMetadata = new uint16[](1);
715
+ rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
716
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
717
+ // Pay to the project and mint an NFT
718
+ vm.prank(_users[i]);
719
+ jbMultiTerminal().pay{value: 1 ether}(
720
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
721
+ );
722
+ // Forward 1 block, user should receive all the voting power of the tier, as its the only NFT
723
+ vm.warp(block.timestamp + 1);
724
+ assertEq(
725
+ _governor.MAX_ATTESTATION_POWER_TIER(),
726
+ _governor.getAttestationWeight(_gameId, _users[i], uint48(block.timestamp))
727
+ );
728
+ } else {
729
+ // Build metadata to buy specific NFT
730
+ uint16[] memory rawMetadata = new uint16[](1);
731
+ rawMetadata[0] = uint16(nOfOtherTiers + 1); // reward tier, 1 indexed
732
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
733
+ // Pay to the project and mint an NFT
734
+ vm.prank(_users[i]);
735
+ jbMultiTerminal().pay{value: 1 ether}(
736
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
737
+ );
738
+ // Forward 1 block, user should have a part of the voting power of their tier
739
+ vm.warp(block.timestamp + 1);
740
+ assertEq(
741
+ _governor.MAX_ATTESTATION_POWER_TIER() / (i - nOfOtherTiers + 1),
742
+ _governor.getAttestationWeight(_gameId, _users[i], uint48(block.timestamp))
743
+ );
744
+ }
745
+ }
746
+ // Phase 2: Redeem
747
+ vm.warp(block.timestamp + defifaData.mintPeriodDuration);
748
+ //deployer.queueNextPhaseOf(_projectId);
749
+
750
+ vm.prank(_users[0]);
751
+ vm.expectRevert(abi.encodeWithSignature("DefifaHook_DelegateChangesUnavailableInThisPhase()"));
752
+ _nft.setTierDelegateTo(_users[1], 1);
753
+ }
754
+
755
+ function testSetCashOutRatesAndRedeem_singleTier(
756
+ uint8 nUsersWithWinningTier,
757
+ uint8 winningTierExtraWeight,
758
+ uint8 baseCashOutWeight
759
+ )
760
+ public
761
+ {
762
+ uint256 nOfOtherTiers = 31;
763
+ vm.assume(nUsersWithWinningTier > 1 && nUsersWithWinningTier < 100);
764
+ uint256 totalWeight = baseCashOutWeight * (nOfOtherTiers + 1) + winningTierExtraWeight;
765
+ vm.assume(totalWeight > 1);
766
+
767
+ address[] memory _users = new address[](nOfOtherTiers + nUsersWithWinningTier);
768
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(uint8(nOfOtherTiers + 1));
769
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
770
+ // Phase 1: minting
771
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
772
+ //deployer.queueNextPhaseOf(_projectId);
773
+
774
+ for (uint256 i = 0; i < nOfOtherTiers + nUsersWithWinningTier; i++) {
775
+ // Generate a new address for each tier
776
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
777
+ // fund user
778
+ vm.deal(_users[i], 1 ether);
779
+ if (i < nOfOtherTiers) {
780
+ // Build metadata to buy specific NFT
781
+ uint16[] memory rawMetadata = new uint16[](1);
782
+ rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
783
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
784
+ // Pay to the project and mint an NFT
785
+ vm.prank(_users[i]);
786
+ jbMultiTerminal().pay{value: 1 ether}(
787
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
788
+ );
789
+ // Forward 1 block, user should receive all the voting power of the tier, as its the only NFT
790
+ vm.warp(block.timestamp + 1);
791
+ assertEq(
792
+ _governor.MAX_ATTESTATION_POWER_TIER(),
793
+ _governor.getAttestationWeight(_gameId, _users[i], uint48(block.timestamp))
794
+ );
795
+ } else {
796
+ // Build metadata to buy specific NFT
797
+ uint16[] memory rawMetadata = new uint16[](1);
798
+ rawMetadata[0] = uint16(nOfOtherTiers + 1); // reward tier, 1 indexed
799
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
800
+ // Pay to the project and mint an NFT
801
+ vm.prank(_users[i]);
802
+ jbMultiTerminal().pay{value: 1 ether}(
803
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
804
+ );
805
+ // Forward 1 block, user should have a part of the voting power of their tier
806
+ vm.warp(block.timestamp + 1);
807
+ assertEq(
808
+ _governor.MAX_ATTESTATION_POWER_TIER() / (i - nOfOtherTiers + 1),
809
+ _governor.getAttestationWeight(_gameId, _users[i], uint48(block.timestamp))
810
+ );
811
+ }
812
+ }
813
+ // Have a user mint and refund the tier
814
+ mintAndRefund(_nft, _projectId, 1);
815
+ // Warp to scoring phase (past start time)
816
+ vm.warp(defifaData.start + 1);
817
+ // Generate the scorecards
818
+ DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nOfOtherTiers + 1);
819
+
820
+ uint256 totalCashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
821
+
822
+ // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
823
+ uint256 assignedCashOutWeight;
824
+ for (uint256 i = 0; i < scorecards.length; i++) {
825
+ scorecards[i].id = i + 1;
826
+ if (baseCashOutWeight != 0) {
827
+ scorecards[i].cashOutWeight = (totalCashOutWeight * uint256(baseCashOutWeight)) / totalWeight;
828
+ }
829
+ if (i == nOfOtherTiers && winningTierExtraWeight != 0) {
830
+ scorecards[i].cashOutWeight += (totalCashOutWeight * uint256(winningTierExtraWeight)) / totalWeight;
831
+ }
832
+ assignedCashOutWeight += scorecards[i].cashOutWeight;
833
+ }
834
+ // Absorb rounding remainder into first tier with weight
835
+ if (assignedCashOutWeight < totalCashOutWeight) {
836
+ uint256 remainder = totalCashOutWeight - assignedCashOutWeight;
837
+ for (uint256 i = 0; i < scorecards.length; i++) {
838
+ if (scorecards[i].cashOutWeight > 0) {
839
+ scorecards[i].cashOutWeight += remainder;
840
+ break;
841
+ }
842
+ }
843
+ }
844
+ {
845
+ // Forward time so proposals can be created
846
+ uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
847
+ // Forward time so voting becomes active
848
+ vm.warp(block.timestamp + _governor.attestationStartTimeOf(_gameId) + 1);
849
+ // No voting delay after the initial voting delay has passed in
850
+ // assertEq(_governor.attestationStartTimeOf(_gameId), 0);
851
+ // All the users vote
852
+ // 0 = Against
853
+ // 1 = For
854
+ // 2 = Abstain
855
+ for (uint256 i = 0; i < _users.length; i++) {
856
+ vm.prank(_users[i]);
857
+ _governor.attestToScorecardFrom(_gameId, _proposalId);
858
+ }
859
+ }
860
+
861
+ // Forward the amount of blocks needed to reach the end (and round up)
862
+ vm.warp(block.timestamp + _governor.attestationGracePeriodOf(_gameId));
863
+
864
+ _governor.ratifyScorecardFrom(_gameId, scorecards);
865
+ vm.warp(block.timestamp + 1);
866
+
867
+ uint256 _pot = jbMultiTerminal()
868
+ .currentSurplusOf(_projectId, jbMultiTerminal().accountingContextsOf(_projectId), 18, JBCurrencyIds.ETH);
869
+
870
+ // Verify that the cashOutWeights actually changed
871
+ for (uint256 i = 0; i < _users.length; i++) {
872
+ address _user = _users[i];
873
+ uint256 _tier = i <= nOfOtherTiers ? i + 1 : nOfOtherTiers + 1;
874
+ // Craft the metadata: redeem the tokenId
875
+ bytes memory cashOutMetadata;
876
+ {
877
+ uint256[] memory cashOutId = new uint256[](1);
878
+ cashOutId[0] = _generateTokenId(_tier, _tier == nOfOtherTiers + 1 ? i - nOfOtherTiers + 1 : 1);
879
+ cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutId));
880
+ }
881
+ uint256 _expectedTierCashOut;
882
+ {
883
+ // Calculate how much weight his tier has
884
+ uint256 _tierWeight = _tier == nOfOtherTiers + 1
885
+ ? uint256(baseCashOutWeight) + uint256(winningTierExtraWeight)
886
+ : baseCashOutWeight;
887
+
888
+ // If the cashOut is 0 this will revert
889
+ vm.prank(_user);
890
+ JBMultiTerminal(address(jbMultiTerminal()))
891
+ .cashOutTokensOf({
892
+ holder: _user,
893
+ projectId: _projectId,
894
+ cashOutCount: 0,
895
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
896
+ minTokensReclaimed: 0,
897
+ beneficiary: payable(_user),
898
+ metadata: cashOutMetadata
899
+ });
900
+ // We calculate the expected output based on the given distribution and how much is in the pot
901
+ _expectedTierCashOut = (_pot * _tierWeight) / totalWeight;
902
+ }
903
+ {
904
+ // If this is the winning tier then the amount is divided among the nUsersWithWinningTier
905
+ if (_tier == nOfOtherTiers + 1) {
906
+ _expectedTierCashOut = _expectedTierCashOut / nUsersWithWinningTier;
907
+ }
908
+ }
909
+ // Assert that our expected tier cashOut is ~equal to the actual amount
910
+ // Allowing for some rounding errors, max allowed error is 0.000001 ether
911
+ assertApproxEqRel(_expectedTierCashOut, _user.balance, 0.0001 ether);
912
+ }
913
+ // All NFTs should have been redeemed, only some dust should be left
914
+ // Max allowed dust is 0.0001
915
+ uint256 remainingSurplus = jbMultiTerminal()
916
+ .currentSurplusOf(_projectId, jbMultiTerminal().accountingContextsOf(_projectId), 18, JBCurrencyIds.ETH);
917
+ assertApproxEqAbs(
918
+ remainingSurplus, _pot * (totalCashOutWeight - assignedCashOutWeight) / totalCashOutWeight, 10 ** 14
919
+ );
920
+ }
921
+
922
+ function testPhaseTimes(
923
+ uint16 _durationUntilProjectLaunch,
924
+ uint16 _mintPeriodDuration,
925
+ uint16 _inBetweenMintAndFifa,
926
+ uint16 _fifaDuration
927
+ )
928
+ public
929
+ {
930
+ vm.assume(
931
+ _durationUntilProjectLaunch > 2 && _mintPeriodDuration > 1 && _inBetweenMintAndFifa > 1 && _fifaDuration > 1
932
+ );
933
+ uint48 _launchProjectAt = uint48(block.timestamp) + _durationUntilProjectLaunch;
934
+ uint48 _end =
935
+ _launchProjectAt + uint48(_mintPeriodDuration) + uint48(_inBetweenMintAndFifa) + uint48(_fifaDuration);
936
+ DefifaTierParams[] memory tierParams = new DefifaTierParams[](1);
937
+ tierParams[0] = DefifaTierParams({
938
+ reservedRate: 1001,
939
+ reservedTokenBeneficiary: address(0),
940
+ encodedIPFSUri: bytes32(0), // this way we dont need more tokenUris
941
+ shouldUseReservedTokenBeneficiaryAsDefault: false,
942
+ name: "DEFIFA"
943
+ });
944
+
945
+ DefifaLaunchProjectData memory _launchData = DefifaLaunchProjectData({
946
+ name: "DEFIFA",
947
+ projectUri: "",
948
+ contractUri: "",
949
+ baseUri: "",
950
+ tierPrice: 1 ether,
951
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
952
+ mintPeriodDuration: _mintPeriodDuration,
953
+ start: _launchProjectAt + uint48(_mintPeriodDuration) + _inBetweenMintAndFifa,
954
+ refundPeriodDuration: _inBetweenMintAndFifa,
955
+ store: new JB721TiersHookStore(),
956
+ splits: new JBSplit[](0),
957
+ attestationStartTime: 0,
958
+ attestationGracePeriod: 100_381,
959
+ defaultAttestationDelegate: address(0),
960
+ tiers: tierParams,
961
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
962
+ terminal: jbMultiTerminal(),
963
+ minParticipation: 0,
964
+ scorecardTimeout: 0
965
+ });
966
+ (uint256 _projectId, DefifaHook _nft,) = createDefifaProject(_launchData);
967
+ // Wait until the phase 1 start
968
+ vm.warp(_launchProjectAt);
969
+ // Get the hook
970
+ _nft = DefifaHook(jbRulesets().currentOf(_projectId).dataHook());
971
+ // We should be in the minting phase now
972
+ assertEq(jbRulesets().currentOf(_projectId).cycleNumber, 1);
973
+ // Queue Phase 2
974
+ //deployer.queueNextPhaseOf(_projectId);
975
+ // Go to the end of the minting phase and check if we are still in the minting phase
976
+ vm.warp(_launchProjectAt + _mintPeriodDuration - 1);
977
+ assertEq(jbRulesets().currentOf(_projectId).cycleNumber, 1);
978
+ // We should now be in phase 2, minting is paused and the treasury is frozen
979
+ vm.warp(_launchProjectAt + _mintPeriodDuration);
980
+ assertEq(jbRulesets().currentOf(_projectId).cycleNumber, 2);
981
+ // Queue Phase 3
982
+
983
+ //deployer.queueNextPhaseOf(_projectId);
984
+ // We should now be in phase 4, game has ended
985
+ vm.warp(_launchProjectAt + _mintPeriodDuration + _inBetweenMintAndFifa + _fifaDuration);
986
+ assertEq(jbRulesets().currentOf(_projectId).cycleNumber, 3);
987
+ }
988
+
989
+ function testWhenScorecardIsSubmittedWithUnmintedTier() public {
990
+ uint8 nTiers = 10;
991
+ address[] memory _users = new address[](nTiers);
992
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
993
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
994
+ // Phase 1: Mint
995
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
996
+ //deployer.queueNextPhaseOf(_projectId);
997
+
998
+ // Warp to scoring phase (past start time)
999
+ vm.warp(defifaData.start + 1);
1000
+ // Generate the scorecards
1001
+ DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
1002
+ // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
1003
+ for (uint256 i = 0; i < scorecards.length; i++) {
1004
+ scorecards[i].id = i + 1;
1005
+ scorecards[i].cashOutWeight = i % 2 == 0 ? 1_000_000_000 / (scorecards.length / 2) : 0;
1006
+ }
1007
+
1008
+ vm.expectRevert(abi.encodeWithSignature("DefifaGovernor_UnownedProposedCashoutValue()"));
1009
+ // Forward time so proposals can be created
1010
+ uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
1011
+ }
1012
+
1013
+ // function testWhenPhaseIsAlreadyQueued() public {
1014
+ // uint8 nTiers = 10;
1015
+ // address[] memory _users = new address[](nTiers);
1016
+ // DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
1017
+ // (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
1018
+ // // Phase 1: Mint
1019
+ // vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
1020
+ // //deployer.queueNextPhaseOf(_projectId);
1021
+ // for (uint256 i = 0; i < nTiers; i++) {
1022
+ // // Generate a new address for each tier
1023
+ // _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
1024
+ // // fund user
1025
+ // vm.deal(_users[i], 1 ether);
1026
+ // // Build metadata to buy specific NFT
1027
+ // uint16[] memory rawMetadata = new uint16[](1);
1028
+ // rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
1029
+ // bytes memory metadata =
1030
+ // _buildPayMetadata(abi.encode(_users[i], rawMetadata);
1031
+ // // Pay to the project and mint an NFT
1032
+ // vm.prank(_users[i]);
1033
+ // jbMultiTerminal().pay{value: 1 ether}(_projectId, JBConstants.NATIVE_TOKEN, 1 ether,_users[i], 0, "",
1034
+ // metadata); // Set the delegate as the user themselves
1035
+ // DefifaDelegation[] memory tiered721SetDelegatesData =
1036
+ // new DefifaDelegation[](1);
1037
+ // tiered721SetDelegatesData[0] =
1038
+ // DefifaDelegation({delegatee: _users[i], tierId: uint256(i + 1)});
1039
+ // vm.prank(_users[i]);
1040
+ // _nft.setTierDelegatesTo(tiered721SetDelegatesData);
1041
+ // // Forward 1 block, user should receive all the voting power of the tier, as its the only NFT
1042
+ // vm.roll(block.number + 1);
1043
+ // assertEq(_governor.MAX_ATTESTATION_POWER_TIER(), _governor.getAttestationWeight(_gameId, _users[i],
1044
+ // block.number - 1)); }
1045
+ // // Phase 2: Redeem
1046
+ // vm.warp(block.timestamp + defifaData.mintPeriodDuration);
1047
+ // //deployer.queueNextPhaseOf(_projectId);
1048
+ // // Right at the end of Phase 2
1049
+ // vm.warp(defifaData.start - 1);
1050
+ // vm.expectRevert(abi.encodeWithSignature("PHASE_ALREADY_QUEUED()"));
1051
+ // //deployer.queueNextPhaseOf(_projectId);
1052
+ // }
1053
+
1054
+ function testSettingTierCashOutWeightBeforeEndPhase() public {
1055
+ uint8 nTiers = 10;
1056
+ address[] memory _users = new address[](nTiers);
1057
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
1058
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
1059
+ // Phase 1: Mint
1060
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
1061
+ //deployer.queueNextPhaseOf(_projectId);
1062
+ for (uint256 i = 0; i < nTiers; i++) {
1063
+ // Generate a new address for each tier
1064
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
1065
+ // fund user
1066
+ vm.deal(_users[i], 1 ether);
1067
+ // Build metadata to buy specific NFT
1068
+ uint16[] memory rawMetadata = new uint16[](1);
1069
+ rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
1070
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
1071
+ // Pay to the project and mint an NFT
1072
+ vm.prank(_users[i]);
1073
+ jbMultiTerminal().pay{value: 1 ether}(
1074
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
1075
+ );
1076
+ // Set the delegate as the user themselves
1077
+ DefifaDelegation[] memory tiered721SetDelegatesData = new DefifaDelegation[](1);
1078
+ tiered721SetDelegatesData[0] = DefifaDelegation({delegatee: _users[i], tierId: uint256(i + 1)});
1079
+ vm.prank(_users[i]);
1080
+ _nft.setTierDelegatesTo(tiered721SetDelegatesData);
1081
+ // Forward 1 block, user should receive all the voting power of the tier, as its the only NFT
1082
+ vm.warp(_tsReader.timestamp() + 1);
1083
+ assertEq(
1084
+ _governor.MAX_ATTESTATION_POWER_TIER(),
1085
+ _governor.getAttestationWeight(_gameId, _users[i], uint48(_tsReader.timestamp()))
1086
+ );
1087
+ }
1088
+ // Warp to scoring phase (past start time)
1089
+ vm.warp(defifaData.start + 1);
1090
+ // Generate the scorecards
1091
+ DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
1092
+ // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
1093
+ for (uint256 i = 0; i < scorecards.length; i++) {
1094
+ scorecards[i].id = i + 1;
1095
+ scorecards[i].cashOutWeight = i % 2 == 0 ? 1_000_000_000 / (scorecards.length / 2) : 0;
1096
+ }
1097
+ // Forward time so proposals can be created
1098
+ uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
1099
+ // Forward time so voting becomes active
1100
+ vm.warp(_tsReader.timestamp() + _governor.attestationStartTimeOf(_gameId));
1101
+ // All the users vote
1102
+ for (uint256 i = 0; i < _users.length; i++) {
1103
+ vm.prank(_users[i]);
1104
+ _governor.attestToScorecardFrom(_gameId, _proposalId);
1105
+ }
1106
+ // Execute the proposal — should fail because grace period hasn't ended
1107
+ vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
1108
+ _governor.ratifyScorecardFrom(_gameId, scorecards);
1109
+ }
1110
+
1111
+ function testWhenCashOutWeightisMoreThanMaxCashOutWeight(uint8 nTiers) public {
1112
+ // Anything above 10 should cause the error we are looking for.
1113
+ // As a sanity check we let it also run for less than 10 to see if it does not error in that case.
1114
+ nTiers = uint8(bound(nTiers, 2, 20));
1115
+
1116
+ address[] memory _users = new address[](nTiers);
1117
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
1118
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
1119
+
1120
+ uint256 cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 10;
1121
+
1122
+ // Phase 1: Mint
1123
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
1124
+ //deployer.queueNextPhaseOf(_projectId);
1125
+ for (uint256 i = 0; i < nTiers; i++) {
1126
+ // Generate a new address for each tier
1127
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
1128
+ // fund user
1129
+ vm.deal(_users[i], 1 ether);
1130
+ // Build metadata to buy specific NFT
1131
+ uint16[] memory rawMetadata = new uint16[](1);
1132
+ rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
1133
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
1134
+ // Pay to the project and mint an NFT
1135
+ vm.prank(_users[i]);
1136
+ jbMultiTerminal().pay{value: 1 ether}(
1137
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
1138
+ );
1139
+ // Set the delegate as the user themselves
1140
+ DefifaDelegation[] memory tiered721SetDelegatesData = new DefifaDelegation[](1);
1141
+ tiered721SetDelegatesData[0] = DefifaDelegation({delegatee: _users[i], tierId: uint256(i + 1)});
1142
+ vm.prank(_users[i]);
1143
+ _nft.setTierDelegatesTo(tiered721SetDelegatesData);
1144
+ // Forward 1 block, user should receive all the voting power of the tier, as its the only NFT
1145
+ assertEq(
1146
+ _governor.MAX_ATTESTATION_POWER_TIER(),
1147
+ _governor.getAttestationWeight(_gameId, _users[i], uint48(block.timestamp))
1148
+ );
1149
+ }
1150
+ // Warp to scoring phase (past start time)
1151
+ vm.warp(defifaData.start + 1);
1152
+
1153
+ // Generate the scorecards
1154
+ DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
1155
+
1156
+ // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
1157
+ for (uint256 i = 0; i < scorecards.length; i++) {
1158
+ scorecards[i].id = i + 1;
1159
+ scorecards[i].cashOutWeight = cashOutWeight;
1160
+ }
1161
+
1162
+ // Forward time so proposals can be created
1163
+ uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
1164
+ // Forward time so voting becomes active
1165
+ vm.warp(block.timestamp + _governor.attestationStartTimeOf(_gameId));
1166
+ // No voting delay after the initial voting delay has passed in
1167
+ // assertEq(_governor.attestationStartTimeOf(_gameId), 0);
1168
+ // All the users vote
1169
+ // 0 = Against
1170
+ // 1 = For
1171
+ // 2 = Abstain
1172
+ for (uint256 i = 0; i < _users.length; i++) {
1173
+ vm.prank(_users[i]);
1174
+ _governor.attestToScorecardFrom(_gameId, _proposalId);
1175
+ }
1176
+
1177
+ // Forward the amount of blocks needed to reach the end (and round up)
1178
+ vm.warp(block.timestamp + _governor.attestationGracePeriodOf(_gameId) + 1);
1179
+
1180
+ // With exact-weight validation, only nTiers == 10 produces an exact sum.
1181
+ // Any other count (under or over) triggers INVALID_CASHOUT_WEIGHTS.
1182
+ if (nTiers != 10) {
1183
+ vm.expectRevert(DefifaHook.DefifaHook_InvalidCashoutWeights.selector);
1184
+ }
1185
+
1186
+ // Execute the proposal
1187
+ _governor.ratifyScorecardFrom(_gameId, scorecards);
1188
+ }
1189
+
1190
+ function getBasicDefifaLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
1191
+ DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
1192
+ for (uint256 i = 0; i < nTiers; i++) {
1193
+ tierParams[i] = DefifaTierParams({
1194
+ reservedRate: 1001,
1195
+ reservedTokenBeneficiary: address(0),
1196
+ encodedIPFSUri: bytes32(0), // this way we dont need more tokenUris
1197
+ shouldUseReservedTokenBeneficiaryAsDefault: false,
1198
+ name: "DEFIFA"
1199
+ });
1200
+ }
1201
+
1202
+ return DefifaLaunchProjectData({
1203
+ name: "DEFIFA",
1204
+ projectUri: "",
1205
+ contractUri: "",
1206
+ baseUri: "",
1207
+ tierPrice: 1 ether,
1208
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
1209
+ mintPeriodDuration: 1 days,
1210
+ start: uint48(block.timestamp + 3 days),
1211
+ refundPeriodDuration: 1 days,
1212
+ store: new JB721TiersHookStore(),
1213
+ splits: new JBSplit[](0),
1214
+ attestationStartTime: 0,
1215
+ attestationGracePeriod: 100_381,
1216
+ defaultAttestationDelegate: address(0),
1217
+ tiers: tierParams,
1218
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
1219
+ terminal: jbMultiTerminal(),
1220
+ minParticipation: 0,
1221
+ scorecardTimeout: 0
1222
+ });
1223
+ }
1224
+
1225
+ // ----- internal helpers ------
1226
+ function createDefifaProject(DefifaLaunchProjectData memory defifaLaunchData)
1227
+ internal
1228
+ returns (uint256 projectId, DefifaHook nft, DefifaGovernor _governor)
1229
+ {
1230
+ _governor = governor;
1231
+ (projectId) = deployer.launchGameWith(defifaLaunchData);
1232
+ // Get a reference to the latest configured funding cycle's data hook, which should be the hook that was
1233
+ // deployed and attached to the project.
1234
+ JBRuleset memory _fc = jbRulesets().currentOf(projectId);
1235
+ if (_fc.dataHook() == address(0)) {
1236
+ (_fc,) = jbRulesets().latestQueuedOf(projectId);
1237
+ }
1238
+ nft = DefifaHook(_fc.dataHook());
1239
+ }
1240
+
1241
+ function mintAndRefund(DefifaHook _hook, uint256 _projectId, uint256 _tierId) internal {
1242
+ JB721Tier memory _tier = _hook.store().tierOf(address(_hook), _tierId, false);
1243
+ uint256 _cost = _tier.price;
1244
+ address _refundUser = address(bytes20(keccak256("refund_user")));
1245
+ // The user should have no balance
1246
+ assertEq(_hook.balanceOf(_refundUser), 0);
1247
+ // Build metadata to buy specific NFT
1248
+ uint16[] memory rawMetadata = new uint16[](1);
1249
+ rawMetadata[0] = uint16(_tierId); // reward tier, 1 indexed
1250
+ bytes memory metadata = _buildPayMetadata(abi.encode(_refundUser, rawMetadata));
1251
+ // Pay to the project and mint an NFT
1252
+ vm.deal(_refundUser, _cost);
1253
+ vm.prank(_refundUser);
1254
+ jbMultiTerminal().pay{value: _cost}(_projectId, JBConstants.NATIVE_TOKEN, _cost, _refundUser, 0, "", metadata);
1255
+ // User should no longer have any funds
1256
+ assertEq(_refundUser.balance, 0);
1257
+ // The user should have have a token
1258
+ assertEq(_hook.balanceOf(_refundUser), 1);
1259
+ uint256 _numberBurned = _hook.store().numberOfBurnedFor(address(_hook), _tierId);
1260
+ // Craft the metadata: redeem the tokenId
1261
+ bytes memory cashOutMetadata;
1262
+ {
1263
+ uint256[] memory cashOutId = new uint256[](1);
1264
+ cashOutId[0] = _generateTokenId(_tierId, _tier.initialSupply - --_tier.remainingSupply);
1265
+ cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutId));
1266
+ }
1267
+ vm.prank(_refundUser);
1268
+ JBMultiTerminal(address(jbMultiTerminal()))
1269
+ .cashOutTokensOf({
1270
+ holder: _refundUser,
1271
+ projectId: _projectId,
1272
+ cashOutCount: 0,
1273
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
1274
+ minTokensReclaimed: 0,
1275
+ beneficiary: payable(_refundUser),
1276
+ metadata: cashOutMetadata
1277
+ });
1278
+ // User should have their original funds again
1279
+ assertEq(_refundUser.balance, _cost);
1280
+ // User should no longer have the NFT
1281
+ assertEq(_hook.balanceOf(_refundUser), 0);
1282
+ }
1283
+
1284
+ // Create launchProjectFor(..) payload
1285
+ string name = "NAME";
1286
+ string symbol = "SYM";
1287
+ string baseUri = "http://www.null.com/";
1288
+ string contractUri = "ipfs://null";
1289
+ address reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
1290
+ //QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz
1291
+ bytes32[] tokenUris = [
1292
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
1293
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
1294
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
1295
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
1296
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
1297
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
1298
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
1299
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
1300
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
1301
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89)
1302
+ ];
1303
+
1304
+ function _generateTokenId(uint256 _tierId, uint256 _tokenNumber) internal pure returns (uint256) {
1305
+ return (_tierId * 1_000_000_000) + _tokenNumber;
1306
+ }
1307
+
1308
+ function _buildPayMetadata(bytes memory metadata) internal returns (bytes memory) {
1309
+ // Build the metadata using the tiers to mint and the overspending flag.
1310
+ bytes[] memory data = new bytes[](1);
1311
+ data[0] = metadata;
1312
+
1313
+ // Pass the hook ID.
1314
+ bytes4[] memory ids = new bytes4[](1);
1315
+ ids[0] = metadataHelper().getId("pay", address(hook));
1316
+
1317
+ // Generate the metadata.
1318
+ return metadataHelper().createMetadata(ids, data);
1319
+ }
1320
+
1321
+ function _buildCashOutMetadata(bytes memory metadata) internal returns (bytes memory) {
1322
+ // Build the metadata using the tiers to mint and the overspending flag.
1323
+ bytes[] memory data = new bytes[](1);
1324
+ data[0] = metadata;
1325
+
1326
+ // Pass the hook ID.
1327
+ bytes4[] memory ids = new bytes4[](1);
1328
+ ids[0] = metadataHelper().getId("cashOut", address(hook));
1329
+
1330
+ // Generate the metadata.
1331
+ return metadataHelper().createMetadata(ids, data);
1332
+ }
1333
+ }