@ballkidz/defifa 0.0.28 → 0.0.29

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/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 treasury balance required for the game to proceed to scoring. The `currentGamePhaseOf()` function checks the treasury balance against this threshold before returning SCORING. If the balance is below the threshold, it returns NO_CONTEST.
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.
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 refund enough tokens during the refund phase to push the balance below the threshold. Mitigation: set the threshold conservatively low relative to expected participation (e.g., 10% of the maximum expected pot).
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.
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` (balance 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` (token supply 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ballkidz/defifa",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": "25.9.0"
@@ -254,13 +254,12 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
254
254
  // Get the game's ops data for the safety mechanism checks. Cache to avoid repeated SLOAD.
255
255
  DefifaOpsData memory ops = _opsOf[gameId];
256
256
 
257
- // Check minimum participation threshold: if the treasury balance is below the threshold, the game is
258
- // NO_CONTEST.
257
+ // Check minimum participation threshold using token supply (not terminal balance).
258
+ // Token supply reflects actual minted participation — direct `addToBalanceOf` top-ups
259
+ // don't mint tokens and therefore can't bypass this check.
259
260
  if (ops.minParticipation > 0) {
260
- IJBTerminal terminal = CONTROLLER.DIRECTORY().primaryTerminalOf({projectId: gameId, token: ops.token});
261
- uint256 balance = IJBMultiTerminal(address(terminal)).STORE()
262
- .balanceOf({terminal: address(terminal), projectId: gameId, token: ops.token});
263
- if (balance < ops.minParticipation) return DefifaGamePhase.NO_CONTEST;
261
+ uint256 totalTokenSupply = CONTROLLER.TOKENS().totalSupplyOf(gameId);
262
+ if (totalTokenSupply < ops.minParticipation) return DefifaGamePhase.NO_CONTEST;
264
263
  }
265
264
 
266
265
  // Check scorecard ratification timeout: if enough time has passed without a ratified scorecard, the game is
@@ -427,13 +426,19 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
427
426
  revert DefifaDeployer_InvalidCurrency();
428
427
  }
429
428
 
430
- // If a scorecard timeout is set, it must exceed the grace period + timelock duration.
429
+ // If a scorecard timeout is set, it must exceed the full ratification window:
430
+ // attestation delay (time from scoring start to attestation start) + grace period + timelock.
431
431
  // Otherwise the game would enter NO_CONTEST before a scorecard could ever reach SUCCEEDED.
432
- if (
433
- launchProjectData.scorecardTimeout > 0
434
- && launchProjectData.scorecardTimeout
435
- <= launchProjectData.attestationGracePeriod + launchProjectData.timelockDuration
436
- ) revert DefifaDeployer_InvalidGameConfiguration();
432
+ if (launchProjectData.scorecardTimeout > 0) {
433
+ // Attestation delay: how long after scoring starts before attestations can begin.
434
+ uint256 attestationDelay = launchProjectData.attestationStartTime > launchProjectData.start
435
+ ? launchProjectData.attestationStartTime - launchProjectData.start
436
+ : 0;
437
+ if (
438
+ launchProjectData.scorecardTimeout
439
+ <= attestationDelay + launchProjectData.attestationGracePeriod + launchProjectData.timelockDuration
440
+ ) revert DefifaDeployer_InvalidGameConfiguration();
441
+ }
437
442
 
438
443
  // Reserve the game ID up front so permissionless project creations cannot invalidate hook deployment.
439
444
  gameId = CONTROLLER.PROJECTS().createFor(address(this));
@@ -282,9 +282,19 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
282
282
  }
283
283
 
284
284
  // Concatenate the strings
285
- return isEth
286
- ? string(abi.encodePacked("\u039E", integerPart, ".", decimalPartStr))
287
- : string(abi.encodePacked(integerPart, ".", decimalPartStr, " ", IERC20Metadata(token).symbol()));
285
+ if (isEth) {
286
+ return string(abi.encodePacked("\u039E", integerPart, ".", decimalPartStr));
287
+ }
288
+
289
+ // Try to get the token symbol; fall back to a truncated hex address if the call reverts.
290
+ string memory tokenSymbol;
291
+ try IERC20Metadata(token).symbol() returns (string memory s) {
292
+ tokenSymbol = _escapeSvg(s);
293
+ } catch {
294
+ tokenSymbol = Strings.toHexString(uint160(token), 20);
295
+ }
296
+
297
+ return string(abi.encodePacked(integerPart, ".", decimalPartStr, " ", tokenSymbol));
288
298
  }
289
299
 
290
300
  /// @notice Gets a substring.