@ballkidz/defifa 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/package.json +5 -5
  2. package/src/DefifaDeployer.sol +125 -124
  3. package/src/DefifaGovernor.sol +160 -145
  4. package/src/DefifaHook.sol +287 -293
  5. package/src/DefifaTokenUriResolver.sol +30 -30
  6. package/src/interfaces/IDefifaGovernor.sol +2 -0
  7. package/test/regression/M35_GracePeriodBypass.t.sol +296 -0
  8. package/test/regression/M36_FulfillmentBlocksRatification.t.sol +272 -0
  9. package/.gas-snapshot +0 -2
  10. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDelegate.json +0 -4867
  11. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDeployer.json +0 -1719
  12. package/deployments/defifa-v5/arbitrum_sepolia/DefifaGovernor.json +0 -1535
  13. package/deployments/defifa-v5/arbitrum_sepolia/DefifaTokenUriResolver.json +0 -295
  14. package/deployments/defifa-v5/base_sepolia/DefifaDelegate.json +0 -4875
  15. package/deployments/defifa-v5/base_sepolia/DefifaDeployer.json +0 -1725
  16. package/deployments/defifa-v5/base_sepolia/DefifaGovernor.json +0 -1543
  17. package/deployments/defifa-v5/base_sepolia/DefifaTokenUriResolver.json +0 -301
  18. package/deployments/defifa-v5/optimism_sepolia/DefifaDelegate.json +0 -4875
  19. package/deployments/defifa-v5/optimism_sepolia/DefifaDeployer.json +0 -1725
  20. package/deployments/defifa-v5/optimism_sepolia/DefifaGovernor.json +0 -1543
  21. package/deployments/defifa-v5/optimism_sepolia/DefifaTokenUriResolver.json +0 -301
  22. package/deployments/defifa-v5/sepolia/DefifaDelegate.json +0 -4875
  23. package/deployments/defifa-v5/sepolia/DefifaDeployer.json +0 -1725
  24. package/deployments/defifa-v5/sepolia/DefifaGovernor.json +0 -1543
  25. package/deployments/defifa-v5/sepolia/DefifaTokenUriResolver.json +0 -301
  26. package/foundry.lock +0 -17
@@ -241,36 +241,6 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
241
241
  return string.concat(parts[0], Base64.encode(abi.encodePacked(parts[1], parts[2], parts[3])));
242
242
  }
243
243
 
244
- /// @notice Gets a substring.
245
- /// @dev If the first character is a space, it is not included.
246
- /// @param _str The string to get a substring of.
247
- /// @param _startIndex The first index of the substring from within the string.
248
- /// @param _endIndex The last index of the string from within the string.
249
- /// @return substring The substring.
250
- function _getSubstring(
251
- string memory _str,
252
- uint256 _startIndex,
253
- uint256 _endIndex
254
- )
255
- internal
256
- pure
257
- returns (string memory substring)
258
- {
259
- bytes memory _strBytes = bytes(_str);
260
- if (_startIndex >= _strBytes.length) return "";
261
- if (_endIndex > _strBytes.length) _endIndex = _strBytes.length;
262
- _startIndex = _strBytes[_startIndex] == bytes1(0x20) ? _startIndex + 1 : _startIndex;
263
- if (_startIndex >= _endIndex) return "";
264
- bytes memory _result = new bytes(_endIndex - _startIndex);
265
- for (uint256 _i = _startIndex; _i < _endIndex;) {
266
- _result[_i - _startIndex] = _strBytes[_i];
267
- unchecked {
268
- ++_i;
269
- }
270
- }
271
- return string(_result);
272
- }
273
-
274
244
  /// @notice Formats a balance from a fixed point number to a string.
275
245
  /// @param _amount The fixed point amount.
276
246
  /// @param _token The token the amount is in.
@@ -309,4 +279,34 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
309
279
  ? string(abi.encodePacked("\u039E", _integerPart, ".", _decimalPartStr))
310
280
  : string(abi.encodePacked(_integerPart, ".", _decimalPartStr, " ", IERC20Metadata(_token).symbol()));
311
281
  }
282
+
283
+ /// @notice Gets a substring.
284
+ /// @dev If the first character is a space, it is not included.
285
+ /// @param _str The string to get a substring of.
286
+ /// @param _startIndex The first index of the substring from within the string.
287
+ /// @param _endIndex The last index of the string from within the string.
288
+ /// @return substring The substring.
289
+ function _getSubstring(
290
+ string memory _str,
291
+ uint256 _startIndex,
292
+ uint256 _endIndex
293
+ )
294
+ internal
295
+ pure
296
+ returns (string memory substring)
297
+ {
298
+ bytes memory _strBytes = bytes(_str);
299
+ if (_startIndex >= _strBytes.length) return "";
300
+ if (_endIndex > _strBytes.length) _endIndex = _strBytes.length;
301
+ _startIndex = _strBytes[_startIndex] == bytes1(0x20) ? _startIndex + 1 : _startIndex;
302
+ if (_startIndex >= _endIndex) return "";
303
+ bytes memory _result = new bytes(_endIndex - _startIndex);
304
+ for (uint256 _i = _startIndex; _i < _endIndex;) {
305
+ _result[_i - _startIndex] = _strBytes[_i];
306
+ unchecked {
307
+ ++_i;
308
+ }
309
+ }
310
+ return string(_result);
311
+ }
312
312
  }
@@ -24,6 +24,8 @@ interface IDefifaGovernor {
24
24
 
25
25
  event ScorecardRatified(uint256 indexed gameId, uint256 indexed scorecardId, address caller);
26
26
 
27
+ event FulfillmentFailed(uint256 indexed gameId, bytes reason);
28
+
27
29
  /// @notice The maximum tier ID that contributes attestation power.
28
30
  /// @return The maximum attestation power tier.
29
31
  function MAX_ATTESTATION_POWER_TIER() external view returns (uint256);
@@ -0,0 +1,296 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+ import "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
6
+
7
+ import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
8
+ import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
9
+ import {DefifaHook} from "../../src/DefifaHook.sol";
10
+ import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
11
+ import {DefifaScorecardState} from "../../src/enums/DefifaScorecardState.sol";
12
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
13
+
14
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
15
+ import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
16
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
17
+ import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
18
+ import {
19
+ JB721TiersRulesetMetadataResolver
20
+ } from "@bananapus/721-hook-v6/src/libraries/JB721TiersRulesetMetadataResolver.sol";
21
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
22
+
23
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
24
+ import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
25
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
26
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
27
+ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
28
+ import {DefifaDelegation} from "../../src/structs/DefifaDelegation.sol";
29
+ import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
30
+ import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
31
+ import {DefifaTierCashOutWeight} from "../../src/structs/DefifaTierCashOutWeight.sol";
32
+
33
+ /// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
34
+ contract TimestampReader2 {
35
+ function timestamp() external view returns (uint256) {
36
+ return block.timestamp;
37
+ }
38
+ }
39
+
40
+ /// @title M35_GracePeriodBypass
41
+ /// @notice Regression test: grace period should extend from attestation start, not submission time.
42
+ /// When a scorecard is submitted early (before attestationStartTime), the grace period
43
+ /// must not expire before attestations begin.
44
+ contract M35_GracePeriodBypass is JBTest, TestBaseWorkflow {
45
+ using JBRulesetMetadataResolver for JBRuleset;
46
+
47
+ TimestampReader2 private _tsReader = new TimestampReader2();
48
+
49
+ address _protocolFeeProjectTokenAccount;
50
+ address _defifaProjectTokenAccount;
51
+ uint256 _protocolFeeProjectId;
52
+ uint256 _defifaProjectId;
53
+ uint256 _gameId = 3;
54
+
55
+ DefifaDeployer deployer;
56
+ DefifaHook hook;
57
+ DefifaGovernor governor;
58
+
59
+ address projectOwner = address(bytes20(keccak256("projectOwner")));
60
+
61
+ function setUp() public virtual override {
62
+ super.setUp();
63
+
64
+ JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
65
+ _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
66
+
67
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
68
+ terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
69
+
70
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
71
+ rulesetConfigs[0] = JBRulesetConfig({
72
+ mustStartAtOrAfter: 0,
73
+ duration: 10 days,
74
+ weight: 1e18,
75
+ weightCutPercent: 0,
76
+ approvalHook: IJBRulesetApprovalHook(address(0)),
77
+ metadata: JBRulesetMetadata({
78
+ reservedPercent: 0,
79
+ cashOutTaxRate: 0,
80
+ baseCurrency: JBCurrencyIds.ETH,
81
+ pausePay: false,
82
+ pauseCreditTransfers: false,
83
+ allowOwnerMinting: false,
84
+ allowSetCustomToken: false,
85
+ allowTerminalMigration: false,
86
+ allowSetTerminals: false,
87
+ allowSetController: false,
88
+ allowAddAccountingContext: false,
89
+ allowAddPriceFeed: false,
90
+ ownerMustSendPayouts: false,
91
+ holdFees: false,
92
+ useTotalSurplusForCashOuts: false,
93
+ useDataHookForPay: true,
94
+ useDataHookForCashOut: true,
95
+ dataHook: address(0),
96
+ metadata: 0
97
+ }),
98
+ splitGroups: new JBSplitGroup[](0),
99
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
100
+ });
101
+
102
+ _protocolFeeProjectId =
103
+ jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
104
+ vm.prank(projectOwner);
105
+ _protocolFeeProjectTokenAccount =
106
+ address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
107
+
108
+ _defifaProjectId =
109
+ jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
110
+ vm.prank(projectOwner);
111
+ _defifaProjectTokenAccount =
112
+ address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
113
+
114
+ hook = new DefifaHook(
115
+ jbDirectory(), IERC20(address(_defifaProjectTokenAccount)), IERC20(_protocolFeeProjectTokenAccount)
116
+ );
117
+ governor = new DefifaGovernor(jbController(), address(this));
118
+ JBAddressRegistry _registry = new JBAddressRegistry();
119
+ DefifaTokenUriResolver _tokenURIResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
120
+ deployer = new DefifaDeployer(
121
+ address(hook),
122
+ _tokenURIResolver,
123
+ governor,
124
+ jbController(),
125
+ _registry,
126
+ _defifaProjectId,
127
+ _protocolFeeProjectId
128
+ );
129
+
130
+ hook.transferOwnership(address(deployer));
131
+ governor.transferOwnership(address(deployer));
132
+ }
133
+
134
+ /// @notice Test that grace period extends from attestation start, not submission time.
135
+ /// @dev With the fix, a scorecard submitted early should have its grace period start after
136
+ /// attestationsBegin, ensuring the grace period doesn't expire before attestations start.
137
+ function test_gracePeriodExtendsFromAttestationStart() public {
138
+ uint8 nTiers = 4;
139
+ address[] memory _users = new address[](nTiers);
140
+
141
+ // Set attestation start time far in the future (e.g. block.timestamp + 10 days)
142
+ // Grace period of 1 day
143
+ uint256 futureAttestationStart = block.timestamp + 10 days;
144
+ uint256 gracePeriod = 1 days;
145
+
146
+ DefifaLaunchProjectData memory defifaData =
147
+ _getBasicLaunchDataWithAttestationTiming(nTiers, futureAttestationStart, gracePeriod);
148
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = _createProject(defifaData);
149
+
150
+ // Phase 1: Mint
151
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
152
+ for (uint256 i = 0; i < nTiers; i++) {
153
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
154
+ vm.deal(_users[i], 1 ether);
155
+
156
+ uint16[] memory rawMetadata = new uint16[](1);
157
+ rawMetadata[0] = uint16(i + 1);
158
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
159
+
160
+ vm.prank(_users[i]);
161
+ jbMultiTerminal().pay{value: 1 ether}(
162
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
163
+ );
164
+
165
+ DefifaDelegation[] memory delegations = new DefifaDelegation[](1);
166
+ delegations[0] = DefifaDelegation({delegatee: _users[i], tierId: uint256(i + 1)});
167
+ vm.prank(_users[i]);
168
+ _nft.setTierDelegatesTo(delegations);
169
+
170
+ vm.warp(_tsReader.timestamp() + 1);
171
+ }
172
+
173
+ // Warp to scoring phase
174
+ vm.warp(defifaData.start + 1);
175
+
176
+ // Submit scorecard early (attestation start time is still in the future)
177
+ DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
178
+ uint256 weightPerTier = _nft.TOTAL_CASHOUT_WEIGHT() / nTiers;
179
+ uint256 assigned;
180
+ for (uint256 i = 0; i < nTiers; i++) {
181
+ scorecards[i].id = i + 1;
182
+ scorecards[i].cashOutWeight = weightPerTier;
183
+ assigned += weightPerTier;
184
+ }
185
+ if (assigned < _nft.TOTAL_CASHOUT_WEIGHT()) {
186
+ scorecards[0].cashOutWeight += _nft.TOTAL_CASHOUT_WEIGHT() - assigned;
187
+ }
188
+
189
+ uint256 submissionTime = _tsReader.timestamp();
190
+ uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
191
+
192
+ // The scorecard should be PENDING (attestations haven't started yet)
193
+ assertEq(
194
+ uint256(_governor.stateOf(_gameId, _proposalId)),
195
+ uint256(DefifaScorecardState.PENDING),
196
+ "Scorecard should be PENDING before attestation start"
197
+ );
198
+
199
+ // Key assertion: warp past the old grace period end (submissionTime + gracePeriod)
200
+ // but BEFORE attestations begin. The scorecard should still be PENDING, NOT in a post-grace state.
201
+ vm.warp(submissionTime + gracePeriod + 1);
202
+
203
+ // With the fix, the scorecard should still be PENDING because attestationsBegin hasn't arrived yet.
204
+ assertEq(
205
+ uint256(_governor.stateOf(_gameId, _proposalId)),
206
+ uint256(DefifaScorecardState.PENDING),
207
+ "Scorecard should still be PENDING even after old grace period would have ended"
208
+ );
209
+
210
+ // Now warp to after attestation start (attestation begin + 1)
211
+ vm.warp(futureAttestationStart + 1);
212
+
213
+ // Now the scorecard should be ACTIVE (attestations are open and grace period hasn't ended yet)
214
+ assertEq(
215
+ uint256(_governor.stateOf(_gameId, _proposalId)),
216
+ uint256(DefifaScorecardState.ACTIVE),
217
+ "Scorecard should be ACTIVE after attestation start but before grace period ends"
218
+ );
219
+
220
+ // Warp to after attestation start + grace period
221
+ vm.warp(futureAttestationStart + gracePeriod + 1);
222
+
223
+ // Now grace period has truly ended, so the state should be ACTIVE (quorum not met)
224
+ // The key here is that it transitioned properly - grace period ran from attestation start
225
+ assertEq(
226
+ uint256(_governor.stateOf(_gameId, _proposalId)),
227
+ uint256(DefifaScorecardState.ACTIVE),
228
+ "Scorecard should be ACTIVE (no quorum) after grace period truly ends"
229
+ );
230
+ }
231
+
232
+ // ----- Internal helpers ------
233
+
234
+ function _getBasicLaunchDataWithAttestationTiming(
235
+ uint8 nTiers,
236
+ uint256 attestationStartTime,
237
+ uint256 attestationGracePeriod
238
+ )
239
+ internal
240
+ returns (DefifaLaunchProjectData memory)
241
+ {
242
+ DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
243
+ for (uint256 i = 0; i < nTiers; i++) {
244
+ tierParams[i] = DefifaTierParams({
245
+ reservedRate: 1001,
246
+ reservedTokenBeneficiary: address(0),
247
+ encodedIPFSUri: bytes32(0),
248
+ shouldUseReservedTokenBeneficiaryAsDefault: false,
249
+ name: "DEFIFA"
250
+ });
251
+ }
252
+
253
+ return DefifaLaunchProjectData({
254
+ name: "DEFIFA",
255
+ projectUri: "",
256
+ contractUri: "",
257
+ baseUri: "",
258
+ tierPrice: 1 ether,
259
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
260
+ mintPeriodDuration: 1 days,
261
+ start: uint48(block.timestamp + 3 days),
262
+ refundPeriodDuration: 1 days,
263
+ store: new JB721TiersHookStore(),
264
+ splits: new JBSplit[](0),
265
+ attestationStartTime: attestationStartTime,
266
+ attestationGracePeriod: attestationGracePeriod,
267
+ defaultAttestationDelegate: address(0),
268
+ tiers: tierParams,
269
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
270
+ terminal: jbMultiTerminal(),
271
+ minParticipation: 0,
272
+ scorecardTimeout: 0
273
+ });
274
+ }
275
+
276
+ function _createProject(DefifaLaunchProjectData memory defifaLaunchData)
277
+ internal
278
+ returns (uint256 projectId, DefifaHook nft, DefifaGovernor _governor)
279
+ {
280
+ _governor = governor;
281
+ (projectId) = deployer.launchGameWith(defifaLaunchData);
282
+ JBRuleset memory _fc = jbRulesets().currentOf(projectId);
283
+ if (_fc.dataHook() == address(0)) {
284
+ (_fc,) = jbRulesets().latestQueuedOf(projectId);
285
+ }
286
+ nft = DefifaHook(_fc.dataHook());
287
+ }
288
+
289
+ function _buildPayMetadata(bytes memory metadata) internal returns (bytes memory) {
290
+ bytes[] memory data = new bytes[](1);
291
+ data[0] = metadata;
292
+ bytes4[] memory ids = new bytes4[](1);
293
+ ids[0] = metadataHelper().getId("pay", address(hook));
294
+ return metadataHelper().createMetadata(ids, data);
295
+ }
296
+ }
@@ -0,0 +1,272 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+ import "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
6
+
7
+ import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
8
+ import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
9
+ import {DefifaHook} from "../../src/DefifaHook.sol";
10
+ import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
11
+ import {DefifaScorecardState} from "../../src/enums/DefifaScorecardState.sol";
12
+ import {IDefifaGovernor} from "../../src/interfaces/IDefifaGovernor.sol";
13
+ import {IDefifaDeployer} from "../../src/interfaces/IDefifaDeployer.sol";
14
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
15
+
16
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
17
+ import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
18
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
19
+ 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
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
24
+
25
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
26
+ import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
27
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
28
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
29
+ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
30
+ import {DefifaDelegation} from "../../src/structs/DefifaDelegation.sol";
31
+ import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
32
+ import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
33
+ import {DefifaTierCashOutWeight} from "../../src/structs/DefifaTierCashOutWeight.sol";
34
+
35
+ /// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
36
+ contract TimestampReader3 {
37
+ function timestamp() external view returns (uint256) {
38
+ return block.timestamp;
39
+ }
40
+ }
41
+
42
+ /// @title M36_FulfillmentBlocksRatification
43
+ /// @notice Regression test: ratification should succeed even when fulfillCommitmentsOf reverts.
44
+ /// @dev Tests the try-catch wrapper around fulfillCommitmentsOf in ratifyScorecardFrom.
45
+ /// The test verifies that the FulfillmentFailed event is emitted and ratification completes.
46
+ contract M36_FulfillmentBlocksRatification is JBTest, TestBaseWorkflow {
47
+ using JBRulesetMetadataResolver for JBRuleset;
48
+
49
+ TimestampReader3 private _tsReader = new TimestampReader3();
50
+
51
+ address _protocolFeeProjectTokenAccount;
52
+ address _defifaProjectTokenAccount;
53
+ uint256 _protocolFeeProjectId;
54
+ uint256 _defifaProjectId;
55
+ uint256 _gameId = 3;
56
+
57
+ DefifaDeployer deployer;
58
+ DefifaHook hook;
59
+ DefifaGovernor governor;
60
+
61
+ address projectOwner = address(bytes20(keccak256("projectOwner")));
62
+
63
+ function setUp() public virtual override {
64
+ super.setUp();
65
+
66
+ JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
67
+ _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
68
+
69
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
70
+ terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
71
+
72
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
73
+ rulesetConfigs[0] = JBRulesetConfig({
74
+ mustStartAtOrAfter: 0,
75
+ duration: 10 days,
76
+ weight: 1e18,
77
+ weightCutPercent: 0,
78
+ approvalHook: IJBRulesetApprovalHook(address(0)),
79
+ metadata: JBRulesetMetadata({
80
+ reservedPercent: 0,
81
+ cashOutTaxRate: 0,
82
+ baseCurrency: JBCurrencyIds.ETH,
83
+ pausePay: false,
84
+ pauseCreditTransfers: false,
85
+ allowOwnerMinting: false,
86
+ allowSetCustomToken: false,
87
+ allowTerminalMigration: false,
88
+ allowSetTerminals: false,
89
+ allowSetController: false,
90
+ allowAddAccountingContext: false,
91
+ allowAddPriceFeed: false,
92
+ ownerMustSendPayouts: false,
93
+ holdFees: false,
94
+ useTotalSurplusForCashOuts: false,
95
+ useDataHookForPay: true,
96
+ useDataHookForCashOut: true,
97
+ dataHook: address(0),
98
+ metadata: 0
99
+ }),
100
+ splitGroups: new JBSplitGroup[](0),
101
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
102
+ });
103
+
104
+ _protocolFeeProjectId =
105
+ jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
106
+ vm.prank(projectOwner);
107
+ _protocolFeeProjectTokenAccount =
108
+ address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
109
+
110
+ _defifaProjectId =
111
+ jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
112
+ vm.prank(projectOwner);
113
+ _defifaProjectTokenAccount =
114
+ address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
115
+
116
+ hook = new DefifaHook(
117
+ jbDirectory(), IERC20(address(_defifaProjectTokenAccount)), IERC20(_protocolFeeProjectTokenAccount)
118
+ );
119
+ governor = new DefifaGovernor(jbController(), address(this));
120
+ JBAddressRegistry _registry = new JBAddressRegistry();
121
+ DefifaTokenUriResolver _tokenURIResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
122
+ deployer = new DefifaDeployer(
123
+ address(hook),
124
+ _tokenURIResolver,
125
+ governor,
126
+ jbController(),
127
+ _registry,
128
+ _defifaProjectId,
129
+ _protocolFeeProjectId
130
+ );
131
+
132
+ hook.transferOwnership(address(deployer));
133
+ governor.transferOwnership(address(deployer));
134
+ }
135
+
136
+ /// @notice Test that ratification emits FulfillmentFailed when fulfillment reverts,
137
+ /// but the scorecard is still ratified.
138
+ /// @dev We mock fulfillCommitmentsOf to revert, then verify the ratification still succeeds.
139
+ function test_ratificationSucceedsWhenFulfillmentReverts() public {
140
+ uint8 nTiers = 4;
141
+ address[] memory _users = new address[](nTiers);
142
+ DefifaLaunchProjectData memory defifaData = _getBasicLaunchData(nTiers);
143
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = _createProject(defifaData);
144
+
145
+ // Phase 1: Mint
146
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
147
+ for (uint256 i = 0; i < nTiers; i++) {
148
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
149
+ vm.deal(_users[i], 1 ether);
150
+ uint16[] memory rawMetadata = new uint16[](1);
151
+ rawMetadata[0] = uint16(i + 1);
152
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
153
+ vm.prank(_users[i]);
154
+ jbMultiTerminal().pay{value: 1 ether}(
155
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
156
+ );
157
+ vm.warp(_tsReader.timestamp() + 1);
158
+ }
159
+
160
+ // Warp to scoring phase
161
+ vm.warp(defifaData.start + 1);
162
+
163
+ // Build scorecards
164
+ DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
165
+ uint256 weightPerTier = _nft.TOTAL_CASHOUT_WEIGHT() / nTiers;
166
+ uint256 assigned;
167
+ for (uint256 i = 0; i < nTiers; i++) {
168
+ scorecards[i].id = i + 1;
169
+ scorecards[i].cashOutWeight = weightPerTier;
170
+ assigned += weightPerTier;
171
+ }
172
+ if (assigned < _nft.TOTAL_CASHOUT_WEIGHT()) {
173
+ scorecards[0].cashOutWeight += _nft.TOTAL_CASHOUT_WEIGHT() - assigned;
174
+ }
175
+
176
+ // Submit and attest
177
+ uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
178
+ vm.warp(_tsReader.timestamp() + _governor.attestationStartTimeOf(_gameId) + 1);
179
+ for (uint256 i = 0; i < _users.length; i++) {
180
+ vm.prank(_users[i]);
181
+ _governor.attestToScorecardFrom(_gameId, _proposalId);
182
+ }
183
+ vm.warp(_tsReader.timestamp() + _governor.attestationGracePeriodOf(_gameId) + 1);
184
+
185
+ // Mock fulfillCommitmentsOf to revert
186
+ address _deployer = jbController().PROJECTS().ownerOf(_gameId);
187
+ vm.mockCallRevert(
188
+ _deployer,
189
+ abi.encodeWithSelector(IDefifaDeployer.fulfillCommitmentsOf.selector, _gameId),
190
+ abi.encodeWithSignature("Error(string)", "simulated fulfillment failure")
191
+ );
192
+
193
+ // Ratification should succeed even though fulfillment will revert.
194
+ // We expect the FulfillmentFailed event to be emitted.
195
+ vm.expectEmit(true, false, false, false);
196
+ emit IDefifaGovernor.FulfillmentFailed(_gameId, "");
197
+
198
+ _governor.ratifyScorecardFrom(_gameId, scorecards);
199
+
200
+ // Verify the scorecard was ratified
201
+ assertEq(
202
+ _governor.ratifiedScorecardIdOf(_gameId),
203
+ _proposalId,
204
+ "Scorecard should be ratified despite fulfillment failure"
205
+ );
206
+
207
+ // Verify the state is RATIFIED
208
+ assertEq(
209
+ uint256(_governor.stateOf(_gameId, _proposalId)),
210
+ uint256(DefifaScorecardState.RATIFIED),
211
+ "Scorecard state should be RATIFIED"
212
+ );
213
+ }
214
+
215
+ // ----- Internal helpers ------
216
+
217
+ function _getBasicLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
218
+ DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
219
+ for (uint256 i = 0; i < nTiers; i++) {
220
+ tierParams[i] = DefifaTierParams({
221
+ reservedRate: 1001,
222
+ reservedTokenBeneficiary: address(0),
223
+ encodedIPFSUri: bytes32(0),
224
+ shouldUseReservedTokenBeneficiaryAsDefault: false,
225
+ name: "DEFIFA"
226
+ });
227
+ }
228
+
229
+ return DefifaLaunchProjectData({
230
+ name: "DEFIFA",
231
+ projectUri: "",
232
+ contractUri: "",
233
+ baseUri: "",
234
+ tierPrice: 1 ether,
235
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
236
+ mintPeriodDuration: 1 days,
237
+ start: uint48(block.timestamp + 3 days),
238
+ refundPeriodDuration: 1 days,
239
+ store: new JB721TiersHookStore(),
240
+ splits: new JBSplit[](0),
241
+ attestationStartTime: 0,
242
+ attestationGracePeriod: 100_381,
243
+ defaultAttestationDelegate: address(0),
244
+ tiers: tierParams,
245
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
246
+ terminal: jbMultiTerminal(),
247
+ minParticipation: 0,
248
+ scorecardTimeout: 0
249
+ });
250
+ }
251
+
252
+ function _createProject(DefifaLaunchProjectData memory defifaLaunchData)
253
+ internal
254
+ returns (uint256 projectId, DefifaHook nft, DefifaGovernor _governor)
255
+ {
256
+ _governor = governor;
257
+ (projectId) = deployer.launchGameWith(defifaLaunchData);
258
+ JBRuleset memory _fc = jbRulesets().currentOf(projectId);
259
+ if (_fc.dataHook() == address(0)) {
260
+ (_fc,) = jbRulesets().latestQueuedOf(projectId);
261
+ }
262
+ nft = DefifaHook(_fc.dataHook());
263
+ }
264
+
265
+ function _buildPayMetadata(bytes memory metadata) internal returns (bytes memory) {
266
+ bytes[] memory data = new bytes[](1);
267
+ data[0] = metadata;
268
+ bytes4[] memory ids = new bytes4[](1);
269
+ ids[0] = metadataHelper().getId("pay", address(hook));
270
+ return metadataHelper().createMetadata(ids, data);
271
+ }
272
+ }
package/.gas-snapshot DELETED
@@ -1,2 +0,0 @@
1
- DefifaGovernorTest:testReceiveVotingPower(uint8,uint8) (runs: 256, μ: 13609553, ~: 12959755)
2
- DefifaGovernorTest:testSetRedemptionRates(bool) (runs: 256, μ: 16433285, ~: 16434669)