@ballkidz/defifa 0.0.18 → 0.0.20

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/foundry.lock ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "lib/base64": {
3
+ "branch": {
4
+ "name": "v1.1.0",
5
+ "rev": "dcbf852ba545b3d15de0ac0ef88dce934c090c8e"
6
+ }
7
+ },
8
+ "lib/capsules": {
9
+ "rev": "7432ba95ac69ba4902076660b0dc9a90aeb26706"
10
+ },
11
+ "lib/forge-std": {
12
+ "rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824"
13
+ },
14
+ "lib/typeface": {
15
+ "rev": "edd52e2f7c8adc44e75b207bd279710f9b72f5e9"
16
+ }
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ballkidz/defifa",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -13,17 +13,17 @@
13
13
  "url": "https://github.com/BallKidz/defifa-collection-deployer"
14
14
  },
15
15
  "dependencies": {
16
- "@bananapus/721-hook-v6": "^0.0.30",
17
- "@bananapus/address-registry-v6": "^0.0.16",
18
- "@bananapus/core-v6": "^0.0.30",
16
+ "@bananapus/721-hook-v6": "^0.0.31",
17
+ "@bananapus/core-v6": "^0.0.31",
19
18
  "@bananapus/permission-ids-v6": "^0.0.15",
20
- "@croptop/core-v6": "^0.0.26",
19
+ "@croptop/core-v6": "^0.0.30",
21
20
  "@openzeppelin/contracts": "^5.6.1",
22
21
  "@prb/math": "^4.1.1",
23
- "@rev-net/core-v6": "^0.0.24",
22
+ "@rev-net/core-v6": "^0.0.28",
24
23
  "scripty.sol": "^2.1.1"
25
24
  },
26
25
  "devDependencies": {
26
+ "@bananapus/address-registry-v6": "^0.0.17",
27
27
  "@sphinx-labs/plugins": "^0.33.1"
28
28
  },
29
29
  "scripts": {
@@ -22,9 +22,8 @@ contract DeployMainnet is Script, Sphinx {
22
22
  /// @notice tracks the deployment of the address registry for the chain we are deploying to.
23
23
  AddressRegistryDeployment registry;
24
24
 
25
- // NOTE: This id is revnet, this is temporary until we have a defifa revnet.
26
- uint256 _defifaProjectId = 3;
27
- uint256 _baseProtocolProjectId = 1;
25
+ uint256 _defifaProjectId;
26
+ uint256 _baseProtocolProjectId;
28
27
 
29
28
  bytes32 _salt = bytes32(keccak256("0.0.2"));
30
29
 
@@ -53,6 +52,9 @@ contract DeployMainnet is Script, Sphinx {
53
52
  )
54
53
  );
55
54
 
55
+ _defifaProjectId = vm.envUint("DEFIFA_PROJECT_ID");
56
+ _baseProtocolProjectId = vm.envUint("BASE_PROTOCOL_PROJECT_ID");
57
+
56
58
  defifaToken = IERC20(address(core.tokens.tokenOf(_defifaProjectId)));
57
59
  baseProtocolToken = IERC20(address(core.tokens.tokenOf(_baseProtocolProjectId)));
58
60
 
@@ -275,6 +275,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
275
275
  uint256 _defifaProjectId,
276
276
  uint256 _baseProtocolProjectId
277
277
  ) {
278
+ // slither-disable-next-line missing-zero-check
278
279
  HOOK_CODE_ORIGIN = _hookCodeOrigin;
279
280
  TOKEN_URI_RESOLVER = _tokenUriResolver;
280
281
  GOVERNOR = _governor;
@@ -549,8 +550,13 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
549
550
  // Transfer ownership to the specified owner.
550
551
  hook.transferOwnership(address(GOVERNOR));
551
552
 
552
- // Add the hook to the registry, contract nonce starts at 1
553
- REGISTRY.registerAddress({deployer: address(this), nonce: currentNonce});
553
+ // Register the actual CREATE2 clone address using the same salt and minimal-proxy init code
554
+ // that produced the deployed hook.
555
+ REGISTRY.registerAddress({
556
+ deployer: address(this),
557
+ salt: keccak256(abi.encodePacked(msg.sender, currentNonce)),
558
+ bytecode: _cloneCreationCodeFor(address(HOOK_CODE_ORIGIN))
559
+ });
554
560
 
555
561
  // slither-disable-next-line reentrancy-events
556
562
  emit LaunchGame(gameId, hook, GOVERNOR, uriResolver, msg.sender);
@@ -939,4 +945,19 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
939
945
  projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game has finished."
940
946
  });
941
947
  }
948
+
949
+ /// @notice Returns the minimal-proxy init code used to deploy a clone for the provided implementation.
950
+ /// @dev Defifa deploys hooks with `Clones.cloneDeterministic`, which uses `CREATE2`.
951
+ /// The address registry's CREATE2 path must be given the exact init code hash that was used at deployment time,
952
+ /// not the runtime bytecode and not a CREATE nonce. This helper reconstructs the standard EIP-1167 creation code
953
+ /// by inserting the implementation address into OpenZeppelin's minimal-proxy init-code template.
954
+ /// @param implementation The contract address the clone will delegate all calls to.
955
+ /// @return bytecode The full EIP-1167 creation bytecode hashed by CREATE2 to derive the clone address.
956
+ function _cloneCreationCodeFor(address implementation) internal pure returns (bytes memory bytecode) {
957
+ // EIP-1167 minimal proxy init code, mirroring OpenZeppelin's Clones.sol layout:
958
+ // [prefix (20 bytes)] [implementation address (20 bytes)] [suffix (15 bytes)]
959
+ bytecode = abi.encodePacked(
960
+ hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", bytes20(implementation), hex"5af43d82803e903d91602b57fd5bf3"
961
+ );
962
+ }
942
963
  }
@@ -18,6 +18,7 @@ import {IDefifaHook} from "./interfaces/IDefifaHook.sol";
18
18
  import {DefifaAttestations} from "./structs/DefifaAttestations.sol";
19
19
  import {DefifaScorecard} from "./structs/DefifaScorecard.sol";
20
20
  import {DefifaTierCashOutWeight} from "./structs/DefifaTierCashOutWeight.sol";
21
+ import {DefifaHookLib} from "./libraries/DefifaHookLib.sol";
21
22
 
22
23
  /// @notice Manages the ratification of Defifa scorecards.
23
24
  contract DefifaGovernor is Ownable, IDefifaGovernor {
@@ -83,12 +84,20 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
83
84
  mapping(uint256 => mapping(uint256 => DefifaAttestations)) internal _scorecardAttestationsOf;
84
85
 
85
86
  /// @notice Snapshot of pending reserves per tier at scorecard submission time.
86
- /// @dev Prevents reserve dilution between submission and attestation.
87
+ /// @dev Used to keep unminted reserve units in the BWA denominator.
87
88
  /// @custom:param gameId The ID of the game.
88
89
  /// @custom:param scorecardId The ID of the scorecard.
89
90
  /// @custom:param tierId The tier ID (1-indexed).
90
91
  mapping(uint256 => mapping(uint256 => mapping(uint256 => uint256))) internal _pendingReservesSnapshotOf;
91
92
 
93
+ /// @notice Snapshot of each tier's minted attestation units at scorecard submission time.
94
+ /// @dev Caps later checkpoint reads so reserve mints after submission can't increase the denominator
95
+ /// before the pending-reserve snapshot is added back in.
96
+ /// @custom:param gameId The ID of the game.
97
+ /// @custom:param scorecardId The ID of the scorecard.
98
+ /// @custom:param tierId The tier ID (1-indexed).
99
+ mapping(uint256 => mapping(uint256 => mapping(uint256 => uint256))) internal _submittedTierAttestationUnitsOf;
100
+
92
101
  /// @notice Tier weights per scorecard for BWA computation.
93
102
  /// @custom:param gameId The ID of the game.
94
103
  /// @custom:param scorecardId The ID of the scorecard.
@@ -260,15 +269,23 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
260
269
  }
261
270
 
262
271
  // If there's a weight assigned to the tier, make sure there is a token backed by it.
272
+ // slither-disable-next-line calls-loop
263
273
  for (uint256 i; i < numberOfTierWeights; i++) {
264
- if (
265
- tierWeights[i].cashOutWeight > 0
266
- && IDefifaHook(metadata.dataHook).currentSupplyOfTier(tierWeights[i].id) == 0
267
- ) {
274
+ // A nonzero cashout weight is only valid once that tier has live ownership.
275
+ // slither-disable-next-line calls-loop
276
+ uint256 currentTierSupply = IDefifaHook(metadata.dataHook).currentSupplyOfTier(tierWeights[i].id);
277
+ if (tierWeights[i].cashOutWeight > 0 && currentTierSupply == 0) {
268
278
  revert DefifaGovernor_UnownedProposedCashoutValue();
269
279
  }
270
280
  }
271
281
 
282
+ // Run the same structural validation the hook will apply at ratification time so malformed
283
+ // scorecards fail on submission instead of reaching a misleading SUCCEEDED state first.
284
+ // slither-disable-next-line unused-return
285
+ DefifaHookLib.validateAndBuildWeights({
286
+ tierWeights: tierWeights, hookStore: IDefifaHook(metadata.dataHook).store(), hook: metadata.dataHook
287
+ });
288
+
272
289
  // Hash the scorecard.
273
290
  scorecardId =
274
291
  _hashScorecardOf({gameHook: metadata.dataHook, calldataBytes: _buildScorecardCalldataFor(tierWeights)});
@@ -295,15 +312,25 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
295
312
  _scorecardTierWeightsOf[gameId][scorecardId][tierWeights[i].id - 1] = tierWeights[i].cashOutWeight;
296
313
  }
297
314
 
298
- // Snapshot pending reserves for each tier at submission time.
299
- // This prevents reserve minting between submission and attestation from diluting votes.
315
+ // Snapshot each tier's pending reserves and minted attestation units at submission time.
316
+ // BWA later reads an account checkpoint at `attestationsBegin - 1`. If reserve mints happen
317
+ // after submission but before that checkpoint, clamp the live total back down to the minted
318
+ // units that existed at submission and only then add the snapshotted pending reserves.
300
319
  {
301
320
  IJB721TiersHookStore _store = IDefifaHook(metadata.dataHook).store();
302
321
  uint256 _numberOfTiers = _store.maxTierIdOf(metadata.dataHook);
322
+ // slither-disable-next-line calls-loop
303
323
  for (uint256 i; i < _numberOfTiers; i++) {
304
324
  uint256 tierId = i + 1;
305
- _pendingReservesSnapshotOf[gameId][scorecardId][tierId] =
306
- _store.numberOfPendingReservesFor(metadata.dataHook, tierId);
325
+ // slither-disable-next-line calls-loop
326
+ JB721Tier memory tier = _store.tierOf({hook: metadata.dataHook, id: tierId, includeResolvedUri: false});
327
+ // slither-disable-next-line calls-loop
328
+ uint256 pendingReserves = _store.numberOfPendingReservesFor(metadata.dataHook, tierId);
329
+ // slither-disable-next-line calls-loop
330
+ uint256 submittedTierAttestationUnits =
331
+ IDefifaHook(metadata.dataHook).currentSupplyOfTier(tierId) * tier.votingUnits;
332
+ _pendingReservesSnapshotOf[gameId][scorecardId][tierId] = pendingReserves;
333
+ _submittedTierAttestationUnitsOf[gameId][scorecardId][tierId] = submittedTierAttestationUnits;
307
334
  }
308
335
  }
309
336
 
@@ -490,16 +517,17 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
490
517
  // Get a reference to the number of tiers.
491
518
  uint256 numberOfTiers = store.maxTierIdOf(metadata.dataHook);
492
519
 
493
- // slither-disable-next-line calls-inside-a-loop
494
520
  for (uint256 i; i < numberOfTiers; i++) {
495
521
  // Tiers are 1-indexed.
496
522
  uint256 tierId = i + 1;
497
523
 
498
524
  // Get this account's attestation units within the tier (snapshot at timestamp).
525
+ // slither-disable-next-line calls-loop
499
526
  uint256 tierAttestationUnitsForAccount =
500
527
  hook.getPastTierAttestationUnitsOf({account: account, tier: tierId, timestamp: timestamp});
501
528
 
502
529
  // Get the total attestation units for this tier (snapshot at timestamp).
530
+ // slither-disable-next-line calls-loop
503
531
  uint256 tierTotalAttestationUnits =
504
532
  hook.getPastTierTotalAttestationUnitsOf({tier: tierId, timestamp: timestamp});
505
533
 
@@ -508,8 +536,10 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
508
536
  // When the reserve beneficiary later mints, their new NFTs add to the numerator while
509
537
  // pending reserves decrease by the same amount — so no one's voting power shifts.
510
538
  {
539
+ // slither-disable-next-line calls-loop
511
540
  uint256 pendingReserves = store.numberOfPendingReservesFor(metadata.dataHook, tierId);
512
541
  if (pendingReserves != 0) {
542
+ // slither-disable-next-line calls-loop
513
543
  JB721Tier memory tier =
514
544
  store.tierOf({hook: metadata.dataHook, id: tierId, includeResolvedUri: false});
515
545
  tierTotalAttestationUnits += pendingReserves * tier.votingUnits;
@@ -567,30 +597,33 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
567
597
  // Cache the total cashout weight denominator from the hook.
568
598
  uint256 totalCashOutWeight = hook.TOTAL_CASHOUT_WEIGHT();
569
599
 
570
- // slither-disable-next-line calls-inside-a-loop
571
600
  for (uint256 i; i < numberOfTiers; i++) {
572
601
  // Tiers are 1-indexed.
573
602
  uint256 tierId = i + 1;
574
603
 
575
604
  // Get this account's attestation units within the tier (snapshot at timestamp).
605
+ // slither-disable-next-line calls-loop
576
606
  uint256 tierAttestationUnitsForAccount =
577
607
  hook.getPastTierAttestationUnitsOf({account: account, tier: tierId, timestamp: timestamp});
578
608
 
579
609
  if (tierAttestationUnitsForAccount != 0) {
580
- // Get the total attestation units for this tier (snapshot at timestamp).
610
+ // Start from the checkpointed tier total at the requested timestamp.
611
+ // If reserve mints happened after submission, clamp them out before adding the
612
+ // pending-reserve snapshot back in so each reserve unit is counted exactly once.
613
+ // slither-disable-next-line calls-loop
581
614
  uint256 tierTotalAttestationUnits =
582
615
  hook.getPastTierTotalAttestationUnitsOf({tier: tierId, timestamp: timestamp});
616
+ uint256 submittedTierAttestationUnits = _submittedTierAttestationUnitsOf[gameId][scorecardId][tierId];
617
+ if (tierTotalAttestationUnits > submittedTierAttestationUnits) {
618
+ tierTotalAttestationUnits = submittedTierAttestationUnits;
619
+ }
583
620
 
584
- // Include unminted pending reserves in the total (denominator only).
585
- // Uses the snapshot taken at scorecard submission time to prevent reserve
586
- // minting between submission and attestation from diluting votes.
587
- {
588
- uint256 pendingReserves = _pendingReservesSnapshotOf[gameId][scorecardId][tierId];
589
- if (pendingReserves != 0) {
590
- JB721Tier memory tier =
591
- store.tierOf({hook: metadata.dataHook, id: tierId, includeResolvedUri: false});
592
- tierTotalAttestationUnits += pendingReserves * tier.votingUnits;
593
- }
621
+ uint256 pendingReserves = _pendingReservesSnapshotOf[gameId][scorecardId][tierId];
622
+ if (pendingReserves != 0) {
623
+ // slither-disable-next-line calls-loop
624
+ JB721Tier memory tier =
625
+ store.tierOf({hook: metadata.dataHook, id: tierId, includeResolvedUri: false});
626
+ tierTotalAttestationUnits += pendingReserves * tier.votingUnits;
594
627
  }
595
628
 
596
629
  // Raw power for this tier.
@@ -630,18 +663,18 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
630
663
  // Keep a reference to the total eligible tier weight.
631
664
  uint256 eligibleTierWeights;
632
665
 
633
- // slither-disable-next-line calls-inside-a-loop
634
666
  for (uint256 i; i < numberOfTiers; i++) {
635
667
  uint256 tierId = i + 1;
636
668
 
637
669
  // A tier contributes to quorum if it has circulating tokens OR unminted pending reserves.
638
- // Pending reserves exist when participation occurred (mints triggered reserve accrual),
639
- // even if all paid tokens were later burned during REFUND. The reserve beneficiary still
640
- // has a stake in that tier's outcome, so the tier should count toward governance quorum.
641
- if (
642
- hook.currentSupplyOfTier(tierId) != 0
643
- || store.numberOfPendingReservesFor(metadata.dataHook, tierId) != 0
644
- ) {
670
+ // Pending reserves still belong economically to the reserve beneficiary, even after the
671
+ // last paid token in the tier is burned during REFUND, so excluding them would let a
672
+ // burner erase another participant's quorum contribution without erasing their claim.
673
+ // slither-disable-next-line calls-loop
674
+ uint256 currentTierSupply = hook.currentSupplyOfTier(tierId);
675
+ // slither-disable-next-line calls-loop
676
+ uint256 pendingReserves = store.numberOfPendingReservesFor(metadata.dataHook, tierId);
677
+ if (currentTierSupply != 0 || pendingReserves != 0) {
645
678
  eligibleTierWeights += MAX_ATTESTATION_POWER_TIER;
646
679
  }
647
680
  }
@@ -59,6 +59,8 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
59
59
  error DefifaHook_TransfersPaused();
60
60
  error DefifaHook_Unauthorized(uint256 tokenId, address owner, address caller);
61
61
 
62
+ event PricingCurrencySet(uint256 currency, address caller);
63
+
62
64
  //*********************************************************************//
63
65
  // --------------------- public constant properties ------------------ //
64
66
  //*********************************************************************//
@@ -497,6 +499,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
497
499
  pricingCurrency = _currency;
498
500
  gamePhaseReporter = _gamePhaseReporter;
499
501
  gamePotReporter = _gamePotReporter;
502
+ // slither-disable-next-line missing-zero-check
500
503
  defaultAttestationDelegate = _defaultAttestationDelegate;
501
504
 
502
505
  // Store the base URI if provided.
@@ -529,19 +532,22 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
529
532
 
530
533
  // Transfer ownership to the initializer.
531
534
  _transferOwnership(msg.sender);
535
+
536
+ emit PricingCurrencySet(_currency, msg.sender);
532
537
  }
533
538
 
534
539
  /// @notice Mint reserved tokens within the tier for the provided value.
535
540
  /// @param tierId The ID of the tier to mint within.
536
541
  /// @param count The number of reserved tokens to mint.
537
- // slither-disable-next-line reentrancy-no-eth
538
542
  function mintReservesFor(uint256 tierId, uint256 count) public override {
539
543
  // Minting reserves must not be paused.
544
+ // slither-disable-next-line calls-loop
540
545
  if (JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(
541
546
  (JBRulesetMetadataResolver.metadata(rulesets.currentOf(PROJECT_ID)))
542
547
  )) revert DefifaHook_ReservedTokenMintingPaused();
543
548
 
544
549
  // Keep a reference to the reserved token beneficiary.
550
+ // slither-disable-next-line calls-loop
545
551
  address reservedTokenBeneficiary = store.reserveBeneficiaryOf({hook: address(this), tierId: tierId});
546
552
 
547
553
  // Get a reference to the old delegate.
@@ -559,12 +565,14 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
559
565
  }
560
566
 
561
567
  // Record the minted reserves for the tier.
568
+ // slither-disable-next-line calls-loop
562
569
  uint256[] memory tokenIds = store.recordMintReservesFor({tierId: tierId, count: count});
563
570
 
564
571
  // Keep a reference to the token ID being iterated on.
565
572
  uint256 tokenId;
566
573
 
567
574
  // Fetch the tier details (needed for votingUnits below).
575
+ // slither-disable-next-line calls-loop
568
576
  JB721Tier memory tier = store.tierOf({hook: address(this), id: tierId, includeResolvedUri: false});
569
577
 
570
578
  // Increment _totalMintCost so reserved recipients can claim their share of fee tokens ($DEFIFA/$NANA).
@@ -579,6 +587,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
579
587
  tokenId = tokenIds[i];
580
588
 
581
589
  // Mint the token.
590
+ // slither-disable-next-line reentrancy-no-eth
582
591
  _mint({to: reservedTokenBeneficiary, tokenId: tokenId});
583
592
 
584
593
  emit MintReservedToken(tokenId, tierId, reservedTokenBeneficiary, msg.sender);
@@ -589,6 +598,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
589
598
  }
590
599
 
591
600
  // Transfer the attestation units to the delegate.
601
+ // slither-disable-next-line reentrancy-no-eth
592
602
  _transferTierAttestationUnits({
593
603
  from: address(0), to: reservedTokenBeneficiary, tierId: tierId, amount: tier.votingUnits * tokenIds.length
594
604
  });
@@ -602,7 +612,6 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
602
612
  /// `context.beneficiary`. Part of `IJBCashOutHook`.
603
613
  /// @dev Reverts if the calling contract is not one of the project's terminals.
604
614
  /// @param context The cash out context passed in by the terminal.
605
- // slither-disable-next-line locked-ether,reentrancy-no-eth
606
615
  function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
607
616
  external
608
617
  payable
@@ -654,6 +663,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
654
663
 
655
664
  if (isComplete) {
656
665
  unchecked {
666
+ // slither-disable-next-line reentrancy-no-eth,calls-loop
657
667
  ++tokensRedeemedFrom[store.tierIdOfToken(tokenId)];
658
668
  }
659
669
  }
@@ -674,6 +684,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
674
684
  // Claim the $DEFIFA and $NANA tokens for the user.
675
685
  // Include pending reserve mint cost in the denominator so that unminted reserves
676
686
  // are accounted for, preventing paid holders from claiming a disproportionate share.
687
+ // slither-disable-next-line reentrancy-events
677
688
  beneficiaryReceivedTokens = _claimTokensFor({
678
689
  beneficiary: context.holder,
679
690
  shareToBeneficiary: cumulativeMintPrice,
@@ -795,8 +806,10 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
795
806
 
796
807
  for (uint256 i; i < numberOfTiers;) {
797
808
  uint256 tierId = i + 1;
809
+ // slither-disable-next-line calls-loop
798
810
  uint256 pendingReserves = _store.numberOfPendingReservesFor({hook: hook, tierId: tierId});
799
811
  if (pendingReserves != 0) {
812
+ // slither-disable-next-line calls-loop
800
813
  JB721Tier memory tier = _store.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
801
814
  cost += pendingReserves * tier.price;
802
815
  }
@@ -862,6 +875,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
862
875
  /// @param tierId The ID of the tier to get attestation units for.
863
876
  /// @return The attestation units.
864
877
  function _getTierAttestationUnits(address account, uint256 tierId) internal view virtual returns (uint256) {
878
+ // slither-disable-next-line calls-loop
865
879
  return store.tierVotingUnitsOf({hook: address(this), account: account, tierId: tierId});
866
880
  }
867
881
 
@@ -994,8 +1008,12 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
994
1008
 
995
1009
  // If there's either a new delegate or old delegate, set delegation and transfer units.
996
1010
  if (attestationDelegate != address(0) || oldDelegate != address(0)) {
997
- // Switch delegates if needed.
998
- if (attestationDelegate != address(0) && attestationDelegate != oldDelegate) {
1011
+ // Delegation is beneficiary-owned state. A third-party payer can fund this mint, but
1012
+ // cannot overwrite the beneficiary's long-lived delegate preference through metadata.
1013
+ if (
1014
+ context.payer == context.beneficiary && attestationDelegate != address(0)
1015
+ && attestationDelegate != oldDelegate
1016
+ ) {
999
1017
  _delegateTier({account: context.beneficiary, delegatee: attestationDelegate, tierId: tierId});
1000
1018
  }
1001
1019
 
@@ -1085,6 +1103,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
1085
1103
  // If transfers are pausable, check if they're paused.
1086
1104
  if (tier.flags.transfersPausable) {
1087
1105
  // Get a reference to the project's current ruleset.
1106
+ // slither-disable-next-line calls-loop
1088
1107
  JBRuleset memory ruleset = rulesets.currentOf(PROJECT_ID);
1089
1108
 
1090
1109
  // If transfers are paused and the NFT isn't being transferred to the zero address, revert.
@@ -1101,14 +1120,15 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
1101
1120
  if (_firstOwnerOf[tokenId] == address(0)) _firstOwnerOf[tokenId] = from;
1102
1121
  }
1103
1122
 
1104
- // Record the transfer.
1105
- // slither-disable-next-line reentrancy-events,calls-loop
1106
- store.recordTransferForTier({tierId: tier.id, from: from, to: to});
1107
-
1108
1123
  // Dont transfer on mint since the delegation will be transferred more efficiently in _processPayment.
1109
- if (from == address(0)) return from;
1124
+ if (from != address(0)) {
1125
+ _transferTierAttestationUnits({from: from, to: to, tierId: tier.id, amount: tier.votingUnits});
1126
+ }
1127
+
1128
+ // Record the transfer after local delegation state has been finalized.
1129
+ // slither-disable-next-line calls-loop
1130
+ store.recordTransferForTier({tierId: tier.id, from: from, to: to});
1110
1131
 
1111
- // Transfer the attestation units.
1112
- _transferTierAttestationUnits({from: from, to: to, tierId: tier.id, amount: tier.votingUnits});
1132
+ return from;
1113
1133
  }
1114
1134
  }
@@ -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
  }