@ballkidz/defifa 0.0.7 → 0.0.8
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/ADMINISTRATION.md +3 -3
- package/AUDIT_INSTRUCTIONS.md +422 -0
- package/CRYPTO_ECON.md +5 -5
- package/RISKS.md +38 -335
- package/SKILLS.md +1 -1
- package/USER_JOURNEYS.md +691 -0
- package/package.json +7 -7
- package/script/Deploy.s.sol +14 -3
- package/script/helpers/DefifaDeploymentLib.sol +13 -15
- package/src/DefifaDeployer.sol +221 -192
- package/src/DefifaGovernor.sol +286 -276
- package/src/DefifaHook.sol +65 -32
- package/src/DefifaProjectOwner.sol +27 -4
- package/src/DefifaTokenUriResolver.sol +136 -134
- package/src/enums/DefifaGamePhase.sol +1 -1
- package/src/enums/DefifaScorecardState.sol +1 -1
- package/src/interfaces/IDefifaDeployer.sol +52 -50
- package/src/interfaces/IDefifaGamePhaseReporter.sol +2 -2
- package/src/interfaces/IDefifaGamePotReporter.sol +1 -1
- package/src/interfaces/IDefifaGovernor.sol +53 -54
- package/src/interfaces/IDefifaHook.sol +104 -103
- package/src/interfaces/IDefifaTokenUriResolver.sol +2 -2
- package/src/libraries/DefifaFontImporter.sol +11 -9
- package/src/libraries/DefifaHookLib.sol +66 -53
- package/src/structs/DefifaAttestations.sol +1 -1
- package/src/structs/DefifaDelegation.sol +1 -1
- package/src/structs/DefifaLaunchProjectData.sol +4 -4
- package/src/structs/DefifaOpsData.sol +1 -1
- package/src/structs/DefifaScorecard.sol +1 -1
- package/src/structs/DefifaTierCashOutWeight.sol +1 -1
- package/src/structs/DefifaTierParams.sol +2 -1
- package/test/DefifaAdversarialQuorum.t.sol +602 -0
- package/test/DefifaAuditLowGuards.t.sol +304 -0
- package/test/DefifaFeeAccounting.t.sol +37 -16
- package/test/DefifaGovernor.t.sol +37 -11
- package/test/DefifaHookRegressions.t.sol +14 -12
- package/test/DefifaMintCostInvariant.t.sol +31 -12
- package/test/DefifaNoContest.t.sol +33 -13
- package/test/DefifaSecurity.t.sol +45 -25
- package/test/DefifaUSDC.t.sol +44 -34
- package/test/Fork.t.sol +42 -40
- package/test/SVG.t.sol +2 -2
- package/test/TestAuditGaps.sol +982 -0
- package/test/TestQALastMile.t.sol +511 -0
- package/test/regression/FulfillmentBlocksRatification.t.sol +36 -30
- package/test/regression/GracePeriodBypass.t.sol +15 -10
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.26;
|
|
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 {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
17
|
+
import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
|
|
18
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
19
|
+
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
20
|
+
import {DefifaDelegation} from "../src/structs/DefifaDelegation.sol";
|
|
21
|
+
import {DefifaLaunchProjectData} from "../src/structs/DefifaLaunchProjectData.sol";
|
|
22
|
+
import {DefifaTierParams} from "../src/structs/DefifaTierParams.sol";
|
|
23
|
+
import {DefifaTierCashOutWeight} from "../src/structs/DefifaTierCashOutWeight.sol";
|
|
24
|
+
import {DefifaGamePhase} from "../src/enums/DefifaGamePhase.sol";
|
|
25
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
26
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
27
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
28
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
29
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
30
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
31
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
32
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
33
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
34
|
+
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
35
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
36
|
+
import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
|
|
37
|
+
|
|
38
|
+
/// @notice Mock ERC-20 token with configurable decimals for testing.
|
|
39
|
+
contract AuditGapsMockToken is ERC20 {
|
|
40
|
+
uint8 private _decimals;
|
|
41
|
+
|
|
42
|
+
constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
|
|
43
|
+
_decimals = decimals_;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function decimals() public view override returns (uint8) {
|
|
47
|
+
return _decimals;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mint(address to, uint256 amount) external {
|
|
51
|
+
_mint(to, amount);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
|
|
56
|
+
contract AuditGapsTimestampReader {
|
|
57
|
+
function timestamp() external view returns (uint256) {
|
|
58
|
+
return block.timestamp;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// GAP 1: ERC-20 GAMES
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
/// @title TestAuditGapsERC20Games
|
|
67
|
+
/// @notice Tests Defifa game mechanics when using ERC-20 tokens instead of native ETH.
|
|
68
|
+
/// Exercises 18-decimal ERC-20 token flows: minting, refunding, scoring, fee fulfillment,
|
|
69
|
+
/// cash-out distribution, and no-contest mechanisms.
|
|
70
|
+
contract TestAuditGapsERC20Games is JBTest, TestBaseWorkflow {
|
|
71
|
+
using JBRulesetMetadataResolver for JBRuleset;
|
|
72
|
+
|
|
73
|
+
AuditGapsTimestampReader private _tsReader;
|
|
74
|
+
AuditGapsMockToken token;
|
|
75
|
+
|
|
76
|
+
address _protocolFeeProjectTokenAccount;
|
|
77
|
+
address _defifaProjectTokenAccount;
|
|
78
|
+
uint256 _protocolFeeProjectId;
|
|
79
|
+
uint256 _defifaProjectId;
|
|
80
|
+
uint256 _gameId = 3;
|
|
81
|
+
|
|
82
|
+
DefifaDeployer deployer;
|
|
83
|
+
DefifaHook hook;
|
|
84
|
+
DefifaGovernor governor;
|
|
85
|
+
address projectOwner = address(bytes20(keccak256("projectOwner")));
|
|
86
|
+
|
|
87
|
+
// Shared test state.
|
|
88
|
+
uint256 _pid;
|
|
89
|
+
DefifaHook _nft;
|
|
90
|
+
DefifaGovernor _gov;
|
|
91
|
+
address[] _users;
|
|
92
|
+
|
|
93
|
+
function setUp() public virtual override {
|
|
94
|
+
super.setUp();
|
|
95
|
+
|
|
96
|
+
_tsReader = new AuditGapsTimestampReader();
|
|
97
|
+
token = new AuditGapsMockToken("Test Token", "TT", 18);
|
|
98
|
+
|
|
99
|
+
// Terminal configurations using the ERC-20.
|
|
100
|
+
JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
|
|
101
|
+
_tokens[0] =
|
|
102
|
+
JBAccountingContext({token: address(token), decimals: 18, currency: uint32(uint160(address(token)))});
|
|
103
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
104
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
|
|
105
|
+
|
|
106
|
+
JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
|
|
107
|
+
rc[0] = JBRulesetConfig({
|
|
108
|
+
mustStartAtOrAfter: 0,
|
|
109
|
+
duration: 10 days,
|
|
110
|
+
weight: 1e18,
|
|
111
|
+
weightCutPercent: 0,
|
|
112
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
113
|
+
metadata: JBRulesetMetadata({
|
|
114
|
+
reservedPercent: 0,
|
|
115
|
+
cashOutTaxRate: 0,
|
|
116
|
+
baseCurrency: uint32(uint160(address(token))),
|
|
117
|
+
pausePay: false,
|
|
118
|
+
pauseCreditTransfers: false,
|
|
119
|
+
allowOwnerMinting: false,
|
|
120
|
+
allowSetCustomToken: false,
|
|
121
|
+
allowTerminalMigration: false,
|
|
122
|
+
allowSetTerminals: false,
|
|
123
|
+
allowSetController: false,
|
|
124
|
+
allowAddAccountingContext: false,
|
|
125
|
+
allowAddPriceFeed: false,
|
|
126
|
+
ownerMustSendPayouts: false,
|
|
127
|
+
holdFees: false,
|
|
128
|
+
useTotalSurplusForCashOuts: false,
|
|
129
|
+
useDataHookForPay: true,
|
|
130
|
+
useDataHookForCashOut: true,
|
|
131
|
+
dataHook: address(0),
|
|
132
|
+
metadata: 0
|
|
133
|
+
}),
|
|
134
|
+
splitGroups: new JBSplitGroup[](0),
|
|
135
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
_protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
139
|
+
vm.prank(projectOwner);
|
|
140
|
+
_protocolFeeProjectTokenAccount =
|
|
141
|
+
address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
|
|
142
|
+
_defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
143
|
+
vm.prank(projectOwner);
|
|
144
|
+
_defifaProjectTokenAccount =
|
|
145
|
+
address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
|
|
146
|
+
|
|
147
|
+
hook =
|
|
148
|
+
new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
|
|
149
|
+
governor = new DefifaGovernor(jbController(), address(this));
|
|
150
|
+
deployer = new DefifaDeployer(
|
|
151
|
+
address(hook),
|
|
152
|
+
new DefifaTokenUriResolver(ITypeface(address(0))),
|
|
153
|
+
governor,
|
|
154
|
+
jbController(),
|
|
155
|
+
new JBAddressRegistry(),
|
|
156
|
+
_defifaProjectId,
|
|
157
|
+
_protocolFeeProjectId
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
hook.transferOwnership(address(deployer));
|
|
161
|
+
governor.transferOwnership(address(deployer));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// =========================================================================
|
|
165
|
+
// LAUNCH DATA HELPERS
|
|
166
|
+
// =========================================================================
|
|
167
|
+
|
|
168
|
+
function _launchData(uint8 n, uint104 tierPrice) internal returns (DefifaLaunchProjectData memory) {
|
|
169
|
+
return _launchDataWith(n, tierPrice, 0, 0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _launchDataWith(
|
|
173
|
+
uint8 n,
|
|
174
|
+
uint104 tierPrice,
|
|
175
|
+
uint256 minParticipation,
|
|
176
|
+
uint32 scorecardTimeout
|
|
177
|
+
)
|
|
178
|
+
internal
|
|
179
|
+
returns (DefifaLaunchProjectData memory)
|
|
180
|
+
{
|
|
181
|
+
DefifaTierParams[] memory tp = new DefifaTierParams[](n);
|
|
182
|
+
for (uint256 i; i < n; i++) {
|
|
183
|
+
tp[i] = DefifaTierParams({
|
|
184
|
+
reservedRate: 1001,
|
|
185
|
+
reservedTokenBeneficiary: address(0),
|
|
186
|
+
encodedIPFSUri: bytes32(0),
|
|
187
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
188
|
+
name: "DEFIFA"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return DefifaLaunchProjectData({
|
|
193
|
+
name: "DEFIFA_ERC20",
|
|
194
|
+
projectUri: "",
|
|
195
|
+
contractUri: "",
|
|
196
|
+
baseUri: "",
|
|
197
|
+
token: JBAccountingContext({
|
|
198
|
+
token: address(token), decimals: 18, currency: uint32(uint160(address(token)))
|
|
199
|
+
}),
|
|
200
|
+
mintPeriodDuration: 1 days,
|
|
201
|
+
start: uint48(block.timestamp + 3 days),
|
|
202
|
+
refundPeriodDuration: 1 days,
|
|
203
|
+
store: new JB721TiersHookStore(),
|
|
204
|
+
splits: new JBSplit[](0),
|
|
205
|
+
attestationStartTime: 0,
|
|
206
|
+
attestationGracePeriod: 100_381,
|
|
207
|
+
defaultAttestationDelegate: address(0),
|
|
208
|
+
tierPrice: tierPrice,
|
|
209
|
+
tiers: tp,
|
|
210
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
211
|
+
terminal: jbMultiTerminal(),
|
|
212
|
+
minParticipation: minParticipation,
|
|
213
|
+
scorecardTimeout: scorecardTimeout
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
|
|
218
|
+
g = governor;
|
|
219
|
+
p = deployer.launchGameWith(d);
|
|
220
|
+
JBRuleset memory fc = jbRulesets().currentOf(p);
|
|
221
|
+
if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
|
|
222
|
+
n = DefifaHook(fc.dataHook());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _addr(uint256 i) internal pure returns (address) {
|
|
226
|
+
return address(bytes20(keccak256(abi.encode("erc20_user", i))));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function _mintErc20(address user, uint256 tid, uint104 amt) internal {
|
|
230
|
+
token.mint(user, amt);
|
|
231
|
+
uint16[] memory m = new uint16[](1);
|
|
232
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
233
|
+
m[0] = uint16(tid);
|
|
234
|
+
bytes[] memory data = new bytes[](1);
|
|
235
|
+
data[0] = abi.encode(user, m);
|
|
236
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
237
|
+
ids[0] = metadataHelper().getId("pay", address(hook));
|
|
238
|
+
vm.startPrank(user);
|
|
239
|
+
token.approve(address(jbMultiTerminal()), amt);
|
|
240
|
+
jbMultiTerminal().pay(_pid, address(token), amt, user, 0, "", metadataHelper().createMetadata(ids, data));
|
|
241
|
+
vm.stopPrank();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function _delegateSelf(address user, uint256 tid) internal {
|
|
245
|
+
DefifaDelegation[] memory dd = new DefifaDelegation[](1);
|
|
246
|
+
dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
|
|
247
|
+
vm.prank(user);
|
|
248
|
+
_nft.setTierDelegatesTo(dd);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _buildScorecard(uint256 n) internal pure returns (DefifaTierCashOutWeight[] memory sc) {
|
|
252
|
+
sc = new DefifaTierCashOutWeight[](n);
|
|
253
|
+
for (uint256 i; i < n; i++) {
|
|
254
|
+
sc[i].id = i + 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function _evenScorecard(uint256 n) internal view returns (DefifaTierCashOutWeight[] memory sc) {
|
|
259
|
+
sc = _buildScorecard(n);
|
|
260
|
+
uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
|
|
261
|
+
uint256 assigned;
|
|
262
|
+
for (uint256 i; i < n; i++) {
|
|
263
|
+
if (i == n - 1) {
|
|
264
|
+
sc[i].cashOutWeight = tw - assigned;
|
|
265
|
+
} else {
|
|
266
|
+
sc[i].cashOutWeight = tw / n;
|
|
267
|
+
}
|
|
268
|
+
assigned += sc[i].cashOutWeight;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function _attestAndRatify(DefifaTierCashOutWeight[] memory sc) internal {
|
|
273
|
+
uint256 pid = _gov.submitScorecardFor(_gameId, sc);
|
|
274
|
+
uint256 attestStart = _gov.attestationStartTimeOf(_gameId);
|
|
275
|
+
uint256 current = _tsReader.timestamp();
|
|
276
|
+
vm.warp((attestStart > current ? attestStart : current) + 1);
|
|
277
|
+
for (uint256 i; i < _users.length; i++) {
|
|
278
|
+
vm.prank(_users[i]);
|
|
279
|
+
_gov.attestToScorecardFrom(_gameId, pid);
|
|
280
|
+
}
|
|
281
|
+
vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
|
|
282
|
+
_gov.ratifyScorecardFrom(_gameId, sc);
|
|
283
|
+
vm.warp(_tsReader.timestamp() + 1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function _toScoring() internal {
|
|
287
|
+
vm.warp(_tsReader.timestamp() + 3 days + 1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function _setupGame(uint8 nTiers, uint104 tierPrice) internal {
|
|
291
|
+
DefifaLaunchProjectData memory d = _launchData(nTiers, tierPrice);
|
|
292
|
+
(_pid, _nft, _gov) = _launch(d);
|
|
293
|
+
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
294
|
+
_users = new address[](nTiers);
|
|
295
|
+
for (uint256 i; i < nTiers; i++) {
|
|
296
|
+
_users[i] = _addr(i);
|
|
297
|
+
_mintErc20(_users[i], i + 1, tierPrice);
|
|
298
|
+
_delegateSelf(_users[i], i + 1);
|
|
299
|
+
vm.warp(_tsReader.timestamp() + 1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function _balance() internal view returns (uint256) {
|
|
304
|
+
return jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), _pid, address(token));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function _generateTokenId(uint256 tierId, uint256 tokenNumber) internal pure returns (uint256) {
|
|
308
|
+
return (tierId * 1_000_000_000) + tokenNumber;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function _buildCashOutMetadata(bytes memory decodedData) internal view returns (bytes memory) {
|
|
312
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
313
|
+
ids[0] = metadataHelper().getId("cashOut", address(hook));
|
|
314
|
+
bytes[] memory datas = new bytes[](1);
|
|
315
|
+
datas[0] = decodedData;
|
|
316
|
+
return metadataHelper().createMetadata(ids, datas);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function _cashOut(address user, uint256 tid, uint256 tnum) internal {
|
|
320
|
+
uint256[] memory cashOutIds = new uint256[](1);
|
|
321
|
+
cashOutIds[0] = _generateTokenId(tid, tnum);
|
|
322
|
+
bytes memory cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutIds));
|
|
323
|
+
|
|
324
|
+
vm.prank(user);
|
|
325
|
+
JBMultiTerminal(address(jbMultiTerminal()))
|
|
326
|
+
.cashOutTokensOf({
|
|
327
|
+
holder: user,
|
|
328
|
+
projectId: _pid,
|
|
329
|
+
cashOutCount: 0,
|
|
330
|
+
tokenToReclaim: address(token),
|
|
331
|
+
minTokensReclaimed: 0,
|
|
332
|
+
beneficiary: payable(user),
|
|
333
|
+
metadata: cashOutMetadata
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function _refund(address user, uint256 tid) internal {
|
|
338
|
+
JB721Tier memory tier = _nft.store().tierOf(address(_nft), tid, false);
|
|
339
|
+
uint256 nb = _nft.store().numberOfBurnedFor(address(_nft), tid);
|
|
340
|
+
uint256 tnum = tier.initialSupply - tier.remainingSupply + nb;
|
|
341
|
+
_cashOut(user, tid, tnum);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// =========================================================================
|
|
345
|
+
// TESTS
|
|
346
|
+
// =========================================================================
|
|
347
|
+
|
|
348
|
+
/// @notice ERC-20: Mint and verify NFT ownership and terminal balance.
|
|
349
|
+
function test_erc20_mintAndBalance() external {
|
|
350
|
+
uint104 tierPrice = 1 ether;
|
|
351
|
+
_setupGame(4, tierPrice);
|
|
352
|
+
|
|
353
|
+
// Verify MINT phase.
|
|
354
|
+
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.MINT));
|
|
355
|
+
|
|
356
|
+
// All 4 users should hold NFTs.
|
|
357
|
+
for (uint256 i; i < 4; i++) {
|
|
358
|
+
assertEq(_nft.balanceOf(_users[i]), 1, "each user holds 1 NFT");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Terminal should hold 4 ether worth of tokens.
|
|
362
|
+
assertEq(_balance(), 4 ether, "terminal balance = 4 tokens");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// @notice ERC-20: Refund during MINT phase returns exact mint price in ERC-20 tokens.
|
|
366
|
+
function test_erc20_refundReturnsMintPrice() external {
|
|
367
|
+
uint104 tierPrice = 1 ether;
|
|
368
|
+
_setupGame(4, tierPrice);
|
|
369
|
+
|
|
370
|
+
// Refund user 0 during MINT phase.
|
|
371
|
+
uint256 balBefore = token.balanceOf(_users[0]);
|
|
372
|
+
_refund(_users[0], 1);
|
|
373
|
+
assertEq(token.balanceOf(_users[0]) - balBefore, 1 ether, "refund = 1 token");
|
|
374
|
+
assertEq(_nft.balanceOf(_users[0]), 0, "NFT burned on refund");
|
|
375
|
+
|
|
376
|
+
// Remaining balance = 3 tokens.
|
|
377
|
+
assertEq(_balance(), 3 ether, "terminal balance after refund");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/// @notice ERC-20: Full scoring lifecycle -- scorecard ratification and cash-out distribution.
|
|
381
|
+
function test_erc20_scorecardAndDistribute() external {
|
|
382
|
+
uint104 tierPrice = 1 ether;
|
|
383
|
+
_setupGame(4, tierPrice);
|
|
384
|
+
|
|
385
|
+
_toScoring();
|
|
386
|
+
|
|
387
|
+
// Tier 1 = 100% weight (winner takes all).
|
|
388
|
+
DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
|
|
389
|
+
sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
|
|
390
|
+
_attestAndRatify(sc);
|
|
391
|
+
|
|
392
|
+
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE));
|
|
393
|
+
|
|
394
|
+
// Winner cashes out and receives ERC-20 tokens.
|
|
395
|
+
uint256 winnerBalBefore = token.balanceOf(_users[0]);
|
|
396
|
+
_cashOut(_users[0], 1, 1);
|
|
397
|
+
uint256 winnerReceived = token.balanceOf(_users[0]) - winnerBalBefore;
|
|
398
|
+
assertGt(winnerReceived, 0, "winner received tokens");
|
|
399
|
+
|
|
400
|
+
// Losers get 0 tokens.
|
|
401
|
+
for (uint256 i = 1; i < 4; i++) {
|
|
402
|
+
uint256 bb = token.balanceOf(_users[i]);
|
|
403
|
+
_cashOut(_users[i], i + 1, 1);
|
|
404
|
+
assertEq(token.balanceOf(_users[i]), bb, "loser gets 0 tokens");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/// @notice ERC-20: Fee accounting -- 7.5% fee (2.5% NANA + 5% DEFIFA).
|
|
409
|
+
function test_erc20_feeAccounting() external {
|
|
410
|
+
uint104 tierPrice = 1 ether;
|
|
411
|
+
_setupGame(4, tierPrice);
|
|
412
|
+
|
|
413
|
+
uint256 potBefore = _balance();
|
|
414
|
+
assertEq(potBefore, 4 ether, "pot = 4 tokens");
|
|
415
|
+
|
|
416
|
+
// Expected fee: 7.5%.
|
|
417
|
+
uint256 expectedFee = (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
418
|
+
uint256 expectedSurplus = potBefore - expectedFee;
|
|
419
|
+
|
|
420
|
+
_toScoring();
|
|
421
|
+
_attestAndRatify(_evenScorecard(4));
|
|
422
|
+
|
|
423
|
+
uint256 potAfter = _balance();
|
|
424
|
+
assertEq(potAfter, expectedSurplus, "surplus after fees = pot - 7.5%");
|
|
425
|
+
|
|
426
|
+
uint256 fulfilled = deployer.fulfilledCommitmentsOf(_pid);
|
|
427
|
+
assertEq(fulfilled, expectedFee, "fulfilled = fee amount");
|
|
428
|
+
assertEq(fulfilled + potAfter, potBefore, "fee + surplus = original pot exactly");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/// @notice ERC-20: Even scorecard distributes equally among tiers.
|
|
432
|
+
function test_erc20_evenDistribution() external {
|
|
433
|
+
uint104 tierPrice = 1 ether;
|
|
434
|
+
_setupGame(4, tierPrice);
|
|
435
|
+
|
|
436
|
+
_toScoring();
|
|
437
|
+
_attestAndRatify(_evenScorecard(4));
|
|
438
|
+
|
|
439
|
+
// All users should receive roughly equal amounts.
|
|
440
|
+
uint256[] memory received = new uint256[](4);
|
|
441
|
+
for (uint256 i; i < 4; i++) {
|
|
442
|
+
uint256 bb = token.balanceOf(_users[i]);
|
|
443
|
+
_cashOut(_users[i], i + 1, 1);
|
|
444
|
+
received[i] = token.balanceOf(_users[i]) - bb;
|
|
445
|
+
assertGt(received[i], 0, "each user receives tokens");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// All should be within 1% of each other.
|
|
449
|
+
for (uint256 i = 1; i < 4; i++) {
|
|
450
|
+
assertApproxEqRel(received[0], received[i], 0.01 ether, "all users get roughly equal share");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/// @notice ERC-20: No-contest mechanism works with ERC-20 (minParticipation threshold).
|
|
455
|
+
function test_erc20_noContestMinParticipation() external {
|
|
456
|
+
uint104 tierPrice = 1 ether;
|
|
457
|
+
// 10 ether threshold, but only mint 1 ether total.
|
|
458
|
+
DefifaLaunchProjectData memory d = _launchDataWith(4, tierPrice, 10 ether, 0);
|
|
459
|
+
(_pid, _nft, _gov) = _launch(d);
|
|
460
|
+
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
461
|
+
|
|
462
|
+
// Mint only 1 token.
|
|
463
|
+
_users = new address[](1);
|
|
464
|
+
_users[0] = _addr(0);
|
|
465
|
+
_mintErc20(_users[0], 1, tierPrice);
|
|
466
|
+
|
|
467
|
+
_toScoring();
|
|
468
|
+
|
|
469
|
+
// Balance = 1 token < 10 tokens threshold -> NO_CONTEST.
|
|
470
|
+
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/// @notice ERC-20: No-contest with trigger and refund returns exact mint price in ERC-20.
|
|
474
|
+
function test_erc20_noContestRefund() external {
|
|
475
|
+
uint104 tierPrice = 1 ether;
|
|
476
|
+
DefifaLaunchProjectData memory d = _launchDataWith(4, tierPrice, 10 ether, 0);
|
|
477
|
+
(_pid, _nft, _gov) = _launch(d);
|
|
478
|
+
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
479
|
+
|
|
480
|
+
_users = new address[](2);
|
|
481
|
+
_users[0] = _addr(0);
|
|
482
|
+
_users[1] = _addr(1);
|
|
483
|
+
_mintErc20(_users[0], 1, tierPrice);
|
|
484
|
+
_mintErc20(_users[1], 2, tierPrice);
|
|
485
|
+
|
|
486
|
+
_toScoring();
|
|
487
|
+
|
|
488
|
+
// Confirm NO_CONTEST.
|
|
489
|
+
assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
490
|
+
|
|
491
|
+
// Trigger no-contest to queue a ruleset without payout limits.
|
|
492
|
+
deployer.triggerNoContestFor(_pid);
|
|
493
|
+
|
|
494
|
+
// Cash out should return exactly 1 ether of ERC-20.
|
|
495
|
+
uint256 balBefore = token.balanceOf(_users[0]);
|
|
496
|
+
_refund(_users[0], 1);
|
|
497
|
+
uint256 received = token.balanceOf(_users[0]) - balBefore;
|
|
498
|
+
assertEq(received, 1 ether, "should receive exact mint price in ERC-20");
|
|
499
|
+
assertEq(_nft.balanceOf(_users[0]), 0, "NFT should be burned");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/// @notice ERC-20: Pot reporting works correctly with ERC-20 tokens.
|
|
503
|
+
function test_erc20_potCalculation() external {
|
|
504
|
+
uint104 tierPrice = 1 ether;
|
|
505
|
+
_setupGame(4, tierPrice);
|
|
506
|
+
|
|
507
|
+
_toScoring();
|
|
508
|
+
|
|
509
|
+
(uint256 potExcluding,,) = deployer.currentGamePotOf(_pid, false);
|
|
510
|
+
(uint256 potIncluding,,) = deployer.currentGamePotOf(_pid, true);
|
|
511
|
+
assertEq(potExcluding, 4 ether, "pot excluding = 4 tokens");
|
|
512
|
+
assertEq(potIncluding, 4 ether, "pot including = 4 tokens (no fulfillment yet)");
|
|
513
|
+
|
|
514
|
+
_attestAndRatify(_evenScorecard(4));
|
|
515
|
+
|
|
516
|
+
uint256 fee = deployer.fulfilledCommitmentsOf(_pid);
|
|
517
|
+
(potExcluding,,) = deployer.currentGamePotOf(_pid, false);
|
|
518
|
+
(potIncluding,,) = deployer.currentGamePotOf(_pid, true);
|
|
519
|
+
assertEq(potExcluding, 4 ether - fee, "pot excluding = surplus");
|
|
520
|
+
assertEq(potIncluding, 4 ether, "pot including = original pot");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// =============================================================================
|
|
525
|
+
// GAP 2: MULTI-GAME GOVERNOR ISOLATION
|
|
526
|
+
// =============================================================================
|
|
527
|
+
|
|
528
|
+
/// @title TestAuditGapsMultiGameIsolation
|
|
529
|
+
/// @notice Tests that multiple simultaneous Defifa games are properly isolated.
|
|
530
|
+
/// Ensures governor actions on one game (scorecard submission, attestation,
|
|
531
|
+
/// ratification) do not affect the other game.
|
|
532
|
+
contract TestAuditGapsMultiGameIsolation is JBTest, TestBaseWorkflow {
|
|
533
|
+
using JBRulesetMetadataResolver for JBRuleset;
|
|
534
|
+
|
|
535
|
+
AuditGapsTimestampReader private _tsReader;
|
|
536
|
+
|
|
537
|
+
address _protocolFeeProjectTokenAccount;
|
|
538
|
+
address _defifaProjectTokenAccount;
|
|
539
|
+
uint256 _protocolFeeProjectId;
|
|
540
|
+
uint256 _defifaProjectId;
|
|
541
|
+
|
|
542
|
+
DefifaDeployer deployer;
|
|
543
|
+
DefifaHook hook;
|
|
544
|
+
DefifaGovernor governor;
|
|
545
|
+
address projectOwner = address(bytes20(keccak256("projectOwner")));
|
|
546
|
+
|
|
547
|
+
// Game A state.
|
|
548
|
+
uint256 pidA;
|
|
549
|
+
DefifaHook nftA;
|
|
550
|
+
uint256 gameIdA;
|
|
551
|
+
address[] usersA;
|
|
552
|
+
|
|
553
|
+
// Game B state.
|
|
554
|
+
uint256 pidB;
|
|
555
|
+
DefifaHook nftB;
|
|
556
|
+
uint256 gameIdB;
|
|
557
|
+
address[] usersB;
|
|
558
|
+
|
|
559
|
+
function setUp() public virtual override {
|
|
560
|
+
super.setUp();
|
|
561
|
+
|
|
562
|
+
_tsReader = new AuditGapsTimestampReader();
|
|
563
|
+
|
|
564
|
+
JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
|
|
565
|
+
_tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
|
|
566
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
567
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
|
|
568
|
+
JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
|
|
569
|
+
rc[0] = JBRulesetConfig({
|
|
570
|
+
mustStartAtOrAfter: 0,
|
|
571
|
+
duration: 10 days,
|
|
572
|
+
weight: 1e18,
|
|
573
|
+
weightCutPercent: 0,
|
|
574
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
575
|
+
metadata: JBRulesetMetadata({
|
|
576
|
+
reservedPercent: 0,
|
|
577
|
+
cashOutTaxRate: 0,
|
|
578
|
+
baseCurrency: JBCurrencyIds.ETH,
|
|
579
|
+
pausePay: false,
|
|
580
|
+
pauseCreditTransfers: false,
|
|
581
|
+
allowOwnerMinting: false,
|
|
582
|
+
allowSetCustomToken: false,
|
|
583
|
+
allowTerminalMigration: false,
|
|
584
|
+
allowSetTerminals: false,
|
|
585
|
+
allowSetController: false,
|
|
586
|
+
allowAddAccountingContext: false,
|
|
587
|
+
allowAddPriceFeed: false,
|
|
588
|
+
ownerMustSendPayouts: false,
|
|
589
|
+
holdFees: false,
|
|
590
|
+
useTotalSurplusForCashOuts: false,
|
|
591
|
+
useDataHookForPay: true,
|
|
592
|
+
useDataHookForCashOut: true,
|
|
593
|
+
dataHook: address(0),
|
|
594
|
+
metadata: 0
|
|
595
|
+
}),
|
|
596
|
+
splitGroups: new JBSplitGroup[](0),
|
|
597
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
_protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
601
|
+
vm.prank(projectOwner);
|
|
602
|
+
_protocolFeeProjectTokenAccount =
|
|
603
|
+
address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
|
|
604
|
+
_defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
605
|
+
vm.prank(projectOwner);
|
|
606
|
+
_defifaProjectTokenAccount =
|
|
607
|
+
address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
|
|
608
|
+
|
|
609
|
+
hook =
|
|
610
|
+
new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
|
|
611
|
+
governor = new DefifaGovernor(jbController(), address(this));
|
|
612
|
+
deployer = new DefifaDeployer(
|
|
613
|
+
address(hook),
|
|
614
|
+
new DefifaTokenUriResolver(ITypeface(address(0))),
|
|
615
|
+
governor,
|
|
616
|
+
jbController(),
|
|
617
|
+
new JBAddressRegistry(),
|
|
618
|
+
_defifaProjectId,
|
|
619
|
+
_protocolFeeProjectId
|
|
620
|
+
);
|
|
621
|
+
hook.transferOwnership(address(deployer));
|
|
622
|
+
governor.transferOwnership(address(deployer));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// =========================================================================
|
|
626
|
+
// HELPERS
|
|
627
|
+
// =========================================================================
|
|
628
|
+
|
|
629
|
+
function _launchData(uint8 n) internal returns (DefifaLaunchProjectData memory) {
|
|
630
|
+
DefifaTierParams[] memory tp = new DefifaTierParams[](n);
|
|
631
|
+
for (uint256 i; i < n; i++) {
|
|
632
|
+
tp[i] = DefifaTierParams({
|
|
633
|
+
reservedRate: 1001,
|
|
634
|
+
reservedTokenBeneficiary: address(0),
|
|
635
|
+
encodedIPFSUri: bytes32(0),
|
|
636
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
637
|
+
name: "DEFIFA"
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
return DefifaLaunchProjectData({
|
|
641
|
+
name: "DEFIFA",
|
|
642
|
+
projectUri: "",
|
|
643
|
+
contractUri: "",
|
|
644
|
+
baseUri: "",
|
|
645
|
+
token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
|
|
646
|
+
mintPeriodDuration: 1 days,
|
|
647
|
+
start: uint48(block.timestamp + 3 days),
|
|
648
|
+
refundPeriodDuration: 1 days,
|
|
649
|
+
store: new JB721TiersHookStore(),
|
|
650
|
+
splits: new JBSplit[](0),
|
|
651
|
+
attestationStartTime: 0,
|
|
652
|
+
attestationGracePeriod: 100_381,
|
|
653
|
+
defaultAttestationDelegate: address(0),
|
|
654
|
+
tierPrice: 1 ether,
|
|
655
|
+
tiers: tp,
|
|
656
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
657
|
+
terminal: jbMultiTerminal(),
|
|
658
|
+
minParticipation: 0,
|
|
659
|
+
scorecardTimeout: 0
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function _launchGame(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n) {
|
|
664
|
+
p = deployer.launchGameWith(d);
|
|
665
|
+
JBRuleset memory fc = jbRulesets().currentOf(p);
|
|
666
|
+
if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
|
|
667
|
+
n = DefifaHook(fc.dataHook());
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function _addrA(uint256 i) internal pure returns (address) {
|
|
671
|
+
return address(bytes20(keccak256(abi.encode("gameA_user", i))));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function _addrB(uint256 i) internal pure returns (address) {
|
|
675
|
+
return address(bytes20(keccak256(abi.encode("gameB_user", i))));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function _mint(address user, uint256 pid, uint256 tid, uint256 amt) internal {
|
|
679
|
+
vm.deal(user, amt);
|
|
680
|
+
uint16[] memory m = new uint16[](1);
|
|
681
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
682
|
+
m[0] = uint16(tid);
|
|
683
|
+
bytes[] memory data = new bytes[](1);
|
|
684
|
+
data[0] = abi.encode(user, m);
|
|
685
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
686
|
+
ids[0] = metadataHelper().getId("pay", address(hook));
|
|
687
|
+
vm.prank(user);
|
|
688
|
+
jbMultiTerminal().pay{value: amt}(
|
|
689
|
+
pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadataHelper().createMetadata(ids, data)
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function _delegateSelf(DefifaHook nft, address user, uint256 tid) internal {
|
|
694
|
+
DefifaDelegation[] memory dd = new DefifaDelegation[](1);
|
|
695
|
+
dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
|
|
696
|
+
vm.prank(user);
|
|
697
|
+
nft.setTierDelegatesTo(dd);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function _buildScorecard(uint256 n) internal pure returns (DefifaTierCashOutWeight[] memory sc) {
|
|
701
|
+
sc = new DefifaTierCashOutWeight[](n);
|
|
702
|
+
for (uint256 i; i < n; i++) {
|
|
703
|
+
sc[i].id = i + 1;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function _evenScorecard(DefifaHook nft, uint256 n) internal view returns (DefifaTierCashOutWeight[] memory sc) {
|
|
708
|
+
sc = _buildScorecard(n);
|
|
709
|
+
uint256 tw = nft.TOTAL_CASHOUT_WEIGHT();
|
|
710
|
+
uint256 assigned;
|
|
711
|
+
for (uint256 i; i < n; i++) {
|
|
712
|
+
if (i == n - 1) {
|
|
713
|
+
sc[i].cashOutWeight = tw - assigned;
|
|
714
|
+
} else {
|
|
715
|
+
sc[i].cashOutWeight = tw / n;
|
|
716
|
+
}
|
|
717
|
+
assigned += sc[i].cashOutWeight;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function _cashOutMeta(uint256 tid, uint256 tnum) internal view returns (bytes memory) {
|
|
722
|
+
uint256[] memory cid = new uint256[](1);
|
|
723
|
+
cid[0] = (tid * 1_000_000_000) + tnum;
|
|
724
|
+
bytes[] memory data = new bytes[](1);
|
|
725
|
+
data[0] = abi.encode(cid);
|
|
726
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
727
|
+
ids[0] = metadataHelper().getId("cashOut", address(hook));
|
|
728
|
+
return metadataHelper().createMetadata(ids, data);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function _cashOut(address user, uint256 pid, uint256 tid, uint256 tnum) internal {
|
|
732
|
+
bytes memory meta = _cashOutMeta(tid, tnum);
|
|
733
|
+
vm.prank(user);
|
|
734
|
+
JBMultiTerminal(address(jbMultiTerminal()))
|
|
735
|
+
.cashOutTokensOf({
|
|
736
|
+
holder: user,
|
|
737
|
+
projectId: pid,
|
|
738
|
+
cashOutCount: 0,
|
|
739
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
740
|
+
minTokensReclaimed: 0,
|
|
741
|
+
beneficiary: payable(user),
|
|
742
|
+
metadata: meta
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/// @dev Deploy both games and mint NFTs for both. Game A has 4 tiers, Game B has 3 tiers.
|
|
747
|
+
function _setupBothGames() internal {
|
|
748
|
+
DefifaLaunchProjectData memory dA = _launchData(4);
|
|
749
|
+
(pidA, nftA) = _launchGame(dA);
|
|
750
|
+
gameIdA = pidA;
|
|
751
|
+
|
|
752
|
+
DefifaLaunchProjectData memory dB = _launchData(3);
|
|
753
|
+
(pidB, nftB) = _launchGame(dB);
|
|
754
|
+
gameIdB = pidB;
|
|
755
|
+
|
|
756
|
+
// Warp to the MINT phase for both games (they share the same start window).
|
|
757
|
+
vm.warp(dA.start - dA.mintPeriodDuration - dA.refundPeriodDuration);
|
|
758
|
+
|
|
759
|
+
// Mint for Game A: 4 users, 1 per tier.
|
|
760
|
+
usersA = new address[](4);
|
|
761
|
+
for (uint256 i; i < 4; i++) {
|
|
762
|
+
usersA[i] = _addrA(i);
|
|
763
|
+
_mint(usersA[i], pidA, i + 1, 1 ether);
|
|
764
|
+
_delegateSelf(nftA, usersA[i], i + 1);
|
|
765
|
+
vm.warp(_tsReader.timestamp() + 1);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Mint for Game B: 3 users, 1 per tier.
|
|
769
|
+
usersB = new address[](3);
|
|
770
|
+
for (uint256 i; i < 3; i++) {
|
|
771
|
+
usersB[i] = _addrB(i);
|
|
772
|
+
_mint(usersB[i], pidB, i + 1, 1 ether);
|
|
773
|
+
_delegateSelf(nftB, usersB[i], i + 1);
|
|
774
|
+
vm.warp(_tsReader.timestamp() + 1);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function _attestAndRatify(uint256 gameId, address[] memory users, DefifaTierCashOutWeight[] memory sc) internal {
|
|
779
|
+
uint256 pid = governor.submitScorecardFor(gameId, sc);
|
|
780
|
+
uint256 attestStart = governor.attestationStartTimeOf(gameId);
|
|
781
|
+
uint256 current = _tsReader.timestamp();
|
|
782
|
+
vm.warp((attestStart > current ? attestStart : current) + 1);
|
|
783
|
+
for (uint256 i; i < users.length; i++) {
|
|
784
|
+
vm.prank(users[i]);
|
|
785
|
+
governor.attestToScorecardFrom(gameId, pid);
|
|
786
|
+
}
|
|
787
|
+
vm.warp(_tsReader.timestamp() + governor.attestationGracePeriodOf(gameId) + 1);
|
|
788
|
+
governor.ratifyScorecardFrom(gameId, sc);
|
|
789
|
+
vm.warp(_tsReader.timestamp() + 1);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// =========================================================================
|
|
793
|
+
// TESTS
|
|
794
|
+
// =========================================================================
|
|
795
|
+
|
|
796
|
+
/// @notice Both games can be launched and are in independent project IDs.
|
|
797
|
+
function test_multiGame_independentProjectIds() external {
|
|
798
|
+
_setupBothGames();
|
|
799
|
+
|
|
800
|
+
assertFalse(pidA == pidB, "game IDs should be different");
|
|
801
|
+
assertEq(uint256(deployer.currentGamePhaseOf(pidA)), uint256(DefifaGamePhase.MINT), "Game A in MINT");
|
|
802
|
+
assertEq(uint256(deployer.currentGamePhaseOf(pidB)), uint256(DefifaGamePhase.MINT), "Game B in MINT");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/// @notice Each game has independent treasury balances.
|
|
806
|
+
function test_multiGame_independentBalances() external {
|
|
807
|
+
_setupBothGames();
|
|
808
|
+
|
|
809
|
+
uint256 balA = jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), pidA, JBConstants.NATIVE_TOKEN);
|
|
810
|
+
uint256 balB = jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), pidB, JBConstants.NATIVE_TOKEN);
|
|
811
|
+
|
|
812
|
+
assertEq(balA, 4 ether, "Game A balance = 4 ETH (4 minters)");
|
|
813
|
+
assertEq(balB, 3 ether, "Game B balance = 3 ETH (3 minters)");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/// @notice Each game has independent NFT hooks.
|
|
817
|
+
function test_multiGame_independentNFTHooks() external {
|
|
818
|
+
_setupBothGames();
|
|
819
|
+
|
|
820
|
+
// The hooks should be different contracts.
|
|
821
|
+
assertFalse(address(nftA) == address(nftB), "hooks should be different contracts");
|
|
822
|
+
|
|
823
|
+
// Game A has 4 tiers, Game B has 3 tiers.
|
|
824
|
+
assertEq(nftA.store().maxTierIdOf(address(nftA)), 4, "Game A has 4 tiers");
|
|
825
|
+
assertEq(nftB.store().maxTierIdOf(address(nftB)), 3, "Game B has 3 tiers");
|
|
826
|
+
|
|
827
|
+
// Game A users should not own tokens in Game B and vice versa.
|
|
828
|
+
for (uint256 i; i < 4; i++) {
|
|
829
|
+
assertEq(nftA.balanceOf(usersA[i]), 1, "Game A user has 1 NFT in Game A");
|
|
830
|
+
assertEq(nftB.balanceOf(usersA[i]), 0, "Game A user has 0 NFTs in Game B");
|
|
831
|
+
}
|
|
832
|
+
for (uint256 i; i < 3; i++) {
|
|
833
|
+
assertEq(nftB.balanceOf(usersB[i]), 1, "Game B user has 1 NFT in Game B");
|
|
834
|
+
assertEq(nftA.balanceOf(usersB[i]), 0, "Game B user has 0 NFTs in Game A");
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/// @notice Ratifying Game A's scorecard does not affect Game B's phase.
|
|
839
|
+
function test_multiGame_ratifyOneDoesNotAffectOther() external {
|
|
840
|
+
_setupBothGames();
|
|
841
|
+
|
|
842
|
+
// Advance to scoring phase for both.
|
|
843
|
+
vm.warp(_tsReader.timestamp() + 3 days + 1);
|
|
844
|
+
|
|
845
|
+
assertEq(uint256(deployer.currentGamePhaseOf(pidA)), uint256(DefifaGamePhase.SCORING), "Game A SCORING");
|
|
846
|
+
assertEq(uint256(deployer.currentGamePhaseOf(pidB)), uint256(DefifaGamePhase.SCORING), "Game B SCORING");
|
|
847
|
+
|
|
848
|
+
// Ratify Game A's scorecard.
|
|
849
|
+
_attestAndRatify(gameIdA, usersA, _evenScorecard(nftA, 4));
|
|
850
|
+
|
|
851
|
+
// Game A should be COMPLETE.
|
|
852
|
+
assertEq(uint256(deployer.currentGamePhaseOf(pidA)), uint256(DefifaGamePhase.COMPLETE), "Game A COMPLETE");
|
|
853
|
+
|
|
854
|
+
// Game B should still be in SCORING -- not affected by Game A's ratification.
|
|
855
|
+
assertEq(uint256(deployer.currentGamePhaseOf(pidB)), uint256(DefifaGamePhase.SCORING), "Game B still SCORING");
|
|
856
|
+
|
|
857
|
+
// Game B's ratified scorecard should still be zero.
|
|
858
|
+
assertEq(governor.ratifiedScorecardIdOf(gameIdB), 0, "Game B has no ratified scorecard");
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/// @notice Both games can independently complete their full lifecycles.
|
|
862
|
+
function test_multiGame_bothCompleteIndependently() external {
|
|
863
|
+
_setupBothGames();
|
|
864
|
+
|
|
865
|
+
// Advance to scoring.
|
|
866
|
+
vm.warp(_tsReader.timestamp() + 3 days + 1);
|
|
867
|
+
|
|
868
|
+
// Ratify Game A: tier 1 wins all.
|
|
869
|
+
DefifaTierCashOutWeight[] memory scA = _buildScorecard(4);
|
|
870
|
+
scA[0].cashOutWeight = nftA.TOTAL_CASHOUT_WEIGHT();
|
|
871
|
+
_attestAndRatify(gameIdA, usersA, scA);
|
|
872
|
+
|
|
873
|
+
assertEq(uint256(deployer.currentGamePhaseOf(pidA)), uint256(DefifaGamePhase.COMPLETE), "Game A COMPLETE");
|
|
874
|
+
|
|
875
|
+
// Ratify Game B: even distribution.
|
|
876
|
+
_attestAndRatify(gameIdB, usersB, _evenScorecard(nftB, 3));
|
|
877
|
+
|
|
878
|
+
assertEq(uint256(deployer.currentGamePhaseOf(pidB)), uint256(DefifaGamePhase.COMPLETE), "Game B COMPLETE");
|
|
879
|
+
|
|
880
|
+
// Cash out from Game A: winner gets tokens.
|
|
881
|
+
uint256 bbA = usersA[0].balance;
|
|
882
|
+
_cashOut(usersA[0], pidA, 1, 1);
|
|
883
|
+
assertGt(usersA[0].balance - bbA, 0, "Game A winner received ETH");
|
|
884
|
+
|
|
885
|
+
// Cash out from Game B: all get roughly equal share.
|
|
886
|
+
uint256 bbB0 = usersB[0].balance;
|
|
887
|
+
_cashOut(usersB[0], pidB, 1, 1);
|
|
888
|
+
uint256 receivedB0 = usersB[0].balance - bbB0;
|
|
889
|
+
assertGt(receivedB0, 0, "Game B user 0 received ETH");
|
|
890
|
+
|
|
891
|
+
uint256 bbB1 = usersB[1].balance;
|
|
892
|
+
_cashOut(usersB[1], pidB, 2, 1);
|
|
893
|
+
uint256 receivedB1 = usersB[1].balance - bbB1;
|
|
894
|
+
assertGt(receivedB1, 0, "Game B user 1 received ETH");
|
|
895
|
+
assertApproxEqRel(receivedB0, receivedB1, 0.01 ether, "Game B users get roughly equal share");
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/// @notice Game A's scorecard submission does not affect Game B, and vice versa.
|
|
899
|
+
function test_multiGame_scorecardSubmissionIsolated() external {
|
|
900
|
+
_setupBothGames();
|
|
901
|
+
|
|
902
|
+
// Advance to scoring.
|
|
903
|
+
vm.warp(_tsReader.timestamp() + 3 days + 1);
|
|
904
|
+
|
|
905
|
+
// Submit a scorecard for Game A only.
|
|
906
|
+
DefifaTierCashOutWeight[] memory scA = _evenScorecard(nftA, 4);
|
|
907
|
+
uint256 scorecardIdA = governor.submitScorecardFor(gameIdA, scA);
|
|
908
|
+
|
|
909
|
+
// The scorecard should be known for Game A.
|
|
910
|
+
// stateOf should not revert for Game A's scorecard.
|
|
911
|
+
governor.stateOf(gameIdA, scorecardIdA);
|
|
912
|
+
|
|
913
|
+
// The same scorecardId should be unknown for Game B (different hooks produce different hashes).
|
|
914
|
+
// Trying to query it on Game B should revert.
|
|
915
|
+
vm.expectRevert(DefifaGovernor.DefifaGovernor_UnknownProposal.selector);
|
|
916
|
+
governor.stateOf(gameIdB, scorecardIdA);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/// @notice Game A users' attestation power is zero in Game B (different hooks).
|
|
920
|
+
function test_multiGame_attestationPowerIsolated() external {
|
|
921
|
+
_setupBothGames();
|
|
922
|
+
|
|
923
|
+
// Advance to scoring.
|
|
924
|
+
vm.warp(_tsReader.timestamp() + 3 days + 1);
|
|
925
|
+
|
|
926
|
+
// Game A user 0 has attestation power in Game A.
|
|
927
|
+
uint256 powerA = governor.getAttestationWeight(gameIdA, usersA[0], uint48(_tsReader.timestamp()));
|
|
928
|
+
assertGt(powerA, 0, "Game A user has power in Game A");
|
|
929
|
+
|
|
930
|
+
// Game A user 0 has NO attestation power in Game B (they don't hold Game B NFTs).
|
|
931
|
+
uint256 powerB = governor.getAttestationWeight(gameIdB, usersA[0], uint48(_tsReader.timestamp()));
|
|
932
|
+
assertEq(powerB, 0, "Game A user has no power in Game B");
|
|
933
|
+
|
|
934
|
+
// Game B user 0 has attestation power in Game B.
|
|
935
|
+
uint256 powerB0 = governor.getAttestationWeight(gameIdB, usersB[0], uint48(_tsReader.timestamp()));
|
|
936
|
+
assertGt(powerB0, 0, "Game B user has power in Game B");
|
|
937
|
+
|
|
938
|
+
// Game B user 0 has NO attestation power in Game A.
|
|
939
|
+
uint256 powerA0 = governor.getAttestationWeight(gameIdA, usersB[0], uint48(_tsReader.timestamp()));
|
|
940
|
+
assertEq(powerA0, 0, "Game B user has no power in Game A");
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/// @notice Each game tracks fulfilled commitments independently.
|
|
944
|
+
function test_multiGame_fulfilledCommitmentsIsolated() external {
|
|
945
|
+
_setupBothGames();
|
|
946
|
+
|
|
947
|
+
// Advance to scoring.
|
|
948
|
+
vm.warp(_tsReader.timestamp() + 3 days + 1);
|
|
949
|
+
|
|
950
|
+
// Ratify and fulfill Game A only.
|
|
951
|
+
_attestAndRatify(gameIdA, usersA, _evenScorecard(nftA, 4));
|
|
952
|
+
assertGt(deployer.fulfilledCommitmentsOf(pidA), 0, "Game A has fulfilled commitments");
|
|
953
|
+
assertEq(deployer.fulfilledCommitmentsOf(pidB), 0, "Game B has no fulfilled commitments yet");
|
|
954
|
+
|
|
955
|
+
// Now ratify and fulfill Game B.
|
|
956
|
+
_attestAndRatify(gameIdB, usersB, _evenScorecard(nftB, 3));
|
|
957
|
+
assertGt(deployer.fulfilledCommitmentsOf(pidB), 0, "Game B has fulfilled commitments");
|
|
958
|
+
|
|
959
|
+
// Both fulfilled but independently.
|
|
960
|
+
assertFalse(
|
|
961
|
+
deployer.fulfilledCommitmentsOf(pidA) == deployer.fulfilledCommitmentsOf(pidB),
|
|
962
|
+
"fulfilled amounts differ because game pots differ"
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/// @notice Quorum values are independent for each game.
|
|
967
|
+
function test_multiGame_quorumIsolated() external {
|
|
968
|
+
_setupBothGames();
|
|
969
|
+
|
|
970
|
+
// Advance to scoring.
|
|
971
|
+
vm.warp(_tsReader.timestamp() + 3 days + 1);
|
|
972
|
+
|
|
973
|
+
uint256 quorumA = governor.quorum(gameIdA);
|
|
974
|
+
uint256 quorumB = governor.quorum(gameIdB);
|
|
975
|
+
|
|
976
|
+
// Game A has 4 minted tiers, Game B has 3 minted tiers.
|
|
977
|
+
// Quorum = 50% of minted tiers * MAX_ATTESTATION_POWER_TIER.
|
|
978
|
+
assertGt(quorumA, quorumB, "Game A quorum > Game B quorum (more tiers)");
|
|
979
|
+
assertEq(quorumA, 4 * governor.MAX_ATTESTATION_POWER_TIER() / 2, "Game A quorum = 4 tiers * 50%");
|
|
980
|
+
assertEq(quorumB, 3 * governor.MAX_ATTESTATION_POWER_TIER() / 2, "Game B quorum = 3 tiers * 50%");
|
|
981
|
+
}
|
|
982
|
+
}
|