@ballkidz/defifa 0.0.25 → 0.0.27
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/AUDIT_INSTRUCTIONS.md +6 -2
- package/README.md +11 -2
- package/RISKS.md +3 -1
- package/STYLE_GUIDE.md +14 -11
- package/package.json +31 -14
- package/script/Deploy.s.sol +4 -1
- package/src/DefifaDeployer.sol +79 -47
- package/src/DefifaGovernor.sol +57 -12
- package/src/DefifaHook.sol +83 -26
- package/src/DefifaProjectOwner.sol +4 -3
- package/src/DefifaTokenUriResolver.sol +113 -20
- package/src/enums/DefifaGamePhase.sol +6 -0
- package/src/enums/DefifaScorecardState.sol +4 -0
- package/src/interfaces/IDefifaDeployer.sol +5 -0
- package/src/interfaces/IDefifaGamePhaseReporter.sol +4 -0
- package/src/interfaces/IDefifaGamePotReporter.sol +10 -0
- package/src/interfaces/IDefifaGovernor.sol +4 -0
- package/src/interfaces/IDefifaHook.sol +5 -0
- package/src/interfaces/IDefifaTokenUriResolver.sol +3 -0
- package/src/libraries/DefifaFontImporter.sol +1 -1
- package/src/libraries/DefifaHookLib.sol +9 -10
- package/src/structs/DefifaAttestations.sol +3 -2
- package/src/structs/DefifaDelegation.sol +1 -0
- package/src/structs/DefifaLaunchProjectData.sol +2 -3
- package/src/structs/DefifaOpsData.sol +1 -0
- package/src/structs/DefifaScorecard.sol +2 -0
- package/src/structs/DefifaTierCashOutWeight.sol +3 -1
- package/src/structs/DefifaTierParams.sol +1 -0
- package/CRYPTO_ECON.pdf +0 -0
- package/CRYPTO_ECON.tex +0 -997
- package/foundry.lock +0 -17
- package/references/operations.md +0 -32
- package/references/runtime.md +0 -43
- package/slither-ci.config.json +0 -10
- package/sphinx.lock +0 -521
- package/test/BWAFunctionComparison.t.sol +0 -1320
- package/test/DefifaAdversarialQuorum.t.sol +0 -617
- package/test/DefifaAuditLowGuards.t.sol +0 -308
- package/test/DefifaFeeAccounting.t.sol +0 -581
- package/test/DefifaGovernanceHardening.t.sol +0 -1315
- package/test/DefifaGovernor.t.sol +0 -1378
- package/test/DefifaHookRegressions.t.sol +0 -415
- package/test/DefifaMintCostInvariant.t.sol +0 -319
- package/test/DefifaNoContest.t.sol +0 -941
- package/test/DefifaSecurity.t.sol +0 -741
- package/test/DefifaUSDC.t.sol +0 -480
- package/test/Fork.t.sol +0 -2388
- package/test/TestAuditGaps.sol +0 -984
- package/test/TestQALastMile.t.sol +0 -514
- package/test/audit/AttestationDoubleCount.t.sol +0 -218
- package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +0 -112
- package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +0 -238
- package/test/audit/CodexNemesisOneTierZeroTimeoutLockVerified.t.sol +0 -218
- package/test/audit/CodexNemesisSingleTierTimeoutLock.t.sol +0 -237
- package/test/audit/CodexRegistryMismatch.t.sol +0 -191
- package/test/audit/CodexTierCapMismatch.t.sol +0 -171
- package/test/audit/CurrencyMismatchFix.t.sol +0 -265
- package/test/audit/FixPendingReserveDilution.t.sol +0 -366
- package/test/audit/H5TierCapValidation.t.sol +0 -184
- package/test/audit/PendingReserveDilution.t.sol +0 -298
- package/test/audit/PendingReserveQuorumGrief.t.sol +0 -355
- package/test/audit/PendingReserveSnapshotBypass.t.sol +0 -319
- package/test/regression/AttestationDelegateBeneficiary.t.sol +0 -271
- package/test/regression/FulfillmentBlocksRatification.t.sol +0 -279
- package/test/regression/GracePeriodBypass.t.sol +0 -302
|
@@ -1,941 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {DefifaGovernor} from "../src/DefifaGovernor.sol";
|
|
5
|
-
import {DefifaDeployer} from "../src/DefifaDeployer.sol";
|
|
6
|
-
import {DefifaHook} from "../src/DefifaHook.sol";
|
|
7
|
-
import {DefifaTokenUriResolver} from "../src/DefifaTokenUriResolver.sol";
|
|
8
|
-
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
9
|
-
|
|
10
|
-
import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
11
|
-
import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
|
|
12
|
-
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
13
|
-
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
14
|
-
|
|
15
|
-
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
16
|
-
import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
|
|
17
|
-
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
18
|
-
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
19
|
-
import {DefifaDelegation} from "../src/structs/DefifaDelegation.sol";
|
|
20
|
-
import {DefifaLaunchProjectData} from "../src/structs/DefifaLaunchProjectData.sol";
|
|
21
|
-
import {DefifaTierParams} from "../src/structs/DefifaTierParams.sol";
|
|
22
|
-
import {DefifaTierCashOutWeight} from "../src/structs/DefifaTierCashOutWeight.sol";
|
|
23
|
-
import {DefifaGamePhase} from "../src/enums/DefifaGamePhase.sol";
|
|
24
|
-
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
25
|
-
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
26
|
-
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
27
|
-
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
28
|
-
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
29
|
-
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
30
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
31
|
-
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
32
|
-
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
33
|
-
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
34
|
-
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
35
|
-
import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
|
|
36
|
-
|
|
37
|
-
/// @title DefifaNoContestTest
|
|
38
|
-
/// @notice Tests for the NO_CONTEST safety mechanisms: minParticipation threshold and scorecardTimeout.
|
|
39
|
-
contract DefifaNoContestTest is JBTest, TestBaseWorkflow {
|
|
40
|
-
using JBRulesetMetadataResolver for JBRuleset;
|
|
41
|
-
|
|
42
|
-
address _protocolFeeProjectTokenAccount;
|
|
43
|
-
address _defifaProjectTokenAccount;
|
|
44
|
-
uint256 _protocolFeeProjectId;
|
|
45
|
-
uint256 _defifaProjectId;
|
|
46
|
-
uint256 _gameId = 3;
|
|
47
|
-
|
|
48
|
-
DefifaDeployer deployer;
|
|
49
|
-
DefifaHook hook;
|
|
50
|
-
DefifaGovernor governor;
|
|
51
|
-
address projectOwner = address(bytes20(keccak256("projectOwner")));
|
|
52
|
-
|
|
53
|
-
// Shared test state
|
|
54
|
-
uint256 _pid;
|
|
55
|
-
DefifaHook _nft;
|
|
56
|
-
DefifaGovernor _gov;
|
|
57
|
-
address[] _users;
|
|
58
|
-
|
|
59
|
-
function setUp() public virtual override {
|
|
60
|
-
super.setUp();
|
|
61
|
-
|
|
62
|
-
JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
|
|
63
|
-
_tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
|
|
64
|
-
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
65
|
-
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
|
|
66
|
-
JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
|
|
67
|
-
rc[0] = JBRulesetConfig({
|
|
68
|
-
mustStartAtOrAfter: 0,
|
|
69
|
-
duration: 10 days,
|
|
70
|
-
weight: 1e18,
|
|
71
|
-
weightCutPercent: 0,
|
|
72
|
-
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
73
|
-
metadata: JBRulesetMetadata({
|
|
74
|
-
reservedPercent: 0,
|
|
75
|
-
cashOutTaxRate: 0,
|
|
76
|
-
baseCurrency: JBCurrencyIds.ETH,
|
|
77
|
-
pausePay: false,
|
|
78
|
-
pauseCreditTransfers: false,
|
|
79
|
-
allowOwnerMinting: false,
|
|
80
|
-
allowSetCustomToken: false,
|
|
81
|
-
allowTerminalMigration: false,
|
|
82
|
-
allowSetTerminals: false,
|
|
83
|
-
allowSetController: false,
|
|
84
|
-
allowAddAccountingContext: false,
|
|
85
|
-
allowAddPriceFeed: false,
|
|
86
|
-
ownerMustSendPayouts: false,
|
|
87
|
-
holdFees: false,
|
|
88
|
-
useTotalSurplusForCashOuts: false,
|
|
89
|
-
useDataHookForPay: true,
|
|
90
|
-
useDataHookForCashOut: true,
|
|
91
|
-
dataHook: address(0),
|
|
92
|
-
metadata: 0
|
|
93
|
-
}),
|
|
94
|
-
splitGroups: new JBSplitGroup[](0),
|
|
95
|
-
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
_protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
99
|
-
vm.prank(projectOwner);
|
|
100
|
-
_protocolFeeProjectTokenAccount =
|
|
101
|
-
address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
|
|
102
|
-
_defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
103
|
-
vm.prank(projectOwner);
|
|
104
|
-
_defifaProjectTokenAccount =
|
|
105
|
-
address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
|
|
106
|
-
|
|
107
|
-
hook =
|
|
108
|
-
new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
|
|
109
|
-
governor = new DefifaGovernor(jbController(), address(this));
|
|
110
|
-
deployer = new DefifaDeployer(
|
|
111
|
-
address(hook),
|
|
112
|
-
new DefifaTokenUriResolver(ITypeface(address(0))),
|
|
113
|
-
governor,
|
|
114
|
-
jbController(),
|
|
115
|
-
new JBAddressRegistry(),
|
|
116
|
-
_protocolFeeProjectId,
|
|
117
|
-
_defifaProjectId
|
|
118
|
-
);
|
|
119
|
-
hook.transferOwnership(address(deployer));
|
|
120
|
-
governor.transferOwnership(address(deployer));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// =========================================================================
|
|
124
|
-
// MIN PARTICIPATION THRESHOLD
|
|
125
|
-
// =========================================================================
|
|
126
|
-
|
|
127
|
-
/// @notice Game with balance below minParticipation returns NO_CONTEST.
|
|
128
|
-
function testMinParticipation_belowThreshold_noContest() external {
|
|
129
|
-
// Set threshold to 5 ETH, but only mint 1 ETH total
|
|
130
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 5 ether, 0);
|
|
131
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
132
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
133
|
-
|
|
134
|
-
// Mint 1 token at 1 ETH — pot = 1 ETH < 5 ETH threshold
|
|
135
|
-
_users = new address[](1);
|
|
136
|
-
_users[0] = _addr(0);
|
|
137
|
-
_mint(_users[0], 1, 1 ether);
|
|
138
|
-
|
|
139
|
-
// Advance to scoring phase
|
|
140
|
-
_toScoring();
|
|
141
|
-
|
|
142
|
-
// Should be NO_CONTEST, not SCORING
|
|
143
|
-
assertEq(
|
|
144
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
145
|
-
uint256(DefifaGamePhase.NO_CONTEST),
|
|
146
|
-
"phase should be NO_CONTEST"
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/// @notice Game with balance at or above minParticipation proceeds to SCORING.
|
|
151
|
-
function testMinParticipation_atThreshold_scoring() external {
|
|
152
|
-
// Set threshold to 4 ETH, mint exactly 4 ETH
|
|
153
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 4 ether, 0);
|
|
154
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
155
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
156
|
-
|
|
157
|
-
_users = new address[](4);
|
|
158
|
-
for (uint256 i; i < 4; i++) {
|
|
159
|
-
_users[i] = _addr(i);
|
|
160
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
161
|
-
_delegateSelf(_users[i], i + 1);
|
|
162
|
-
vm.warp(block.timestamp + 1);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
_toScoring();
|
|
166
|
-
|
|
167
|
-
// Balance = 4 ETH >= 4 ETH threshold → SCORING
|
|
168
|
-
assertEq(
|
|
169
|
-
uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING), "phase should be SCORING"
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/// @notice Cash-out during NO_CONTEST (from threshold) returns mint price after triggering.
|
|
174
|
-
function testMinParticipation_cashOutReturnsMintPrice() external {
|
|
175
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 10 ether, 0);
|
|
176
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
177
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
178
|
-
|
|
179
|
-
_users = new address[](2);
|
|
180
|
-
_users[0] = _addr(0);
|
|
181
|
-
_users[1] = _addr(1);
|
|
182
|
-
_mint(_users[0], 1, 1 ether);
|
|
183
|
-
_mint(_users[1], 2, 1 ether);
|
|
184
|
-
|
|
185
|
-
_toScoring();
|
|
186
|
-
|
|
187
|
-
// Confirm NO_CONTEST
|
|
188
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
189
|
-
|
|
190
|
-
// Trigger no-contest to queue a ruleset without payout limits (anyone can call this)
|
|
191
|
-
deployer.triggerNoContestFor(_pid);
|
|
192
|
-
|
|
193
|
-
// Still NO_CONTEST after trigger
|
|
194
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
195
|
-
|
|
196
|
-
// Cash out should return exactly 1 ETH (mint price)
|
|
197
|
-
uint256 balBefore = _users[0].balance;
|
|
198
|
-
_refund(_users[0], 1);
|
|
199
|
-
uint256 received = _users[0].balance - balBefore;
|
|
200
|
-
assertEq(received, 1 ether, "should receive exact mint price");
|
|
201
|
-
assertEq(_nft.balanceOf(_users[0]), 0, "NFT should be burned");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/// @notice Refunds during REFUND phase can push balance below threshold, triggering NO_CONTEST when SCORING starts.
|
|
205
|
-
function testMinParticipation_refundPushesBelow() external {
|
|
206
|
-
// 4 tiers at 1 ETH, threshold 3 ETH
|
|
207
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 3 ether, 0);
|
|
208
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
209
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
210
|
-
|
|
211
|
-
_users = new address[](4);
|
|
212
|
-
for (uint256 i; i < 4; i++) {
|
|
213
|
-
_users[i] = _addr(i);
|
|
214
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
215
|
-
_delegateSelf(_users[i], i + 1);
|
|
216
|
-
vm.warp(block.timestamp + 1);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// During MINT, pot = 4 ETH > 3 ETH threshold
|
|
220
|
-
// Refund 2 users during MINT phase (balance drops to 2 ETH)
|
|
221
|
-
_refund(_users[2], 3);
|
|
222
|
-
_refund(_users[3], 4);
|
|
223
|
-
|
|
224
|
-
_toScoring();
|
|
225
|
-
|
|
226
|
-
// Now balance = 2 ETH < 3 ETH threshold → NO_CONTEST
|
|
227
|
-
assertEq(
|
|
228
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
229
|
-
uint256(DefifaGamePhase.NO_CONTEST),
|
|
230
|
-
"refunds push below threshold"
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/// @notice minParticipation = 0 means the check is disabled.
|
|
235
|
-
function testMinParticipation_zeroDisabled() external {
|
|
236
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
|
|
237
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
238
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
239
|
-
|
|
240
|
-
// Mint only 1 token (very low participation)
|
|
241
|
-
_users = new address[](1);
|
|
242
|
-
_users[0] = _addr(0);
|
|
243
|
-
_mint(_users[0], 1, 1 ether);
|
|
244
|
-
_delegateSelf(_users[0], 1);
|
|
245
|
-
|
|
246
|
-
_toScoring();
|
|
247
|
-
|
|
248
|
-
// With threshold = 0, game proceeds to SCORING regardless
|
|
249
|
-
assertEq(
|
|
250
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
251
|
-
uint256(DefifaGamePhase.SCORING),
|
|
252
|
-
"should be SCORING when threshold disabled"
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// =========================================================================
|
|
257
|
-
// SCORECARD TIMEOUT
|
|
258
|
-
// =========================================================================
|
|
259
|
-
|
|
260
|
-
/// @notice Game enters NO_CONTEST after scorecardTimeout elapses without ratification.
|
|
261
|
-
function testScorecardTimeout_elapsed_noContest() external {
|
|
262
|
-
// 30-day timeout
|
|
263
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 30 days);
|
|
264
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
265
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
266
|
-
|
|
267
|
-
_users = new address[](4);
|
|
268
|
-
for (uint256 i; i < 4; i++) {
|
|
269
|
-
_users[i] = _addr(i);
|
|
270
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
271
|
-
_delegateSelf(_users[i], i + 1);
|
|
272
|
-
vm.warp(block.timestamp + 1);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
_toScoring();
|
|
276
|
-
|
|
277
|
-
// Still within timeout → SCORING
|
|
278
|
-
assertEq(
|
|
279
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
280
|
-
uint256(DefifaGamePhase.SCORING),
|
|
281
|
-
"should be SCORING before timeout"
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
// Warp past the timeout
|
|
285
|
-
vm.warp(block.timestamp + 30 days + 1);
|
|
286
|
-
|
|
287
|
-
// Now NO_CONTEST
|
|
288
|
-
assertEq(
|
|
289
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
290
|
-
uint256(DefifaGamePhase.NO_CONTEST),
|
|
291
|
-
"should be NO_CONTEST after timeout"
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/// @notice Game at exactly the timeout boundary is still SCORING.
|
|
296
|
-
function testScorecardTimeout_exactBoundary_scoring() external {
|
|
297
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 30 days);
|
|
298
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
299
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
300
|
-
|
|
301
|
-
_users = new address[](4);
|
|
302
|
-
for (uint256 i; i < 4; i++) {
|
|
303
|
-
_users[i] = _addr(i);
|
|
304
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
305
|
-
_delegateSelf(_users[i], i + 1);
|
|
306
|
-
vm.warp(block.timestamp + 1);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Advance exactly to scoring start
|
|
310
|
-
vm.warp(d.start);
|
|
311
|
-
|
|
312
|
-
// At exactly scoringStart + timeout, block.timestamp == start + timeout, so NOT >
|
|
313
|
-
vm.warp(d.start + 30 days);
|
|
314
|
-
assertEq(
|
|
315
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
316
|
-
uint256(DefifaGamePhase.SCORING),
|
|
317
|
-
"should be SCORING at exact boundary"
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
// One second later → NO_CONTEST
|
|
321
|
-
vm.warp(d.start + 30 days + 1);
|
|
322
|
-
assertEq(
|
|
323
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
324
|
-
uint256(DefifaGamePhase.NO_CONTEST),
|
|
325
|
-
"should be NO_CONTEST one second after"
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/// @notice Cash-out during NO_CONTEST (from timeout) returns mint price after triggering.
|
|
330
|
-
function testScorecardTimeout_cashOutReturnsMintPrice() external {
|
|
331
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
|
|
332
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
333
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
334
|
-
|
|
335
|
-
_users = new address[](4);
|
|
336
|
-
for (uint256 i; i < 4; i++) {
|
|
337
|
-
_users[i] = _addr(i);
|
|
338
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
339
|
-
_delegateSelf(_users[i], i + 1);
|
|
340
|
-
vm.warp(block.timestamp + 1);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Warp past scoring start + timeout
|
|
344
|
-
vm.warp(d.start + 7 days + 1);
|
|
345
|
-
|
|
346
|
-
// Confirm NO_CONTEST
|
|
347
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
348
|
-
|
|
349
|
-
// Trigger no-contest to unlock refunds
|
|
350
|
-
deployer.triggerNoContestFor(_pid);
|
|
351
|
-
|
|
352
|
-
// Cash out all users — each should get 1 ETH back
|
|
353
|
-
for (uint256 i; i < 4; i++) {
|
|
354
|
-
uint256 balBefore = _users[i].balance;
|
|
355
|
-
_refund(_users[i], i + 1);
|
|
356
|
-
uint256 received = _users[i].balance - balBefore;
|
|
357
|
-
assertEq(received, 1 ether, "should receive exact mint price");
|
|
358
|
-
assertEq(_nft.balanceOf(_users[i]), 0, "NFT should be burned");
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/// @notice scorecardTimeout = 0 means the check is disabled.
|
|
363
|
-
function testScorecardTimeout_zeroDisabled() external {
|
|
364
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
|
|
365
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
366
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
367
|
-
|
|
368
|
-
_users = new address[](4);
|
|
369
|
-
for (uint256 i; i < 4; i++) {
|
|
370
|
-
_users[i] = _addr(i);
|
|
371
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
372
|
-
_delegateSelf(_users[i], i + 1);
|
|
373
|
-
vm.warp(block.timestamp + 1);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
_toScoring();
|
|
377
|
-
|
|
378
|
-
// Warp very far forward (1 year) — should still be SCORING
|
|
379
|
-
vm.warp(block.timestamp + 365 days);
|
|
380
|
-
assertEq(
|
|
381
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
382
|
-
uint256(DefifaGamePhase.SCORING),
|
|
383
|
-
"should be SCORING forever when timeout disabled"
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// =========================================================================
|
|
388
|
-
// SCORECARD BLOCKED DURING NO_CONTEST
|
|
389
|
-
// =========================================================================
|
|
390
|
-
|
|
391
|
-
/// @notice setTierCashOutWeightsTo reverts during NO_CONTEST (requires SCORING).
|
|
392
|
-
function testNoContest_scorecardBlocked() external {
|
|
393
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
|
|
394
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
395
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
396
|
-
|
|
397
|
-
_users = new address[](4);
|
|
398
|
-
for (uint256 i; i < 4; i++) {
|
|
399
|
-
_users[i] = _addr(i);
|
|
400
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
401
|
-
_delegateSelf(_users[i], i + 1);
|
|
402
|
-
vm.warp(block.timestamp + 1);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Submit and attest to a scorecard while still in SCORING
|
|
406
|
-
_toScoring();
|
|
407
|
-
DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
|
|
408
|
-
for (uint256 i; i < 4; i++) {
|
|
409
|
-
sc[i].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 4;
|
|
410
|
-
}
|
|
411
|
-
uint256 pid = _gov.submitScorecardFor(_gameId, sc);
|
|
412
|
-
_attestAllFor(pid);
|
|
413
|
-
|
|
414
|
-
// Now warp past timeout → NO_CONTEST
|
|
415
|
-
vm.warp(d.start + 7 days + 1);
|
|
416
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
417
|
-
|
|
418
|
-
// Attempting to ratify should revert because setTierCashOutWeightsTo checks for SCORING phase
|
|
419
|
-
vm.expectRevert(DefifaHook.DefifaHook_GameIsntScoringYet.selector);
|
|
420
|
-
_gov.ratifyScorecardFrom(_gameId, sc);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// =========================================================================
|
|
424
|
-
// RATIFICATION BEFORE TIMEOUT PREVENTS NO_CONTEST
|
|
425
|
-
// =========================================================================
|
|
426
|
-
|
|
427
|
-
/// @notice If scorecard is ratified before timeout, game becomes COMPLETE (not NO_CONTEST).
|
|
428
|
-
function testScorecardTimeout_ratifiedBeforeTimeout_complete() external {
|
|
429
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 30 days);
|
|
430
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
431
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
432
|
-
|
|
433
|
-
_users = new address[](4);
|
|
434
|
-
for (uint256 i; i < 4; i++) {
|
|
435
|
-
_users[i] = _addr(i);
|
|
436
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
437
|
-
_delegateSelf(_users[i], i + 1);
|
|
438
|
-
vm.warp(block.timestamp + 1);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
_toScoring();
|
|
442
|
-
|
|
443
|
-
// Ratify a scorecard before timeout
|
|
444
|
-
DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
|
|
445
|
-
for (uint256 i; i < 4; i++) {
|
|
446
|
-
sc[i].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 4;
|
|
447
|
-
}
|
|
448
|
-
_attestAndRatify(sc);
|
|
449
|
-
|
|
450
|
-
// Should be COMPLETE, not SCORING or NO_CONTEST
|
|
451
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE), "should be COMPLETE");
|
|
452
|
-
|
|
453
|
-
// Even after timeout elapses, stays COMPLETE (cashOutWeightIsSet is checked first)
|
|
454
|
-
vm.warp(d.start + 30 days + 1);
|
|
455
|
-
assertEq(
|
|
456
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
457
|
-
uint256(DefifaGamePhase.COMPLETE),
|
|
458
|
-
"should stay COMPLETE after timeout"
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// =========================================================================
|
|
463
|
-
// BOTH MECHANISMS COMBINED
|
|
464
|
-
// =========================================================================
|
|
465
|
-
|
|
466
|
-
/// @notice When both are set, minParticipation triggers first if balance is low.
|
|
467
|
-
function testBothMechanisms_thresholdTriggersFirst() external {
|
|
468
|
-
// Threshold: 10 ETH, Timeout: 90 days — but only mint 1 ETH
|
|
469
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 10 ether, uint32(90 days));
|
|
470
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
471
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
472
|
-
|
|
473
|
-
_users = new address[](1);
|
|
474
|
-
_users[0] = _addr(0);
|
|
475
|
-
_mint(_users[0], 1, 1 ether);
|
|
476
|
-
|
|
477
|
-
_toScoring();
|
|
478
|
-
|
|
479
|
-
// Threshold triggers NO_CONTEST immediately (no need to wait for timeout)
|
|
480
|
-
assertEq(
|
|
481
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
482
|
-
uint256(DefifaGamePhase.NO_CONTEST),
|
|
483
|
-
"threshold should trigger NO_CONTEST"
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/// @notice When both set and balance is above threshold, timeout triggers eventually.
|
|
488
|
-
function testBothMechanisms_timeoutTriggersIfThresholdMet() external {
|
|
489
|
-
// Threshold: 2 ETH, Timeout: 7 days — mint 4 ETH
|
|
490
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 2 ether, uint32(7 days));
|
|
491
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
492
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
493
|
-
|
|
494
|
-
_users = new address[](4);
|
|
495
|
-
for (uint256 i; i < 4; i++) {
|
|
496
|
-
_users[i] = _addr(i);
|
|
497
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
498
|
-
_delegateSelf(_users[i], i + 1);
|
|
499
|
-
vm.warp(block.timestamp + 1);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
_toScoring();
|
|
503
|
-
|
|
504
|
-
// Balance = 4 ETH > 2 ETH threshold → SCORING
|
|
505
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING), "should be SCORING");
|
|
506
|
-
|
|
507
|
-
// After timeout → NO_CONTEST
|
|
508
|
-
vm.warp(d.start + 7 days + 1);
|
|
509
|
-
assertEq(
|
|
510
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
511
|
-
uint256(DefifaGamePhase.NO_CONTEST),
|
|
512
|
-
"timeout should trigger NO_CONTEST"
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// =========================================================================
|
|
517
|
-
// BACKWARD COMPATIBILITY
|
|
518
|
-
// =========================================================================
|
|
519
|
-
|
|
520
|
-
/// @notice Both mechanisms disabled (0) — game functions exactly as before.
|
|
521
|
-
function testBackwardCompat_noSafetyMechanisms() external {
|
|
522
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
|
|
523
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
524
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
525
|
-
|
|
526
|
-
_users = new address[](4);
|
|
527
|
-
for (uint256 i; i < 4; i++) {
|
|
528
|
-
_users[i] = _addr(i);
|
|
529
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
530
|
-
_delegateSelf(_users[i], i + 1);
|
|
531
|
-
vm.warp(block.timestamp + 1);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
_toScoring();
|
|
535
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
|
|
536
|
-
|
|
537
|
-
// Full lifecycle: submit scorecard, attest, ratify, cash out
|
|
538
|
-
DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
|
|
539
|
-
for (uint256 i; i < 4; i++) {
|
|
540
|
-
sc[i].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 4;
|
|
541
|
-
}
|
|
542
|
-
_attestAndRatify(sc);
|
|
543
|
-
|
|
544
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE));
|
|
545
|
-
|
|
546
|
-
// Cash out all users
|
|
547
|
-
uint256 totalOut;
|
|
548
|
-
for (uint256 i; i < 4; i++) {
|
|
549
|
-
uint256 bb = _users[i].balance;
|
|
550
|
-
_cashOut(_users[i], i + 1, 1);
|
|
551
|
-
totalOut += _users[i].balance - bb;
|
|
552
|
-
}
|
|
553
|
-
assertGt(totalOut, 0, "should receive ETH");
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// =========================================================================
|
|
557
|
-
// SAFETY PARAMS VIEW
|
|
558
|
-
// =========================================================================
|
|
559
|
-
|
|
560
|
-
/// @notice safetyParamsOf returns the stored parameters.
|
|
561
|
-
function testSafetyParamsOf() external {
|
|
562
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 42 ether, uint32(90 days));
|
|
563
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
564
|
-
|
|
565
|
-
(uint256 minP, uint32 timeout) = deployer.safetyParamsOf(_pid);
|
|
566
|
-
assertEq(minP, 42 ether, "minParticipation should match");
|
|
567
|
-
assertEq(timeout, uint32(90 days), "scorecardTimeout should match");
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/// @notice safetyParamsOf returns 0s when not set.
|
|
571
|
-
function testSafetyParamsOf_defaults() external {
|
|
572
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
|
|
573
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
574
|
-
|
|
575
|
-
(uint256 minP, uint32 timeout) = deployer.safetyParamsOf(_pid);
|
|
576
|
-
assertEq(minP, 0, "default minParticipation should be 0");
|
|
577
|
-
assertEq(timeout, 0, "default scorecardTimeout should be 0");
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// =========================================================================
|
|
581
|
-
// FUND CONSERVATION DURING NO_CONTEST
|
|
582
|
-
// =========================================================================
|
|
583
|
-
|
|
584
|
-
/// @notice All users can refund at mint price during NO_CONTEST — no funds stuck.
|
|
585
|
-
function testNoContest_allUsersCanRefund() external {
|
|
586
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 2 ether, 0, 7 days);
|
|
587
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
588
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
589
|
-
|
|
590
|
-
_users = new address[](4);
|
|
591
|
-
for (uint256 i; i < 4; i++) {
|
|
592
|
-
_users[i] = _addr(i);
|
|
593
|
-
_mint(_users[i], i + 1, 2 ether);
|
|
594
|
-
vm.warp(block.timestamp + 1);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Warp past timeout
|
|
598
|
-
vm.warp(d.start + 7 days + 1);
|
|
599
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
600
|
-
|
|
601
|
-
// Trigger no-contest to unlock refunds
|
|
602
|
-
deployer.triggerNoContestFor(_pid);
|
|
603
|
-
|
|
604
|
-
// All users refund
|
|
605
|
-
uint256 totalRefunded;
|
|
606
|
-
for (uint256 i; i < 4; i++) {
|
|
607
|
-
uint256 bb = _users[i].balance;
|
|
608
|
-
_refund(_users[i], i + 1);
|
|
609
|
-
uint256 received = _users[i].balance - bb;
|
|
610
|
-
assertEq(received, 2 ether, "each user gets exact mint price back");
|
|
611
|
-
totalRefunded += received;
|
|
612
|
-
}
|
|
613
|
-
assertEq(totalRefunded, 8 ether, "total refunded = total minted");
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// =========================================================================
|
|
617
|
-
// PHASE TRANSITIONS: NO_CONTEST only during SCORING phase window
|
|
618
|
-
// =========================================================================
|
|
619
|
-
|
|
620
|
-
/// @notice During COUNTDOWN, MINT, and REFUND phases, NO_CONTEST is not returned even if threshold/timeout would
|
|
621
|
-
/// trigger.
|
|
622
|
-
function testNoContest_onlyDuringScoringWindow() external {
|
|
623
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 100 ether, uint32(1));
|
|
624
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
625
|
-
|
|
626
|
-
// COUNTDOWN
|
|
627
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COUNTDOWN), "should be COUNTDOWN");
|
|
628
|
-
|
|
629
|
-
// MINT
|
|
630
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
631
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.MINT), "should be MINT");
|
|
632
|
-
|
|
633
|
-
// REFUND (warp past mint duration)
|
|
634
|
-
vm.warp(d.start - d.refundPeriodDuration);
|
|
635
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.REFUND), "should be REFUND");
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// =========================================================================
|
|
639
|
-
// TRIGGER NO_CONTEST MECHANISM
|
|
640
|
-
// =========================================================================
|
|
641
|
-
|
|
642
|
-
/// @notice triggerNoContestFor reverts when the game is not in NO_CONTEST phase.
|
|
643
|
-
function testTriggerNoContest_revertsWhenNotNoContest() external {
|
|
644
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
|
|
645
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
646
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
647
|
-
|
|
648
|
-
_users = new address[](4);
|
|
649
|
-
for (uint256 i; i < 4; i++) {
|
|
650
|
-
_users[i] = _addr(i);
|
|
651
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
652
|
-
_delegateSelf(_users[i], i + 1);
|
|
653
|
-
vm.warp(block.timestamp + 1);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
_toScoring();
|
|
657
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
|
|
658
|
-
|
|
659
|
-
// Should revert since the game is SCORING, not NO_CONTEST
|
|
660
|
-
vm.expectRevert(DefifaDeployer.DefifaDeployer_NotNoContest.selector);
|
|
661
|
-
deployer.triggerNoContestFor(_pid);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/// @notice triggerNoContestFor reverts when called twice.
|
|
665
|
-
function testTriggerNoContest_revertsWhenAlreadyTriggered() external {
|
|
666
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
|
|
667
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
668
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
669
|
-
|
|
670
|
-
_users = new address[](1);
|
|
671
|
-
_users[0] = _addr(0);
|
|
672
|
-
_mint(_users[0], 1, 1 ether);
|
|
673
|
-
|
|
674
|
-
vm.warp(d.start + 7 days + 1);
|
|
675
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
676
|
-
|
|
677
|
-
// First trigger succeeds
|
|
678
|
-
deployer.triggerNoContestFor(_pid);
|
|
679
|
-
|
|
680
|
-
// Second trigger reverts
|
|
681
|
-
vm.expectRevert(DefifaDeployer.DefifaDeployer_NoContestAlreadyTriggered.selector);
|
|
682
|
-
deployer.triggerNoContestFor(_pid);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/// @notice Cash-out before triggerNoContestFor reverts with NOTHING_TO_CLAIM (surplus = 0).
|
|
686
|
-
function testNoContest_cashOutBeforeTrigger_reverts() external {
|
|
687
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 10 ether, 0);
|
|
688
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
689
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
690
|
-
|
|
691
|
-
_users = new address[](1);
|
|
692
|
-
_users[0] = _addr(0);
|
|
693
|
-
_mint(_users[0], 1, 1 ether);
|
|
694
|
-
|
|
695
|
-
_toScoring();
|
|
696
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
697
|
-
|
|
698
|
-
// Build the cash-out metadata inline so vm.expectRevert is right before the terminal call
|
|
699
|
-
uint256[] memory cid = new uint256[](1);
|
|
700
|
-
JB721Tier memory tier = _nft.store().tierOf(address(_nft), 1, false);
|
|
701
|
-
uint256 nb = _nft.store().numberOfBurnedFor(address(_nft), 1);
|
|
702
|
-
uint256 tnum = tier.initialSupply - tier.remainingSupply + nb;
|
|
703
|
-
cid[0] = (1 * 1_000_000_000) + tnum;
|
|
704
|
-
bytes[] memory data = new bytes[](1);
|
|
705
|
-
data[0] = abi.encode(cid);
|
|
706
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
707
|
-
ids[0] = metadataHelper().getId("cashOut", address(hook));
|
|
708
|
-
bytes memory meta = metadataHelper().createMetadata(ids, data);
|
|
709
|
-
|
|
710
|
-
// Cash out should revert before trigger (surplus = 0 on SCORING ruleset)
|
|
711
|
-
vm.prank(_users[0]);
|
|
712
|
-
vm.expectRevert(DefifaHook.DefifaHook_NothingToClaim.selector);
|
|
713
|
-
JBMultiTerminal(address(jbMultiTerminal()))
|
|
714
|
-
.cashOutTokensOf({
|
|
715
|
-
holder: _users[0],
|
|
716
|
-
projectId: _pid,
|
|
717
|
-
cashOutCount: 0,
|
|
718
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
719
|
-
minTokensReclaimed: 0,
|
|
720
|
-
beneficiary: payable(_users[0]),
|
|
721
|
-
metadata: meta
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// =========================================================================
|
|
726
|
-
// RATIFIED SCORECARD: PREVENTS NO_CONTEST FOREVER + CASH-OUTS WORK FOREVER
|
|
727
|
-
// =========================================================================
|
|
728
|
-
|
|
729
|
-
/// @notice After scorecard ratification and commitment fulfillment, cash-outs work at ratified values
|
|
730
|
-
/// even long after the timeout would have elapsed. NO_CONTEST never occurs.
|
|
731
|
-
function testRatifiedScorecard_cashOutsWorkForever() external {
|
|
732
|
-
// Set a 7-day timeout, but we'll ratify before it
|
|
733
|
-
DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
|
|
734
|
-
(_pid, _nft, _gov) = _launch(d);
|
|
735
|
-
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
736
|
-
|
|
737
|
-
_users = new address[](4);
|
|
738
|
-
for (uint256 i; i < 4; i++) {
|
|
739
|
-
_users[i] = _addr(i);
|
|
740
|
-
_mint(_users[i], i + 1, 1 ether);
|
|
741
|
-
_delegateSelf(_users[i], i + 1);
|
|
742
|
-
vm.warp(block.timestamp + 1);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
_toScoring();
|
|
746
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
|
|
747
|
-
|
|
748
|
-
// Ratify scorecard: equal distribution (25% each)
|
|
749
|
-
DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
|
|
750
|
-
for (uint256 i; i < 4; i++) {
|
|
751
|
-
sc[i].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 4;
|
|
752
|
-
}
|
|
753
|
-
_attestAndRatify(sc);
|
|
754
|
-
|
|
755
|
-
// Should be COMPLETE
|
|
756
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE));
|
|
757
|
-
|
|
758
|
-
// Fulfill commitments (sends payouts and queues final ruleset)
|
|
759
|
-
deployer.fulfillCommitmentsOf(_pid);
|
|
760
|
-
|
|
761
|
-
// Warp far past the timeout (1 year) — should STILL be COMPLETE, never NO_CONTEST
|
|
762
|
-
vm.warp(block.timestamp + 365 days);
|
|
763
|
-
assertEq(
|
|
764
|
-
uint256(deployer.currentGamePhaseOf(_pid)),
|
|
765
|
-
uint256(DefifaGamePhase.COMPLETE),
|
|
766
|
-
"should stay COMPLETE forever"
|
|
767
|
-
);
|
|
768
|
-
|
|
769
|
-
// triggerNoContestFor should revert since we're COMPLETE, not NO_CONTEST
|
|
770
|
-
vm.expectRevert(DefifaDeployer.DefifaDeployer_NotNoContest.selector);
|
|
771
|
-
deployer.triggerNoContestFor(_pid);
|
|
772
|
-
|
|
773
|
-
// Cash out user 0 — should receive their share (approximately 1 ETH minus fees)
|
|
774
|
-
uint256 balBefore = _users[0].balance;
|
|
775
|
-
_cashOut(_users[0], 1, 1);
|
|
776
|
-
uint256 received = _users[0].balance - balBefore;
|
|
777
|
-
assertGt(received, 0, "should receive ETH from ratified scorecard");
|
|
778
|
-
|
|
779
|
-
// Cash out user 3 — should also work even after a very long time
|
|
780
|
-
uint256 balBefore3 = _users[3].balance;
|
|
781
|
-
_cashOut(_users[3], 4, 1);
|
|
782
|
-
uint256 received3 = _users[3].balance - balBefore3;
|
|
783
|
-
assertGt(received3, 0, "should still receive ETH long after timeout");
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// =========================================================================
|
|
787
|
-
// SETUP HELPERS
|
|
788
|
-
// =========================================================================
|
|
789
|
-
|
|
790
|
-
function _launchDataWith(
|
|
791
|
-
uint8 n,
|
|
792
|
-
uint256 tierPrice,
|
|
793
|
-
uint256 minParticipation,
|
|
794
|
-
uint32 scorecardTimeout
|
|
795
|
-
)
|
|
796
|
-
internal
|
|
797
|
-
returns (DefifaLaunchProjectData memory)
|
|
798
|
-
{
|
|
799
|
-
DefifaTierParams[] memory tp = new DefifaTierParams[](n);
|
|
800
|
-
for (uint256 i; i < n; i++) {
|
|
801
|
-
tp[i] = DefifaTierParams({
|
|
802
|
-
reservedRate: 1001,
|
|
803
|
-
reservedTokenBeneficiary: address(0),
|
|
804
|
-
encodedIPFSUri: bytes32(0),
|
|
805
|
-
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
806
|
-
name: "DEFIFA"
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
return DefifaLaunchProjectData({
|
|
810
|
-
name: "DEFIFA",
|
|
811
|
-
projectUri: "",
|
|
812
|
-
contractUri: "",
|
|
813
|
-
baseUri: "",
|
|
814
|
-
token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
|
|
815
|
-
mintPeriodDuration: 1 days,
|
|
816
|
-
start: uint48(block.timestamp + 3 days),
|
|
817
|
-
refundPeriodDuration: 1 days,
|
|
818
|
-
store: new JB721TiersHookStore(),
|
|
819
|
-
splits: new JBSplit[](0),
|
|
820
|
-
attestationStartTime: 0,
|
|
821
|
-
attestationGracePeriod: 100_381,
|
|
822
|
-
defaultAttestationDelegate: address(0),
|
|
823
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
824
|
-
tierPrice: uint104(tierPrice),
|
|
825
|
-
tiers: tp,
|
|
826
|
-
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
827
|
-
terminal: jbMultiTerminal(),
|
|
828
|
-
minParticipation: minParticipation,
|
|
829
|
-
scorecardTimeout: scorecardTimeout,
|
|
830
|
-
timelockDuration: 0
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function _toScoring() internal {
|
|
835
|
-
vm.warp(block.timestamp + 3 days + 1);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
|
|
839
|
-
g = governor;
|
|
840
|
-
p = deployer.launchGameWith(d);
|
|
841
|
-
JBRuleset memory fc = jbRulesets().currentOf(p);
|
|
842
|
-
if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
|
|
843
|
-
n = DefifaHook(fc.dataHook());
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function _addr(uint256 i) internal pure returns (address) {
|
|
847
|
-
return address(bytes20(keccak256(abi.encode("su", i))));
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function _mint(address user, uint256 tid, uint256 amt) internal {
|
|
851
|
-
vm.deal(user, amt);
|
|
852
|
-
uint16[] memory m = new uint16[](1);
|
|
853
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
854
|
-
m[0] = uint16(tid);
|
|
855
|
-
bytes[] memory data = new bytes[](1);
|
|
856
|
-
data[0] = abi.encode(user, m);
|
|
857
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
858
|
-
ids[0] = metadataHelper().getId("pay", address(hook));
|
|
859
|
-
vm.prank(user);
|
|
860
|
-
jbMultiTerminal().pay{value: amt}(
|
|
861
|
-
_pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadataHelper().createMetadata(ids, data)
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function _delegateSelf(address user, uint256 tid) internal {
|
|
866
|
-
DefifaDelegation[] memory dd = new DefifaDelegation[](1);
|
|
867
|
-
dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
|
|
868
|
-
vm.prank(user);
|
|
869
|
-
_nft.setTierDelegatesTo(dd);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function _buildScorecard(uint256 n) internal pure returns (DefifaTierCashOutWeight[] memory sc) {
|
|
873
|
-
sc = new DefifaTierCashOutWeight[](n);
|
|
874
|
-
for (uint256 i; i < n; i++) {
|
|
875
|
-
sc[i].id = i + 1;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function _attestAndRatify(DefifaTierCashOutWeight[] memory sc) internal {
|
|
880
|
-
uint256 pid = _gov.submitScorecardFor(_gameId, sc);
|
|
881
|
-
_attestAllFor(pid);
|
|
882
|
-
_gov.ratifyScorecardFrom(_gameId, sc);
|
|
883
|
-
vm.warp(block.timestamp + 1);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function _attestAllFor(uint256 pid) internal {
|
|
887
|
-
vm.warp(block.timestamp + _gov.attestationStartTimeOf(_gameId) + 1);
|
|
888
|
-
for (uint256 i; i < _users.length; i++) {
|
|
889
|
-
vm.prank(_users[i]);
|
|
890
|
-
try _gov.attestToScorecardFrom(_gameId, pid) {} catch {}
|
|
891
|
-
}
|
|
892
|
-
vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function _surplus() internal view returns (uint256) {
|
|
896
|
-
return jbMultiTerminal().currentSurplusOf(_pid, new address[](0), 18, JBCurrencyIds.ETH);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function _cashOut(address user, uint256 tid, uint256 tnum) internal {
|
|
900
|
-
bytes memory meta = _cashOutMeta(tid, tnum);
|
|
901
|
-
vm.prank(user);
|
|
902
|
-
JBMultiTerminal(address(jbMultiTerminal()))
|
|
903
|
-
.cashOutTokensOf({
|
|
904
|
-
holder: user,
|
|
905
|
-
projectId: _pid,
|
|
906
|
-
cashOutCount: 0,
|
|
907
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
908
|
-
minTokensReclaimed: 0,
|
|
909
|
-
beneficiary: payable(user),
|
|
910
|
-
metadata: meta
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
function _cashOutMeta(uint256 tid, uint256 tnum) internal view returns (bytes memory) {
|
|
915
|
-
uint256[] memory cid = new uint256[](1);
|
|
916
|
-
cid[0] = (tid * 1_000_000_000) + tnum;
|
|
917
|
-
bytes[] memory data = new bytes[](1);
|
|
918
|
-
data[0] = abi.encode(cid);
|
|
919
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
920
|
-
ids[0] = metadataHelper().getId("cashOut", address(hook));
|
|
921
|
-
return metadataHelper().createMetadata(ids, data);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
function _refund(address user, uint256 tid) internal {
|
|
925
|
-
JB721Tier memory tier = _nft.store().tierOf(address(_nft), tid, false);
|
|
926
|
-
uint256 nb = _nft.store().numberOfBurnedFor(address(_nft), tid);
|
|
927
|
-
uint256 tnum = tier.initialSupply - tier.remainingSupply + nb;
|
|
928
|
-
bytes memory meta = _cashOutMeta(tid, tnum);
|
|
929
|
-
vm.prank(user);
|
|
930
|
-
JBMultiTerminal(address(jbMultiTerminal()))
|
|
931
|
-
.cashOutTokensOf({
|
|
932
|
-
holder: user,
|
|
933
|
-
projectId: _pid,
|
|
934
|
-
cashOutCount: 0,
|
|
935
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
936
|
-
minTokensReclaimed: 0,
|
|
937
|
-
beneficiary: payable(user),
|
|
938
|
-
metadata: meta
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
}
|