@ballkidz/defifa 0.0.17 → 0.0.19

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.
@@ -8,6 +8,12 @@ import {DefifaTierCashOutWeight} from "../structs/DefifaTierCashOutWeight.sol";
8
8
 
9
9
  /// @notice Manages the ratification of Defifa scorecards through attestation-based governance.
10
10
  interface IDefifaGovernor {
11
+ /// @notice Emitted when governance is initialized for a game.
12
+ /// @param gameId The ID of the game.
13
+ /// @param attestationStartTime The timestamp when attestation begins.
14
+ /// @param attestationGracePeriod The grace period after attestation begins.
15
+ /// @param timelockDuration The timelock duration after quorum is met.
16
+ /// @param caller The address that initialized the game.
11
17
  event GameInitialized(
12
18
  uint256 indexed gameId,
13
19
  uint256 attestationStartTime,
@@ -16,10 +22,25 @@ interface IDefifaGovernor {
16
22
  address caller
17
23
  );
18
24
 
25
+ /// @notice Emitted when an account attests to a scorecard.
26
+ /// @param gameId The ID of the game.
27
+ /// @param scorecardId The ID of the scorecard being attested to.
28
+ /// @param weight The attestation weight applied.
29
+ /// @param caller The address that submitted the attestation.
19
30
  event ScorecardAttested(uint256 indexed gameId, uint256 indexed scorecardId, uint256 weight, address caller);
20
31
 
32
+ /// @notice Emitted when a scorecard is ratified.
33
+ /// @param gameId The ID of the game.
34
+ /// @param scorecardId The ID of the ratified scorecard.
35
+ /// @param caller The address that ratified the scorecard.
21
36
  event ScorecardRatified(uint256 indexed gameId, uint256 indexed scorecardId, address caller);
22
37
 
38
+ /// @notice Emitted when a scorecard is submitted for attestation.
39
+ /// @param gameId The ID of the game.
40
+ /// @param scorecardId The ID of the submitted scorecard.
41
+ /// @param tierWeights The proposed tier cash out weights.
42
+ /// @param isDefaultAttestationDelegate Whether the submitter is the default attestation delegate.
43
+ /// @param caller The address that submitted the scorecard.
23
44
  event ScorecardSubmitted(
24
45
  uint256 indexed gameId,
25
46
  uint256 indexed scorecardId,
@@ -28,6 +49,11 @@ interface IDefifaGovernor {
28
49
  address caller
29
50
  );
30
51
 
52
+ /// @notice Emitted when an attestation is revoked from a scorecard.
53
+ /// @param gameId The ID of the game.
54
+ /// @param scorecardId The ID of the scorecard.
55
+ /// @param account The address whose attestation was revoked.
56
+ /// @param weight The revoked attestation weight.
31
57
  event AttestationRevoked(uint256 indexed gameId, uint256 indexed scorecardId, address account, uint256 weight);
32
58
 
33
59
  /// @notice The number of attestations for a scorecard.
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {IJB721Hook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Hook.sol";
5
4
  import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
6
5
  import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
6
+ import {IJB721Hook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Hook.sol";
7
7
  import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
8
8
  import {JB721TiersMintReservesConfig} from "@bananapus/721-hook-v6/src/structs/JB721TiersMintReservesConfig.sol";
9
9
  import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
@@ -17,6 +17,12 @@ import {IDefifaGamePotReporter} from "./IDefifaGamePotReporter.sol";
17
17
  /// @notice The hook interface for Defifa games, extending the 721 hook with game-specific attestation delegation,
18
18
  /// scorecard-based cash out weights, and token claiming.
19
19
  interface IDefifaHook is IJB721Hook {
20
+ /// @notice Emitted when an NFT is minted from a contribution.
21
+ /// @param tokenId The token ID of the minted NFT.
22
+ /// @param tierId The tier the NFT was minted from.
23
+ /// @param beneficiary The address that received the NFT.
24
+ /// @param totalAmountContributed The total amount contributed in the minting transaction.
25
+ /// @param caller The address that triggered the mint.
20
26
  event Mint(
21
27
  uint256 indexed tokenId,
22
28
  uint256 indexed tierId,
@@ -25,20 +31,43 @@ interface IDefifaHook is IJB721Hook {
25
31
  address caller
26
32
  );
27
33
 
34
+ /// @notice Emitted when a reserved token is minted.
35
+ /// @param tokenId The token ID of the minted reserved token.
36
+ /// @param tierId The tier the reserved token was minted from.
37
+ /// @param beneficiary The address that received the reserved token.
38
+ /// @param caller The address that triggered the mint.
28
39
  event MintReservedToken(
29
40
  uint256 indexed tokenId, uint256 indexed tierId, address indexed beneficiary, address caller
30
41
  );
31
42
 
43
+ /// @notice Emitted when a delegate's attestation balance changes for a tier.
44
+ /// @param delegate The delegate whose attestation balance changed.
45
+ /// @param tierId The tier whose attestation balance changed.
46
+ /// @param previousBalance The prior attestation balance.
47
+ /// @param newBalance The updated attestation balance.
48
+ /// @param caller The address that triggered the change.
32
49
  event TierDelegateAttestationsChanged(
33
50
  address indexed delegate, uint256 indexed tierId, uint256 previousBalance, uint256 newBalance, address caller
34
51
  );
35
52
 
53
+ /// @notice Emitted when a delegator changes delegates for a tier.
54
+ /// @param delegator The address changing its delegate.
55
+ /// @param fromDelegate The previous delegate.
56
+ /// @param toDelegate The new delegate.
36
57
  event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);
37
58
 
59
+ /// @notice Emitted when claimable game tokens are claimed.
60
+ /// @param beneficiary The address receiving the claimed tokens.
61
+ /// @param defifaTokenAmount The amount of Defifa tokens claimed.
62
+ /// @param baseProtocolTokenAmount The amount of base protocol tokens claimed.
63
+ /// @param caller The address that triggered the claim.
38
64
  event ClaimedTokens(
39
65
  address indexed beneficiary, uint256 defifaTokenAmount, uint256 baseProtocolTokenAmount, address caller
40
66
  );
41
67
 
68
+ /// @notice Emitted when tier cash out weights are set.
69
+ /// @param tierWeights The cash out weights that were set for each tier.
70
+ /// @param caller The address that set the tier weights.
42
71
  event TierCashOutWeightsSet(DefifaTierCashOutWeight[] tierWeights, address caller);
43
72
 
44
73
  /// @notice The total amount redeemed from this game (refunds not counted).
@@ -62,6 +62,7 @@ library DefifaHookLib {
62
62
  lastTierId = tierWeights[i].id;
63
63
 
64
64
  // Get the tier.
65
+ // slither-disable-next-line calls-loop
65
66
  tier = hookStore.tierOf({hook: hook, id: tierWeights[i].id, includeResolvedUri: false});
66
67
 
67
68
  // Can't set a cashOut weight for tiers not in category 0.
@@ -104,9 +105,11 @@ library DefifaHookLib {
104
105
  returns (uint256)
105
106
  {
106
107
  // Keep a reference to the token's tier ID.
108
+ // slither-disable-next-line calls-loop
107
109
  uint256 tierId = hookStore.tierIdOfToken(tokenId);
108
110
 
109
111
  // Keep a reference to the tier.
112
+ // slither-disable-next-line calls-loop
110
113
  JB721Tier memory tier = hookStore.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
111
114
 
112
115
  // Get the tier's weight.
@@ -116,6 +119,7 @@ library DefifaHookLib {
116
119
  if (weight == 0) return 0;
117
120
 
118
121
  // Get the amount of tokens that have already been burned.
122
+ // slither-disable-next-line calls-loop
119
123
  uint256 burnedTokens = hookStore.numberOfBurnedFor({hook: hook, tierId: tierId});
120
124
 
121
125
  // If no tiers were minted, nothing to redeem.
@@ -129,6 +133,7 @@ library DefifaHookLib {
129
133
  // could cash out before reserves are minted and extract value that should be diluted across
130
134
  // both paid and reserved holders. By counting pending reserves, each token's share of the
131
135
  // tier weight is computed against the full eventual supply.
136
+ // slither-disable-next-line calls-loop
132
137
  uint256 pendingReserves = hookStore.numberOfPendingReservesFor({hook: hook, tierId: tierId});
133
138
  totalTokensForCashoutInTier += pendingReserves;
134
139
 
@@ -202,6 +207,7 @@ library DefifaHookLib {
202
207
  // Calculate the amount paid to mint the tokens that are being burned.
203
208
  uint256 cumulativeMintPrice;
204
209
  for (uint256 i; i < numberOfTokens; i++) {
210
+ // slither-disable-next-line calls-loop
205
211
  cumulativeMintPrice += hookStore.tierOfTokenId({
206
212
  hook: hook, tokenId: tokenIds[i], includeResolvedUri: false
207
213
  })
@@ -229,6 +235,7 @@ library DefifaHookLib {
229
235
  {
230
236
  uint256 numberOfTokenIds = tokenIds.length;
231
237
  for (uint256 i; i < numberOfTokenIds; i++) {
238
+ // slither-disable-next-line calls-loop
232
239
  cumulativeMintPrice += hookStore.tierOfTokenId({
233
240
  hook: hook, tokenId: tokenIds[i], includeResolvedUri: false
234
241
  })
@@ -323,6 +330,7 @@ library DefifaHookLib {
323
330
  }
324
331
  if (tierIdsToMint[i] < currentTierId) revert DefifaHook_BadTierOrder();
325
332
  currentTierId = tierIdsToMint[i];
333
+ // slither-disable-next-line calls-loop
326
334
  attestationUnits =
327
335
  hookStore.tierOf({hook: hook, id: currentTierId, includeResolvedUri: false}).votingUnits;
328
336
  accumulated = attestationUnits;
@@ -433,7 +433,7 @@ contract DefifaGovernorTest is JBTest, TestBaseWorkflow {
433
433
  // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
434
434
  for (uint256 i = 0; i < scorecards.length; i++) {
435
435
  scorecards[i].id = i + 1;
436
- scorecards[i].cashOutWeight = i % 2 == 0 ? 1_000_000_000 / (scorecards.length / 2) : 0;
436
+ scorecards[i].cashOutWeight = i % 2 == 0 ? 1e18 / (scorecards.length / 2) : 0;
437
437
  }
438
438
  // Forward time so proposals can be created
439
439
  uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
@@ -1019,7 +1019,7 @@ contract DefifaGovernorTest is JBTest, TestBaseWorkflow {
1019
1019
  // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
1020
1020
  for (uint256 i = 0; i < scorecards.length; i++) {
1021
1021
  scorecards[i].id = i + 1;
1022
- scorecards[i].cashOutWeight = i % 2 == 0 ? 1_000_000_000 / (scorecards.length / 2) : 0;
1022
+ scorecards[i].cashOutWeight = i % 2 == 0 ? 1e18 / (scorecards.length / 2) : 0;
1023
1023
  }
1024
1024
 
1025
1025
  vm.expectRevert(abi.encodeWithSignature("DefifaGovernor_UnownedProposedCashoutValue()"));
@@ -1111,7 +1111,7 @@ contract DefifaGovernorTest is JBTest, TestBaseWorkflow {
1111
1111
  // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
1112
1112
  for (uint256 i = 0; i < scorecards.length; i++) {
1113
1113
  scorecards[i].id = i + 1;
1114
- scorecards[i].cashOutWeight = i % 2 == 0 ? 1_000_000_000 / (scorecards.length / 2) : 0;
1114
+ scorecards[i].cashOutWeight = i % 2 == 0 ? 1e18 / (scorecards.length / 2) : 0;
1115
1115
  }
1116
1116
  // Forward time so proposals can be created
1117
1117
  uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
@@ -1132,80 +1132,103 @@ contract DefifaGovernorTest is JBTest, TestBaseWorkflow {
1132
1132
  // As a sanity check we let it also run for less than 10 to see if it does not error in that case.
1133
1133
  nTiers = uint8(bound(nTiers, 2, 20));
1134
1134
 
1135
+ // With exact-weight validation, only nTiers == 10 produces weights that sum to TOTAL_CASHOUT_WEIGHT.
1136
+ // Delegate to separate helpers to avoid stack-too-deep.
1137
+ if (nTiers == 10) {
1138
+ _testCashOutWeightExact(nTiers);
1139
+ } else {
1140
+ _testCashOutWeightInvalid(nTiers);
1141
+ }
1142
+ }
1143
+
1144
+ /// @dev nTiers == 10: all weights valid, full flow (submit → vote → ratify).
1145
+ function _testCashOutWeightExact(uint8 nTiers) internal {
1135
1146
  address[] memory _users = new address[](nTiers);
1136
1147
  DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
1137
1148
  (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
1138
1149
 
1139
1150
  uint256 cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 10;
1140
1151
 
1141
- // Phase 1: Mint
1142
1152
  vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
1143
- //deployer.queueNextPhaseOf(_projectId);
1144
1153
  for (uint256 i = 0; i < nTiers; i++) {
1145
- // Generate a new address for each tier
1146
1154
  _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
1147
- // fund user
1148
1155
  vm.deal(_users[i], 1 ether);
1149
- // Build metadata to buy specific NFT
1150
1156
  uint16[] memory rawMetadata = new uint16[](1);
1151
1157
  // forge-lint: disable-next-line(unsafe-typecast)
1152
- rawMetadata[0] = uint16(i + 1); // reward tier, 1 indexed
1158
+ rawMetadata[0] = uint16(i + 1);
1153
1159
  bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
1154
- // Pay to the project and mint an NFT
1155
1160
  vm.prank(_users[i]);
1156
1161
  jbMultiTerminal().pay{value: 1 ether}(
1157
1162
  _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
1158
1163
  );
1159
- // Set the delegate as the user themselves
1160
1164
  DefifaDelegation[] memory tiered721SetDelegatesData = new DefifaDelegation[](1);
1161
1165
  tiered721SetDelegatesData[0] = DefifaDelegation({delegatee: _users[i], tierId: uint256(i + 1)});
1162
1166
  vm.prank(_users[i]);
1163
1167
  _nft.setTierDelegatesTo(tiered721SetDelegatesData);
1164
- // Forward 1 block, user should receive all the voting power of the tier, as its the only NFT
1165
1168
  assertEq(
1166
1169
  _governor.MAX_ATTESTATION_POWER_TIER(),
1167
1170
  // forge-lint: disable-next-line(unsafe-typecast)
1168
1171
  _governor.getAttestationWeight(_gameId, _users[i], uint48(block.timestamp))
1169
1172
  );
1170
1173
  }
1171
- // Warp to scoring phase (past start time)
1172
1174
  vm.warp(defifaData.start + 1);
1173
1175
 
1174
- // Generate the scorecards
1175
1176
  DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
1176
-
1177
- // We can't have a neutral outcome, so we only give shares to tiers that are an even number (in our array)
1178
1177
  for (uint256 i = 0; i < scorecards.length; i++) {
1179
1178
  scorecards[i].id = i + 1;
1180
1179
  scorecards[i].cashOutWeight = cashOutWeight;
1181
1180
  }
1182
1181
 
1183
- // Forward time so proposals can be created
1184
1182
  uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
1185
- // Forward time so voting becomes active
1186
1183
  vm.warp(block.timestamp + _governor.attestationStartTimeOf(_gameId));
1187
- // No voting delay after the initial voting delay has passed in
1188
- // assertEq(_governor.attestationStartTimeOf(_gameId), 0);
1189
- // All the users vote
1190
- // 0 = Against
1191
- // 1 = For
1192
- // 2 = Abstain
1193
1184
  for (uint256 i = 0; i < _users.length; i++) {
1194
1185
  vm.prank(_users[i]);
1195
1186
  _governor.attestToScorecardFrom(_gameId, _proposalId);
1196
1187
  }
1197
-
1198
- // Forward the amount of blocks needed to reach the end (and round up)
1199
1188
  vm.warp(block.timestamp + _governor.attestationGracePeriodOf(_gameId) + 1);
1189
+ _governor.ratifyScorecardFrom(_gameId, scorecards);
1190
+ }
1191
+
1192
+ /// @dev nTiers != 10: weights don't sum to TOTAL_CASHOUT_WEIGHT, submitScorecardFor reverts.
1193
+ function _testCashOutWeightInvalid(uint8 nTiers) internal {
1194
+ address[] memory _users = new address[](nTiers);
1195
+ DefifaLaunchProjectData memory defifaData = getBasicDefifaLaunchData(nTiers);
1196
+ (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = createDefifaProject(defifaData);
1200
1197
 
1201
- // With exact-weight validation, only nTiers == 10 produces an exact sum.
1202
- // Any other count (under or over) triggers INVALID_CASHOUT_WEIGHTS.
1203
- if (nTiers != 10) {
1204
- vm.expectRevert(DefifaHook.DefifaHook_InvalidCashoutWeights.selector);
1198
+ uint256 cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 10;
1199
+
1200
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
1201
+ for (uint256 i = 0; i < nTiers; i++) {
1202
+ _users[i] = address(bytes20(keccak256(abi.encode("user", Strings.toString(i)))));
1203
+ vm.deal(_users[i], 1 ether);
1204
+ uint16[] memory rawMetadata = new uint16[](1);
1205
+ // forge-lint: disable-next-line(unsafe-typecast)
1206
+ rawMetadata[0] = uint16(i + 1);
1207
+ bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
1208
+ vm.prank(_users[i]);
1209
+ jbMultiTerminal().pay{value: 1 ether}(
1210
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
1211
+ );
1212
+ DefifaDelegation[] memory tiered721SetDelegatesData = new DefifaDelegation[](1);
1213
+ tiered721SetDelegatesData[0] = DefifaDelegation({delegatee: _users[i], tierId: uint256(i + 1)});
1214
+ vm.prank(_users[i]);
1215
+ _nft.setTierDelegatesTo(tiered721SetDelegatesData);
1216
+ assertEq(
1217
+ _governor.MAX_ATTESTATION_POWER_TIER(),
1218
+ // forge-lint: disable-next-line(unsafe-typecast)
1219
+ _governor.getAttestationWeight(_gameId, _users[i], uint48(block.timestamp))
1220
+ );
1205
1221
  }
1222
+ vm.warp(defifaData.start + 1);
1206
1223
 
1207
- // Execute the proposal
1208
- _governor.ratifyScorecardFrom(_gameId, scorecards);
1224
+ DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
1225
+ for (uint256 i = 0; i < scorecards.length; i++) {
1226
+ scorecards[i].id = i + 1;
1227
+ scorecards[i].cashOutWeight = cashOutWeight;
1228
+ }
1229
+
1230
+ vm.expectRevert(DefifaHook.DefifaHook_InvalidCashoutWeights.selector);
1231
+ _governor.submitScorecardFor(_gameId, scorecards);
1209
1232
  }
1210
1233
 
1211
1234
  function getBasicDefifaLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
@@ -253,10 +253,8 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
253
253
  sc[i].cashOutWeight = (_nft.TOTAL_CASHOUT_WEIGHT() * 30) / 100; // 120% total
254
254
  }
255
255
 
256
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
257
- _attestAllFor(pid);
258
256
  vm.expectRevert(DefifaHook.DefifaHook_InvalidCashoutWeights.selector);
259
- _gov.ratifyScorecardFrom(_gameId, sc);
257
+ _gov.submitScorecardFor(_gameId, sc);
260
258
  }
261
259
 
262
260
  // =========================================================================
package/test/Fork.t.sol CHANGED
@@ -339,10 +339,8 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
339
339
  sc[i].cashOutWeight = (_nft.TOTAL_CASHOUT_WEIGHT() * 30) / 100; // 120% total
340
340
  }
341
341
 
342
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
343
- _attestAllFor(pid);
344
342
  vm.expectRevert(DefifaHook.DefifaHook_InvalidCashoutWeights.selector);
345
- _gov.ratifyScorecardFrom(_gameId, sc);
343
+ _gov.submitScorecardFor(_gameId, sc);
346
344
  }
347
345
 
348
346
  // =========================================================================
@@ -358,10 +356,8 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
358
356
  sc[i].cashOutWeight = (_nft.TOTAL_CASHOUT_WEIGHT() * 20) / 100; // 80% total
359
357
  }
360
358
 
361
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
362
- _attestAllFor(pid);
363
359
  vm.expectRevert(DefifaHook.DefifaHook_InvalidCashoutWeights.selector);
364
- _gov.ratifyScorecardFrom(_gameId, sc);
360
+ _gov.submitScorecardFor(_gameId, sc);
365
361
  }
366
362
 
367
363
  // =========================================================================
@@ -2010,10 +2006,8 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
2010
2006
  sc[2] = DefifaTierCashOutWeight({id: 2, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT() / 4});
2011
2007
  sc[3] = DefifaTierCashOutWeight({id: 4, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT() / 4});
2012
2008
 
2013
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
2014
- _attestAllFor(pid);
2015
2009
  vm.expectRevert(DefifaHookLib.DefifaHook_BadTierOrder.selector);
2016
- _gov.ratifyScorecardFrom(_gameId, sc);
2010
+ _gov.submitScorecardFor(_gameId, sc);
2017
2011
  }
2018
2012
 
2019
2013
  // =========================================================================
@@ -0,0 +1,191 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity 0.8.28;
3
+
4
+ import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
5
+ import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
6
+ import {DefifaHook} from "../../src/DefifaHook.sol";
7
+ import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
8
+ import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
9
+ import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
10
+
11
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
12
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
13
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
14
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
15
+ import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
16
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
17
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
18
+ import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
19
+ import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
20
+ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
21
+ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
22
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
23
+ import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
24
+ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
25
+ import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
26
+
27
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
28
+ import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
29
+
30
+ contract CodexRegistryMismatchTest is JBTest, TestBaseWorkflow {
31
+ JBAddressRegistry internal registry;
32
+ DefifaDeployer internal deployer;
33
+
34
+ function setUp() public virtual override {
35
+ super.setUp();
36
+
37
+ JBAccountingContext[] memory tokens = new JBAccountingContext[](1);
38
+ tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
39
+
40
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
41
+ terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: tokens});
42
+
43
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
44
+ rulesetConfigs[0] = JBRulesetConfig({
45
+ mustStartAtOrAfter: 0,
46
+ duration: 10 days,
47
+ weight: 1e18,
48
+ weightCutPercent: 0,
49
+ approvalHook: IJBRulesetApprovalHook(address(0)),
50
+ metadata: JBRulesetMetadata({
51
+ reservedPercent: 0,
52
+ cashOutTaxRate: 0,
53
+ baseCurrency: JBCurrencyIds.ETH,
54
+ pausePay: false,
55
+ pauseCreditTransfers: false,
56
+ allowOwnerMinting: false,
57
+ allowSetCustomToken: false,
58
+ allowTerminalMigration: false,
59
+ allowSetTerminals: false,
60
+ allowSetController: false,
61
+ allowAddAccountingContext: false,
62
+ allowAddPriceFeed: false,
63
+ ownerMustSendPayouts: false,
64
+ holdFees: false,
65
+ useTotalSurplusForCashOuts: false,
66
+ useDataHookForPay: true,
67
+ useDataHookForCashOut: true,
68
+ dataHook: address(0),
69
+ metadata: 0
70
+ }),
71
+ splitGroups: new JBSplitGroup[](0),
72
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
73
+ });
74
+
75
+ address projectOwner = address(bytes20(keccak256("projectOwner")));
76
+ uint256 protocolFeeProjectId =
77
+ jbController().launchProjectFor(projectOwner, "", rulesetConfigs, terminalConfigs, "");
78
+ vm.prank(projectOwner);
79
+ address nanaToken =
80
+ address(jbController().deployERC20For(protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
81
+
82
+ uint256 defifaProjectId = jbController().launchProjectFor(projectOwner, "", rulesetConfigs, terminalConfigs, "");
83
+ vm.prank(projectOwner);
84
+ address defifaToken = address(jbController().deployERC20For(defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
85
+
86
+ DefifaHook hookCodeOrigin = new DefifaHook(jbDirectory(), IERC20(defifaToken), IERC20(nanaToken));
87
+ DefifaGovernor governor = new DefifaGovernor(jbController(), address(this));
88
+ registry = new JBAddressRegistry();
89
+ deployer = new DefifaDeployer(
90
+ address(hookCodeOrigin),
91
+ new DefifaTokenUriResolver(ITypeface(address(0))),
92
+ governor,
93
+ jbController(),
94
+ registry,
95
+ defifaProjectId,
96
+ protocolFeeProjectId
97
+ );
98
+
99
+ hookCodeOrigin.transferOwnership(address(deployer));
100
+ governor.transferOwnership(address(deployer));
101
+ }
102
+
103
+ function test_launchRegistersActualHookAddressInRegistry() external {
104
+ uint256 projectId = deployer.launchGameWith(_launchData());
105
+ (, JBRulesetMetadata memory metadata,) = jbController().latestQueuedRulesetOf(projectId);
106
+ address actualHook = metadata.dataHook;
107
+
108
+ address expectedCreateAddress = _createAddress(address(deployer), 1);
109
+
110
+ assertNotEq(actualHook, address(0), "queued ruleset should reference the deployed hook");
111
+ assertNotEq(actualHook, expectedCreateAddress, "cloneDeterministic did not use CREATE");
112
+ assertEq(registry.deployerOf(actualHook), address(deployer), "actual hook should be registered");
113
+ assertEq(
114
+ registry.deployerOf(expectedCreateAddress), address(0), "legacy CREATE address should stay unregistered"
115
+ );
116
+ }
117
+
118
+ function _launchData() internal returns (DefifaLaunchProjectData memory) {
119
+ DefifaTierParams[] memory tiers = new DefifaTierParams[](1);
120
+ tiers[0] = DefifaTierParams({
121
+ name: "Team 1",
122
+ reservedRate: 0,
123
+ reservedTokenBeneficiary: address(0),
124
+ encodedIPFSUri: bytes32(0),
125
+ shouldUseReservedTokenBeneficiaryAsDefault: false
126
+ });
127
+
128
+ return DefifaLaunchProjectData({
129
+ name: "DEFIFA",
130
+ projectUri: "",
131
+ contractUri: "",
132
+ baseUri: "",
133
+ tiers: tiers,
134
+ tierPrice: 1 ether,
135
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
136
+ mintPeriodDuration: 1 days,
137
+ refundPeriodDuration: 1 days,
138
+ start: uint48(block.timestamp + 3 days),
139
+ splits: new JBSplit[](0),
140
+ attestationStartTime: 0,
141
+ attestationGracePeriod: 100_381,
142
+ defaultAttestationDelegate: address(0),
143
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
144
+ terminal: jbMultiTerminal(),
145
+ store: new JB721TiersHookStore(),
146
+ minParticipation: 0,
147
+ scorecardTimeout: 0,
148
+ timelockDuration: 0
149
+ });
150
+ }
151
+
152
+ function _createAddress(address origin, uint256 nonce) internal pure returns (address addr) {
153
+ bytes memory data;
154
+ if (nonce == 0x00) {
155
+ data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), origin, bytes1(0x80));
156
+ } else if (nonce <= 0x7f) {
157
+ // forge-lint: disable-next-line(unsafe-typecast)
158
+ data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), origin, uint8(nonce));
159
+ } else if (nonce <= 0xff) {
160
+ // forge-lint: disable-next-line(unsafe-typecast)
161
+ data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), origin, bytes1(0x81), uint8(nonce));
162
+ } else if (nonce <= 0xffff) {
163
+ // forge-lint: disable-next-line(unsafe-typecast)
164
+ data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), origin, bytes1(0x82), uint16(nonce));
165
+ } else if (nonce <= 0xffffff) {
166
+ // forge-lint: disable-next-line(unsafe-typecast)
167
+ data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), origin, bytes1(0x83), uint24(nonce));
168
+ } else if (nonce <= 0xffffffff) {
169
+ // forge-lint: disable-next-line(unsafe-typecast)
170
+ data = abi.encodePacked(bytes1(0xda), bytes1(0x94), origin, bytes1(0x84), uint32(nonce));
171
+ } else if (nonce <= 0xffffffffff) {
172
+ // forge-lint: disable-next-line(unsafe-typecast)
173
+ data = abi.encodePacked(bytes1(0xdb), bytes1(0x94), origin, bytes1(0x85), uint40(nonce));
174
+ } else if (nonce <= 0xffffffffffff) {
175
+ // forge-lint: disable-next-line(unsafe-typecast)
176
+ data = abi.encodePacked(bytes1(0xdc), bytes1(0x94), origin, bytes1(0x86), uint48(nonce));
177
+ } else if (nonce <= 0xffffffffffffff) {
178
+ // forge-lint: disable-next-line(unsafe-typecast)
179
+ data = abi.encodePacked(bytes1(0xdd), bytes1(0x94), origin, bytes1(0x87), uint56(nonce));
180
+ } else {
181
+ // forge-lint: disable-next-line(unsafe-typecast)
182
+ data = abi.encodePacked(bytes1(0xde), bytes1(0x94), origin, bytes1(0x88), uint64(nonce));
183
+ }
184
+
185
+ bytes32 hash = keccak256(data);
186
+ assembly {
187
+ mstore(0, hash)
188
+ addr := mload(0)
189
+ }
190
+ }
191
+ }
@@ -193,6 +193,45 @@ contract PendingReserveSnapshotBypassTest is JBTest, TestBaseWorkflow {
193
193
  );
194
194
  }
195
195
 
196
+ /// @notice Pending reserve mints in the delayed-attestation window must not change BWA power.
197
+ function test_mintingPendingReserveBeforeDelayedAttestationDoesNotChangeBWA() external {
198
+ DefifaLaunchProjectData memory data = _launchData();
199
+ data.attestationStartTime = uint48(block.timestamp + 5 days);
200
+
201
+ (_pid, _nft, _gov) = _launch(data);
202
+
203
+ vm.warp(block.timestamp + 1 days + 1);
204
+ _mint(_player0, 1);
205
+ _mint(_player1, 2);
206
+ _mint(_player2, 3);
207
+ _mint(_player3, 4);
208
+ _delegateSelf(_player0, 1);
209
+ _delegateSelf(_player1, 2);
210
+ _delegateSelf(_player2, 3);
211
+ _delegateSelf(_player3, 4);
212
+
213
+ vm.warp(block.timestamp + 2 days);
214
+
215
+ DefifaTierCashOutWeight[] memory scorecard = _evenScorecard();
216
+ uint256 scorecardId = _gov.submitScorecardFor(_gameId, scorecard);
217
+ uint48 futureSnapshotTime = uint48(_gov.attestationStartTimeOf(_gameId) - 1);
218
+
219
+ uint256 preRaw = _gov.getAttestationWeight(_gameId, _player0, futureSnapshotTime);
220
+ uint256 preBwa = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player0, futureSnapshotTime);
221
+
222
+ JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
223
+ reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
224
+ _nft.mintReservesFor(reserveConfigs);
225
+
226
+ uint256 postRaw = _gov.getAttestationWeight(_gameId, _player0, futureSnapshotTime);
227
+ uint256 postBwa = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player0, futureSnapshotTime);
228
+
229
+ assertEq(preRaw, 500_000_000, "future raw snapshot includes the pending reserve exactly once");
230
+ assertEq(preBwa, 375_000_000, "future BWA starts from the reserve-adjusted submission denominator");
231
+ assertEq(postRaw, preRaw, "future raw power stays frozen before attestation begins");
232
+ assertEq(postBwa, preBwa, "reserve mint in delayed window must not change BWA power");
233
+ }
234
+
196
235
  function _evenScorecard() internal view returns (DefifaTierCashOutWeight[] memory scorecard) {
197
236
  scorecard = new DefifaTierCashOutWeight[](4);
198
237
  uint256 totalWeight = _nft.TOTAL_CASHOUT_WEIGHT();
@@ -259,6 +298,7 @@ contract PendingReserveSnapshotBypassTest is JBTest, TestBaseWorkflow {
259
298
  function _mint(address user, uint256 tierId) internal {
260
299
  vm.deal(user, 1 ether);
261
300
  uint16[] memory tiers = new uint16[](1);
301
+ // forge-lint: disable-next-line(unsafe-typecast)
262
302
  tiers[0] = uint16(tierId);
263
303
  bytes[] memory data = new bytes[](1);
264
304
  data[0] = abi.encode(user, tiers);
@@ -178,8 +178,8 @@ contract AttestationDelegateBeneficiary is JBTest, TestBaseWorkflow {
178
178
  assertEq(delegate, user, "Default delegate should be self when payer == beneficiary");
179
179
  }
180
180
 
181
- /// @notice When an explicit delegate is set, it should be used regardless of payer/beneficiary.
182
- function test_explicitDelegateOverridesDefault() public {
181
+ /// @notice A third-party payer cannot override the beneficiary's delegate.
182
+ function test_explicitDelegateFromThirdPartyDoesNotOverrideBeneficiaryDefault() public {
183
183
  address payer = address(bytes20(keccak256("payer2")));
184
184
  address beneficiary = address(bytes20(keccak256("beneficiary2")));
185
185
  address explicitDelegate = address(bytes20(keccak256("explicitDelegate")));
@@ -204,10 +204,8 @@ contract AttestationDelegateBeneficiary is JBTest, TestBaseWorkflow {
204
204
  metadata: metadata
205
205
  });
206
206
 
207
- // With the fix, delegation is stored on the beneficiary's account, not the payer's.
208
207
  address beneficiaryDelegate = _nft.getTierDelegateOf(beneficiary, 1);
209
- assertEq(beneficiaryDelegate, explicitDelegate, "Explicit delegate should override default on beneficiary");
210
- // Payer should have no delegation.
208
+ assertEq(beneficiaryDelegate, beneficiary, "third-party payer cannot overwrite beneficiary delegation");
211
209
  address payerDelegate = _nft.getTierDelegateOf(payer, 1);
212
210
  assertEq(payerDelegate, address(0), "Payer should have no delegation when payer != beneficiary");
213
211
  }