@ballkidz/defifa 0.0.25 → 0.0.26
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 +74 -46
- package/src/DefifaGovernor.sol +53 -11
- package/src/DefifaHook.sol +79 -25
- package/src/DefifaTokenUriResolver.sol +111 -19
- package/src/interfaces/IDefifaDeployer.sol +5 -0
- package/src/interfaces/IDefifaGovernor.sol +4 -0
- package/src/interfaces/IDefifaHook.sol +5 -0
- package/src/libraries/DefifaHookLib.sol +9 -10
- package/src/structs/DefifaLaunchProjectData.sol +0 -3
- 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,514 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
5
|
-
|
|
6
|
-
import {DefifaGovernor} from "../src/DefifaGovernor.sol";
|
|
7
|
-
import {DefifaDeployer} from "../src/DefifaDeployer.sol";
|
|
8
|
-
import {DefifaHook} from "../src/DefifaHook.sol";
|
|
9
|
-
import {DefifaTokenUriResolver} from "../src/DefifaTokenUriResolver.sol";
|
|
10
|
-
import {DefifaGamePhase} from "../src/enums/DefifaGamePhase.sol";
|
|
11
|
-
import {IDefifaDeployer} from "../src/interfaces/IDefifaDeployer.sol";
|
|
12
|
-
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
13
|
-
|
|
14
|
-
import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
|
|
15
|
-
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
16
|
-
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
17
|
-
|
|
18
|
-
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
19
|
-
import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
|
|
20
|
-
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
21
|
-
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
|
|
22
|
-
import {DefifaDelegation} from "../src/structs/DefifaDelegation.sol";
|
|
23
|
-
import {DefifaLaunchProjectData} from "../src/structs/DefifaLaunchProjectData.sol";
|
|
24
|
-
import {DefifaTierParams} from "../src/structs/DefifaTierParams.sol";
|
|
25
|
-
import {DefifaTierCashOutWeight} from "../src/structs/DefifaTierCashOutWeight.sol";
|
|
26
|
-
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
27
|
-
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
28
|
-
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
29
|
-
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
30
|
-
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
31
|
-
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
32
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
33
|
-
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
34
|
-
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
35
|
-
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
36
|
-
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
37
|
-
import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
|
|
38
|
-
|
|
39
|
-
/// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
|
|
40
|
-
contract QATimestampReader {
|
|
41
|
-
function timestamp() external view returns (uint256) {
|
|
42
|
-
return block.timestamp;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// =============================================================================
|
|
47
|
-
// QA LAST-MILE TEST 1: CASHOUT DoS WHEN FULFILLMENT FAILS DURING RATIFICATION
|
|
48
|
-
// =============================================================================
|
|
49
|
-
|
|
50
|
-
/// @title TestQACashOutDoSDuringFulfillmentWindow
|
|
51
|
-
/// @notice Documents the cashout denial-of-service window when fulfillCommitmentsOf reverts during ratification.
|
|
52
|
-
/// @dev When fulfillCommitmentsOf reverts during ratification (try-catch), the game enters COMPLETE phase
|
|
53
|
-
/// (scorecard is set) but the final ruleset — which has empty fundAccessLimitGroups (surplus = balance) —
|
|
54
|
-
/// is never queued. The SCORING ruleset remains active with payoutLimits = type(uint224).max, making
|
|
55
|
-
/// surplus = 0, which causes cashOutCount = 0 in the hook's computeCashOutCount.
|
|
56
|
-
/// Players cannot cash out until fulfillCommitmentsOf is successfully retried.
|
|
57
|
-
/// This is a known, accepted behavior: the DoS is temporary and funds are safe.
|
|
58
|
-
contract TestQACashOutDoSDuringFulfillmentWindow is JBTest, TestBaseWorkflow {
|
|
59
|
-
using JBRulesetMetadataResolver for JBRuleset;
|
|
60
|
-
|
|
61
|
-
QATimestampReader private _tsReader = new QATimestampReader();
|
|
62
|
-
|
|
63
|
-
address _protocolFeeProjectTokenAccount;
|
|
64
|
-
address _defifaProjectTokenAccount;
|
|
65
|
-
uint256 _protocolFeeProjectId;
|
|
66
|
-
uint256 _defifaProjectId;
|
|
67
|
-
uint256 _gameId = 3;
|
|
68
|
-
|
|
69
|
-
DefifaDeployer deployer;
|
|
70
|
-
DefifaHook hook;
|
|
71
|
-
DefifaGovernor governor;
|
|
72
|
-
|
|
73
|
-
address projectOwner = address(bytes20(keccak256("projectOwner")));
|
|
74
|
-
|
|
75
|
-
function setUp() public virtual override {
|
|
76
|
-
super.setUp();
|
|
77
|
-
|
|
78
|
-
JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
|
|
79
|
-
_tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
|
|
80
|
-
|
|
81
|
-
JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
|
|
82
|
-
terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
|
|
83
|
-
|
|
84
|
-
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
85
|
-
rulesetConfigs[0] = JBRulesetConfig({
|
|
86
|
-
mustStartAtOrAfter: 0,
|
|
87
|
-
duration: 10 days,
|
|
88
|
-
weight: 1e18,
|
|
89
|
-
weightCutPercent: 0,
|
|
90
|
-
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
91
|
-
metadata: JBRulesetMetadata({
|
|
92
|
-
reservedPercent: 0,
|
|
93
|
-
cashOutTaxRate: 0,
|
|
94
|
-
baseCurrency: JBCurrencyIds.ETH,
|
|
95
|
-
pausePay: false,
|
|
96
|
-
pauseCreditTransfers: false,
|
|
97
|
-
allowOwnerMinting: false,
|
|
98
|
-
allowSetCustomToken: false,
|
|
99
|
-
allowTerminalMigration: false,
|
|
100
|
-
allowSetTerminals: false,
|
|
101
|
-
allowSetController: false,
|
|
102
|
-
allowAddAccountingContext: false,
|
|
103
|
-
allowAddPriceFeed: false,
|
|
104
|
-
ownerMustSendPayouts: false,
|
|
105
|
-
holdFees: false,
|
|
106
|
-
useTotalSurplusForCashOuts: false,
|
|
107
|
-
useDataHookForPay: true,
|
|
108
|
-
useDataHookForCashOut: true,
|
|
109
|
-
dataHook: address(0),
|
|
110
|
-
metadata: 0
|
|
111
|
-
}),
|
|
112
|
-
splitGroups: new JBSplitGroup[](0),
|
|
113
|
-
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
_protocolFeeProjectId =
|
|
117
|
-
jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
|
|
118
|
-
vm.prank(projectOwner);
|
|
119
|
-
_protocolFeeProjectTokenAccount =
|
|
120
|
-
address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
|
|
121
|
-
|
|
122
|
-
_defifaProjectId =
|
|
123
|
-
jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
|
|
124
|
-
vm.prank(projectOwner);
|
|
125
|
-
_defifaProjectTokenAccount =
|
|
126
|
-
address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
|
|
127
|
-
|
|
128
|
-
hook = new DefifaHook(
|
|
129
|
-
jbDirectory(), IERC20(address(_defifaProjectTokenAccount)), IERC20(_protocolFeeProjectTokenAccount)
|
|
130
|
-
);
|
|
131
|
-
governor = new DefifaGovernor(jbController(), address(this));
|
|
132
|
-
JBAddressRegistry _registry = new JBAddressRegistry();
|
|
133
|
-
DefifaTokenUriResolver _tokenUriResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
|
|
134
|
-
deployer = new DefifaDeployer(
|
|
135
|
-
address(hook),
|
|
136
|
-
_tokenUriResolver,
|
|
137
|
-
governor,
|
|
138
|
-
jbController(),
|
|
139
|
-
_registry,
|
|
140
|
-
_defifaProjectId,
|
|
141
|
-
_protocolFeeProjectId
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
hook.transferOwnership(address(deployer));
|
|
145
|
-
governor.transferOwnership(address(deployer));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/// @notice Proves the fix: when sendPayoutsOf reverts during fulfillCommitmentsOf,
|
|
149
|
-
/// the internal try-catch handles it gracefully. The final ruleset is still queued,
|
|
150
|
-
/// and players can cash out immediately (no DoS).
|
|
151
|
-
function test_cashOutDoSDuringFulfillmentWindow() public {
|
|
152
|
-
uint8 nTiers = 4;
|
|
153
|
-
address[] memory _users = new address[](nTiers);
|
|
154
|
-
DefifaLaunchProjectData memory defifaData = _getBasicLaunchData(nTiers);
|
|
155
|
-
(uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = _createProject(defifaData);
|
|
156
|
-
|
|
157
|
-
// --- Phase 1: Mint NFTs (1 ETH per user, 4 users = 4 ETH pot) ---
|
|
158
|
-
vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
|
|
159
|
-
for (uint256 i = 0; i < nTiers; i++) {
|
|
160
|
-
_users[i] = address(bytes20(keccak256(abi.encode("qa_user", Strings.toString(i)))));
|
|
161
|
-
vm.deal(_users[i], 1 ether);
|
|
162
|
-
uint16[] memory rawMetadata = new uint16[](1);
|
|
163
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
164
|
-
rawMetadata[0] = uint16(i + 1);
|
|
165
|
-
bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
|
|
166
|
-
|
|
167
|
-
vm.prank(_users[i]);
|
|
168
|
-
jbMultiTerminal().pay{value: 1 ether}(
|
|
169
|
-
_projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
DefifaDelegation[] memory dd = new DefifaDelegation[](1);
|
|
173
|
-
dd[0] = DefifaDelegation({delegatee: _users[i], tierId: i + 1});
|
|
174
|
-
vm.prank(_users[i]);
|
|
175
|
-
_nft.setTierDelegatesTo(dd);
|
|
176
|
-
|
|
177
|
-
vm.warp(_tsReader.timestamp() + 1);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Verify the pot is 4 ETH.
|
|
181
|
-
uint256 potBefore =
|
|
182
|
-
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), _projectId, JBConstants.NATIVE_TOKEN);
|
|
183
|
-
assertEq(potBefore, 4 ether, "pot should be 4 ETH");
|
|
184
|
-
|
|
185
|
-
// --- Advance to SCORING phase ---
|
|
186
|
-
vm.warp(defifaData.start + 1);
|
|
187
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_projectId)), uint256(DefifaGamePhase.SCORING));
|
|
188
|
-
|
|
189
|
-
// --- Build and ratify scorecard with tier 1 getting all weight ---
|
|
190
|
-
DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
|
|
191
|
-
scorecards[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
|
|
192
|
-
for (uint256 i = 1; i < nTiers; i++) {
|
|
193
|
-
scorecards[i] = DefifaTierCashOutWeight({id: i + 1, cashOutWeight: 0});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
|
|
197
|
-
vm.warp(_tsReader.timestamp() + _governor.attestationStartTimeOf(_gameId) + 1);
|
|
198
|
-
// Start from i=1: user 0 holds tier 1 which gets 100% weight, so BWA reduces their power to 0.
|
|
199
|
-
for (uint256 i = 1; i < _users.length; i++) {
|
|
200
|
-
vm.prank(_users[i]);
|
|
201
|
-
_governor.attestToScorecardFrom(_gameId, _proposalId);
|
|
202
|
-
}
|
|
203
|
-
vm.warp(_tsReader.timestamp() + _governor.attestationGracePeriodOf(_gameId) + 1);
|
|
204
|
-
|
|
205
|
-
// --- Mock sendPayoutsOf on the terminal to revert (the actual failure point) ---
|
|
206
|
-
vm.mockCallRevert(
|
|
207
|
-
address(jbMultiTerminal()),
|
|
208
|
-
abi.encodeWithSelector(JBMultiTerminal.sendPayoutsOf.selector),
|
|
209
|
-
abi.encodeWithSignature("Error(string)", "simulated payout failure")
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
// Ratify — sendPayoutsOf fails but fulfillCommitmentsOf handles it gracefully.
|
|
213
|
-
// CommitmentPayoutFailed is emitted and the final ruleset is still queued.
|
|
214
|
-
vm.expectEmit(true, false, false, false);
|
|
215
|
-
emit IDefifaDeployer.CommitmentPayoutFailed(_gameId, 0, "");
|
|
216
|
-
_governor.ratifyScorecardFrom(_gameId, scorecards);
|
|
217
|
-
|
|
218
|
-
// Clear mock so subsequent calls work.
|
|
219
|
-
vm.clearMockedCalls();
|
|
220
|
-
|
|
221
|
-
// Verify game is in COMPLETE phase (scorecard is set).
|
|
222
|
-
assertEq(uint256(deployer.currentGamePhaseOf(_projectId)), uint256(DefifaGamePhase.COMPLETE));
|
|
223
|
-
|
|
224
|
-
// Verify fulfilledCommitmentsOf returns 1 (sentinel).
|
|
225
|
-
assertEq(deployer.fulfilledCommitmentsOf(_projectId), 1, "should be sentinel value 1");
|
|
226
|
-
|
|
227
|
-
// --- Players CAN cash out immediately (no DoS) ---
|
|
228
|
-
uint256 user0BalBefore = _users[0].balance;
|
|
229
|
-
{
|
|
230
|
-
uint256[] memory cashOutIds = new uint256[](1);
|
|
231
|
-
cashOutIds[0] = _generateTokenId(1, 1);
|
|
232
|
-
bytes memory cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutIds));
|
|
233
|
-
|
|
234
|
-
vm.prank(_users[0]);
|
|
235
|
-
JBMultiTerminal(address(jbMultiTerminal()))
|
|
236
|
-
.cashOutTokensOf({
|
|
237
|
-
holder: _users[0],
|
|
238
|
-
projectId: _projectId,
|
|
239
|
-
cashOutCount: 0,
|
|
240
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
241
|
-
minTokensReclaimed: 0,
|
|
242
|
-
beneficiary: payable(_users[0]),
|
|
243
|
-
metadata: cashOutMetadata
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
uint256 reclaimed = _users[0].balance - user0BalBefore;
|
|
247
|
-
assertGt(reclaimed, 0, "winner should reclaim ETH immediately (no DoS)");
|
|
248
|
-
|
|
249
|
-
// --- Calling fulfillCommitmentsOf again is a no-op (idempotent) ---
|
|
250
|
-
deployer.fulfillCommitmentsOf(_projectId);
|
|
251
|
-
assertEq(deployer.fulfilledCommitmentsOf(_projectId), 1, "should still be sentinel value 1");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ----- Internal helpers ------
|
|
255
|
-
|
|
256
|
-
function _getBasicLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
|
|
257
|
-
DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
|
|
258
|
-
for (uint256 i = 0; i < nTiers; i++) {
|
|
259
|
-
tierParams[i] = DefifaTierParams({
|
|
260
|
-
reservedRate: 1001,
|
|
261
|
-
reservedTokenBeneficiary: address(0),
|
|
262
|
-
encodedIPFSUri: bytes32(0),
|
|
263
|
-
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
264
|
-
name: "DEFIFA"
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return DefifaLaunchProjectData({
|
|
269
|
-
name: "DEFIFA",
|
|
270
|
-
projectUri: "",
|
|
271
|
-
contractUri: "",
|
|
272
|
-
baseUri: "",
|
|
273
|
-
tierPrice: 1 ether,
|
|
274
|
-
token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
|
|
275
|
-
mintPeriodDuration: 1 days,
|
|
276
|
-
start: uint48(block.timestamp + 3 days),
|
|
277
|
-
refundPeriodDuration: 1 days,
|
|
278
|
-
store: new JB721TiersHookStore(),
|
|
279
|
-
splits: new JBSplit[](0),
|
|
280
|
-
attestationStartTime: 0,
|
|
281
|
-
attestationGracePeriod: 100_381,
|
|
282
|
-
defaultAttestationDelegate: address(0),
|
|
283
|
-
tiers: tierParams,
|
|
284
|
-
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
285
|
-
terminal: jbMultiTerminal(),
|
|
286
|
-
minParticipation: 0,
|
|
287
|
-
scorecardTimeout: 0,
|
|
288
|
-
timelockDuration: 0
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function _createProject(DefifaLaunchProjectData memory defifaLaunchData)
|
|
293
|
-
internal
|
|
294
|
-
returns (uint256 projectId, DefifaHook nft, DefifaGovernor _governor)
|
|
295
|
-
{
|
|
296
|
-
_governor = governor;
|
|
297
|
-
(projectId) = deployer.launchGameWith(defifaLaunchData);
|
|
298
|
-
JBRuleset memory _fc = jbRulesets().currentOf(projectId);
|
|
299
|
-
if (_fc.dataHook() == address(0)) {
|
|
300
|
-
(_fc,) = jbRulesets().latestQueuedOf(projectId);
|
|
301
|
-
}
|
|
302
|
-
nft = DefifaHook(_fc.dataHook());
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function _generateTokenId(uint256 _tierId, uint256 _tokenNumber) internal pure returns (uint256) {
|
|
306
|
-
return (_tierId * 1_000_000_000) + _tokenNumber;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function _buildPayMetadata(bytes memory metadata) internal view returns (bytes memory) {
|
|
310
|
-
bytes[] memory data = new bytes[](1);
|
|
311
|
-
data[0] = metadata;
|
|
312
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
313
|
-
ids[0] = metadataHelper().getId("pay", address(hook));
|
|
314
|
-
return metadataHelper().createMetadata(ids, data);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function _buildCashOutMetadata(bytes memory decodedData) internal view returns (bytes memory) {
|
|
318
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
319
|
-
ids[0] = metadataHelper().getId("cashOut", address(hook));
|
|
320
|
-
bytes[] memory datas = new bytes[](1);
|
|
321
|
-
datas[0] = decodedData;
|
|
322
|
-
return metadataHelper().createMetadata(ids, datas);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// =============================================================================
|
|
327
|
-
// QA LAST-MILE TEST 2: GAME-ID PREDICTION RACE — STALE STORAGE ON REVERT
|
|
328
|
-
// =============================================================================
|
|
329
|
-
|
|
330
|
-
/// @title TestQAGameIdPredictionRace
|
|
331
|
-
/// @notice Tests the gameId prediction race condition in DefifaDeployer.launchGameWith.
|
|
332
|
-
/// @dev The deployer predicts gameId = PROJECTS().count() + 1, then clones and initializes a hook with that ID.
|
|
333
|
-
/// If another project is created between the count() read and launchProjectFor(), the actual ID differs and
|
|
334
|
-
/// the transaction reverts with DefifaDeployer_InvalidGameConfiguration. Because the clone uses
|
|
335
|
-
/// cloneDeterministic with msg.sender in the salt, a retry from the same caller succeeds with a new nonce.
|
|
336
|
-
/// No orphaned state remains after the revert.
|
|
337
|
-
contract TestQAGameIdPredictionRace is JBTest, TestBaseWorkflow {
|
|
338
|
-
using JBRulesetMetadataResolver for JBRuleset;
|
|
339
|
-
|
|
340
|
-
QATimestampReader private _tsReader = new QATimestampReader();
|
|
341
|
-
|
|
342
|
-
address _protocolFeeProjectTokenAccount;
|
|
343
|
-
address _defifaProjectTokenAccount;
|
|
344
|
-
uint256 _protocolFeeProjectId;
|
|
345
|
-
uint256 _defifaProjectId;
|
|
346
|
-
|
|
347
|
-
DefifaDeployer deployer;
|
|
348
|
-
DefifaHook hook;
|
|
349
|
-
DefifaGovernor governor;
|
|
350
|
-
|
|
351
|
-
address projectOwner = address(bytes20(keccak256("projectOwner")));
|
|
352
|
-
|
|
353
|
-
function setUp() public virtual override {
|
|
354
|
-
super.setUp();
|
|
355
|
-
|
|
356
|
-
JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
|
|
357
|
-
_tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
|
|
358
|
-
|
|
359
|
-
JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
|
|
360
|
-
terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
|
|
361
|
-
|
|
362
|
-
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
363
|
-
rulesetConfigs[0] = JBRulesetConfig({
|
|
364
|
-
mustStartAtOrAfter: 0,
|
|
365
|
-
duration: 10 days,
|
|
366
|
-
weight: 1e18,
|
|
367
|
-
weightCutPercent: 0,
|
|
368
|
-
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
369
|
-
metadata: JBRulesetMetadata({
|
|
370
|
-
reservedPercent: 0,
|
|
371
|
-
cashOutTaxRate: 0,
|
|
372
|
-
baseCurrency: JBCurrencyIds.ETH,
|
|
373
|
-
pausePay: false,
|
|
374
|
-
pauseCreditTransfers: false,
|
|
375
|
-
allowOwnerMinting: false,
|
|
376
|
-
allowSetCustomToken: false,
|
|
377
|
-
allowTerminalMigration: false,
|
|
378
|
-
allowSetTerminals: false,
|
|
379
|
-
allowSetController: false,
|
|
380
|
-
allowAddAccountingContext: false,
|
|
381
|
-
allowAddPriceFeed: false,
|
|
382
|
-
ownerMustSendPayouts: false,
|
|
383
|
-
holdFees: false,
|
|
384
|
-
useTotalSurplusForCashOuts: false,
|
|
385
|
-
useDataHookForPay: true,
|
|
386
|
-
useDataHookForCashOut: true,
|
|
387
|
-
dataHook: address(0),
|
|
388
|
-
metadata: 0
|
|
389
|
-
}),
|
|
390
|
-
splitGroups: new JBSplitGroup[](0),
|
|
391
|
-
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
_protocolFeeProjectId =
|
|
395
|
-
jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
|
|
396
|
-
vm.prank(projectOwner);
|
|
397
|
-
_protocolFeeProjectTokenAccount =
|
|
398
|
-
address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
|
|
399
|
-
|
|
400
|
-
_defifaProjectId =
|
|
401
|
-
jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
|
|
402
|
-
vm.prank(projectOwner);
|
|
403
|
-
_defifaProjectTokenAccount =
|
|
404
|
-
address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
|
|
405
|
-
|
|
406
|
-
hook = new DefifaHook(
|
|
407
|
-
jbDirectory(), IERC20(address(_defifaProjectTokenAccount)), IERC20(_protocolFeeProjectTokenAccount)
|
|
408
|
-
);
|
|
409
|
-
governor = new DefifaGovernor(jbController(), address(this));
|
|
410
|
-
JBAddressRegistry _registry = new JBAddressRegistry();
|
|
411
|
-
DefifaTokenUriResolver _tokenUriResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
|
|
412
|
-
deployer = new DefifaDeployer(
|
|
413
|
-
address(hook),
|
|
414
|
-
_tokenUriResolver,
|
|
415
|
-
governor,
|
|
416
|
-
jbController(),
|
|
417
|
-
_registry,
|
|
418
|
-
_defifaProjectId,
|
|
419
|
-
_protocolFeeProjectId
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
hook.transferOwnership(address(deployer));
|
|
423
|
-
governor.transferOwnership(address(deployer));
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/// @notice Proves the gameId prediction race: when count() returns a stale value (simulating a front-run
|
|
427
|
-
/// where another project is created between the deployer's count() read and launchProjectFor()),
|
|
428
|
-
/// the deployment reverts. The revert is caught by JBDirectory's own project ID validation
|
|
429
|
-
/// (JBDirectory_InvalidProjectIdInDirectory) before reaching the deployer's explicit check,
|
|
430
|
-
/// providing defense-in-depth. The caller can retry successfully with a fresh count.
|
|
431
|
-
function test_gameIdPredictionRaceRevertsAndRetrySucceeds() public {
|
|
432
|
-
// Record the current project count before the race.
|
|
433
|
-
uint256 countBefore = jbController().PROJECTS().count();
|
|
434
|
-
address projectsAddr = address(jbController().PROJECTS());
|
|
435
|
-
|
|
436
|
-
// Build a valid game launch payload.
|
|
437
|
-
DefifaLaunchProjectData memory defifaData = _getBasicLaunchData(4);
|
|
438
|
-
|
|
439
|
-
// --- Simulate the front-run via mock: make count() return a stale (lower) value ---
|
|
440
|
-
vm.mockCall(projectsAddr, abi.encodeWithSignature("count()"), abi.encode(countBefore - 1));
|
|
441
|
-
|
|
442
|
-
// The deployer's launch reverts due to ID mismatch.
|
|
443
|
-
vm.expectRevert();
|
|
444
|
-
deployer.launchGameWith(defifaData);
|
|
445
|
-
|
|
446
|
-
// Clear the mock so future calls get the real count.
|
|
447
|
-
vm.clearMockedCalls();
|
|
448
|
-
|
|
449
|
-
// --- Verify no orphaned state exists after the revert ---
|
|
450
|
-
uint256 staleGameId = countBefore;
|
|
451
|
-
|
|
452
|
-
assertEq(governor.attestationStartTimeOf(staleGameId), 0, "governor should not be initialized for stale gameId");
|
|
453
|
-
assertEq(
|
|
454
|
-
governor.attestationGracePeriodOf(staleGameId), 0, "governor grace period should be 0 for stale gameId"
|
|
455
|
-
);
|
|
456
|
-
assertEq(deployer.tokenOf(staleGameId), address(0), "no ops stored for stale gameId");
|
|
457
|
-
assertEq(deployer.fulfilledCommitmentsOf(staleGameId), 0, "no commitments for stale gameId");
|
|
458
|
-
|
|
459
|
-
// --- Retry: the caller can successfully launch with the correct gameId ---
|
|
460
|
-
DefifaLaunchProjectData memory retryData = _getBasicLaunchData(4);
|
|
461
|
-
|
|
462
|
-
uint256 countNow = jbController().PROJECTS().count();
|
|
463
|
-
assertEq(countNow, countBefore, "count unchanged after reverted launch");
|
|
464
|
-
|
|
465
|
-
uint256 retryGameId = deployer.launchGameWith(retryData);
|
|
466
|
-
|
|
467
|
-
assertEq(retryGameId, countNow + 1, "retry should get the correct gameId");
|
|
468
|
-
assertEq(
|
|
469
|
-
uint256(deployer.currentGamePhaseOf(retryGameId)),
|
|
470
|
-
uint256(DefifaGamePhase.COUNTDOWN),
|
|
471
|
-
"game should be in COUNTDOWN phase"
|
|
472
|
-
);
|
|
473
|
-
assertGt(governor.attestationGracePeriodOf(retryGameId), 0, "governor should be initialized for retry gameId");
|
|
474
|
-
assertEq(deployer.tokenOf(retryGameId), JBConstants.NATIVE_TOKEN, "ops stored correctly for retry gameId");
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// ----- Internal helpers ------
|
|
478
|
-
|
|
479
|
-
function _getBasicLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
|
|
480
|
-
DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
|
|
481
|
-
for (uint256 i = 0; i < nTiers; i++) {
|
|
482
|
-
tierParams[i] = DefifaTierParams({
|
|
483
|
-
reservedRate: 1001,
|
|
484
|
-
reservedTokenBeneficiary: address(0),
|
|
485
|
-
encodedIPFSUri: bytes32(0),
|
|
486
|
-
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
487
|
-
name: "DEFIFA"
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return DefifaLaunchProjectData({
|
|
492
|
-
name: "DEFIFA",
|
|
493
|
-
projectUri: "",
|
|
494
|
-
contractUri: "",
|
|
495
|
-
baseUri: "",
|
|
496
|
-
tierPrice: 1 ether,
|
|
497
|
-
token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
|
|
498
|
-
mintPeriodDuration: 1 days,
|
|
499
|
-
start: uint48(block.timestamp + 3 days),
|
|
500
|
-
refundPeriodDuration: 1 days,
|
|
501
|
-
store: new JB721TiersHookStore(),
|
|
502
|
-
splits: new JBSplit[](0),
|
|
503
|
-
attestationStartTime: 0,
|
|
504
|
-
attestationGracePeriod: 100_381,
|
|
505
|
-
defaultAttestationDelegate: address(0),
|
|
506
|
-
tiers: tierParams,
|
|
507
|
-
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
508
|
-
terminal: jbMultiTerminal(),
|
|
509
|
-
minParticipation: 0,
|
|
510
|
-
scorecardTimeout: 0,
|
|
511
|
-
timelockDuration: 0
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
}
|