@ballkidz/defifa 0.0.30 → 0.0.32

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.
@@ -4,7 +4,9 @@ Defifa is a staged prediction-game system built on Juicebox and the tiered 721 s
4
4
 
5
5
  ## Audit Objective
6
6
 
7
- Find issues that:
7
+ There is a billion dollars of well-meaning projects' money in the Juicebox Money Engine, growing exponentially. Your job is to hack it before anyone else. Whoever hacks it first saves/steals the money, and you are obsessed with being this winner, while also being a steward of the protocol and wanting it to keep growing safely.
8
+
9
+ Suggestions of where to look:
8
10
 
9
11
  - let players extract more than their fair share of the game pot
10
12
  - break the game-phase lifecycle or allow actions in the wrong phase
package/CRYPTO_ECON.md CHANGED
@@ -777,11 +777,11 @@ Defifa includes a comprehensive safety system — the **NO_CONTEST** mechanism
777
777
 
778
778
  #### 9.1.1 Trigger 1: Minimum Participation Threshold
779
779
 
780
- **Mechanism.** At game creation, the organizer sets `minParticipation` — a minimum token supply required for the game to proceed to scoring. The `currentGamePhaseOf()` function checks the total token supply (via `CONTROLLER.TOKENS().totalSupplyOf(gameId)`) against this threshold before returning SCORING. If the supply is below the threshold, it returns NO_CONTEST.
780
+ **Mechanism.** At game creation, the organizer sets `minParticipation` — a minimum cumulative NFT mint cost required for the game to proceed to scoring. The `currentGamePhaseOf()` function checks the hook's `totalMintCost()` against this threshold before returning SCORING. If the mint cost is below the threshold, it returns NO_CONTEST. `totalMintCost` tracks only actual paid mint value and is decremented on refunds/burns — it cannot be inflated by `addToBalanceOf` top-ups.
781
781
 
782
782
  **What it solves.** Ghost games with negligible participation skip directly to refundability without requiring any governance action. A 32-team World Cup game with `minParticipation = 1 ETH` won't enter scoring if only 50 people mint (0.5 ETH pot).
783
783
 
784
- **Attack surface.** An adversary who wants to force no-contest can cash out enough tokens during the refund phase to push the supply below the threshold. Note that direct balance top-ups (via `addToBalanceOf`) cannot inflate participation since the check uses token supply, not treasury balance. Mitigation: set the threshold conservatively low relative to expected participation.
784
+ **Attack surface.** An adversary who wants to force no-contest can refund enough NFTs during the refund phase to push `totalMintCost` below the threshold. Direct balance top-ups (via `addToBalanceOf`) cannot inflate participation since the check uses `totalMintCost`, not treasury balance. Mitigation: set the threshold conservatively low relative to expected participation.
785
785
 
786
786
  **Configuration.** Set to 0 to disable. The threshold is set at launch before any minting occurs, so calibration depends on organizer judgment.
787
787
 
@@ -820,7 +820,7 @@ The phase resolution follows strict priority:
820
820
 
821
821
  2. **Explicit trigger is sticky.** Once `noContestTriggeredFor[gameId]` is set, the game stays in NO_CONTEST permanently (cannot transition to SCORING even if conditions change).
822
822
 
823
- 3. **Both thresholds are checked independently.** A game can enter NO_CONTEST from either `minParticipation` (token supply too low) or `scorecardTimeout` (time elapsed) — whichever condition is met first.
823
+ 3. **Both thresholds are checked independently.** A game can enter NO_CONTEST from either `minParticipation` (cumulative mint cost too low) or `scorecardTimeout` (time elapsed) — whichever condition is met first.
824
824
 
825
825
  #### 9.1.5 The Default Attestation Delegate
826
826
 
package/RISKS.md CHANGED
@@ -22,6 +22,7 @@ This file focuses on the game-theoretic, governance, and settlement risks in Def
22
22
  - **Deployer as project owner.** The deployer owns game projects and controls ruleset queuing and commitment fulfillment.
23
23
  - **DefifaProjectOwner irrecoverability.** Once the project NFT is transferred there, it cannot be recovered.
24
24
  - **External dependencies.** Core protocol and shared 721-store behavior remain upstream trust boundaries.
25
+ - **Terminal provenance is the launcher's responsibility.** The hook authenticates terminals via directory registration, not implementation identity. A game is only as trustworthy as the terminal its launcher chose. Users must verify a game's terminal before participating.
25
26
  - **Default attestation delegate.** If set, it can accumulate meaningful governance power across new minters.
26
27
 
27
28
  ## 2. Economic Risks
@@ -86,6 +87,10 @@ Completion and ratification paths use one-way state to prevent replay or double-
86
87
 
87
88
  This is conservative, but it prevents users from front-running reserve dilution out of governance power or fee-token distribution.
88
89
 
89
- ### 8.5 One-tier games always resolve via no-contest
90
+ ### 8.5 Launcher-selected terminals are trusted per game
91
+
92
+ `DefifaDeployer.launchGameWith(...)` is permissionless and allows the launcher to choose the terminal registered for the game. `DefifaHook` trusts any registered terminal as the source of pay and cash-out callbacks. This means a malicious launcher can register a callback-forging terminal that fabricates hook contexts without recording real payments. Users and integrators must verify a game's registered terminal before trusting or participating in it. The same applies to scorecard timing parameters and tier configuration — a game's safety depends on the launcher choosing sane inputs. Frontends and aggregators should cross-reference a game's terminal against the canonical `JBMultiTerminal` for the chain before displaying it as trustworthy.
93
+
94
+ ### 8.6 One-tier games always resolve via no-contest
90
95
 
91
96
  A single-tier game cannot complete normal governance because the governance attestation model gives zero weight to holders of a tier that receives 100% of the scorecard, making quorum unreachable. This is expected: the game falls through to `NO_CONTEST` once `scorecardTimeout` elapses, and players recover their mint price via the permissionless `triggerNoContestFor()` refund path that queues a refund ruleset. This only works when `scorecardTimeout > 0`. A one-tier game launched with `scorecardTimeout = 0` disables the timeout path entirely, and funds become permanently locked with no exit. Game deployers must ensure `scorecardTimeout > 0` for single-tier configurations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ballkidz/defifa",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": "25.9.0"
@@ -31,7 +31,7 @@
31
31
  "remappings.txt"
32
32
  ],
33
33
  "dependencies": {
34
- "@bananapus/721-hook-v6": "0.0.43",
34
+ "@bananapus/721-hook-v6": "0.0.46",
35
35
  "@bananapus/address-registry-v6": "0.0.25",
36
36
  "@bananapus/core-v6": "0.0.44",
37
37
  "@bananapus/permission-ids-v6": "0.0.22",
@@ -186,7 +186,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
186
186
 
187
187
  /// @notice The safety mechanism parameters of a game.
188
188
  /// @param gameId The ID of the game to get the safety params of.
189
- /// @return minParticipation The minimum treasury balance for the game to proceed to scoring.
189
+ /// @return minParticipation The minimum cumulative NFT mint cost for the game to proceed to scoring.
190
190
  /// @return scorecardTimeout The maximum time after scoring begins for a scorecard to be ratified.
191
191
  function safetyParamsOf(uint256 gameId)
192
192
  external
@@ -249,12 +249,13 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
249
249
  // Get the game's ops data for the safety mechanism checks. Cache to avoid repeated SLOAD.
250
250
  DefifaOpsData memory ops = _opsOf[gameId];
251
251
 
252
- // Check minimum participation threshold using token supply (not terminal balance).
253
- // Token supply reflects actual minted participation — direct `addToBalanceOf` top-ups
254
- // don't mint tokens and therefore can't bypass this check.
252
+ // Check minimum participation threshold using cumulative NFT mint cost from the hook.
253
+ // Terminal balance is NOT used because `addToBalanceOf` can inflate it without minting NFTs.
254
+ // `totalMintCost` tracks only actual paid mint value and is decremented on refunds/burns.
255
255
  if (ops.minParticipation > 0) {
256
- uint256 totalTokenSupply = CONTROLLER.TOKENS().totalSupplyOf(gameId);
257
- if (totalTokenSupply < ops.minParticipation) return DefifaGamePhase.NO_CONTEST;
256
+ if (IDefifaHook(metadata.dataHook).totalMintCost() < ops.minParticipation) {
257
+ return DefifaGamePhase.NO_CONTEST;
258
+ }
258
259
  }
259
260
 
260
261
  // Check scorecard ratification timeout: if enough time has passed without a ratified scorecard, the game is
@@ -97,10 +97,6 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
97
97
  /// @dev _tierId The ID of the tier to get a name for.
98
98
  mapping(uint256 => string) internal _tierNameOf;
99
99
 
100
- /// @notice The cumulative mint price of all tokens (paid and reserved). Used as the denominator for fee token
101
- /// ($DEFIFA/$NANA) distribution.
102
- uint256 internal _totalMintCost;
103
-
104
100
  //*********************************************************************//
105
101
  // ---------------- public immutable stored properties --------------- //
106
102
  //*********************************************************************//
@@ -115,6 +111,10 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
115
111
  // --------------------- public stored properties -------------------- //
116
112
  //*********************************************************************//
117
113
 
114
+ /// @notice The cumulative mint price of all paid and reserved NFTs. Decremented on burns/refunds. Used as the
115
+ /// participation metric — immune to `addToBalanceOf` inflation because only actual mints increment it.
116
+ uint256 public override totalMintCost;
117
+
118
118
  /// @notice The amount that has been redeemed from this game, refunds are not counted.
119
119
  uint256 public override amountRedeemed;
120
120
 
@@ -285,7 +285,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
285
285
  )
286
286
  {
287
287
  // Make sure (fungible) project tokens aren't also being cashed out.
288
- if (context.cashOutCount > 0) revert JB721Hook_UnexpectedTokenCashedOut();
288
+ if (context.cashOutCount > 0) revert JB721Hook_UnexpectedTokenCashedOut({cashOutCount: context.cashOutCount});
289
289
 
290
290
  // Fetch the cash out hook metadata using the corresponding metadata ID.
291
291
  (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
@@ -411,7 +411,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
411
411
  tokenIds: tokenIds,
412
412
  hookStore: store,
413
413
  hook: address(this),
414
- totalMintCost: _totalMintCost + _pendingReserveMintCost(),
414
+ totalMintCost: totalMintCost + _pendingReserveMintCost(),
415
415
  defifaBalance: DEFIFA_TOKEN.balanceOf(address(this)),
416
416
  baseProtocolBalance: BASE_PROTOCOL_TOKEN.balanceOf(address(this))
417
417
  });
@@ -466,7 +466,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
466
466
  // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
467
467
  // interaction with the correct project.
468
468
  if (msg.value != 0 || !_isProjectTerminal(projectId) || context.projectId != projectId) {
469
- revert JB721Hook_InvalidPay();
469
+ revert JB721Hook_InvalidPay({caller: msg.sender, contextProjectId: context.projectId, projectId: projectId});
470
470
  }
471
471
 
472
472
  // Process the payment.
@@ -597,11 +597,11 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
597
597
  // Fetch the tier details (needed for votingUnits below).
598
598
  JB721Tier memory tier = hookStore.tierOf({hook: address(this), id: tierId, includeResolvedUri: false});
599
599
 
600
- // Increment _totalMintCost so reserved recipients can claim their share of fee tokens ($DEFIFA/$NANA).
600
+ // Increment totalMintCost so reserved recipients can claim their share of fee tokens ($DEFIFA/$NANA).
601
601
  // Note: reserved mints dilute existing fee token claimants because they increase the total mint cost
602
602
  // denominator without contributing new funds to the fee token balances. This is the intended design —
603
603
  // reserved recipients receive a proportional claim on fee tokens as if they had paid to mint.
604
- _totalMintCost += tier.price * count;
604
+ totalMintCost += tier.price * count;
605
605
 
606
606
  for (uint256 i; i < count;) {
607
607
  // Set the token ID.
@@ -645,7 +645,9 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
645
645
  // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
646
646
  // interaction with the correct project.
647
647
  if (msg.value != 0 || !_isProjectTerminal(PROJECT_ID) || context.projectId != PROJECT_ID) {
648
- revert JB721Hook_InvalidCashOut();
648
+ revert JB721Hook_InvalidCashOut({
649
+ caller: msg.sender, contextProjectId: context.projectId, projectId: PROJECT_ID, msgValue: msg.value
650
+ });
649
651
  }
650
652
 
651
653
  // Fetch the cash out hook metadata using the corresponding metadata ID.
@@ -722,7 +724,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
722
724
  beneficiaryReceivedTokens = _claimTokensFor({
723
725
  beneficiary: context.beneficiary,
724
726
  shareToBeneficiary: cumulativeMintPrice,
725
- outOfTotal: _totalMintCost + _pendingReserveMintCost()
727
+ outOfTotal: totalMintCost + _pendingReserveMintCost()
726
728
  });
727
729
  }
728
730
 
@@ -737,7 +739,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
737
739
  }
738
740
 
739
741
  // Decrement the paid mint cost by the cumulative mint price of the tokens being burned.
740
- _totalMintCost -= cumulativeMintPrice;
742
+ totalMintCost -= cumulativeMintPrice;
741
743
  }
742
744
 
743
745
  /// @notice Mint reserved tokens within the tier for the provided value.
@@ -915,7 +917,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
915
917
  uint256 tokenId;
916
918
 
917
919
  // Increment the paid mint cost.
918
- _totalMintCost += amount;
920
+ totalMintCost += amount;
919
921
 
920
922
  // Loop through each token ID and mint.
921
923
  for (uint256 i; i < mintsLength;) {
@@ -117,6 +117,11 @@ interface IDefifaHook is IJB721Hook {
117
117
  /// @return The Defifa ERC-20 token.
118
118
  function DEFIFA_TOKEN() external view returns (IERC20);
119
119
 
120
+ /// @notice The cumulative mint price of all paid and reserved NFTs. Decremented on burns/refunds. Used as the
121
+ /// participation metric — immune to `addToBalanceOf` inflation because only actual mints increment it.
122
+ /// @return The total mint cost in the game's payment token.
123
+ function totalMintCost() external view returns (uint256);
124
+
120
125
  /// @notice The first owner of a given token ID.
121
126
  /// @param tokenId The token ID.
122
127
  /// @return The address of the first owner.
@@ -30,8 +30,9 @@ import {DefifaTierParams} from "./DefifaTierParams.sol";
30
30
  /// @custom:member defaultAttestationDelegate The address that'll be set as the attestation delegate by default.
31
31
  /// @custom:member defaultTokenUriResolver The contract used to resolve token URIs if not provided by a tier
32
32
  /// specifically. @custom:member terminal The payment terminal where the project will accept funds through.
33
- /// @custom:member minParticipation The minimum treasury balance required for the game to proceed to scoring. If the
34
- /// balance is below this when scoring would begin, the game enters NO_CONTEST. Set to 0 to disable. @custom:member
33
+ /// @custom:member minParticipation The minimum cumulative NFT mint cost required for the game to proceed to scoring.
34
+ /// Compared against the hook's `totalMintCost` (immune to `addToBalanceOf` inflation). If below this when scoring
35
+ /// would begin, the game enters NO_CONTEST. Set to 0 to disable. @custom:member
35
36
  /// scorecardTimeout The maximum time (in seconds) after the scoring phase begins for a scorecard to be ratified. If
36
37
  /// exceeded, the game enters NO_CONTEST. Set to 0 to disable.
37
38
  struct DefifaLaunchProjectData {
@@ -6,9 +6,10 @@ pragma solidity ^0.8.0;
6
6
  /// @custom:member start The time at which the game should start, measured in seconds.
7
7
  /// @custom:member mintPeriodDuration The duration of the game's mint phase, measured in seconds.
8
8
  /// @custom:member refundPeriodDuration The time between the mint phase and the start time when mint's are no longer
9
- /// open but refunds are still allowed, measured in seconds. @custom:member minParticipation The minimum treasury
10
- /// balance required for the game to proceed to scoring. If the balance is below this when scoring would begin, the game
11
- /// enters NO_CONTEST. Set to 0 to disable.
9
+ /// open but refunds are still allowed, measured in seconds. @custom:member minParticipation The minimum cumulative NFT
10
+ /// mint cost required for the game to proceed to scoring. Compared against the hook's `totalMintCost` (immune to
11
+ /// `addToBalanceOf` inflation). If below this when scoring would begin, the game enters NO_CONTEST. Set to 0 to
12
+ /// disable.
12
13
  /// @custom:member scorecardTimeout The maximum time (in seconds) after the scoring phase begins for a scorecard to be
13
14
  /// ratified. If exceeded, the game enters NO_CONTEST. Set to 0 to disable.
14
15
  struct DefifaOpsData {