@ballkidz/defifa 0.0.31 → 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.
- package/AUDIT_INSTRUCTIONS.md +3 -1
- package/CRYPTO_ECON.md +3 -3
- package/RISKS.md +6 -1
- package/package.json +1 -1
- package/src/DefifaDeployer.sol +7 -6
- package/src/DefifaHook.sol +10 -10
- package/src/interfaces/IDefifaHook.sol +5 -0
- package/src/structs/DefifaLaunchProjectData.sol +3 -2
- package/src/structs/DefifaOpsData.sol +4 -3
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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` (
|
|
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
|
|
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
package/src/DefifaDeployer.sol
CHANGED
|
@@ -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
|
|
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
|
|
253
|
-
//
|
|
254
|
-
//
|
|
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
|
-
|
|
257
|
-
|
|
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
|
package/src/DefifaHook.sol
CHANGED
|
@@ -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
|
|
|
@@ -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:
|
|
414
|
+
totalMintCost: totalMintCost + _pendingReserveMintCost(),
|
|
415
415
|
defifaBalance: DEFIFA_TOKEN.balanceOf(address(this)),
|
|
416
416
|
baseProtocolBalance: BASE_PROTOCOL_TOKEN.balanceOf(address(this))
|
|
417
417
|
});
|
|
@@ -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
|
|
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
|
-
|
|
604
|
+
totalMintCost += tier.price * count;
|
|
605
605
|
|
|
606
606
|
for (uint256 i; i < count;) {
|
|
607
607
|
// Set the token ID.
|
|
@@ -724,7 +724,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
724
724
|
beneficiaryReceivedTokens = _claimTokensFor({
|
|
725
725
|
beneficiary: context.beneficiary,
|
|
726
726
|
shareToBeneficiary: cumulativeMintPrice,
|
|
727
|
-
outOfTotal:
|
|
727
|
+
outOfTotal: totalMintCost + _pendingReserveMintCost()
|
|
728
728
|
});
|
|
729
729
|
}
|
|
730
730
|
|
|
@@ -739,7 +739,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
739
739
|
}
|
|
740
740
|
|
|
741
741
|
// Decrement the paid mint cost by the cumulative mint price of the tokens being burned.
|
|
742
|
-
|
|
742
|
+
totalMintCost -= cumulativeMintPrice;
|
|
743
743
|
}
|
|
744
744
|
|
|
745
745
|
/// @notice Mint reserved tokens within the tier for the provided value.
|
|
@@ -917,7 +917,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
917
917
|
uint256 tokenId;
|
|
918
918
|
|
|
919
919
|
// Increment the paid mint cost.
|
|
920
|
-
|
|
920
|
+
totalMintCost += amount;
|
|
921
921
|
|
|
922
922
|
// Loop through each token ID and mint.
|
|
923
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
|
|
34
|
-
///
|
|
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
|
|
10
|
-
///
|
|
11
|
-
/// enters NO_CONTEST. Set to 0 to
|
|
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 {
|