@ballkidz/defifa 0.0.1 → 0.0.2

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 (25) hide show
  1. package/package.json +5 -5
  2. package/src/DefifaDeployer.sol +1 -0
  3. package/src/DefifaGovernor.sol +22 -7
  4. package/src/DefifaHook.sol +0 -6
  5. package/src/interfaces/IDefifaGovernor.sol +2 -0
  6. package/test/regression/M35_GracePeriodBypass.t.sol +296 -0
  7. package/test/regression/M36_FulfillmentBlocksRatification.t.sol +272 -0
  8. package/.gas-snapshot +0 -2
  9. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDelegate.json +0 -4867
  10. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDeployer.json +0 -1719
  11. package/deployments/defifa-v5/arbitrum_sepolia/DefifaGovernor.json +0 -1535
  12. package/deployments/defifa-v5/arbitrum_sepolia/DefifaTokenUriResolver.json +0 -295
  13. package/deployments/defifa-v5/base_sepolia/DefifaDelegate.json +0 -4875
  14. package/deployments/defifa-v5/base_sepolia/DefifaDeployer.json +0 -1725
  15. package/deployments/defifa-v5/base_sepolia/DefifaGovernor.json +0 -1543
  16. package/deployments/defifa-v5/base_sepolia/DefifaTokenUriResolver.json +0 -301
  17. package/deployments/defifa-v5/optimism_sepolia/DefifaDelegate.json +0 -4875
  18. package/deployments/defifa-v5/optimism_sepolia/DefifaDeployer.json +0 -1725
  19. package/deployments/defifa-v5/optimism_sepolia/DefifaGovernor.json +0 -1543
  20. package/deployments/defifa-v5/optimism_sepolia/DefifaTokenUriResolver.json +0 -301
  21. package/deployments/defifa-v5/sepolia/DefifaDelegate.json +0 -4875
  22. package/deployments/defifa-v5/sepolia/DefifaDeployer.json +0 -1725
  23. package/deployments/defifa-v5/sepolia/DefifaGovernor.json +0 -1543
  24. package/deployments/defifa-v5/sepolia/DefifaTokenUriResolver.json +0 -301
  25. package/foundry.lock +0 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ballkidz/defifa",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -13,10 +13,10 @@
13
13
  "url": "https://github.com/BallKidz/defifa-collection-deployer"
14
14
  },
15
15
  "dependencies": {
16
- "@bananapus/721-hook-v6": "^0.0.8",
17
- "@bananapus/address-registry-v6": "^0.0.3",
18
- "@bananapus/core-v6": "^0.0.6",
19
- "@bananapus/permission-ids-v6": "^0.0.3",
16
+ "@bananapus/721-hook-v6": "^0.0.9",
17
+ "@bananapus/address-registry-v6": "^0.0.4",
18
+ "@bananapus/core-v6": "^0.0.9",
19
+ "@bananapus/permission-ids-v6": "^0.0.4",
20
20
  "@openzeppelin/contracts": "5.2.0",
21
21
  "@prb/math": "^4.1.1",
22
22
  "scripty.sol": "^2.1.1"
@@ -311,6 +311,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
311
311
  uint48(block.timestamp + launchProjectData.mintPeriodDuration + launchProjectData.refundPeriodDuration);
312
312
  }
313
313
  // Start minting right away if a start time isn't provided.
314
+ // slither-disable-next-line incorrect-equality
314
315
  else if (
315
316
  launchProjectData.mintPeriodDuration == 0
316
317
  && launchProjectData.start > block.timestamp + launchProjectData.refundPeriodDuration
@@ -127,6 +127,10 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
127
127
  /// @param gameId The ID of the game to get a proposal state of.
128
128
  /// @param scorecardId The ID of the proposal to get the state of.
129
129
  /// @return The state.
130
+ /// @dev Boundary semantics (inclusive):
131
+ /// - At exactly `attestationsBegin`, the state transitions from PENDING to ACTIVE (attestations are open).
132
+ /// - At exactly `gracePeriodEnds`, the grace period has elapsed and the state transitions from ACTIVE to
133
+ /// SUCCEEDED (if quorum is met) or remains ACTIVE (if not).
130
134
  function stateOf(uint256 gameId, uint256 scorecardId) public view virtual override returns (DefifaScorecardState) {
131
135
  // Keep a reference to the ratified scorecard ID.
132
136
  uint256 _ratifiedScorecardId = ratifiedScorecardIdOf[gameId];
@@ -147,12 +151,14 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
147
151
  }
148
152
 
149
153
  // If the scorecard has attestations beginning in the future, the state is PENDING.
150
- if (_scorecard.attestationsBegin >= block.timestamp) {
154
+ // At exactly `attestationsBegin`, attestations are open so the state is ACTIVE.
155
+ if (_scorecard.attestationsBegin > block.timestamp) {
151
156
  return DefifaScorecardState.PENDING;
152
157
  }
153
158
 
154
- // If the scorecard has a grace period expiring in the future, the state is ACTIVE.
155
- if (_scorecard.gracePeriodEnds >= block.timestamp) {
159
+ // If the scorecard's grace period has not yet ended, the state is ACTIVE.
160
+ // At exactly `gracePeriodEnds`, the grace period has elapsed so we fall through to the quorum check.
161
+ if (_scorecard.gracePeriodEnds > block.timestamp) {
156
162
  return DefifaScorecardState.ACTIVE;
157
163
  }
158
164
 
@@ -363,8 +369,12 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
363
369
  uint256 _timeUntilAttestationsBegin =
364
370
  block.timestamp > _attestationStartTime ? 0 : _attestationStartTime - block.timestamp;
365
371
 
366
- _scorecard.attestationsBegin = uint48(block.timestamp + _timeUntilAttestationsBegin);
367
- _scorecard.gracePeriodEnds = uint48(block.timestamp + attestationGracePeriodOf(_gameId));
372
+ uint48 _attestationsBegin = uint48(block.timestamp + _timeUntilAttestationsBegin);
373
+ _scorecard.attestationsBegin = _attestationsBegin;
374
+ // Grace period extends from when attestations begin, not from submission time.
375
+ // This prevents the grace period from expiring before attestations even start
376
+ // when a scorecard is submitted early.
377
+ _scorecard.gracePeriodEnds = uint48(_attestationsBegin + attestationGracePeriodOf(_gameId));
368
378
 
369
379
  // Keep a reference to the default attestation delegate.
370
380
  address _defaultAttestationDelegate = IDefifaHook(_metadata.dataHook).defaultAttestationDelegate();
@@ -459,8 +469,13 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
459
469
  // slither-disable-next-line unused-return
460
470
  Address.verifyCallResult({success: success, returndata: returndata});
461
471
 
462
- // Fulfill any commitments for the game.
463
- IDefifaDeployer(controller.PROJECTS().ownerOf(gameId)).fulfillCommitmentsOf(gameId);
472
+ // Fulfill any commitments for the game. Wrapped in try-catch so that a fulfillment
473
+ // failure (e.g. from sendPayoutsOf reverting) does not permanently block ratification.
474
+ // Fulfillment can be retried separately by calling fulfillCommitmentsOf directly.
475
+ try IDefifaDeployer(controller.PROJECTS().ownerOf(gameId)).fulfillCommitmentsOf(gameId) {}
476
+ catch (bytes memory reason) {
477
+ emit FulfillmentFailed(gameId, reason);
478
+ }
464
479
 
465
480
  emit ScorecardRatified(gameId, scorecardId, msg.sender);
466
481
  }
@@ -54,7 +54,6 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
54
54
  error DefifaHook_NothingToClaim();
55
55
  error DefifaHook_NothingToMint();
56
56
  error DefifaHook_WrongCurrency();
57
- error DefifaHook_NoContest();
58
57
  error DefifaHook_Overspending();
59
58
  error DefifaHook_CashoutWeightsAlreadySet();
60
59
  error DefifaHook_ReservedTokenMintingPaused();
@@ -610,11 +609,6 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
610
609
  // Make sure the cashOut weights haven't already been set.
611
610
  if (cashOutWeightIsSet) revert DefifaHook_CashoutWeightsAlreadySet();
612
611
 
613
- // Make sure the game is not in no contest.
614
- if (_gamePhase == DefifaGamePhase.NO_CONTEST) {
615
- revert DefifaHook_NoContest();
616
- }
617
-
618
612
  // Validate weights and build the array. Reverts on invalid input.
619
613
  _tierCashOutWeights =
620
614
  DefifaHookLib.validateAndBuildWeights({tierWeights: tierWeights, _store: store, hook: address(this)});
@@ -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
+ }