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