@ballkidz/defifa 0.0.3 → 0.0.5
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 +127 -0
- package/ARCHITECTURE.md +80 -0
- package/RISKS.md +349 -0
- package/STYLE_GUIDE.md +467 -0
- package/foundry.toml +8 -17
- package/package.json +3 -3
- package/src/DefifaDeployer.sol +1 -1
- package/src/DefifaHook.sol +11 -4
- package/test/DefifaGovernor.t.sol +2 -2
- package/test/DefifaHook_AuditFindings.t.sol +374 -0
- package/test/Fork.t.sol +2400 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Administration
|
|
2
|
+
|
|
3
|
+
Admin privileges and their scope in defifa-collection-deployer-v6.
|
|
4
|
+
|
|
5
|
+
## Roles
|
|
6
|
+
|
|
7
|
+
| Role | Who | How Assigned |
|
|
8
|
+
|------|-----|-------------|
|
|
9
|
+
| **Game Creator** | Any EOA or contract that calls `DefifaDeployer.launchGameWith()` | Self-selected; permissionless |
|
|
10
|
+
| **DefifaDeployer** (contract) | Singleton deployed at protocol setup | Immutable; owns all game JB projects and the governor |
|
|
11
|
+
| **DefifaGovernor** (contract) | Singleton; `Ownable` by the DefifaDeployer | Ownership transferred from deployer at construction |
|
|
12
|
+
| **DefifaHook** (per-game clone) | One clone per game; `Ownable` by the DefifaGovernor | Ownership transferred from deployer during `launchGameWith()` (line 567) |
|
|
13
|
+
| **DefifaProjectOwner** | Optional proxy contract that holds the Defifa fee project NFT | Receives project NFT; grants `SET_SPLIT_GROUPS` to deployer |
|
|
14
|
+
| **Scorecard Submitter** | Any address during SCORING phase | Permissionless (`submitScorecardFor`, DefifaGovernor line 413) |
|
|
15
|
+
| **Attestor** | Any NFT holder with attestation weight | Must hold game NFTs; weight proportional to holdings per tier |
|
|
16
|
+
| **Default Attestation Delegate** | Address set at game launch | Set via `DefifaLaunchProjectData.defaultAttestationDelegate` |
|
|
17
|
+
| **Tier Delegate** | Any address delegated attestation units by an NFT holder | Set via `setTierDelegateTo` / `setTierDelegatesTo` during MINT phase only |
|
|
18
|
+
|
|
19
|
+
## Privileged Functions
|
|
20
|
+
|
|
21
|
+
### DefifaDeployer
|
|
22
|
+
|
|
23
|
+
| Function | Required Role | Permission Check | What It Does |
|
|
24
|
+
|----------|--------------|-----------------|-------------|
|
|
25
|
+
| `launchGameWith()` | Anyone | None (permissionless) | Creates a new JB project, clones DefifaHook, initializes governor, configures rulesets with MINT/REFUND/SCORING phases. Game parameters are immutable after this call. |
|
|
26
|
+
| `fulfillCommitmentsOf()` | Anyone | Guarded by `fulfilledCommitmentsOf[gameId] != 0` reentrancy check; requires `cashOutWeightIsSet` on the hook | Sends fee payouts (Defifa 5% + NANA 2.5% + user splits) via `sendPayoutsOf`, then queues the final COMPLETE ruleset. Can only execute once per game. |
|
|
27
|
+
| `triggerNoContestFor()` | Anyone | Requires `currentGamePhaseOf(gameId) == NO_CONTEST` and `!noContestTriggeredFor[gameId]` | Queues a new ruleset without payout limits so surplus equals balance, enabling full refunds. Can only execute once per game. |
|
|
28
|
+
|
|
29
|
+
### DefifaGovernor
|
|
30
|
+
|
|
31
|
+
| Function | Required Role | Permission Check | What It Does |
|
|
32
|
+
|----------|--------------|-----------------|-------------|
|
|
33
|
+
| `initializeGame()` | DefifaDeployer (owner) | `onlyOwner` (line 294) | Sets attestation start time and grace period for a game. Enforces minimum 1-day grace period. Called automatically during `launchGameWith()`. |
|
|
34
|
+
| `submitScorecardFor()` | Anyone | Must be in SCORING phase; no ratified scorecard yet; no duplicate scorecard hash; weighted tiers must have nonzero supply | Submits a scorecard for attestation. Sets `attestationsBegin` and `gracePeriodEnds` timestamps. |
|
|
35
|
+
| `attestToScorecardFrom()` | Any NFT holder | Must be in SCORING phase; scorecard must be ACTIVE or SUCCEEDED; caller cannot have already attested | Records attestation weight based on tier holdings at the scorecard's `attestationsBegin` timestamp. |
|
|
36
|
+
| `ratifyScorecardFrom()` | Anyone | Scorecard must be in SUCCEEDED state (quorum met + grace period elapsed); no scorecard already ratified | Executes the scorecard via low-level call to `setTierCashOutWeightsTo` on the hook, then tries `fulfillCommitmentsOf`. |
|
|
37
|
+
|
|
38
|
+
### DefifaHook
|
|
39
|
+
|
|
40
|
+
| Function | Required Role | Permission Check | What It Does |
|
|
41
|
+
|----------|--------------|-----------------|-------------|
|
|
42
|
+
| `initialize()` | DefifaDeployer | Reverts if `address(this) == codeOrigin` (line 484) or already initialized (`store != address(0)`, line 487) | One-time initialization of the cloned hook with game configuration, tiers, and tier names. Transfers ownership to caller. |
|
|
43
|
+
| `setTierCashOutWeightsTo()` | DefifaGovernor (owner) | `onlyOwner` (line 703); must be in SCORING phase; weights not already set | Sets the cash-out weight distribution across tiers. Validates weights sum to `TOTAL_CASHOUT_WEIGHT` (1e18). Irreversible -- once set, `cashOutWeightIsSet` is permanently true. |
|
|
44
|
+
| `setTierDelegateTo()` | Any NFT holder | Must be in MINT phase (line 730-732) | Delegates attestation units for a specific tier to another address. |
|
|
45
|
+
| `setTierDelegatesTo()` | Any NFT holder | Must be in MINT phase (line 740-742); delegatee cannot be address(0) | Batch delegation of attestation units across multiple tiers. |
|
|
46
|
+
| `mintReservesFor()` | Anyone | Reverts if `pauseMintPendingReserves` is set in ruleset metadata (line 537-539) | Mints reserved tokens to the tier's reserve beneficiary. Increments `_totalMintCost` so reserved recipients can claim fee tokens. |
|
|
47
|
+
| `afterPayRecordedWith()` | JB Terminal | Caller must be a terminal of the project (line 436-439); `msg.value` must be 0 | Processes payment: validates currency, mints NFTs, sets up attestation delegation. |
|
|
48
|
+
| `afterCashOutRecordedWith()` | JB Terminal | Caller must be a terminal of the project (line 610-613); `msg.value` must be 0 | Burns NFTs on cash-out, tracks redeemed amounts, distributes fee tokens during COMPLETE phase. |
|
|
49
|
+
| `transferOwnership()` | Current owner | `onlyOwner` (inherited from Ownable) | Transfers hook ownership. Used once during deployment to transfer from deployer to governor. |
|
|
50
|
+
|
|
51
|
+
### DefifaProjectOwner
|
|
52
|
+
|
|
53
|
+
| Function | Required Role | Permission Check | What It Does |
|
|
54
|
+
|----------|--------------|-----------------|-------------|
|
|
55
|
+
| `onERC721Received()` | JBProjects contract | `msg.sender` must be the JBProjects contract (line 47) | When the Defifa fee project NFT is transferred here, auto-grants `SET_SPLIT_GROUPS` permission to the DefifaDeployer. The project NFT is permanently locked -- cannot be recovered. |
|
|
56
|
+
|
|
57
|
+
## Game Lifecycle Administration
|
|
58
|
+
|
|
59
|
+
The game lifecycle is fully automated through Juicebox rulesets configured at launch. No admin can change phases manually.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
COUNTDOWN --> MINT --> REFUND (optional) --> SCORING --> COMPLETE or NO_CONTEST
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Phase transitions are time-based, encoded in JBRuleset durations:**
|
|
66
|
+
- **COUNTDOWN**: Before `start - mintPeriodDuration - refundPeriodDuration`. Cycle number 0.
|
|
67
|
+
- **MINT**: Duration = `mintPeriodDuration`. Payments open, refunds at mint price. Cycle number 1.
|
|
68
|
+
- **REFUND**: Duration = `refundPeriodDuration` (optional, only if nonzero). Payments paused, refunds still at mint price. Cycle number 2.
|
|
69
|
+
- **SCORING**: Duration = 0 (no expiry). Payments paused. Cash-out weights must be set via governance. Cycle number 3+.
|
|
70
|
+
- **COMPLETE**: Entered when `cashOutWeightIsSet == true`. Cash-outs use scorecard weights.
|
|
71
|
+
- **NO_CONTEST**: Entered when `minParticipation` threshold is not met, or `scorecardTimeout` elapses without ratification. Requires `triggerNoContestFor()` to unlock refunds.
|
|
72
|
+
|
|
73
|
+
**Who controls scoring:**
|
|
74
|
+
1. Anyone submits a scorecard during SCORING (`submitScorecardFor`)
|
|
75
|
+
2. NFT holders attest based on their per-tier voting weight (`attestToScorecardFrom`)
|
|
76
|
+
3. Once quorum (50% of minted tiers' attestation power) is met and grace period passes, anyone ratifies (`ratifyScorecardFrom`)
|
|
77
|
+
4. The governor calls `setTierCashOutWeightsTo` on the hook via low-level call
|
|
78
|
+
5. `fulfillCommitmentsOf` sends fee payouts and queues the final ruleset
|
|
79
|
+
|
|
80
|
+
**No single entity controls scoring.** The process requires collective attestation from NFT holders across tiers.
|
|
81
|
+
|
|
82
|
+
## Immutable Configuration
|
|
83
|
+
|
|
84
|
+
The following are set at game creation and cannot be changed:
|
|
85
|
+
|
|
86
|
+
| Parameter | Set In | Notes |
|
|
87
|
+
|-----------|--------|-------|
|
|
88
|
+
| Tier prices | `launchGameWith()` | Uniform price across all tiers (`tierPrice`) |
|
|
89
|
+
| Tier count and names | `launchGameWith()` | Stored in hook during `initialize()` |
|
|
90
|
+
| Game timing (start, mint duration, refund duration) | `launchGameWith()` | Encoded as JBRuleset durations |
|
|
91
|
+
| Payment token | `launchGameWith()` | Single token per game |
|
|
92
|
+
| Fee structure | Constructor constants | `DEFIFA_FEE_DIVISOR = 20` (5%), `BASE_PROTOCOL_FEE_DIVISOR = 40` (2.5%) |
|
|
93
|
+
| Attestation start time | `launchGameWith()` | Stored in governor via `initializeGame()` |
|
|
94
|
+
| Attestation grace period | `launchGameWith()` | Minimum 1 day enforced (governor line 300) |
|
|
95
|
+
| Default attestation delegate | `launchGameWith()` | Stored in hook |
|
|
96
|
+
| `minParticipation` threshold | `launchGameWith()` | 0 = disabled |
|
|
97
|
+
| `scorecardTimeout` | `launchGameWith()` | 0 = disabled |
|
|
98
|
+
| User splits | `launchGameWith()` | Stored as JB split groups at game creation |
|
|
99
|
+
| Hook code origin | Constructor | Template contract for cloning |
|
|
100
|
+
| `TOTAL_CASHOUT_WEIGHT` | Constant | 1e18, cannot be changed |
|
|
101
|
+
| JB project ownership | `launchGameWith()` | Project owned by DefifaDeployer contract |
|
|
102
|
+
|
|
103
|
+
## Admin Boundaries
|
|
104
|
+
|
|
105
|
+
What admins CANNOT do:
|
|
106
|
+
|
|
107
|
+
1. **No one can change game rules after launch.** Tier prices, timing, token, fees, and split configuration are all immutable.
|
|
108
|
+
|
|
109
|
+
2. **No one can unilaterally set scorecard weights.** The `setTierCashOutWeightsTo` function requires `onlyOwner` (the governor), and the governor only calls it after a scorecard reaches quorum through collective attestation.
|
|
110
|
+
|
|
111
|
+
3. **No one can pause or cancel a game.** Once launched, the game proceeds through its phases automatically based on time.
|
|
112
|
+
|
|
113
|
+
4. **No one can extract funds from the treasury.** The `ownerMustSendPayouts` flag is set on the SCORING ruleset, and payouts are limited to the pre-configured splits. The deployer can only send payouts matching the split configuration.
|
|
114
|
+
|
|
115
|
+
5. **No one can mint new tiers or change tier supply.** Tiers are set once during `initialize()` with `cannotBeRemoved: true`.
|
|
116
|
+
|
|
117
|
+
6. **No one can change delegates after MINT phase.** `setTierDelegateTo` and `setTierDelegatesTo` both revert with `DefifaHook_DelegateChangesUnavailableInThisPhase` outside MINT.
|
|
118
|
+
|
|
119
|
+
7. **No one can re-set cash-out weights.** The `cashOutWeightIsSet` flag is checked before setting (line 713) and the function reverts with `DefifaHook_CashoutWeightsAlreadySet`.
|
|
120
|
+
|
|
121
|
+
8. **No one can re-ratify a scorecard.** The `ratifiedScorecardIdOf[gameId] != 0` check prevents double ratification (governor line 374).
|
|
122
|
+
|
|
123
|
+
9. **No one can fulfill commitments twice.** The `fulfilledCommitmentsOf[gameId] != 0` check (deployer line 304) prevents re-entry.
|
|
124
|
+
|
|
125
|
+
10. **No one can recover the project NFT from DefifaProjectOwner.** Once transferred, the NFT is permanently locked.
|
|
126
|
+
|
|
127
|
+
11. **The game creator has no ongoing privileges.** After `launchGameWith()` returns, the caller has no special role. All game administration is handled by the protocol contracts and collective governance.
|
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# defifa-collection-deployer-v6 — Architecture
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Prediction game platform built on Juicebox V6. Creates games where players buy NFT tiers representing outcomes, a governance process scores the outcomes, and winners claim treasury funds proportional to their tier's score.
|
|
6
|
+
|
|
7
|
+
## Contract Map
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── DefifaDeployer.sol — Deploys games: project + hook + governor + URI resolver
|
|
12
|
+
├── DefifaHook.sol — Pay/cashout hook with game phase logic and attestation
|
|
13
|
+
├── DefifaGovernor.sol — Scorecard ratification via tier-weighted governance
|
|
14
|
+
├── DefifaProjectOwner.sol — Proxy owner for Defifa projects
|
|
15
|
+
├── DefifaTokenUriResolver.sol — On-chain SVG metadata for game NFTs
|
|
16
|
+
├── enums/
|
|
17
|
+
│ ├── DefifaGamePhase.sol — MINT → REFUND → SCORING → COMPLETE
|
|
18
|
+
│ └── DefifaScorecardState.sol
|
|
19
|
+
├── interfaces/ — IDefifaDeployer, IDefifaHook, IDefifaGovernor, etc.
|
|
20
|
+
├── libraries/
|
|
21
|
+
│ └── DefifaHookLib.sol — Game logic helpers
|
|
22
|
+
└── structs/ — Scorecards, attestations, tier params, delegations
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Key Data Flows
|
|
26
|
+
|
|
27
|
+
### Game Lifecycle
|
|
28
|
+
```
|
|
29
|
+
MINT Phase:
|
|
30
|
+
Creator → DefifaDeployer.launchGameWith()
|
|
31
|
+
→ Create JB project with DefifaHook as data/pay/cashout hook
|
|
32
|
+
→ Deploy DefifaGovernor for scorecard governance
|
|
33
|
+
→ Players buy NFT tiers (outcomes they predict)
|
|
34
|
+
→ Delegation happens during this phase only
|
|
35
|
+
|
|
36
|
+
REFUND Phase:
|
|
37
|
+
→ Players can cash out for full refund (100% redemption rate)
|
|
38
|
+
|
|
39
|
+
SCORING Phase:
|
|
40
|
+
→ Anyone → DefifaGovernor.submitScorecard(weights[])
|
|
41
|
+
→ Tier holders attest to scorecards
|
|
42
|
+
→ Scorecard reaches quorum → ratified
|
|
43
|
+
→ DefifaHook receives final cash-out weights per tier
|
|
44
|
+
|
|
45
|
+
COMPLETE Phase:
|
|
46
|
+
→ Winners → cash out NFTs at scored weights
|
|
47
|
+
→ Deployer → fulfillCommitmentsOf() distributes fee tokens
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Governance Flow
|
|
51
|
+
```
|
|
52
|
+
Scorer → DefifaGovernor.submitScorecard(tierWeights[])
|
|
53
|
+
→ Validate: correct phase, valid tier order, weights sum correctly
|
|
54
|
+
→ Create proposal hash
|
|
55
|
+
|
|
56
|
+
Attestor → DefifaGovernor.attestToScorecard(proposalId)
|
|
57
|
+
→ Must hold NFT tier tokens
|
|
58
|
+
→ Attestation weight = voting power from held tiers
|
|
59
|
+
→ When quorum reached → scorecard ratified
|
|
60
|
+
→ DefifaHook.setScorecard() called
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Extension Points
|
|
64
|
+
|
|
65
|
+
| Point | Interface | Purpose |
|
|
66
|
+
|-------|-----------|---------|
|
|
67
|
+
| Data hook | `IJBRulesetDataHook` | Phase-aware pay/cashout behavior |
|
|
68
|
+
| Pay hook | `IJBPayHook` | NFT minting during MINT phase |
|
|
69
|
+
| Cash out hook | `IJBCashOutHook` | Scored weight redemptions |
|
|
70
|
+
| Token URI resolver | `IJB721TokenUriResolver` | On-chain SVG generation |
|
|
71
|
+
| Governor | `IDefifaGovernor` | Scorecard governance |
|
|
72
|
+
|
|
73
|
+
## Dependencies
|
|
74
|
+
- `@bananapus/core-v6` — Core protocol
|
|
75
|
+
- `@bananapus/721-hook-v6` — NFT tier system
|
|
76
|
+
- `@bananapus/address-registry-v6` — Deterministic deploys
|
|
77
|
+
- `@bananapus/permission-ids-v6` — Permission constants
|
|
78
|
+
- `@openzeppelin/contracts` — Checkpoints, Ownable, Clones
|
|
79
|
+
- `@prb/math` — mulDiv
|
|
80
|
+
- `scripty.sol` — On-chain scripting for SVG
|
package/RISKS.md
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# defifa-collection-deployer-v6 -- Risks
|
|
2
|
+
|
|
3
|
+
Deep implementation-level risk analysis with line references, severity ratings, and test coverage mapping.
|
|
4
|
+
|
|
5
|
+
## Trust Assumptions
|
|
6
|
+
|
|
7
|
+
1. **DefifaGovernor** -- All games share one `DefifaGovernor` instance (singleton `Ownable` by `DefifaDeployer`). A bug in the governor affects every game simultaneously.
|
|
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.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Risk Inventory
|
|
16
|
+
|
|
17
|
+
### RISK-1: Whale Tier Dominance via Multi-Tier Accumulation
|
|
18
|
+
|
|
19
|
+
**Severity:** MEDIUM
|
|
20
|
+
**Status:** KNOWN, ACCEPTED
|
|
21
|
+
**Tested:** `DefifaSecurityTest.testQuorum_50pctMintedTiers`
|
|
22
|
+
|
|
23
|
+
**Description:** An attacker buys the majority of tokens in 50%+ of tiers, gaining enough attestation power to single-handedly reach quorum and ratify a self-serving scorecard.
|
|
24
|
+
|
|
25
|
+
**Mechanism:** Each tier caps attestation power at `MAX_ATTESTATION_POWER_TIER` (1e9, DefifaGovernor line 64). Quorum is 50% of minted tiers' total power (DefifaGovernor `quorum()`, line 203-223). Holding `>50%` of tokens in `>50%` of minted tiers gives the attacker `>25%` of total power per tier, easily exceeding quorum.
|
|
26
|
+
|
|
27
|
+
**Attack scenario:**
|
|
28
|
+
1. Game has 10 tiers, 6 minted. Quorum = `6 * 1e9 / 2 = 3e9`.
|
|
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.
|
|
32
|
+
|
|
33
|
+
**Mitigation:** Per-tier cap ensures dominance in a single high-supply tier is insufficient. Capital cost scales with number of tiers to control. Grace period (minimum 1 day, DefifaGovernor line 300) gives other holders time to counter-attest.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### RISK-2: Dynamic Quorum Based on Live Supply
|
|
38
|
+
|
|
39
|
+
**Severity:** MEDIUM
|
|
40
|
+
**Status:** KNOWN, ACCEPTED
|
|
41
|
+
**Tested:** `DefifaSecurityTest.testQuorum_50pctMintedTiers`
|
|
42
|
+
|
|
43
|
+
**Description:** Quorum is computed from `currentSupplyOfTier()` (live supply = minted - burned) at call time, not from a snapshot. If tokens are burned between attestation and ratification, quorum can decrease, making it easier to ratify.
|
|
44
|
+
|
|
45
|
+
**Mechanism:** `quorum()` (DefifaGovernor line 203-223) calls `IDefifaHook.currentSupplyOfTier()` for each tier. This reads the live minted-minus-burned count from `DefifaHookLib.computeCurrentSupply()` (line 260-271). During SCORING phase, cash-outs burn tokens but revert with `NothingToClaim` (DefifaHook line 675) because weights are not set yet. This effectively prevents burns during SCORING before scorecard ratification.
|
|
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`, `DefifaHook_AuditFindings.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:** `M36_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:** `M35_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:** `DefifaHook_AuditFindings.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/M36_FulfillmentBlocksRatification.t.sol` | `test_ratificationSucceedsWhenFulfillmentReverts` |
|
|
318
|
+
| RISK-10 Grace period bypass | `regression/M35_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 | `DefifaHook_AuditFindings.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 |
|