@ballkidz/defifa 0.0.7 → 0.0.9
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/ADMINISTRATION.md +3 -3
- package/ARCHITECTURE.md +2 -0
- package/AUDIT_INSTRUCTIONS.md +422 -0
- package/CRYPTO_ECON.md +5 -5
- package/README.md +1 -1
- package/RISKS.md +38 -335
- package/SKILLS.md +1 -1
- package/USER_JOURNEYS.md +691 -0
- package/package.json +7 -7
- package/script/Deploy.s.sol +14 -3
- package/script/helpers/DefifaDeploymentLib.sol +13 -15
- package/src/DefifaDeployer.sol +221 -192
- package/src/DefifaGovernor.sol +286 -276
- package/src/DefifaHook.sol +68 -34
- package/src/DefifaProjectOwner.sol +27 -4
- package/src/DefifaTokenUriResolver.sol +136 -134
- package/src/enums/DefifaGamePhase.sol +1 -1
- package/src/enums/DefifaScorecardState.sol +1 -1
- package/src/interfaces/IDefifaDeployer.sol +52 -50
- package/src/interfaces/IDefifaGamePhaseReporter.sol +2 -2
- package/src/interfaces/IDefifaGamePotReporter.sol +1 -1
- package/src/interfaces/IDefifaGovernor.sol +53 -54
- package/src/interfaces/IDefifaHook.sol +104 -103
- package/src/interfaces/IDefifaTokenUriResolver.sol +2 -2
- package/src/libraries/DefifaFontImporter.sol +11 -9
- package/src/libraries/DefifaHookLib.sol +66 -53
- package/src/structs/DefifaAttestations.sol +1 -1
- package/src/structs/DefifaDelegation.sol +1 -1
- package/src/structs/DefifaLaunchProjectData.sol +4 -4
- package/src/structs/DefifaOpsData.sol +1 -1
- package/src/structs/DefifaScorecard.sol +1 -1
- package/src/structs/DefifaTierCashOutWeight.sol +1 -1
- package/src/structs/DefifaTierParams.sol +2 -1
- package/test/DefifaAdversarialQuorum.t.sol +602 -0
- package/test/DefifaAuditLowGuards.t.sol +304 -0
- package/test/DefifaFeeAccounting.t.sol +37 -16
- package/test/DefifaGovernor.t.sol +43 -19
- package/test/DefifaHookRegressions.t.sol +14 -12
- package/test/DefifaMintCostInvariant.t.sol +31 -12
- package/test/DefifaNoContest.t.sol +34 -16
- package/test/DefifaSecurity.t.sol +46 -28
- package/test/DefifaUSDC.t.sol +45 -36
- package/test/Fork.t.sol +43 -43
- package/test/SVG.t.sol +2 -2
- package/test/TestAuditGaps.sol +982 -0
- package/test/TestQALastMile.t.sol +511 -0
- package/test/regression/FulfillmentBlocksRatification.t.sol +36 -30
- package/test/regression/GracePeriodBypass.t.sol +15 -10
package/RISKS.md
CHANGED
|
@@ -1,349 +1,52 @@
|
|
|
1
|
-
# defifa-collection-deployer-v6
|
|
1
|
+
# RISKS.md -- defifa-collection-deployer-v6
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 1. Trust Assumptions
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **Governor as Hook Owner.** The DefifaGovernor owns each DefifaHook clone. The governor can set tier cash-out weights via `ratifyScorecardFrom`, which executes an arbitrary call to the hook. If the governor is compromised, the hook's cash-out weights can be set to any values.
|
|
6
|
+
- **Deployer as Project Owner.** The DefifaDeployer contract owns all game projects. It controls ruleset queuing, payout sending, and split configuration. Its logic is immutable (no upgradability), so the trust boundary is the contract code itself.
|
|
7
|
+
- **DefifaProjectOwner Irrecoverability.** Once the Defifa project NFT is transferred to DefifaProjectOwner, it cannot be recovered. This is intentional but irreversible.
|
|
8
|
+
- **External Dependencies.** Relies on JB721TiersHookStore, JBController, JBMultiTerminal, JBRulesets, and JBPrices. Bugs in any upstream contract affect all Defifa games.
|
|
9
|
+
- **Default Attestation Delegate.** If set, the default attestation delegate receives delegated attestation power for all new minters who do not specify a delegate. This entity accumulates significant governance power.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
2. **DefifaDeployer** -- Owns all game JB projects. Controls ruleset queuing for `fulfillCommitmentsOf` and `triggerNoContestFor`. Cannot be upgraded.
|
|
9
|
-
3. **Tier Holders (Attestors)** -- Score outcomes via attestation-weighted governance. 50% quorum of minted tiers' attestation power determines the scorecard.
|
|
10
|
-
4. **Core Protocol** -- Relies on `JBMultiTerminal` for payment processing, `JBTerminalStore` for balance tracking, `JB721TiersHookStore` for NFT tier data, and `JBRulesets` for phase management. Bugs in any of these propagate.
|
|
11
|
-
5. **Immutable Fee Configuration** -- `DEFIFA_FEE_DIVISOR = 20` (5%) and `BASE_PROTOCOL_FEE_DIVISOR = 40` (2.5%) are compile-time constants. Cannot be updated without redeploying.
|
|
11
|
+
## 2. Economic Risks
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- **Scorecard manipulation via 50% quorum.** A single entity that acquires 50%+ of attestation power across tiers can unilaterally ratify any scorecard, directing the entire pot to chosen tiers. Per-tier cap at `MAX_ATTESTATION_POWER_TIER` limits single-tier dominance. 1-day minimum grace period gives counter-attestors time to respond.
|
|
14
|
+
- **Dynamic quorum from live supply.** Quorum is computed from `currentSupplyOfTier()` at call time, not from a snapshot. Token burns between attestation and ratification decrease quorum. During SCORING phase, burns revert with `NothingToClaim` preventing practical exploitation, but a future code path allowing SCORING burns could re-enable this.
|
|
15
|
+
- **Cash-out weight integer division truncation.** `_weight / _totalTokensForCashoutInTier` rounds down, permanently locking dust in the contract. Maximum loss: 1 wei per tier per game (128 wei max with 128 tiers).
|
|
16
|
+
- **Fee token dilution from reserved mints.** Reserved mints increment `_totalMintCost` by `tier.price * count` even though no ETH was paid. This dilutes paid minters' share of fee tokens (`$DEFIFA` / `$NANA`).
|
|
17
|
+
- **128-tier limit hard-coded.** `_tierCashOutWeights` is a fixed `uint256[128]` array. Games with more than 128 tiers have tiers beyond index 128 unable to receive cash-out weights.
|
|
14
18
|
|
|
15
|
-
##
|
|
19
|
+
## 3. Governance Risks
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
- **Single governor instance across all games.** All games share one DefifaGovernor. A bug in `ratifyScorecardFrom`, `attestToScorecardFrom`, or `submitScorecardFor` affects every game simultaneously.
|
|
22
|
+
- **Scorecard timeout can block legitimate ratification.** If `scorecardTimeout` elapses before ratification, the game permanently enters NO_CONTEST. Even a scorecard that has reached quorum cannot be ratified. `triggerNoContestFor()` is permissionless and allows fund recovery.
|
|
23
|
+
- **Delegation locked after MINT phase.** `setTierDelegateTo` only works during MINT phase. After MINT, NFT transfers auto-delegate to the recipient, but holders cannot explicitly re-delegate to a third party.
|
|
24
|
+
- **No-contest requires explicit trigger.** In NO_CONTEST, users cannot immediately cash out -- someone must call `triggerNoContestFor()` to queue a refund ruleset. Without this trigger, the SCORING ruleset allocates the entire balance as payouts, leaving surplus at 0.
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
**Status:** KNOWN, ACCEPTED
|
|
21
|
-
**Tested:** `DefifaSecurityTest.testQuorum_50pctMintedTiers`
|
|
26
|
+
## 4. Reentrancy Surface
|
|
22
27
|
|
|
23
|
-
**
|
|
28
|
+
- **afterCashOutRecordedWith.** Burns tokens before external calls. `_claimTokensFor` calls `safeTransfer` on ERC-20 tokens (DEFIFA_TOKEN, BASE_PROTOCOL_TOKEN). Preceding burn and state updates prevent meaningful reentrancy profit.
|
|
29
|
+
- **fulfillCommitmentsOf.** Uses `fulfilledCommitmentsOf[gameId]` as a reentrancy guard (set before `sendPayoutsOf`). Returns early if already non-zero. Uses `max(feeAmount, 1)` to ensure the guard works even when pot rounds to 0. `sendPayoutsOf` is wrapped in try-catch: on failure, resets to sentinel (1) and emits `CommitmentPayoutFailed`, ensuring the final ruleset is always queued.
|
|
30
|
+
- **ratifyScorecardFrom.** Executes arbitrary calldata on the hook via low-level call. The hook's `setTierCashOutWeightsTo` has an `onlyOwner` guard and a `cashOutWeightIsSet` check preventing double-set.
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
## 5. DoS Vectors
|
|
26
33
|
|
|
27
|
-
**
|
|
28
|
-
|
|
29
|
-
2. Attacker mints majority in 4 tiers. Per tier: `1e9 * (attacker_tokens / tier_total)`.
|
|
30
|
-
3. If attacker holds 80% of each of 4 tiers: `4 * 0.8 * 1e9 = 3.2e9 > 3e9` quorum.
|
|
31
|
-
4. Attacker submits scorecard giving 100% weight to their tiers, attests alone, and ratifies after grace period.
|
|
34
|
+
- **Unbounded tier iteration in governance.** `getAttestationWeight` and `quorum` iterate over all tiers (`maxTierIdOf`). Gas cost grows linearly. Games with many tiers may cause view functions to exceed block gas limits.
|
|
35
|
+
- **_buildSplits iteration.** Iterates over user-provided splits array. No explicit cap, but total percent constraint limits practical count.
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
## 6. Integration Risks
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
- **Immutable phase timing.** Game rulesets are queued at launch and progress automatically based on duration. Once deployed, phase timing cannot be changed.
|
|
40
|
+
- **Permanent cash-out weights.** Cash-out weights are set once via the governor. There is no mechanism to correct a ratified scorecard.
|
|
41
|
+
- **No deployer upgrade.** The deployer contract has no upgrade mechanism. Bugs require deploying a new deployer.
|
|
42
|
+
- **Clone initialization.** Clones use `cloneDeterministic` with `msg.sender` + nonce in the salt. Salt includes `msg.sender`, preventing cross-caller collision. `initialize()` has a re-initialization guard.
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
## 7. Invariants to Verify
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
**Residual risk:** If a future code path allows burns during SCORING (e.g., via a different hook), quorum could drift downward.
|
|
48
|
-
|
|
49
|
-
**Mitigation:** `NothingToClaim` revert during SCORING prevents practical exploitation. After ratification, quorum changes are irrelevant.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
### RISK-3: Cash-Out Weight Integer Division Truncation
|
|
54
|
-
|
|
55
|
-
**Severity:** LOW
|
|
56
|
-
**Status:** KNOWN, BOUNDED
|
|
57
|
-
**Tested:** `DefifaSecurityTest.testRounding_extremeWeights`
|
|
58
|
-
|
|
59
|
-
**Description:** `computeCashOutWeight()` (DefifaHookLib line 129) divides `_weight / _totalTokensForCashoutInTier` using integer division, permanently locking dust in the contract.
|
|
60
|
-
|
|
61
|
-
**Bound:** Maximum loss = 1 wei per tier per game. With 128 maximum tiers, at most 128 wei locked per game.
|
|
62
|
-
|
|
63
|
-
**Proof from test:** `testRounding_extremeWeights` allocates weight 1 to tier 1, `TOTAL_CASHOUT_WEIGHT - 2` to tier 2, and weight 1 to tier 3. Verifies fund conservation within 3 wei tolerance.
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
### RISK-4: Fee Token Dilution from Reserved Mints
|
|
68
|
-
|
|
69
|
-
**Severity:** LOW
|
|
70
|
-
**Status:** BY DESIGN
|
|
71
|
-
**Tested:** `DefifaSecurityTest.testC_D3_reservedMintersGetFeeTokens`
|
|
72
|
-
|
|
73
|
-
**Description:** Reserved mints increment `_totalMintCost` by `tier.price * count` (DefifaHook line 568), even though no ETH was actually paid. This dilutes paid minters' share of fee tokens (`$DEFIFA` / `$NANA`).
|
|
74
|
-
|
|
75
|
-
**Mechanism:** `_claimTokensFor()` distributes fee tokens proportional to `shareToBeneficiary / outOfTotal` where `outOfTotal = _totalMintCost` (DefifaHook line 670-671). Reserved mints inflate `_totalMintCost` without adding to the hook's fee token balance, reducing each paid minter's proportional claim.
|
|
76
|
-
|
|
77
|
-
**Example:** 2 paid mints at 1 ETH + 2 reserved mints at 1 ETH tier price. `_totalMintCost = 4 ETH`. Each token gets 25% of fee tokens. Paid minters effectively subsidize reserved recipients.
|
|
78
|
-
|
|
79
|
-
**Mitigation:** By design. The test `testC_D3_reservedMintersGetFeeTokens` verifies this exact behavior: reserved minters receive fee tokens proportional to tier price, paid minters receive proportional to their contribution, and all fee tokens are distributed with nothing left in the hook.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
### RISK-5: Scorecard Timeout Can Block Legitimate Ratification
|
|
84
|
-
|
|
85
|
-
**Severity:** MEDIUM
|
|
86
|
-
**Status:** KNOWN, MITIGATED
|
|
87
|
-
**Tested:** `DefifaNoContestTest.testScorecardTimeout_elapsed_noContest`, `testNoContest_scorecardBlocked`
|
|
88
|
-
|
|
89
|
-
**Description:** If `scorecardTimeout` is set and elapses before a scorecard is ratified, the game permanently enters `NO_CONTEST`. Even a scorecard that has reached quorum cannot be ratified because `setTierCashOutWeightsTo` checks for SCORING phase (DefifaHook line 708).
|
|
90
|
-
|
|
91
|
-
**Mechanism:** `currentGamePhaseOf()` (DefifaDeployer line 258) returns `NO_CONTEST` when `block.timestamp > _currentRuleset.start + _ops.scorecardTimeout`. Once this condition is true, `setTierCashOutWeightsTo` reverts with `DefifaHook_GameIsntScoringYet` because the hook checks `gamePhaseReporter.currentGamePhaseOf(PROJECT_ID) != DefifaGamePhase.SCORING`.
|
|
92
|
-
|
|
93
|
-
**Mitigation:**
|
|
94
|
-
- Ratified scorecards take priority: `cashOutWeightIsSet` is checked before the timeout (DefifaDeployer line 239), so ratifying before timeout is definitive.
|
|
95
|
-
- `scorecardTimeout = 0` disables the mechanism entirely.
|
|
96
|
-
- The `triggerNoContestFor()` function allows anyone to queue a refund ruleset, ensuring players can recover funds.
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
### RISK-6: Delegation Locked After MINT Phase
|
|
101
|
-
|
|
102
|
-
**Severity:** MEDIUM
|
|
103
|
-
**Status:** BY DESIGN
|
|
104
|
-
**Tested:** `DefifaSecurityTest.testM_D6_delegationBlocked`, `DefifaHookRegressions.test_M5_attestationUnitsPreservedOnTransferToUndelegatedRecipient`
|
|
105
|
-
|
|
106
|
-
**Description:** `setTierDelegateTo` and `setTierDelegatesTo` only work during MINT phase (DefifaHook lines 730, 740). After MINT, NFT transfers auto-delegate to the recipient (if no delegate set), but holders cannot explicitly re-delegate.
|
|
107
|
-
|
|
108
|
-
**Implication:** If a holder transfers an NFT to a new owner during REFUND or SCORING, the new owner auto-delegates to themselves (DefifaHook `_transferTierAttestationUnits`, line 1002-1005). But if the new owner wants to delegate to a third party, they cannot.
|
|
109
|
-
|
|
110
|
-
**Mitigation:** By design. Prevents post-MINT governance manipulation. Auto-delegation on transfer (audit finding M-5 fix) ensures attestation units are never lost to `address(0)`.
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
### RISK-7: Single Governor Instance Across All Games
|
|
115
|
-
|
|
116
|
-
**Severity:** MEDIUM
|
|
117
|
-
**Status:** KNOWN, ACCEPTED
|
|
118
|
-
|
|
119
|
-
**Description:** All games share a single `DefifaGovernor` contract. A bug in `ratifyScorecardFrom`, `attestToScorecardFrom`, or `submitScorecardFor` affects every game.
|
|
120
|
-
|
|
121
|
-
**Specific concern:** The governor executes scorecard calldata via low-level call `_metadata.dataHook.call(_calldata)` (DefifaGovernor line 395). If the calldata construction in `_buildScorecardCalldataFor` (line 490-497) has a vulnerability, it could affect all games.
|
|
122
|
-
|
|
123
|
-
**Mitigation:** Governor logic is deliberately simple (no upgradability, no complex state transitions). Scorecard calldata is a deterministic ABI encoding of `setTierCashOutWeightsTo.selector` with tier weights.
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
### RISK-8: Front-Running of Clone Initialization
|
|
128
|
-
|
|
129
|
-
**Severity:** LOW
|
|
130
|
-
**Status:** MITIGATED
|
|
131
|
-
**Tested:** Implicitly by `launchGameWith` tests
|
|
132
|
-
|
|
133
|
-
**Description:** `DefifaHook` clones are created via `Clones.cloneDeterministic` with salt `keccak256(abi.encodePacked(msg.sender, _currentNonce))` (DefifaDeployer line 526). This prevents front-running because a different caller produces a different address.
|
|
134
|
-
|
|
135
|
-
**Residual risk:** The `initialize()` function has a re-initialization guard (`if (address(store) != address(0)) revert()`, DefifaHook line 487), but between clone creation and initialization (within the same transaction), there is no window for front-running.
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
### RISK-9: Fulfillment Failure Does Not Block Ratification
|
|
140
|
-
|
|
141
|
-
**Severity:** LOW
|
|
142
|
-
**Status:** MITIGATED
|
|
143
|
-
**Tested:** `FulfillmentBlocksRatification.test_ratificationSucceedsWhenFulfillmentReverts`
|
|
144
|
-
|
|
145
|
-
**Description:** `ratifyScorecardFrom` wraps `fulfillCommitmentsOf` in a try-catch (DefifaGovernor lines 402-405). If fulfillment fails (e.g., `sendPayoutsOf` reverts), the scorecard is still ratified and `FulfillmentFailed` event is emitted.
|
|
146
|
-
|
|
147
|
-
**Implication:** Fees may not be distributed, but the game proceeds to COMPLETE. `fulfillCommitmentsOf` can be retried separately.
|
|
148
|
-
|
|
149
|
-
**Mitigation:** The try-catch is intentional to prevent fulfillment issues from permanently blocking game completion. The `fulfilledCommitmentsOf` guard (DefifaDeployer line 304) allows exactly one successful fulfillment.
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
### RISK-10: Grace Period Bypass on Early Scorecard Submission
|
|
154
|
-
|
|
155
|
-
**Severity:** LOW
|
|
156
|
-
**Status:** FIXED (Regression tested)
|
|
157
|
-
**Tested:** `GracePeriodBypass.test_gracePeriodExtendsFromAttestationStart`
|
|
158
|
-
|
|
159
|
-
**Description:** When a scorecard is submitted before `attestationStartTime`, the grace period could previously expire before attestations even begin. Fixed by anchoring `gracePeriodEnds` to `attestationsBegin` rather than submission time.
|
|
160
|
-
|
|
161
|
-
**Implementation:** `_scorecard.gracePeriodEnds = uint48(_attestationsBegin + attestationGracePeriodOf(_gameId))` (DefifaGovernor line 468). The `attestationsBegin` is `max(block.timestamp, attestationStartTime)` (lines 460-463).
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
### RISK-11: Overweight Scorecard Rejection
|
|
166
|
-
|
|
167
|
-
**Severity:** LOW
|
|
168
|
-
**Status:** SAFE
|
|
169
|
-
**Tested:** `DefifaSecurityTest.testC_D2_rejectsOverweight`
|
|
170
|
-
|
|
171
|
-
**Description:** `validateAndBuildWeights` (DefifaHookLib line 85) enforces `_cumulativeCashOutWeight == TOTAL_CASHOUT_WEIGHT`. Any scorecard that does not sum to exactly 1e18 reverts with `DefifaHook_InvalidCashoutWeights`.
|
|
172
|
-
|
|
173
|
-
**Additional validations:**
|
|
174
|
-
- Tier IDs must be in strict ascending order (line 61): prevents duplicate tier entries.
|
|
175
|
-
- Tier must be in category 0 (line 68): prevents weight assignment to non-game tiers.
|
|
176
|
-
- Tier must exist (line 71): prevents weight assignment to nonexistent tiers.
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
### RISK-12: No-Contest Cash-Out Requires Explicit Trigger
|
|
181
|
-
|
|
182
|
-
**Severity:** MEDIUM
|
|
183
|
-
**Status:** BY DESIGN
|
|
184
|
-
**Tested:** `DefifaNoContestTest.testNoContest_cashOutBeforeTrigger_reverts`, `testMinParticipation_cashOutReturnsMintPrice`, `testScorecardTimeout_cashOutReturnsMintPrice`, `testNoContest_allUsersCanRefund`
|
|
185
|
-
|
|
186
|
-
**Description:** When a game enters NO_CONTEST, users cannot immediately cash out. They must first call `triggerNoContestFor()` (DefifaDeployer line 585), which queues a new ruleset without payout limits. Without this trigger, the SCORING ruleset has payout limits consuming the entire balance, leaving surplus at 0.
|
|
187
|
-
|
|
188
|
-
**Mechanism:** The SCORING ruleset sets `payoutLimits` to `type(uint224).max` (DefifaDeployer line 759), meaning all balance is allocated as payout. Since `ownerMustSendPayouts = true`, no one can send payouts, but the balance is not counted as surplus either. `triggerNoContestFor()` queues a ruleset with no `fundAccessLimitGroups`, making the entire balance available as surplus.
|
|
189
|
-
|
|
190
|
-
**Mitigation:** `triggerNoContestFor()` is permissionless -- anyone can call it. The function is idempotent (cannot be called twice: line 592).
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
### RISK-13: `_totalMintCost` Accounting Integrity
|
|
195
|
-
|
|
196
|
-
**Severity:** CRITICAL (if violated)
|
|
197
|
-
**Status:** SAFE (proven by invariant tests)
|
|
198
|
-
**Tested:** `DefifaMintCostInvariant.invariant_totalMintCostMatchesExpected`, `invariant_totalMintCostEqualsPriceTimesLiveTokens`, `invariant_tokenCountConsistency`
|
|
199
|
-
|
|
200
|
-
**Description:** `_totalMintCost` tracks the cumulative price of all live (non-burned) NFTs. It is incremented on mint (DefifaHook line 859) and reserved mint (line 568), and decremented on cash-out (line 678). If this value drifts, fee token distribution (`_claimTokensFor`) will over/under-allocate.
|
|
201
|
-
|
|
202
|
-
**Invariant proof:** Stateful fuzz testing (`MintCostHandler`) performs random mints and refunds, verifying after each operation that `_totalMintCost == tierPrice * liveTokenCount` and `_totalMintCost == expectedMintCost` (shadow accounting).
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
### RISK-14: Fee Accounting After Split Normalization
|
|
207
|
-
|
|
208
|
-
**Severity:** MEDIUM
|
|
209
|
-
**Status:** SAFE (proven by tests)
|
|
210
|
-
**Tested:** `DefifaFeeAccountingTest.testFeeAccounting_defaultSplits`, `testCashOutAfterFees`, `testFeeAccounting_noRoundingLoss`, `testFeeAccounting_withUserSplits`, `testCashOutAfterFees_withUserSplits`, `testSplitNormalization_noRoundingLoss`
|
|
211
|
-
|
|
212
|
-
**Description:** Fee splits are normalized in `_buildSplits()` (DefifaDeployer lines 825-894). The NANA split absorbs the rounding remainder (line 883). `_commitmentPercentOf[_gameId]` stores the absolute total, and `fulfillCommitmentsOf` computes `mulDiv(_pot, _commitmentPercentOf[gameId], SPLITS_TOTAL_PERCENT)` (line 326) to determine the fee amount.
|
|
213
|
-
|
|
214
|
-
**Proven property:** `fee + surplus == originalPot` (exact equality, tested in `testFeeAccounting_noRoundingLoss`).
|
|
215
|
-
|
|
216
|
-
---
|
|
217
|
-
|
|
218
|
-
### RISK-15: Reentrancy in `afterCashOutRecordedWith`
|
|
219
|
-
|
|
220
|
-
**Severity:** LOW
|
|
221
|
-
**Status:** SAFE
|
|
222
|
-
|
|
223
|
-
**Description:** `afterCashOutRecordedWith` (DefifaHook lines 602-679) burns tokens before making external calls. The burn sequence:
|
|
224
|
-
1. Burns tokens in a loop (line 648)
|
|
225
|
-
2. Calls `_didBurn` to record burns in the store (line 658)
|
|
226
|
-
3. Increments `amountRedeemed` (line 666)
|
|
227
|
-
4. Calls `_claimTokensFor` which transfers fee tokens (line 669-671)
|
|
228
|
-
|
|
229
|
-
**Analysis:** Tokens are burned before state updates and before any external token transfers. The JB terminal has already committed the cash-out amount before calling this hook. A reentrant call would fail because the burned tokens no longer exist (ownership check at line 643 would revert).
|
|
230
|
-
|
|
231
|
-
---
|
|
232
|
-
|
|
233
|
-
### RISK-16: Reentrancy in `fulfillCommitmentsOf`
|
|
234
|
-
|
|
235
|
-
**Severity:** LOW
|
|
236
|
-
**Status:** SAFE
|
|
237
|
-
|
|
238
|
-
**Description:** `fulfillCommitmentsOf` (DefifaDeployer lines 302-388) sets `fulfilledCommitmentsOf[gameId]` (line 330) before calling `sendPayoutsOf` (line 334) and `queueRulesetsOf` (line 383). The guard at line 304 (`if (fulfilledCommitmentsOf[gameId] != 0) return`) prevents re-entry.
|
|
239
|
-
|
|
240
|
-
**Edge case:** Uses `max(feeAmount, 1)` (line 330) to ensure the guard works even when the pot rounds to 0 fee.
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
### RISK-17: Attestation Unit Conservation on Transfer
|
|
245
|
-
|
|
246
|
-
**Severity:** HIGH (before fix)
|
|
247
|
-
**Status:** FIXED
|
|
248
|
-
**Tested:** `DefifaHookRegressions.test_M5_attestationUnitsPreservedOnTransferToUndelegatedRecipient`, `test_M5_multipleTransfersToUndelegatedRecipientsPreserveUnits`
|
|
249
|
-
|
|
250
|
-
**Description:** Previously, transferring an NFT to a recipient with no delegate set would lose attestation units (sender's delegate lost units but no one gained them). Fixed by auto-delegating undelegated recipients to themselves in `_transferTierAttestationUnits` (DefifaHook lines 1001-1006).
|
|
251
|
-
|
|
252
|
-
**Invariant verified:** Sum of all delegate attestation units equals total attestation supply across chains of 3+ sequential transfers.
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
### RISK-18: Fund Conservation Across Varying Game Parameters
|
|
257
|
-
|
|
258
|
-
**Severity:** CRITICAL (if violated)
|
|
259
|
-
**Status:** SAFE
|
|
260
|
-
**Tested:** `DefifaSecurityTest.testFuzz_fundConservation` (fuzz), `testHighVolume_32tiers`, `testMultiPlayer_winnerTakesAll`, `testRefundIntegrity`
|
|
261
|
-
|
|
262
|
-
**Description:** Total cash-outs + remaining surplus must equal the pre-fulfillment pot (minus fees). This is the fundamental economic invariant.
|
|
263
|
-
|
|
264
|
-
**Fuzz test parameters:** 2-12 tiers, 1-3 players per tier, 1 ETH tier price. Tolerance: N wei where N = total user count.
|
|
265
|
-
|
|
266
|
-
**Proven for edge cases:**
|
|
267
|
-
- 32 tiers at 100 ETH each (3,200 ETH pot): dust <= 1e15 wei
|
|
268
|
-
- Winner-takes-all (100% to one tier): losers get 0 ETH, winners split evenly within 0.1%
|
|
269
|
-
- Extreme weights (1, TOTAL-2, 1): tier 2 gets >99% of pot
|
|
270
|
-
|
|
271
|
-
---
|
|
272
|
-
|
|
273
|
-
### RISK-19: `uint208` Overflow in Attestation Checkpoints
|
|
274
|
-
|
|
275
|
-
**Severity:** LOW
|
|
276
|
-
**Status:** SAFE (bounded)
|
|
277
|
-
|
|
278
|
-
**Description:** Attestation units use OpenZeppelin `Checkpoints.Trace208` which stores values as `uint208`. The `_moveTierDelegateAttestations` function casts amounts to `uint208` (DefifaHook lines 892, 902).
|
|
279
|
-
|
|
280
|
-
**Bound:** Maximum attestation units per tier = `tier.votingUnits * tier.initialSupply`. With `initialSupply = 999_999_999` and typical `votingUnits` values, overflow of `uint208` (max ~4.1e62) is practically impossible.
|
|
281
|
-
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
### RISK-20: Stale `block.timestamp` in Via-IR Compiled Tests
|
|
285
|
-
|
|
286
|
-
**Severity:** LOW (test-only)
|
|
287
|
-
**Status:** MITIGATED
|
|
288
|
-
|
|
289
|
-
**Description:** Multiple test files use a `TimestampReader` helper contract to read `block.timestamp` via an external call, bypassing the Solidity via-IR optimizer's timestamp caching. This is a test infrastructure concern, not a production risk.
|
|
290
|
-
|
|
291
|
-
---
|
|
292
|
-
|
|
293
|
-
## Reentrancy Summary
|
|
294
|
-
|
|
295
|
-
| Function | Protection | External Calls After State Updates | Risk |
|
|
296
|
-
|----------|-----------|-----------------------------------|------|
|
|
297
|
-
| `afterCashOutRecordedWith` | Tokens burned before state updates; terminal already committed | `_claimTokensFor` (fee token transfer) | LOW |
|
|
298
|
-
| `afterPayRecordedWith` | Payment recorded before minting | None after mint | LOW |
|
|
299
|
-
| `fulfillCommitmentsOf` | `fulfilledCommitmentsOf` guard set before `sendPayoutsOf` | `sendPayoutsOf`, `queueRulesetsOf` | LOW |
|
|
300
|
-
| `triggerNoContestFor` | `noContestTriggeredFor` set before `queueRulesetsOf` | `queueRulesetsOf` | LOW |
|
|
301
|
-
| `ratifyScorecardFrom` | `ratifiedScorecardIdOf` set before low-level call | `dataHook.call`, `fulfillCommitmentsOf` (try-catch) | LOW |
|
|
302
|
-
|
|
303
|
-
---
|
|
304
|
-
|
|
305
|
-
## Test Coverage Map
|
|
306
|
-
|
|
307
|
-
| Risk | Test File(s) | Specific Test(s) |
|
|
308
|
-
|------|-------------|-----------------|
|
|
309
|
-
| RISK-1 Whale dominance | `DefifaSecurity.t.sol` | `testQuorum_50pctMintedTiers` |
|
|
310
|
-
| RISK-2 Dynamic quorum | `DefifaSecurity.t.sol` | `testQuorum_50pctMintedTiers`, `testNoCashOut_beforeScorecard` |
|
|
311
|
-
| RISK-3 Weight truncation | `DefifaSecurity.t.sol` | `testRounding_extremeWeights` |
|
|
312
|
-
| RISK-4 Fee token dilution | `DefifaSecurity.t.sol` | `testC_D3_reservedMintersGetFeeTokens` |
|
|
313
|
-
| RISK-5 Scorecard timeout | `DefifaNoContest.t.sol` | `testScorecardTimeout_elapsed_noContest`, `testScorecardTimeout_exactBoundary_scoring`, `testNoContest_scorecardBlocked` |
|
|
314
|
-
| RISK-6 Delegation locked | `DefifaSecurity.t.sol` | `testM_D6_delegationBlocked` |
|
|
315
|
-
| RISK-7 Single governor | (No isolation test) | Design review only |
|
|
316
|
-
| RISK-8 Clone front-running | (Implicit) | All `launchGameWith` tests |
|
|
317
|
-
| RISK-9 Fulfillment failure | `regression/FulfillmentBlocksRatification.t.sol` | `test_ratificationSucceedsWhenFulfillmentReverts` |
|
|
318
|
-
| RISK-10 Grace period bypass | `regression/GracePeriodBypass.t.sol` | `test_gracePeriodExtendsFromAttestationStart` |
|
|
319
|
-
| RISK-11 Overweight scorecard | `DefifaSecurity.t.sol` | `testC_D2_rejectsOverweight` |
|
|
320
|
-
| RISK-12 No-contest trigger | `DefifaNoContest.t.sol` | `testNoContest_cashOutBeforeTrigger_reverts`, `testTriggerNoContest_revertsWhenNotNoContest`, `testTriggerNoContest_revertsWhenAlreadyTriggered` |
|
|
321
|
-
| RISK-13 `_totalMintCost` | `DefifaMintCostInvariant.t.sol` | `invariant_totalMintCostMatchesExpected`, `invariant_totalMintCostEqualsPriceTimesLiveTokens`, `invariant_tokenCountConsistency` |
|
|
322
|
-
| RISK-14 Fee accounting | `DefifaFeeAccounting.t.sol` | All 6 tests |
|
|
323
|
-
| RISK-15 Cash-out reentrancy | (Code review) | State ordering analysis |
|
|
324
|
-
| RISK-16 Fulfillment reentrancy | (Code review) | Guard analysis |
|
|
325
|
-
| RISK-17 Attestation conservation | `DefifaHookRegressions.t.sol` | `test_M5_attestationUnitsPreservedOnTransferToUndelegatedRecipient`, `test_M5_multipleTransfersToUndelegatedRecipientsPreserveUnits` |
|
|
326
|
-
| RISK-18 Fund conservation | `DefifaSecurity.t.sol` | `testFuzz_fundConservation`, `testHighVolume_32tiers`, `testMultiPlayer_winnerTakesAll`, `testRefundIntegrity` |
|
|
327
|
-
| RISK-19 uint208 overflow | (Bounded analysis) | Arithmetic review |
|
|
328
|
-
| RISK-20 Timestamp caching | (Test infrastructure) | `TimestampReader` pattern |
|
|
329
|
-
|
|
330
|
-
### Untested Areas
|
|
331
|
-
|
|
332
|
-
| Area | Reason | Risk |
|
|
333
|
-
|------|--------|------|
|
|
334
|
-
| ERC-20 token games (non-ETH) | All tests use `NATIVE_TOKEN` | LOW -- same code path, `SafeERC20` used |
|
|
335
|
-
| Games with >32 tiers | Fuzz tests cap at 12, security tests at 32 | LOW -- 128-element array bounds |
|
|
336
|
-
| Custom `tokenUriResolver` interaction | SVG test exists but no adversarial URI resolver | LOW -- resolver is view-only |
|
|
337
|
-
| Concurrent multi-game governor state | Tests use single game per governor | MEDIUM -- storage isolation via `gameId` mapping keys |
|
|
338
|
-
| `Clones.cloneDeterministic` collision | Deterministic but no collision test | LOW -- `keccak256(sender, nonce)` salt is unique per caller per call |
|
|
339
|
-
|
|
340
|
-
---
|
|
341
|
-
|
|
342
|
-
## Severity Legend
|
|
343
|
-
|
|
344
|
-
| Rating | Definition |
|
|
345
|
-
|--------|-----------|
|
|
346
|
-
| CRITICAL | Fund loss or protocol-breaking if violated; proven safe by invariant tests |
|
|
347
|
-
| HIGH | Significant impact on game fairness or fund distribution; fixed via audit |
|
|
348
|
-
| MEDIUM | Exploitable under specific conditions; mitigated by design choices or economic constraints |
|
|
349
|
-
| LOW | Theoretical risk with bounded impact or test-only concern |
|
|
46
|
+
- `_totalMintCost == tierPrice * liveTokenCount` after every mint and burn.
|
|
47
|
+
- Total cash-outs + remaining surplus == pre-fulfillment pot minus fees.
|
|
48
|
+
- Scorecard weights sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18).
|
|
49
|
+
- Attestation units are conserved across all transfers (no units lost to `address(0)`).
|
|
50
|
+
- `fulfilledCommitmentsOf[gameId]` is set at most once per game.
|
|
51
|
+
- Per-tier supply never exceeds `initialSupply`.
|
|
52
|
+
- Sum of all delegate attestation units equals total attestation supply.
|
package/SKILLS.md
CHANGED
|
@@ -110,7 +110,7 @@ During COMPLETE phase cash outs, players also receive proportional $DEFIFA and $
|
|
|
110
110
|
- Scorecard attestation weight uses `mulDiv(MAX_ATTESTATION_POWER_TIER, userTierUnits, totalTierUnits)` per tier. If `totalTierUnits` is 0 for a tier (no delegations), that tier contributes no attestation power.
|
|
111
111
|
- The governor's `quorum` is **dynamic**: it only counts tiers that have at least one minted token. Adding minted tiers changes the quorum retroactively for all active proposals.
|
|
112
112
|
- `ratifyScorecardFrom` executes the scorecard via a **low-level `.call`** to the hook address. This is necessary because the hook's `setTierCashOutWeightsTo` is `onlyOwner` and the governor is the hook's owner.
|
|
113
|
-
- `fulfillCommitmentsOf` uses `max(amount, 1)` as a reentrancy guard. If called when the pot is 0, it stores 1 as the fulfilled amount to prevent re-entry.
|
|
113
|
+
- `fulfillCommitmentsOf` uses `max(amount, 1)` as a reentrancy guard. If called when the pot is 0, it stores 1 as the fulfilled amount to prevent re-entry. `sendPayoutsOf` is wrapped in try-catch: on failure, resets to sentinel (1) and emits `CommitmentPayoutFailed`.
|
|
114
114
|
- `_buildSplits` normalizes all split percentages relative to the total absolute percent. Rounding remainder is absorbed by the protocol fee split (last in the array).
|
|
115
115
|
- `_totalMintCost` tracks cumulative mint prices of all live tokens (paid + reserved). It's incremented on pay and reserve mint, decremented on cash out. This is the denominator for fee token ($DEFIFA/$NANA) distribution.
|
|
116
116
|
- Cash outs during COMPLETE phase revert with `DefifaHook_NothingToClaim` if **both** the reclaimed ETH amount is 0 **and** no fee tokens were transferred. This prevents burning NFTs for nothing.
|