@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.
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.17",
3
+ "version": "0.0.19",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -13,14 +13,13 @@
13
13
  "url": "https://github.com/BallKidz/defifa-collection-deployer"
14
14
  },
15
15
  "dependencies": {
16
- "@bananapus/721-hook-v6": "^0.0.28",
17
- "@bananapus/address-registry-v6": "^0.0.16",
18
- "@bananapus/core-v6": "^0.0.30",
16
+ "@bananapus/721-hook-v6": "^0.0.30",
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.29",
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.26",
24
23
  "scripty.sol": "^2.1.1"
25
24
  },
26
25
  "devDependencies": {
@@ -0,0 +1,27 @@
1
+ # Defifa Operations
2
+
3
+ ## Deployment Surface
4
+
5
+ - [`src/DefifaDeployer.sol`](../src/DefifaDeployer.sol) is the first stop for launch-time config, phase queueing, and post-ratification fulfillment.
6
+ - [`script/Deploy.s.sol`](../script/Deploy.s.sol) is the deployment entry point when the task is about current wiring rather than game mechanics.
7
+ - [`src/structs/`](../src/structs/) and [`src/enums/`](../src/enums/) define launch data, phase types, and other inputs that often drift from remembered assumptions.
8
+
9
+ ## Change Checklist
10
+
11
+ - If you edit lifecycle timing, verify phase transitions, no-contest triggers, and the governor's attestation windows together.
12
+ - If you edit hook settlement logic, re-check fee accounting and mint-cost invariants.
13
+ - If you touch governance thresholds or attestation behavior, inspect the governor tests before assuming the change is local.
14
+ - If you touch token metadata or rendering, verify whether the bug belongs in the resolver instead of settlement code.
15
+
16
+ ## Common Failure Modes
17
+
18
+ - Game-state issue is blamed on the hook even though the deployer queued the wrong phase or timing.
19
+ - Governance behavior looks wrong, but the real issue is stale launch configuration.
20
+ - Settlement changes accidentally affect fee distribution or redemption accounting.
21
+ - Resolver issues get misdiagnosed as hook or governor problems because they surface through NFTs.
22
+
23
+ ## Useful Proof Points
24
+
25
+ - [`test/Fork.t.sol`](../test/Fork.t.sol) for live-integration assumptions.
26
+ - [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) and [`test/TestQALastMile.t.sol`](../test/TestQALastMile.t.sol) for pinned edge cases.
27
+ - [`test/BWAFunctionComparison.t.sol`](../test/BWAFunctionComparison.t.sol) and [`test/DefifaUSDC.t.sol`](../test/DefifaUSDC.t.sol) when currency or accounting context matters.
@@ -0,0 +1,32 @@
1
+ # Defifa Runtime
2
+
3
+ ## Contract Roles
4
+
5
+ - [`src/DefifaDeployer.sol`](../src/DefifaDeployer.sol) launches games, manages phase progression, fulfills commitments, and triggers safety exits such as no-contest.
6
+ - [`src/DefifaHook.sol`](../src/DefifaHook.sol) manages the NFT game pieces, delegation, and settlement-side cash-out behavior.
7
+ - [`src/DefifaGovernor.sol`](../src/DefifaGovernor.sol) handles scorecard submission, attestation, quorum, and ratification.
8
+ - [`src/DefifaTokenUriResolver.sol`](../src/DefifaTokenUriResolver.sol) renders game-card metadata.
9
+ - [`src/DefifaProjectOwner.sol`](../src/DefifaProjectOwner.sol) is the ownership helper for the fee project.
10
+
11
+ ## Lifecycle
12
+
13
+ 1. Countdown before minting opens.
14
+ 2. Mint phase where players buy outcome NFTs and can delegate attestation power.
15
+ 3. Optional refund phase.
16
+ 4. Scoring phase where scorecards are submitted, attested, and ratified.
17
+ 5. Complete or no-contest settlement depending on governance and safety conditions.
18
+
19
+ ## High-Risk Areas
20
+
21
+ - Scorecard ratification and quorum assumptions: changes here directly affect who can settle the pot.
22
+ - No-contest and refund behavior: these paths are economic safety valves, not edge-case garnish.
23
+ - Fee accounting and commitment fulfillment: payout ordering and accounting drift can change final redemption value.
24
+ - Hook/governor/deployer coupling: many bugs come from changing one layer while assuming the others are passive.
25
+
26
+ ## Tests To Trust First
27
+
28
+ - [`test/DefifaGovernor.t.sol`](../test/DefifaGovernor.t.sol) for governance flow.
29
+ - [`test/DefifaNoContest.t.sol`](../test/DefifaNoContest.t.sol) for safety exits.
30
+ - [`test/DefifaFeeAccounting.t.sol`](../test/DefifaFeeAccounting.t.sol) and [`test/DefifaMintCostInvariant.t.sol`](../test/DefifaMintCostInvariant.t.sol) for economic correctness.
31
+ - [`test/DefifaHookRegressions.t.sol`](../test/DefifaHookRegressions.t.sol) and [`test/regression/`](../test/regression/) for pinned regressions.
32
+ - [`test/DefifaSecurity.t.sol`](../test/DefifaSecurity.t.sol) and [`test/DefifaGovernanceHardening.t.sol`](../test/DefifaGovernanceHardening.t.sol) for adversarial cases.
@@ -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
 
@@ -7,6 +7,7 @@ import {
7
7
  JB721TiersRulesetMetadataResolver
8
8
  } from "@bananapus/721-hook-v6/src/libraries/JB721TiersRulesetMetadataResolver.sol";
9
9
  import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
10
+ import {JB721TierConfigFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierConfigFlags.sol";
10
11
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
11
12
  import {IJBController, JBRulesetConfig, JBTerminalConfig} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
12
13
  import {IJBMultiTerminal} from "@bananapus/core-v6/src/interfaces/IJBMultiTerminal.sol";
@@ -274,6 +275,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
274
275
  uint256 _defifaProjectId,
275
276
  uint256 _baseProtocolProjectId
276
277
  ) {
278
+ // slither-disable-next-line missing-zero-check
277
279
  HOOK_CODE_ORIGIN = _hookCodeOrigin;
278
280
  TOKEN_URI_RESOLVER = _tokenUriResolver;
279
281
  GOVERNOR = _governor;
@@ -471,13 +473,15 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
471
473
  encodedIPFSUri: defifaTier.encodedIPFSUri,
472
474
  category: 0,
473
475
  discountPercent: 0,
474
- allowOwnerMint: false,
475
- useReserveBeneficiaryAsDefault: defifaTier.shouldUseReservedTokenBeneficiaryAsDefault,
476
- transfersPausable: false,
477
- useVotingUnits: false,
478
- cantBeRemoved: true,
479
- cantIncreaseDiscountPercent: true,
480
- cantBuyWithCredits: false,
476
+ flags: JB721TierConfigFlags({
477
+ allowOwnerMint: false,
478
+ useReserveBeneficiaryAsDefault: defifaTier.shouldUseReservedTokenBeneficiaryAsDefault,
479
+ transfersPausable: false,
480
+ useVotingUnits: false,
481
+ cantBeRemoved: true,
482
+ cantIncreaseDiscountPercent: true,
483
+ cantBuyWithCredits: false
484
+ }),
481
485
  splitPercent: 0,
482
486
  splits: new JBSplit[](0)
483
487
  });
@@ -546,8 +550,13 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
546
550
  // Transfer ownership to the specified owner.
547
551
  hook.transferOwnership(address(GOVERNOR));
548
552
 
549
- // Add the hook to the registry, contract nonce starts at 1
550
- 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
+ });
551
560
 
552
561
  // slither-disable-next-line reentrancy-events
553
562
  emit LaunchGame(gameId, hook, GOVERNOR, uriResolver, msg.sender);
@@ -936,4 +945,19 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
936
945
  projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game has finished."
937
946
  });
938
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
+ }
939
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
 
@@ -882,7 +896,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
882
896
  uint256[] memory tokenIds;
883
897
 
884
898
  // Record the mint. The returned token IDs correspond to the tiers passed in.
885
- // slither-disable-next-line reentrancy-benign
899
+ // slither-disable-next-line unused-return,reentrancy-benign
886
900
  (tokenIds, leftoverAmount,) = store.recordMint({
887
901
  amount: amount,
888
902
  tierIds: mintTierIds,
@@ -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
 
@@ -1083,8 +1101,9 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
1083
1101
  // Transfers must not be paused (when not minting or burning).
1084
1102
  if (from != address(0)) {
1085
1103
  // If transfers are pausable, check if they're paused.
1086
- if (tier.transfersPausable) {
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
  }
@@ -1,8 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
5
4
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
5
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
6
6
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
7
7
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
8
8
 
@@ -13,12 +13,30 @@ import {IDefifaHook} from "./IDefifaHook.sol";
13
13
  /// @notice Deploys and manages Defifa prediction games, including lifecycle phase transitions
14
14
  /// and commitment fulfillment.
15
15
  interface IDefifaDeployer {
16
+ /// @notice Emitted when a commitment payout fails during fulfillment.
17
+ /// @param gameId The ID of the game being fulfilled.
18
+ /// @param amount The amount that failed to pay out.
19
+ /// @param reason The revert reason bytes from the failed payout.
16
20
  event CommitmentPayoutFailed(uint256 indexed gameId, uint256 amount, bytes reason);
17
21
 
22
+ /// @notice Emitted when a split receives a portion of the game pot.
23
+ /// @param split The split that received funds.
24
+ /// @param amount The amount sent to the split.
25
+ /// @param caller The address that triggered the distribution.
18
26
  event DistributeToSplit(JBSplit split, uint256 amount, address caller);
19
27
 
28
+ /// @notice Emitted when a game's commitments have been fulfilled.
29
+ /// @param gameId The ID of the fulfilled game.
30
+ /// @param pot The total game pot that was fulfilled.
31
+ /// @param caller The address that triggered fulfillment.
20
32
  event FulfilledCommitments(uint256 indexed gameId, uint256 pot, address caller);
21
33
 
34
+ /// @notice Emitted when a new Defifa game is launched.
35
+ /// @param gameId The ID of the launched game.
36
+ /// @param hook The hook deployed for the game.
37
+ /// @param governor The governor responsible for scorecard ratification.
38
+ /// @param tokenUriResolver The token URI resolver used for the game's NFTs.
39
+ /// @param caller The address that launched the game.
22
40
  event LaunchGame(
23
41
  uint256 indexed gameId,
24
42
  IDefifaHook indexed hook,
@@ -27,10 +45,19 @@ interface IDefifaDeployer {
27
45
  address caller
28
46
  );
29
47
 
48
+ /// @notice Emitted when a game is queued into its no-contest phase.
49
+ /// @param gameId The ID of the game.
50
+ /// @param caller The address that queued the phase transition.
30
51
  event QueuedNoContest(uint256 indexed gameId, address caller);
31
52
 
53
+ /// @notice Emitted when a game is queued into its refund phase.
54
+ /// @param gameId The ID of the game.
55
+ /// @param caller The address that queued the phase transition.
32
56
  event QueuedRefundPhase(uint256 indexed gameId, address caller);
33
57
 
58
+ /// @notice Emitted when a game is queued into its scoring phase.
59
+ /// @param gameId The ID of the game.
60
+ /// @param caller The address that queued the phase transition.
34
61
  event QueuedScoringPhase(uint256 indexed gameId, address caller);
35
62
 
36
63
  /// @notice The fee divisor for base protocol fees (100 / fee percent).