@ballkidz/defifa 0.0.32 → 0.0.34

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/CHANGELOG.md CHANGED
@@ -23,6 +23,8 @@ This file instead describes the current v6 repo at a high level and the broad mi
23
23
  ## Local review remediations
24
24
 
25
25
  - Reserve-minted NFTs are now excluded from refund calculations during MINT, REFUND, and NO_CONTEST phases. A public `isReserveMint` mapping tracks which tokens were created via tier reserve frequency rather than paid for. `beforeCashOutRecordedWith` subtracts their tier price from `cumulativeMintPrice`, preventing reserve beneficiaries from withdrawing funds they never contributed.
26
+ - One-tier games now revert at launch with `DefifaDeployer_InvalidGameConfiguration` if `scorecardTimeout == 0`. A one-tier game cannot reach quorum (the BWA multiplier reduces the sole tier's power to zero), so a zero timeout would leave funds permanently locked with no exit path. Enforcement moves this from a launcher-side responsibility (previously documented in `RISKS.md §8.6`) to a contract-level guarantee.
27
+ - `DefifaHook.mintReservesFor` now reverts with `DefifaHook_ReservedTokenMintingBlockedInNoContest` while the game is in `NO_CONTEST`. Reserve mints inflate `totalMintCost` so reserved recipients can claim fee tokens; without the block, a game that failed `minParticipation` could be revived back to SCORING via free notional face value before `triggerNoContestFor` latches the failure.
26
28
 
27
29
  ## Migration notes
28
30
 
package/RISKS.md CHANGED
@@ -93,4 +93,16 @@ This is conservative, but it prevents users from front-running reserve dilution
93
93
 
94
94
  ### 8.6 One-tier games always resolve via no-contest
95
95
 
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.
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` would disable the timeout path entirely and leave funds permanently locked. `DefifaDeployer.launchGameWith` now enforces this at the contract level: a one-tier game with `scorecardTimeout == 0` reverts with `DefifaDeployer_InvalidGameConfiguration`. Two-tier games are still rounding-fragile in the same direction and should also be launched with a nonzero `scorecardTimeout`, but this is not enforced because some two-tier configurations can still reach quorum.
97
+
98
+ ### 8.7 Commitment splits are responsible for never reverting
99
+
100
+ `DefifaDeployer.fulfillCommitmentsOf` calls `terminal.sendPayoutsOf` to distribute the commitment portion of the pot. Core processes splits inside a try/catch — when an individual split reverts (rejecting split hook, recipient terminal that does not accept the game token, fee project with a missing currency feed) the failed amount is silently re-credited to the game's terminal balance and the outer call succeeds. The unpaid commitment funds then remain available to game players via the cash-out path.
101
+
102
+ This means **a single bad split cannot block the others from being paid**, and the unpaid funds are never lost — but it also means the recipient configured behind the failing split does not get paid through `fulfillCommitmentsOf` and must be settled separately if the game launcher cares. Game launchers are responsible for configuring commitment splits that will not revert under the game's terminal/currency setup. Recipients of commitment splits should treat split delivery as best-effort; the canonical post-game pot accounting still holds.
103
+
104
+ `fulfilledCommitmentsOf[gameId]` always equals the requested commitment amount whenever any portion was attempted, regardless of whether every split succeeded. `currentGamePotOf(gameId, true)` adds this back to the remaining balance so the displayed pot represents the original pre-commitment value, but the actual on-terminal balance may be higher than the displayed pot when splits silently failed — by exactly the failed split amounts, which remain redeemable by game players. The bound therefore stays one-sided: real balance ≥ reported pot.
105
+
106
+ ### 8.8 Reserve minting is blocked during NO_CONTEST
107
+
108
+ `DefifaHook.mintReservesFor` reverts with `DefifaHook_ReservedTokenMintingBlockedInNoContest` once `currentGamePhaseOf` reports `NO_CONTEST`. Reserve mints inflate `totalMintCost` so reserved recipients can claim a proportional share of fee tokens. Without this block, a game that failed `minParticipation` could be revived back into a SCORING-eligible state via free notional face value (reserves bump `totalMintCost` over the threshold) before `triggerNoContestFor()` latches the failure into a refund ruleset. The block tightens the §8.4 trust assumption that pending reserves are folded into governance and fee accounting: that remains true while the game is live, but once a game has already failed participation the reserve channel is closed so the no-contest outcome is final.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ballkidz/defifa",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": "25.9.0"
@@ -31,10 +31,10 @@
31
31
  "remappings.txt"
32
32
  ],
33
33
  "dependencies": {
34
- "@bananapus/721-hook-v6": "0.0.46",
35
- "@bananapus/address-registry-v6": "0.0.25",
36
- "@bananapus/core-v6": "0.0.44",
37
- "@bananapus/permission-ids-v6": "0.0.22",
34
+ "@bananapus/721-hook-v6": "^0.0.46",
35
+ "@bananapus/address-registry-v6": "^0.0.25",
36
+ "@bananapus/core-v6": "^0.0.48",
37
+ "@bananapus/permission-ids-v6": "^0.0.22",
38
38
  "@openzeppelin/contracts": "5.6.1",
39
39
  "@prb/math": "4.1.1",
40
40
  "scripty.sol": "2.1.1"
@@ -425,6 +425,20 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
425
425
  });
426
426
  }
427
427
 
428
+ // One-tier games cannot reach quorum (the BWA multiplier reduces the sole beneficiary tier's power to zero),
429
+ // so they must have a nonzero scorecardTimeout to resolve via NO_CONTEST. Without a timeout they would lock
430
+ // forever with no exit. Two-tier games are also rounding-fragile (see RISKS.md §8.6) and should set a
431
+ // scorecardTimeout, but this is not enforced at the contract level because some configurations can still
432
+ // reach quorum with luck-of-the-draw holder distributions.
433
+ if (launchProjectData.tiers.length == 1 && launchProjectData.scorecardTimeout == 0) {
434
+ revert DefifaDeployer_InvalidGameConfiguration({
435
+ start: launchProjectData.start,
436
+ mintPeriodDuration: launchProjectData.mintPeriodDuration,
437
+ refundPeriodDuration: launchProjectData.refundPeriodDuration,
438
+ tierCount: launchProjectData.tiers.length
439
+ });
440
+ }
441
+
428
442
  // Reject ERC-20 games with a zero currency. A zero baseCurrency would cause payout limit lookups
429
443
  // in fulfillCommitmentsOf to silently fail, skipping all commitment payouts.
430
444
  if (launchProjectData.token.token != JBConstants.NATIVE_TOKEN && launchProjectData.token.currency == 0) {
@@ -57,6 +57,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
57
57
  error DefifaHook_Overspending(uint256 leftoverAmount);
58
58
  error DefifaHook_CashoutWeightsAlreadySet(uint256 projectId);
59
59
  error DefifaHook_ReservedTokenMintingPaused(uint256 projectId, uint256 tierId);
60
+ error DefifaHook_ReservedTokenMintingBlockedInNoContest(uint256 projectId, uint256 tierId);
60
61
  error DefifaHook_TransfersPaused(uint256 projectId, uint256 tokenId, address from, address to);
61
62
  error DefifaHook_Unauthorized(uint256 tokenId, address owner, address caller);
62
63
 
@@ -568,6 +569,13 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
568
569
  revert DefifaHook_ReservedTokenMintingPaused({projectId: PROJECT_ID, tierId: tierId});
569
570
  }
570
571
 
572
+ // Block reserve minting while the game is in NO_CONTEST. Reserve mints inflate `totalMintCost` (so reserved
573
+ // recipients can claim fee tokens), which would otherwise let a game that failed `minParticipation` revive
574
+ // back to SCORING via free notional face value before `triggerNoContestFor` latches the failure.
575
+ if (_currentGamePhaseOf(PROJECT_ID) == DefifaGamePhase.NO_CONTEST) {
576
+ revert DefifaHook_ReservedTokenMintingBlockedInNoContest({projectId: PROJECT_ID, tierId: tierId});
577
+ }
578
+
571
579
  // Cache the store reference in a local variable to avoid repeated SLOAD.
572
580
  IJB721TiersHookStore hookStore = store;
573
581