@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/ADMINISTRATION.md
CHANGED
|
@@ -23,7 +23,7 @@ Admin privileges and their scope in defifa-collection-deployer-v6.
|
|
|
23
23
|
| Function | Required Role | Permission Check | What It Does |
|
|
24
24
|
|----------|--------------|-----------------|-------------|
|
|
25
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. |
|
|
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 try-catch `sendPayoutsOf`, then queues the final COMPLETE ruleset. If payout fails, emits `CommitmentPayoutFailed` and sets sentinel. Can only execute once per game. |
|
|
27
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
28
|
|
|
29
29
|
### DefifaGovernor
|
|
@@ -33,7 +33,7 @@ Admin privileges and their scope in defifa-collection-deployer-v6.
|
|
|
33
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
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
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
|
|
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 calls `fulfillCommitmentsOf`. |
|
|
37
37
|
|
|
38
38
|
### DefifaHook
|
|
39
39
|
|
|
@@ -75,7 +75,7 @@ COUNTDOWN --> MINT --> REFUND (optional) --> SCORING --> COMPLETE or NO_CONTEST
|
|
|
75
75
|
2. NFT holders attest based on their per-tier voting weight (`attestToScorecardFrom`)
|
|
76
76
|
3. Once quorum (50% of minted tiers' attestation power) is met and grace period passes, anyone ratifies (`ratifyScorecardFrom`)
|
|
77
77
|
4. The governor calls `setTierCashOutWeightsTo` on the hook via low-level call
|
|
78
|
-
5. `fulfillCommitmentsOf` sends fee payouts and queues the final ruleset
|
|
78
|
+
5. `fulfillCommitmentsOf` sends fee payouts (try-catch) and queues the final ruleset
|
|
79
79
|
|
|
80
80
|
**No single entity controls scoring.** The process requires collective attestation from NFT holders across tiers.
|
|
81
81
|
|
package/ARCHITECTURE.md
CHANGED
|
@@ -75,6 +75,8 @@ Attestor → DefifaGovernor.attestToScorecard(proposalId)
|
|
|
75
75
|
- `@bananapus/721-hook-v6` — NFT tier system
|
|
76
76
|
- `@bananapus/address-registry-v6` — Deterministic deploys
|
|
77
77
|
- `@bananapus/permission-ids-v6` — Permission constants
|
|
78
|
+
- `@croptop/core-v6` — Croptop integration
|
|
79
|
+
- `@rev-net/core-v6` — Revnet integration
|
|
78
80
|
- `@openzeppelin/contracts` — Checkpoints, Ownable, Clones
|
|
79
81
|
- `@prb/math` — mulDiv
|
|
80
82
|
- `scripty.sol` — On-chain scripting for SVG
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# defifa-collection-deployer-v6 -- Audit Instructions
|
|
2
|
+
|
|
3
|
+
Prediction game platform built on Juicebox V6. Players buy NFT tiers representing outcomes, a governance process scores the outcomes, and winners claim treasury funds proportional to their tier's score.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
Five contracts, one library. Total ~2,800 lines of production Solidity.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
DefifaDeployer.sol (906 lines) -- Game factory. Owns all game JB projects. Manages lifecycle rulesets, fee splits, fulfillment, no-contest.
|
|
13
|
+
DefifaHook.sol (1082 lines) -- Pay/cashout hook. NFT minting, burning, attestation delegation, fee token distribution, cash-out weight logic.
|
|
14
|
+
DefifaGovernor.sol (514 lines) -- Scorecard governance. Submit, attest, ratify scorecards. Singleton across all games.
|
|
15
|
+
DefifaHookLib.sol (368 lines) -- Pure/view helpers. Weight validation, cash-out math, attestation computation, token claiming.
|
|
16
|
+
DefifaProjectOwner.sol (67 lines) -- Permanent holder of the Defifa project NFT. Grants SET_SPLIT_GROUPS permission.
|
|
17
|
+
DefifaTokenUriResolver.sol (313 lines) -- On-chain SVG metadata for game NFTs.
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Contract Relationships
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
DefifaDeployer
|
|
24
|
+
├── creates JB projects via CONTROLLER.launchProjectFor()
|
|
25
|
+
├── clones DefifaHook via Clones.cloneDeterministic()
|
|
26
|
+
├── initializes DefifaGovernor.initializeGame() for each game
|
|
27
|
+
├── implements IDefifaGamePhaseReporter (phase state machine)
|
|
28
|
+
├── implements IDefifaGamePotReporter (treasury balance queries)
|
|
29
|
+
├── fulfillCommitmentsOf() -- sends fee payouts, queues final ruleset
|
|
30
|
+
└── triggerNoContestFor() -- queues refund ruleset for NO_CONTEST games
|
|
31
|
+
|
|
32
|
+
DefifaHook (clone, one per game)
|
|
33
|
+
├── extends JB721Hook (ERC-721 with Juicebox terminal integration)
|
|
34
|
+
├── extends Ownable (owner = DefifaGovernor)
|
|
35
|
+
├── afterPayRecordedWith() -- mints NFTs on payment
|
|
36
|
+
├── beforeCashOutRecordedWith() -- calculates reclaim amounts
|
|
37
|
+
├── afterCashOutRecordedWith() -- burns NFTs, distributes fee tokens
|
|
38
|
+
├── setTierCashOutWeightsTo() -- onlyOwner, called by governor
|
|
39
|
+
├── attestation delegation system (checkpoints, per-tier, per-account)
|
|
40
|
+
└── delegates to DefifaHookLib for computation
|
|
41
|
+
|
|
42
|
+
DefifaGovernor (singleton, shared across all games)
|
|
43
|
+
├── extends Ownable (owner = DefifaDeployer)
|
|
44
|
+
├── submitScorecardFor() -- anyone during SCORING
|
|
45
|
+
├── attestToScorecardFrom() -- NFT holders during SCORING
|
|
46
|
+
├── ratifyScorecardFrom() -- anyone when SUCCEEDED
|
|
47
|
+
│ ├── calls DefifaHook.setTierCashOutWeightsTo() via low-level call
|
|
48
|
+
│ └── calls DefifaDeployer.fulfillCommitmentsOf() (internal try-catch on sendPayoutsOf)
|
|
49
|
+
└── quorum() -- 50% of minted tiers' max attestation power
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Dependencies
|
|
53
|
+
|
|
54
|
+
| Dependency | Used For |
|
|
55
|
+
|-----------|---------|
|
|
56
|
+
| `@bananapus/core-v6` | JBController, JBMultiTerminal, JBTerminalStore, JBRulesets, JBDirectory, JBPrices |
|
|
57
|
+
| `@bananapus/721-hook-v6` | JB721Hook, JB721TiersHookStore, tier management, NFT minting/burning |
|
|
58
|
+
| `@bananapus/address-registry-v6` | Deterministic hook deployment tracking |
|
|
59
|
+
| `@bananapus/permission-ids-v6` | SET_SPLIT_GROUPS permission constant |
|
|
60
|
+
| `@openzeppelin/contracts` | Ownable, Clones, Checkpoints.Trace208, SafeERC20, IERC721Receiver |
|
|
61
|
+
| `@prb/math` | mulDiv for precise fee and weight calculations |
|
|
62
|
+
| `scripty.sol` / `typeface` | On-chain SVG font rendering |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Game Lifecycle
|
|
67
|
+
|
|
68
|
+
### Phase State Machine
|
|
69
|
+
|
|
70
|
+
Phases are determined by Juicebox ruleset cycle numbers, safety mechanism checks, and scorecard ratification status. The state machine is in `DefifaDeployer.currentGamePhaseOf()` (line 221-257).
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
COUNTDOWN (cycleNumber == 0)
|
|
74
|
+
│
|
|
75
|
+
▼
|
|
76
|
+
MINT (cycleNumber == 1)
|
|
77
|
+
│ Players buy NFTs. Delegation available. Refunds allowed at mint price.
|
|
78
|
+
│ Reserved minting paused (pauseMintPendingReserves: true).
|
|
79
|
+
▼
|
|
80
|
+
REFUND (cycleNumber == 2, if refundPeriodDuration != 0)
|
|
81
|
+
│ No new payments (pausePay: true). Refunds at mint price.
|
|
82
|
+
│ Reserved minting paused.
|
|
83
|
+
▼
|
|
84
|
+
SCORING (cycleNumber >= 2/3, duration == 0)
|
|
85
|
+
│ Checks applied IN THIS ORDER:
|
|
86
|
+
│ 1. cashOutWeightIsSet? → COMPLETE (ratified scorecard is final)
|
|
87
|
+
│ 2. noContestTriggeredFor? → NO_CONTEST
|
|
88
|
+
│ 3. minParticipation check: balance < threshold? → NO_CONTEST
|
|
89
|
+
│ 4. scorecardTimeout check: block.timestamp > start + timeout? → NO_CONTEST
|
|
90
|
+
│ 5. Otherwise → SCORING
|
|
91
|
+
▼
|
|
92
|
+
COMPLETE (cashOutWeightIsSet == true)
|
|
93
|
+
│ Winners cash out at scored weights. Fee tokens distributed.
|
|
94
|
+
│
|
|
95
|
+
NO_CONTEST (safety mechanism triggered)
|
|
96
|
+
│ Requires triggerNoContestFor() before cash-outs work.
|
|
97
|
+
│ Full refund at mint price.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Ruleset Configuration per Phase
|
|
101
|
+
|
|
102
|
+
| Phase | pausePay | cashOutTaxRate | ownerMustSendPayouts | payoutLimits | fundAccessLimitGroups |
|
|
103
|
+
|-------|----------|---------------|---------------------|-------------|----------------------|
|
|
104
|
+
| MINT | false | 0 | false | none | none |
|
|
105
|
+
| REFUND | true | 0 | false | none | none |
|
|
106
|
+
| SCORING | true | 0 | true | uint224.max | yes (fee splits) |
|
|
107
|
+
| COMPLETE (post-fulfill) | true | 0 | true | none | none |
|
|
108
|
+
| NO_CONTEST (post-trigger) | true | 0 | true | none | none |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Key Flows
|
|
113
|
+
|
|
114
|
+
### Payment and Minting (DefifaHook.afterPayRecordedWith → _processPayment)
|
|
115
|
+
|
|
116
|
+
1. Verify caller is a project terminal, currency matches `pricingCurrency`.
|
|
117
|
+
2. Decode metadata: `(address _attestationDelegate, uint16[] _tierIdsToMint)`.
|
|
118
|
+
3. Compute attestation units per unique tier via `DefifaHookLib.computeAttestationUnits()`.
|
|
119
|
+
4. For each unique tier: set delegation if needed, transfer attestation units from address(0) to payer.
|
|
120
|
+
5. Call `_mintAll()`: `store.recordMint()`, increment `_totalMintCost += amount`, mint ERC-721s.
|
|
121
|
+
6. Revert if `leftoverAmount != 0` (exact pricing enforced, `DefifaHook_Overspending`).
|
|
122
|
+
|
|
123
|
+
### Cash-Out (DefifaHook.beforeCashOutRecordedWith + afterCashOutRecordedWith)
|
|
124
|
+
|
|
125
|
+
**Before (view, returns reclaim params):**
|
|
126
|
+
1. Decode token IDs from metadata.
|
|
127
|
+
2. Compute cumulative mint price via `DefifaHookLib.computeCumulativeMintPrice()`.
|
|
128
|
+
3. Compute `cashOutCount` based on game phase:
|
|
129
|
+
- MINT/REFUND/NO_CONTEST: `cashOutCount = cumulativeMintPrice` (full refund).
|
|
130
|
+
- SCORING/COMPLETE: `cashOutCount = mulDiv(surplus + amountRedeemed, cumulativeCashOutWeight, TOTAL_CASHOUT_WEIGHT)`.
|
|
131
|
+
4. Return `totalSupply = surplus.value` (the surplus IS the total supply for Juicebox's bonding curve).
|
|
132
|
+
|
|
133
|
+
**After (state-changing, burns tokens):**
|
|
134
|
+
1. Verify caller is a project terminal.
|
|
135
|
+
2. For each token: verify ownership, burn it, increment `tokensRedeemedFrom[tierId]` if COMPLETE.
|
|
136
|
+
3. Call `store.recordBurn()`.
|
|
137
|
+
4. If COMPLETE: increment `amountRedeemed`, call `_claimTokensFor()` to distribute fee tokens.
|
|
138
|
+
5. Revert with `DefifaHook_NothingToClaim` if reclaimed amount is 0 AND no fee tokens were distributed.
|
|
139
|
+
6. Decrement `_totalMintCost -= cumulativeMintPrice`.
|
|
140
|
+
|
|
141
|
+
### Scorecard Governance
|
|
142
|
+
|
|
143
|
+
**Submit (DefifaGovernor.submitScorecardFor):**
|
|
144
|
+
1. Require SCORING phase, game initialized, no ratified scorecard.
|
|
145
|
+
2. Validate: no weight on tiers with zero supply.
|
|
146
|
+
3. Hash scorecard: `keccak256(abi.encode(dataHook, abi.encodeWithSelector(setTierCashOutWeightsTo.selector, tierWeights)))`.
|
|
147
|
+
4. Store `attestationsBegin = max(block.timestamp, attestationStartTime)`.
|
|
148
|
+
5. Store `gracePeriodEnds = attestationsBegin + attestationGracePeriod`.
|
|
149
|
+
|
|
150
|
+
**Attest (DefifaGovernor.attestToScorecardFrom):**
|
|
151
|
+
1. Require SCORING phase, scorecard ACTIVE or SUCCEEDED.
|
|
152
|
+
2. Prevent double attestation per account per scorecard.
|
|
153
|
+
3. Compute weight via `getAttestationWeight()` at `attestationsBegin` timestamp.
|
|
154
|
+
4. Increment `_scorecardAttestationsOf[gameId][scorecardId].count += weight`.
|
|
155
|
+
|
|
156
|
+
**Ratify (DefifaGovernor.ratifyScorecardFrom):**
|
|
157
|
+
1. Require no prior ratification, scorecard in SUCCEEDED state.
|
|
158
|
+
2. Store `ratifiedScorecardIdOf[gameId] = scorecardId`.
|
|
159
|
+
3. Execute scorecard via low-level call: `dataHook.call(abi.encodeWithSelector(setTierCashOutWeightsTo.selector, tierWeights))`.
|
|
160
|
+
4. Direct call: `IDefifaDeployer(owner).fulfillCommitmentsOf(gameId)`.
|
|
161
|
+
|
|
162
|
+
### Commitment Fulfillment (DefifaDeployer.fulfillCommitmentsOf)
|
|
163
|
+
|
|
164
|
+
1. Guard: `fulfilledCommitmentsOf[gameId] != 0` → return (idempotent).
|
|
165
|
+
2. Require `cashOutWeightIsSet == true`.
|
|
166
|
+
3. Compute `feeAmount = mulDiv(pot, _commitmentPercentOf[gameId], SPLITS_TOTAL_PERCENT)`.
|
|
167
|
+
4. Store `fulfilledCommitmentsOf[gameId] = max(feeAmount, 1)` (reentrancy guard).
|
|
168
|
+
5. Try-catch: `terminal.sendPayoutsOf(gameId, token, feeAmount, ..., minTokensPaidOut: 0)`. On failure, reset to sentinel (1) and emit `CommitmentPayoutFailed`.
|
|
169
|
+
6. Queue final ruleset: no payout limits, no fund access constraints, surplus = entire balance.
|
|
170
|
+
|
|
171
|
+
### No-Contest Trigger (DefifaDeployer.triggerNoContestFor)
|
|
172
|
+
|
|
173
|
+
1. Require `currentGamePhaseOf(gameId) == NO_CONTEST`.
|
|
174
|
+
2. Require `!noContestTriggeredFor[gameId]`.
|
|
175
|
+
3. Set `noContestTriggeredFor[gameId] = true`.
|
|
176
|
+
4. Queue ruleset: no `fundAccessLimitGroups`, making balance = surplus for full refunds.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Attestation and Governance Mechanics
|
|
181
|
+
|
|
182
|
+
### Attestation Power Calculation (DefifaGovernor.getAttestationWeight)
|
|
183
|
+
|
|
184
|
+
Per-tier attestation power for an account:
|
|
185
|
+
```
|
|
186
|
+
tierPower = MAX_ATTESTATION_POWER_TIER * (account's attestation units / tier's total attestation units)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Where:
|
|
190
|
+
- `MAX_ATTESTATION_POWER_TIER = 1,000,000,000` (1e9)
|
|
191
|
+
- Account's units come from `getPastTierAttestationUnitsOf()` (checkpoint at `attestationsBegin` timestamp)
|
|
192
|
+
- Total tier units from `getPastTierTotalAttestationUnitsOf()`
|
|
193
|
+
|
|
194
|
+
Total attestation power = sum of per-tier powers across all tiers.
|
|
195
|
+
|
|
196
|
+
### Quorum Calculation (DefifaGovernor.quorum)
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
quorum = (number_of_minted_tiers * MAX_ATTESTATION_POWER_TIER) / 2
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
A tier is "minted" if `currentSupplyOfTier(tierId) != 0` (live supply, reads current state, not snapshotted).
|
|
203
|
+
|
|
204
|
+
### Scorecard State Machine (DefifaGovernor.stateOf)
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
If ratifiedScorecardIdOf[gameId] != 0:
|
|
208
|
+
This scorecard == ratified? → RATIFIED
|
|
209
|
+
Otherwise → DEFEATED
|
|
210
|
+
If attestationsBegin > block.timestamp → PENDING
|
|
211
|
+
If gracePeriodEnds > block.timestamp → ACTIVE
|
|
212
|
+
If quorum <= attestation count → SUCCEEDED
|
|
213
|
+
Otherwise → ACTIVE
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Cash-Out Weight Validation (DefifaHookLib.validateAndBuildWeights)
|
|
217
|
+
|
|
218
|
+
1. Tier IDs must be in strictly ascending order (prevents duplicates).
|
|
219
|
+
2. Each tier must be in category 0.
|
|
220
|
+
3. Each tier must exist (id <= maxTierId).
|
|
221
|
+
4. Cumulative weight must equal exactly `TOTAL_CASHOUT_WEIGHT` (1e18).
|
|
222
|
+
5. Stored as `uint256[128]` array indexed by `tierId - 1`.
|
|
223
|
+
|
|
224
|
+
### Per-Token Cash-Out Weight (DefifaHookLib.computeCashOutWeight)
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
totalTokensForCashoutInTier = initialSupply - remainingSupply - (burnedTokens - tokensRedeemedFrom[tierId])
|
|
228
|
+
perTokenWeight = tierWeight / totalTokensForCashoutInTier
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Integer division rounds down. Maximum dust loss: 1 wei per tier per game (128 wei max across all tiers).
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Fee Structure
|
|
236
|
+
|
|
237
|
+
### Constants
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
DEFIFA_FEE_DIVISOR = 20 → 5.0% to Defifa project
|
|
241
|
+
BASE_PROTOCOL_FEE_DIVISOR = 40 → 2.5% to NANA/base protocol project
|
|
242
|
+
Total platform fees: 7.5% of the pot
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Split Normalization (_buildSplits)
|
|
246
|
+
|
|
247
|
+
1. Compute absolute percents: `nanaPercent = SPLITS_TOTAL_PERCENT / 40`, `defifaPercent = SPLITS_TOTAL_PERCENT / 20`.
|
|
248
|
+
2. Add any user-defined splits.
|
|
249
|
+
3. Sum total absolute percent; revert if > SPLITS_TOTAL_PERCENT.
|
|
250
|
+
4. Normalize each split: `normalizedPercent = mulDiv(absolutePercent, SPLITS_TOTAL_PERCENT, totalAbsolute)`.
|
|
251
|
+
5. NANA split placed last, absorbs rounding remainder: `SPLITS_TOTAL_PERCENT - normalizedTotal`.
|
|
252
|
+
6. Store `_commitmentPercentOf[gameId] = totalAbsolutePercent` for fulfillment calculation.
|
|
253
|
+
|
|
254
|
+
### Fee Token Distribution (_claimTokensFor via DefifaHookLib.claimTokensFor)
|
|
255
|
+
|
|
256
|
+
During COMPLETE cash-outs, the hook distributes `$DEFIFA` and `$NANA` tokens proportionally:
|
|
257
|
+
```
|
|
258
|
+
defifaAmount = mulDiv(defifaBalance, shareToBeneficiary, outOfTotal)
|
|
259
|
+
baseProtocolAmount = mulDiv(baseProtocolBalance, shareToBeneficiary, outOfTotal)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Where `shareToBeneficiary = cumulativeMintPrice` of burned tokens and `outOfTotal = _totalMintCost`.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Priority Audit Areas
|
|
267
|
+
|
|
268
|
+
### P0 -- Critical (Fund Safety)
|
|
269
|
+
|
|
270
|
+
1. **Cash-out weight arithmetic**: Verify `computeCashOutWeight()` and `computeCashOutCount()` in `DefifaHookLib` cannot overflow or return inflated values. The `_weight / _totalTokensForCashoutInTier` division is the core economic calculation. Confirm `tokensRedeemedFrom` tracking is correct: incremented ONLY during COMPLETE cash-outs (line 656), NOT during MINT/REFUND refunds.
|
|
271
|
+
|
|
272
|
+
2. **`_totalMintCost` integrity**: This variable is the denominator for fee token distribution. It is incremented on paid mint (`_mintAll`, line 869), reserved mint (`mintReservesFor`, line 571), and decremented on cash-out (`afterCashOutRecordedWith`, line 685). Verify no path exists where `_totalMintCost` underflows or becomes inconsistent with actual live token count.
|
|
273
|
+
|
|
274
|
+
3. **Fulfillment reentrancy guard**: `fulfilledCommitmentsOf[gameId]` is set to `max(feeAmount, 1)` BEFORE external calls to `sendPayoutsOf` and `queueRulesetsOf` (DefifaDeployer lines 325-382). Verify this guard prevents double fulfillment via reentrancy through the terminal.
|
|
275
|
+
|
|
276
|
+
4. **Scorecard execution via low-level call**: `ratifyScorecardFrom` calls `_metadata.dataHook.call(_calldata)` (DefifaGovernor line 402). The `_calldata` is `abi.encodeWithSelector(setTierCashOutWeightsTo.selector, tierWeights)`. Verify that the hash-based proposal system prevents any calldata that does not match the submitted scorecard from being executed.
|
|
277
|
+
|
|
278
|
+
5. **Fee accounting during fulfillment**: `fulfillCommitmentsOf` computes `feeAmount = mulDiv(pot, _commitmentPercentOf[gameId], SPLITS_TOTAL_PERCENT)` and sends this amount as payouts via try-catch. On success, `fulfilledCommitmentsOf` retains the fee amount; on failure, it resets to sentinel (1) and the fee stays in the pot. Verify that `currentGamePotOf` correctly subtracts `fulfilledCommitmentsOf` and that the sentinel value (1 wei) does not cause meaningful accounting error.
|
|
279
|
+
|
|
280
|
+
### P1 -- High (Governance Integrity)
|
|
281
|
+
|
|
282
|
+
6. **Quorum manipulation via live supply**: `quorum()` reads `currentSupplyOfTier()` at call time (not snapshotted). Verify that burning tokens during SCORING is prevented by `DefifaHook_NothingToClaim` (cash-out weights not set yet). Check if any other burn path exists that could reduce quorum after attestations have begun.
|
|
283
|
+
|
|
284
|
+
7. **Attestation snapshotting**: Attestation weight is computed at the `attestationsBegin` timestamp via `getPastTierAttestationUnitsOf()`. Verify that the `Checkpoints.Trace208.upperLookup()` correctly captures the state at that exact timestamp, and that minting or transferring NFTs after `attestationsBegin` does not retroactively affect attestation power.
|
|
285
|
+
|
|
286
|
+
8. **Double attestation prevention**: `_attestations.hasAttested[msg.sender]` (DefifaGovernor line 354) prevents double voting. But verify that an attacker cannot attest, transfer NFTs to another address, and have that address attest with the same attestation power (the snapshot at `attestationsBegin` should prevent this, but verify the checkpoint resolution).
|
|
287
|
+
|
|
288
|
+
9. **Grace period anchoring**: `gracePeriodEnds = attestationsBegin + attestationGracePeriod` (DefifaGovernor line 477). Verify that early scorecard submission (before `attestationStartTime`) correctly delays the grace period start, preventing instant ratification.
|
|
289
|
+
|
|
290
|
+
### P2 -- Medium (Access Control and State Transitions)
|
|
291
|
+
|
|
292
|
+
10. **Hook ownership chain**: DefifaDeployer creates the hook clone, calls `initialize()`, then `transferOwnership(GOVERNOR)` (line 568). Verify that no window exists between `initialize()` and `transferOwnership()` where an attacker could call `setTierCashOutWeightsTo()` (requires `onlyOwner`).
|
|
293
|
+
|
|
294
|
+
11. **Phase check ordering in `currentGamePhaseOf()`**: The function checks `cashOutWeightIsSet` BEFORE `noContestTriggeredFor` (lines 233-236). Verify this ordering is correct: a ratified scorecard should always take priority over no-contest.
|
|
295
|
+
|
|
296
|
+
12. **Clone initialization guard**: `DefifaHook.initialize()` checks `address(this) == CODE_ORIGIN` (line 486, prevents initializing the implementation) and `address(store) != address(0)` (line 489, prevents re-initialization). Verify these guards are sufficient against proxy/clone attacks.
|
|
297
|
+
|
|
298
|
+
13. **Delegation lockdown**: `setTierDelegateTo` and `setTierDelegatesTo` require `MINT` phase (DefifaHook lines 740, 751). Verify that auto-delegation on transfer (`_transferTierAttestationUnits`, lines 1027-1031) correctly handles the case where a recipient already has a delegate set.
|
|
299
|
+
|
|
300
|
+
### P3 -- Low (Edge Cases and Rounding)
|
|
301
|
+
|
|
302
|
+
14. **Integer division dust**: `computeCashOutWeight()` returns `_weight / _totalTokensForCashoutInTier`. The maximum loss is 1 wei per tier. With 128 max tiers, at most 128 wei locked per game. Verify this bound is correct.
|
|
303
|
+
|
|
304
|
+
15. **`uint208` overflow in checkpoints**: Attestation units use `Checkpoints.Trace208`. Maximum per tier: `tier.votingUnits * tier.initialSupply`. With `initialSupply = 999_999_999` and typical voting units, verify this cannot overflow `uint208`.
|
|
305
|
+
|
|
306
|
+
16. **Token URI resolver interaction**: `DefifaTokenUriResolver.tokenUriOf()` calls `gamePotReporter.currentGamePotOf()` and `hook.cashOutWeightOf()`. Verify that a malicious URI resolver cannot cause state changes or excessive gas consumption.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Invariants
|
|
311
|
+
|
|
312
|
+
These properties should hold for all games in all states. The test suite validates most of them.
|
|
313
|
+
|
|
314
|
+
### Fund Conservation
|
|
315
|
+
- `totalCashOuts + remainingSurplus + fulfilledCommitments == originalPot` (within N wei where N = total user count)
|
|
316
|
+
- `amountRedeemed` (DefifaHook) only increases during COMPLETE cash-outs, never during MINT/REFUND/NO_CONTEST
|
|
317
|
+
- `fulfilledCommitmentsOf[gameId]` is set exactly once (idempotent guard)
|
|
318
|
+
|
|
319
|
+
### Token Accounting
|
|
320
|
+
- `_totalMintCost == sum(tier.price * liveTokenCount[tier])` for all tiers at all times
|
|
321
|
+
- `tokensRedeemedFrom[tierId]` only incremented during COMPLETE phase cash-outs
|
|
322
|
+
- `_totalMintCost` decremented by exactly `cumulativeMintPrice` on each cash-out
|
|
323
|
+
|
|
324
|
+
### Scorecard Integrity
|
|
325
|
+
- `sum(tierWeights[i].cashOutWeight) == TOTAL_CASHOUT_WEIGHT` (exactly 1e18) for any ratified scorecard
|
|
326
|
+
- Tier IDs in scorecard are strictly ascending (no duplicates)
|
|
327
|
+
- Only tiers in category 0 can receive cash-out weight
|
|
328
|
+
- Only tiers with `currentSupply > 0` can receive nonzero weight at submission time
|
|
329
|
+
|
|
330
|
+
### Governance
|
|
331
|
+
- Each account can attest to a given scorecard at most once
|
|
332
|
+
- Attestation power is snapshotted at `attestationsBegin` (not live)
|
|
333
|
+
- Quorum threshold: 50% of minted tiers' total max attestation power (live at call time)
|
|
334
|
+
- Only one scorecard can be ratified per game
|
|
335
|
+
- Minimum grace period: 1 day (enforced in `initializeGame`, line 303)
|
|
336
|
+
|
|
337
|
+
### Phase Transitions
|
|
338
|
+
- A ratified scorecard (`cashOutWeightIsSet == true`) always produces COMPLETE, regardless of other conditions
|
|
339
|
+
- NO_CONTEST is only reachable from SCORING (never from MINT or REFUND)
|
|
340
|
+
- `triggerNoContestFor` can be called exactly once per game
|
|
341
|
+
- Phase progression is monotonic: COUNTDOWN -> MINT -> REFUND -> SCORING -> COMPLETE/NO_CONTEST
|
|
342
|
+
|
|
343
|
+
### Attestation Units
|
|
344
|
+
- Sum of all delegate attestation units for a tier == total tier attestation units (conservation on transfer)
|
|
345
|
+
- Auto-delegation on transfer prevents units from being lost to `address(0)`
|
|
346
|
+
- Delegation changes only allowed during MINT phase (except auto-delegation on transfer)
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Testing
|
|
351
|
+
|
|
352
|
+
### Test Files (14 files, ~100 test functions)
|
|
353
|
+
|
|
354
|
+
| File | Focus |
|
|
355
|
+
|------|-------|
|
|
356
|
+
| `DefifaGovernor.t.sol` | Core lifecycle: minting, refunding, scoring, cash-out. Fuzz tests on tier counts and distributions. |
|
|
357
|
+
| `DefifaSecurity.t.sol` | Fund conservation (fuzz), high-volume 32 tiers, winner-take-all, extreme weights, quorum manipulation, delegation lockdown, reserved minter fee tokens. |
|
|
358
|
+
| `DefifaNoContest.t.sol` | Both NO_CONTEST triggers: minParticipation threshold and scorecardTimeout. Trigger/refund/idempotency. |
|
|
359
|
+
| `DefifaFeeAccounting.t.sol` | Fee split normalization, rounding loss bounds, cash-out after fees, user splits. |
|
|
360
|
+
| `DefifaMintCostInvariant.t.sol` | Stateful fuzz: `_totalMintCost` invariant across random mints and refunds. |
|
|
361
|
+
| `DefifaHookRegressions.t.sol` | Audit finding M-5: attestation unit conservation on transfer to undelegated recipients. |
|
|
362
|
+
| `DefifaAuditLowGuards.t.sol` | Input validation: double initialization, uint48 overflow, zero-address delegation. |
|
|
363
|
+
| `Fork.t.sol` | Mainnet fork tests: full lifecycle, edge cases, all revert conditions, scorecard state machine. ~50 tests. |
|
|
364
|
+
| `regression/FulfillmentBlocksRatification.t.sol` | Fulfillment failure does not block ratification (try-catch behavior). |
|
|
365
|
+
| `regression/GracePeriodBypass.t.sol` | Grace period extends from attestation start, not submission time. |
|
|
366
|
+
| `DefifaUSDC.t.sol` | ERC-20 (USDC) game variant. |
|
|
367
|
+
| `SVG.t.sol` | Token URI resolver SVG rendering. |
|
|
368
|
+
|
|
369
|
+
### Running Tests
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
forge test --match-path "test/*.t.sol" -vvv
|
|
373
|
+
forge test --match-path "test/regression/*.t.sol" -vvv
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
For invariant tests:
|
|
377
|
+
```bash
|
|
378
|
+
forge test --match-contract DefifaMintCostInvariant -vvv
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Known Test Gaps
|
|
382
|
+
|
|
383
|
+
| Area | Current Coverage | Risk |
|
|
384
|
+
|------|-----------------|------|
|
|
385
|
+
| ERC-20 token games (non-ETH) | Single USDC test file | LOW |
|
|
386
|
+
| Games with >32 tiers | Fuzz caps at 12, one test at 32 | LOW |
|
|
387
|
+
| Concurrent multi-game governor | Tests use single game per governor | MEDIUM |
|
|
388
|
+
| Adversarial token URI resolver | No malicious resolver test | LOW |
|
|
389
|
+
| Clone address collision | No explicit collision test | LOW |
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Constants Reference
|
|
394
|
+
|
|
395
|
+
| Constant | Value | Location |
|
|
396
|
+
|----------|-------|---------|
|
|
397
|
+
| `TOTAL_CASHOUT_WEIGHT` | 1e18 | DefifaHookLib line 28 |
|
|
398
|
+
| `MAX_ATTESTATION_POWER_TIER` | 1e9 | DefifaGovernor line 64 |
|
|
399
|
+
| `DEFIFA_FEE_DIVISOR` | 20 (5%) | DefifaDeployer line 111 |
|
|
400
|
+
| `BASE_PROTOCOL_FEE_DIVISOR` | 40 (2.5%) | DefifaDeployer line 107 |
|
|
401
|
+
| `SPLITS_TOTAL_PERCENT` | 1e9 | JBConstants |
|
|
402
|
+
| `initialSupply` per tier | 999,999,999 | DefifaDeployer line 491 |
|
|
403
|
+
| Max tiers per game | 128 | DefifaHook `uint256[128]` (line 76) |
|
|
404
|
+
| Min grace period | 1 day | DefifaGovernor line 303 |
|
|
405
|
+
| Compiler | Solidity 0.8.26 | All files |
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Entry Points for Review
|
|
410
|
+
|
|
411
|
+
Start with the money: follow ETH from payment to cash-out.
|
|
412
|
+
|
|
413
|
+
1. `DefifaHook._processPayment()` (line 929) -- where tokens enter
|
|
414
|
+
2. `DefifaHook.beforeCashOutRecordedWith()` (line 253) -- reclaim calculation
|
|
415
|
+
3. `DefifaHook.afterCashOutRecordedWith()` (line 605) -- where tokens leave
|
|
416
|
+
4. `DefifaDeployer.fulfillCommitmentsOf()` (line 296) -- fee distribution
|
|
417
|
+
5. `DefifaGovernor.ratifyScorecardFrom()` (line 372) -- scorecard execution
|
|
418
|
+
6. `DefifaHookLib.validateAndBuildWeights()` (line 35) -- weight validation
|
|
419
|
+
7. `DefifaHookLib.computeCashOutWeight()` (line 95) -- per-token value
|
|
420
|
+
8. `DefifaDeployer._buildSplits()` (line 826) -- fee normalization
|
|
421
|
+
9. `DefifaDeployer.currentGamePhaseOf()` (line 221) -- phase state machine
|
|
422
|
+
10. `DefifaDeployer.triggerNoContestFor()` (line 586) -- no-contest safety valve
|
package/CRYPTO_ECON.md
CHANGED
|
@@ -707,7 +707,7 @@ We identify five distinct scenarios in which game funds could become permanently
|
|
|
707
707
|
|
|
708
708
|
**Scenario D: Attestation power in dead addresses.** If >50% of game pieces are transferred to contracts that cannot call `attestToScorecardFrom()`, the exercisable attestation power drops below quorum permanently. This is distinct from Scenario C because the delegation may be correct but the delegatees are inaccessible.
|
|
709
709
|
|
|
710
|
-
**Scenario E: Split target reverts on ratification.** `ratifyScorecardFrom()` calls `fulfillCommitmentsOf()`, which calls `sendPayoutsOf()`. If a split target is a reverting contract, the
|
|
710
|
+
**Scenario E: Split target reverts on ratification.** `ratifyScorecardFrom()` calls `fulfillCommitmentsOf()`, which calls `sendPayoutsOf()`. If a split target is a reverting contract, `sendPayoutsOf` is caught by the internal try-catch in `fulfillCommitmentsOf`. The `CommitmentPayoutFailed` event is emitted, `fulfilledCommitmentsOf` is set to the sentinel value 1, and the final ruleset is still queued. Players can cash out immediately — the fee amount stays in the pot, slightly benefiting cash-out recipients. This is no longer a stuck-funds scenario.
|
|
711
711
|
|
|
712
712
|
| Scenario | Funds stuck? | Delegate resolves? | Automated resolution? |
|
|
713
713
|
|:---------|:------------:|:------------------:|:---------------------:|
|
|
@@ -715,7 +715,7 @@ We identify five distinct scenarios in which game funds could become permanently
|
|
|
715
715
|
| B: Quorum unreachable | Yes | Yes, if has power | No |
|
|
716
716
|
| C: Dead delegate | Yes | No | No |
|
|
717
717
|
| D: Dead attestation holders | Yes | No | No |
|
|
718
|
-
| E: Split target reverts |
|
|
718
|
+
| E: Split target reverts | No | N/A | Yes (try-catch) |
|
|
719
719
|
|
|
720
720
|
Note: the case where *all* minters refund during REFUND is not a deadlock — the treasury balance drops to zero and there are no funds to recover.
|
|
721
721
|
|
|
@@ -900,13 +900,13 @@ _terminal.sendPayoutsOf({
|
|
|
900
900
|
token: _token,
|
|
901
901
|
amount: _pot,
|
|
902
902
|
currency: ...,
|
|
903
|
-
minTokensPaidOut:
|
|
903
|
+
minTokensPaidOut: 0
|
|
904
904
|
});
|
|
905
905
|
```
|
|
906
906
|
|
|
907
|
-
The split structure routes fees (~10%) to protocol projects and returns the remainder (~90%) back to the game treasury via `addToBalanceOf`.
|
|
907
|
+
The split structure routes fees (~10%) to protocol projects and returns the remainder (~90%) back to the game treasury via `addToBalanceOf`. `minTokensPaidOut` is set to 0 to avoid reverts from partial payouts. Additionally, the entire `sendPayoutsOf` call is wrapped in a try-catch: if the payout fails for any reason, `CommitmentPayoutFailed` is emitted, `fulfilledCommitmentsOf` is reset to the sentinel value 1, and the final ruleset is still queued. The fee amount stays in the pot, slightly benefiting cash-out recipients.
|
|
908
908
|
|
|
909
|
-
**
|
|
909
|
+
**Status:** Resolved. `minTokensPaidOut` set to 0 and try-catch ensures the final ruleset is always queued.
|
|
910
910
|
|
|
911
911
|
---
|
|
912
912
|
|