@ballkidz/defifa 0.0.10 → 0.0.11
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 +26 -15
- package/ARCHITECTURE.md +35 -3
- package/AUDIT_INSTRUCTIONS.md +127 -45
- package/CHANGE_LOG.md +107 -0
- package/CRYPTO_ECON.md +2 -2
- package/README.md +120 -2
- package/RISKS.md +21 -4
- package/SKILLS.md +174 -59
- package/STYLE_GUIDE.md +1 -1
- package/USER_JOURNEYS.md +482 -139
- package/package.json +7 -7
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/DefifaDeploymentLib.sol +2 -2
- package/src/DefifaDeployer.sol +2 -2
- package/src/DefifaGovernor.sol +1 -1
- package/src/DefifaHook.sol +7 -6
- package/src/DefifaProjectOwner.sol +1 -1
- package/src/DefifaTokenUriResolver.sol +1 -1
- package/test/DefifaAdversarialQuorum.t.sol +1 -1
- package/test/DefifaAuditLowGuards.t.sol +1 -1
- package/test/DefifaFeeAccounting.t.sol +1 -1
- package/test/DefifaGovernor.t.sol +1 -1
- package/test/DefifaHookRegressions.t.sol +39 -1
- package/test/DefifaMintCostInvariant.t.sol +1 -1
- package/test/DefifaNoContest.t.sol +1 -1
- package/test/DefifaSecurity.t.sol +1 -1
- package/test/DefifaUSDC.t.sol +1 -1
- package/test/Fork.t.sol +1 -1
- package/test/SVG.t.sol +1 -1
- package/test/TestAuditGaps.sol +1 -1
- package/test/TestQALastMile.t.sol +1 -1
- package/test/audit/CodexAttestationDoubleCount.t.sol +217 -0
- package/test/deployScript.t.sol +1 -1
- package/test/regression/AttestationDelegateBeneficiary.t.sol +272 -0
- package/test/regression/FulfillmentBlocksRatification.t.sol +1 -1
- package/test/regression/GracePeriodBypass.t.sol +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Each game is a Juicebox project with phased rulesets that move through Countdown
|
|
|
8
8
|
|
|
9
9
|
### Minting
|
|
10
10
|
|
|
11
|
-
During the Mint phase, players pay the project's terminal to mint ERC-721 game pieces. Each tier represents a team or outcome -- all tiers share the same price. Minting delegates attestation power to the
|
|
11
|
+
During the Mint phase, players pay the project's terminal to mint ERC-721 game pieces. Each tier represents a team or outcome -- all tiers share the same price. Minting delegates attestation power to the beneficiary (or a specified delegate), which is used later during scorecard governance.
|
|
12
12
|
|
|
13
13
|
### Refunds
|
|
14
14
|
|
|
@@ -67,6 +67,51 @@ DefifaDeployer ──── launches ────► Juicebox Project (phased ru
|
|
|
67
67
|
| `COMPLETE` | -- | Scorecard ratified. Commitments fulfilled. Players burn NFTs to claim pot share + fee tokens. |
|
|
68
68
|
| `NO_CONTEST` | -- | Safety mechanism triggered. All players can refund at mint price. |
|
|
69
69
|
|
|
70
|
+
### Game Lifecycle Flow
|
|
71
|
+
|
|
72
|
+
```mermaid
|
|
73
|
+
sequenceDiagram
|
|
74
|
+
participant Deployer as Game Creator
|
|
75
|
+
participant DD as DefifaDeployer
|
|
76
|
+
participant JB as Juicebox Project
|
|
77
|
+
participant Hook as DefifaHook
|
|
78
|
+
participant Gov as DefifaGovernor
|
|
79
|
+
participant Player as Players
|
|
80
|
+
|
|
81
|
+
Note over DD,JB: DEPLOY
|
|
82
|
+
Deployer->>DD: launchGameFor(config)
|
|
83
|
+
DD->>Hook: clone & initialize (tiers, token URI)
|
|
84
|
+
DD->>JB: launchProjectFor (phased rulesets)
|
|
85
|
+
DD->>Gov: initialize (hook, quorum, grace period)
|
|
86
|
+
|
|
87
|
+
Note over JB,Player: COUNTDOWN
|
|
88
|
+
JB-->>Player: project live, minting not yet open
|
|
89
|
+
|
|
90
|
+
Note over JB,Player: MINT
|
|
91
|
+
Player->>JB: pay (mint price)
|
|
92
|
+
JB->>Hook: afterPayRecordedWith (mint NFT)
|
|
93
|
+
Hook-->>Player: ERC-721 game piece
|
|
94
|
+
Player->>Hook: setTierDelegatesTo (delegate attestation power)
|
|
95
|
+
|
|
96
|
+
Note over JB,Player: REFUND (optional)
|
|
97
|
+
Player->>JB: cashOutTokensOf (burn NFT)
|
|
98
|
+
JB->>Hook: afterCashOutRecordedWith (full refund)
|
|
99
|
+
Hook-->>Player: mint price returned
|
|
100
|
+
|
|
101
|
+
Note over Gov,Player: SCORING
|
|
102
|
+
Player->>Gov: submitScorecard (tier weights)
|
|
103
|
+
Player->>Gov: castVote (attest to scorecard)
|
|
104
|
+
Note over Gov: quorum reached + grace period elapsed
|
|
105
|
+
Player->>Gov: ratifyScorecard
|
|
106
|
+
Gov->>Hook: setCashOutWeights (winning scorecard)
|
|
107
|
+
Gov->>DD: fulfillCommitments (pay fees + splits)
|
|
108
|
+
|
|
109
|
+
Note over JB,Player: COMPLETE
|
|
110
|
+
Player->>JB: cashOutTokensOf (burn NFT)
|
|
111
|
+
JB->>Hook: afterCashOutRecordedWith (pot share)
|
|
112
|
+
Hook-->>Player: proportional pot share + fee tokens
|
|
113
|
+
```
|
|
114
|
+
|
|
70
115
|
### Fee Structure
|
|
71
116
|
|
|
72
117
|
| Fee | Rate | Recipient |
|
|
@@ -94,6 +139,79 @@ Fees are taken as payouts during commitment fulfillment. The remaining surplus i
|
|
|
94
139
|
| `DefifaGamePhase` | `COUNTDOWN`, `MINT`, `REFUND`, `SCORING`, `COMPLETE`, `NO_CONTEST` |
|
|
95
140
|
| `DefifaScorecardState` | `PENDING`, `ACTIVE`, `DEFEATED`, `SUCCEEDED`, `RATIFIED` |
|
|
96
141
|
|
|
142
|
+
## Risks
|
|
143
|
+
|
|
144
|
+
- **Scorecard timeout gap.** The `scorecardTimeout` safety mechanism triggers No Contest only if no scorecard is ratified before the deadline. However, if `minParticipation` was already met (pot above threshold), the minimum-participation safety net does not apply. A game can get stuck in the `SCORING` phase indefinitely if valid scorecards are submitted but never reach quorum -- the timeout only fires when _no_ scorecard is ratified in time, so a perpetually-contested game with active but insufficient attestation has no automatic resolution path.
|
|
145
|
+
|
|
146
|
+
- **Attestation power is per-tier, not per-NFT.** Each tier contributes equal governance weight (capped at `MAX_ATTESTATION_POWER_TIER`) regardless of how many NFTs were minted in that tier. A tier with 1 NFT holder has the same attestation power as a tier with 100 holders. This means a player who is the sole holder of a low-supply tier controls disproportionate voting power relative to their capital at risk, which could be exploited to steer scorecard outcomes.
|
|
147
|
+
|
|
148
|
+
- **Fee token accumulation depends on external terminals.** When commitments are fulfilled, fee payments to the Defifa and protocol projects generate $DEFIFA and $NANA tokens that accumulate in the hook for later distribution to players. If the receiving projects' terminals revert (e.g., paused, migrated, or misconfigured), the `fulfillCommitments` call will fail, blocking the game from entering the `COMPLETE` phase. Players would need to wait for the external terminal issue to be resolved before they can cash out.
|
|
149
|
+
|
|
150
|
+
- **Ratified scorecards are permanent.** Once a scorecard is ratified by the governor and cash-out weights are set on the hook, those weights cannot be changed for that game. There is no mechanism to re-score or appeal. If a scorecard is ratified with incorrect weights (e.g., due to a rushed attestation or a governance attack during low participation), the payout distribution is locked in permanently.
|
|
151
|
+
|
|
152
|
+
## Repository Layout
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
defifa-collection-deployer-v6/
|
|
156
|
+
├── src/
|
|
157
|
+
│ ├── DefifaDeployer.sol -- Game factory, phase/pot reporter, commitment fulfillment
|
|
158
|
+
│ ├── DefifaGovernor.sol -- Scorecard governance, attestation, ratification
|
|
159
|
+
│ ├── DefifaHook.sol -- ERC-721 game pieces, cash-out weights, delegation
|
|
160
|
+
│ ├── DefifaProjectOwner.sol -- Defifa fee project ownership holder
|
|
161
|
+
│ ├── DefifaTokenUriResolver.sol -- On-chain SVG token URI renderer
|
|
162
|
+
│ ├── enums/
|
|
163
|
+
│ │ ├── DefifaGamePhase.sol -- COUNTDOWN, MINT, REFUND, SCORING, COMPLETE, NO_CONTEST
|
|
164
|
+
│ │ └── DefifaScorecardState.sol -- PENDING, ACTIVE, DEFEATED, SUCCEEDED, RATIFIED
|
|
165
|
+
│ ├── interfaces/
|
|
166
|
+
│ │ ├── IDefifaDeployer.sol
|
|
167
|
+
│ │ ├── IDefifaGamePhaseReporter.sol
|
|
168
|
+
│ │ ├── IDefifaGamePotReporter.sol
|
|
169
|
+
│ │ ├── IDefifaGovernor.sol
|
|
170
|
+
│ │ ├── IDefifaHook.sol
|
|
171
|
+
│ │ └── IDefifaTokenUriResolver.sol
|
|
172
|
+
│ ├── libraries/
|
|
173
|
+
│ │ ├── DefifaFontImporter.sol -- Capsules typeface loader for SVG rendering
|
|
174
|
+
│ │ └── DefifaHookLib.sol -- Scorecard validation, cash-out math, fee distribution
|
|
175
|
+
│ └── structs/
|
|
176
|
+
│ ├── DefifaAttestations.sol
|
|
177
|
+
│ ├── DefifaDelegation.sol
|
|
178
|
+
│ ├── DefifaLaunchProjectData.sol
|
|
179
|
+
│ ├── DefifaOpsData.sol
|
|
180
|
+
│ ├── DefifaScorecard.sol
|
|
181
|
+
│ ├── DefifaTierCashOutWeight.sol
|
|
182
|
+
│ └── DefifaTierParams.sol
|
|
183
|
+
├── test/
|
|
184
|
+
│ ├── DefifaAdversarialQuorum.t.sol
|
|
185
|
+
│ ├── DefifaAuditLowGuards.t.sol
|
|
186
|
+
│ ├── DefifaFeeAccounting.t.sol
|
|
187
|
+
│ ├── DefifaGovernor.t.sol
|
|
188
|
+
│ ├── DefifaHookRegressions.t.sol
|
|
189
|
+
│ ├── DefifaMintCostInvariant.t.sol
|
|
190
|
+
│ ├── DefifaNoContest.t.sol
|
|
191
|
+
│ ├── DefifaSecurity.t.sol
|
|
192
|
+
│ ├── DefifaUSDC.t.sol
|
|
193
|
+
│ ├── Fork.t.sol
|
|
194
|
+
│ ├── SVG.t.sol
|
|
195
|
+
│ ├── TestAuditGaps.sol
|
|
196
|
+
│ ├── TestQALastMile.t.sol
|
|
197
|
+
│ ├── deployScript.t.sol
|
|
198
|
+
│ └── regression/
|
|
199
|
+
│ ├── AttestationDelegateBeneficiary.t.sol
|
|
200
|
+
│ ├── FulfillmentBlocksRatification.t.sol
|
|
201
|
+
│ └── GracePeriodBypass.t.sol
|
|
202
|
+
├── script/
|
|
203
|
+
│ ├── Deploy.s.sol -- Deployment script
|
|
204
|
+
│ └── helpers/
|
|
205
|
+
│ └── DefifaDeploymentLib.sol -- Deployment helper library
|
|
206
|
+
├── lib/ -- Git submodule dependencies
|
|
207
|
+
│ ├── base64/
|
|
208
|
+
│ ├── capsules/
|
|
209
|
+
│ ├── forge-std/
|
|
210
|
+
│ └── typeface/
|
|
211
|
+
└── docs/
|
|
212
|
+
└── plans/ -- Design documents
|
|
213
|
+
```
|
|
214
|
+
|
|
97
215
|
## Install
|
|
98
216
|
|
|
99
217
|
```bash
|
|
@@ -112,7 +230,7 @@ npm install && forge install
|
|
|
112
230
|
| Command | Description |
|
|
113
231
|
|---------|-------------|
|
|
114
232
|
| `forge build` | Compile contracts and write artifacts to `out`. |
|
|
115
|
-
| `forge test` | Run the test suite (
|
|
233
|
+
| `forge test` | Run the test suite (155 tests: unit, fuzz, invariant). |
|
|
116
234
|
| `forge test -vvvv` | Run tests with full traces. |
|
|
117
235
|
| `forge fmt` | Format Solidity files. |
|
|
118
236
|
| `forge build --sizes` | Get contract sizes. |
|
package/RISKS.md
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
- **DefifaProjectOwner Irrecoverability.** Once the Defifa project NFT is transferred to DefifaProjectOwner, it cannot be recovered. This is intentional but irreversible.
|
|
8
8
|
- **External Dependencies.** Relies on JB721TiersHookStore, JBController, JBMultiTerminal, JBRulesets, and JBPrices. Bugs in any upstream contract affect all Defifa games.
|
|
9
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.
|
|
10
|
+
- **721 hook store shared with nana-721-hook-v6.** DefifaHook extends `JB721TiersHook`, sharing the same `JB721TiersHookStore`. All store-level risks from [nana-721-hook-v6 RISKS.md](../nana-721-hook-v6/RISKS.md) apply — including the `totalCashOutWeight` tier iteration cost and the category sort order enforcement. Store bugs affect all Defifa games simultaneously.
|
|
10
11
|
|
|
11
12
|
## 2. Economic Risks
|
|
12
13
|
|
|
13
14
|
- **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
15
|
- **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
16
|
- **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
|
+
- **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`). Example: if 1000 NFTs are minted by payers (paying 1 ETH each = 1000 ETH total), and 100 reserved NFTs are minted (adding 100 ETH to `_totalMintCost` with no ETH deposited), fee token claims are diluted by ~9.1% (100/1100). The dilution is bounded by the reserve frequency — at `reserveFrequency=10`, every 10th mint is a reserve, capping dilution at ~10%.
|
|
17
18
|
- **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.
|
|
18
19
|
|
|
19
20
|
## 3. Governance Risks
|
|
@@ -26,12 +27,10 @@
|
|
|
26
27
|
## 4. Reentrancy Surface
|
|
27
28
|
|
|
28
29
|
- **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.
|
|
31
30
|
|
|
32
31
|
## 5. DoS Vectors
|
|
33
32
|
|
|
34
|
-
- **Unbounded tier iteration in governance.** `getAttestationWeight` and `quorum` iterate over all tiers (`maxTierIdOf`). Gas cost
|
|
33
|
+
- **Unbounded tier iteration in governance.** `getAttestationWeight` and `quorum` iterate over all tiers (`maxTierIdOf`). Gas cost: ~3-5k per tier (storage read + bitmap check). At 128 tiers (the hard cap), ~400-650k gas for a single `quorum()` call. At the block gas limit (30M), this is safe, but composing `quorum()` inside a larger transaction (e.g., `ratifyScorecardFrom`) adds the iteration cost on top of the ratification logic. Games should target <64 tiers for comfortable gas headroom.
|
|
35
34
|
- **_buildSplits iteration.** Iterates over user-provided splits array. No explicit cap, but total percent constraint limits practical count.
|
|
36
35
|
|
|
37
36
|
## 6. Integration Risks
|
|
@@ -50,3 +49,21 @@
|
|
|
50
49
|
- `fulfilledCommitmentsOf[gameId]` is set at most once per game.
|
|
51
50
|
- Per-tier supply never exceeds `initialSupply`.
|
|
52
51
|
- Sum of all delegate attestation units equals total attestation supply.
|
|
52
|
+
|
|
53
|
+
## 8. Accepted Behaviors
|
|
54
|
+
|
|
55
|
+
### 8.1 Scorecard timeout is intentionally irreversible
|
|
56
|
+
|
|
57
|
+
If `scorecardTimeout` elapses before ratification, the game permanently enters NO_CONTEST. Even a scorecard that has reached quorum cannot be ratified after timeout. This is accepted because: (1) allowing late ratification would keep player funds locked indefinitely while governance debates, (2) NO_CONTEST triggers a refund path (`triggerNoContestFor`) that returns funds pro-rata, and (3) the timeout creates a credible commitment to resolve the game within a bounded time. The timeout duration is set at deployment and cannot be changed.
|
|
58
|
+
|
|
59
|
+
### 8.2 Permanent cash-out weights (no correction mechanism)
|
|
60
|
+
|
|
61
|
+
Cash-out weights set via `ratifyScorecardFrom` cannot be updated or corrected. This is accepted because: (1) allowing weight changes would introduce governance attack surfaces where a quorum re-ratifies to steal from other tiers, (2) the attestation process provides a dispute window (grace period) before ratification finalizes, and (3) the alternative (upgradeable weights) would undermine the trust-minimized game design. If a scorecard is wrong, the game should be allowed to timeout into NO_CONTEST for refunds.
|
|
62
|
+
|
|
63
|
+
### 8.3 fulfillCommitmentsOf reentrancy is guarded
|
|
64
|
+
|
|
65
|
+
`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.
|
|
66
|
+
|
|
67
|
+
### 8.4 ratifyScorecardFrom reentrancy is double-guarded
|
|
68
|
+
|
|
69
|
+
`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. Both guards prevent reentrancy exploitation.
|
package/SKILLS.md
CHANGED
|
@@ -4,47 +4,79 @@
|
|
|
4
4
|
|
|
5
5
|
On-chain prediction game framework built on Juicebox V6. Players mint NFT game pieces representing teams/outcomes, a governor-based scorecard system determines tier payouts, and winners burn NFTs to claim proportional shares of the pot plus accumulated fee tokens ($DEFIFA/$NANA).
|
|
6
6
|
|
|
7
|
+
## Game Lifecycle
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
start time reached
|
|
11
|
+
COUNTDOWN ──────────────────────────────► MINT
|
|
12
|
+
│
|
|
13
|
+
mintPeriodDuration expires
|
|
14
|
+
│
|
|
15
|
+
┌───────────────────────────┤
|
|
16
|
+
│ ▼
|
|
17
|
+
│ (refundPeriodDuration=0) REFUND
|
|
18
|
+
│ │
|
|
19
|
+
│ refundPeriodDuration expires
|
|
20
|
+
│ │
|
|
21
|
+
├◄──────────────────────────┘
|
|
22
|
+
▼
|
|
23
|
+
SCORING
|
|
24
|
+
│
|
|
25
|
+
├── scorecard ratified + commitments fulfilled ──► COMPLETE
|
|
26
|
+
│
|
|
27
|
+
└── safety trigger (minParticipation not met
|
|
28
|
+
OR scorecardTimeout elapsed) ──────────────► NO_CONTEST
|
|
29
|
+
(full refunds)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **COUNTDOWN**: Before `start`. No minting.
|
|
33
|
+
- **MINT**: Players mint NFTs and delegate attestation power. Delegation changes only allowed here.
|
|
34
|
+
- **REFUND**: Optional. Players can burn NFTs for full refund at mint price. Skipped if `refundPeriodDuration=0`.
|
|
35
|
+
- **SCORING**: Scorecards are submitted, attested, and ratified. Cash outs blocked until scorecard is set.
|
|
36
|
+
- **COMPLETE**: Commitments fulfilled. Players burn NFTs for weighted pot share + fee tokens.
|
|
37
|
+
- **NO_CONTEST**: Safety exit. Full refunds enabled. Irreversible once triggered.
|
|
38
|
+
|
|
7
39
|
## Contracts
|
|
8
40
|
|
|
9
41
|
| Contract | Role |
|
|
10
42
|
|----------|------|
|
|
11
43
|
| `DefifaDeployer` | Factory that creates games as Juicebox projects with phased rulesets, cloned hooks, and governor initialization. Manages post-game commitment fulfillment. Implements `IDefifaGamePhaseReporter` and `IDefifaGamePotReporter`. |
|
|
12
|
-
| `DefifaHook` | ERC-721 hook (extends `JB721Hook`)
|
|
13
|
-
| `DefifaGovernor` |
|
|
14
|
-
| `DefifaHookLib` | External library
|
|
15
|
-
| `DefifaTokenUriResolver` | On-chain SVG renderer for game card metadata with phase-aware display
|
|
44
|
+
| `DefifaHook` | ERC-721 hook (extends `JB721Hook`) managing cash-out weights, attestation delegation with checkpointed voting, and proportional pot distribution. Deployed as minimal proxy clones. |
|
|
45
|
+
| `DefifaGovernor` | Shared singleton for scorecard submission, attestation, and ratification. |
|
|
46
|
+
| `DefifaHookLib` | External library: scorecard validation, cash-out weight calculation, fee token distribution, attestation aggregation. |
|
|
47
|
+
| `DefifaTokenUriResolver` | On-chain SVG renderer for game card metadata with phase-aware display. Uses embedded Capsules typeface. |
|
|
48
|
+
| `DefifaFontImporter` | Loads Capsules typeface font for SVG renderer. |
|
|
16
49
|
| `DefifaProjectOwner` | Receives Defifa fee project's ownership NFT and permanently grants the deployer `SET_SPLIT_GROUPS` permission. |
|
|
17
50
|
|
|
18
51
|
## Key Functions
|
|
19
52
|
|
|
20
53
|
| Function | Contract | What it does |
|
|
21
54
|
|----------|----------|--------------|
|
|
22
|
-
| `launchGameWith(data)` | `DefifaDeployer` | Creates a
|
|
23
|
-
| `fulfillCommitmentsOf(gameId)` | `DefifaDeployer` | After
|
|
24
|
-
| `triggerNoContestFor(gameId)` | `DefifaDeployer` | Checks safety conditions
|
|
25
|
-
| `currentGamePhaseOf(gameId)` | `DefifaDeployer` | Returns the current
|
|
26
|
-
| `currentGamePotOf(gameId, includeCommitments)` | `DefifaDeployer` | Returns pot size, token address, and decimals.
|
|
27
|
-
| `timesFor(gameId)` | `DefifaDeployer` | Returns `(start, mintPeriodDuration, refundPeriodDuration)
|
|
28
|
-
| `safetyParamsOf(gameId)` | `DefifaDeployer` | Returns `(minParticipation, scorecardTimeout)
|
|
29
|
-
| `nextPhaseNeedsQueueing(gameId)` | `DefifaDeployer` |
|
|
30
|
-
| `submitScorecardFor(gameId, tierWeights)` | `DefifaGovernor` | Submits a
|
|
31
|
-
| `attestToScorecardFrom(gameId, scorecardId)` | `DefifaGovernor` | Attests to a scorecard
|
|
32
|
-
| `ratifyScorecardFrom(gameId, tierWeights)` | `DefifaGovernor` | Ratifies a
|
|
33
|
-
| `initializeGame(gameId,
|
|
34
|
-
| `quorum(gameId)` | `DefifaGovernor` | Returns
|
|
35
|
-
| `getAttestationWeight(gameId, account, timestamp)` | `DefifaGovernor` |
|
|
36
|
-
| `stateOf(gameId, scorecardId)` | `DefifaGovernor` | Returns scorecard state: `RATIFIED
|
|
37
|
-
| `setTierCashOutWeightsTo(tierWeights)` | `DefifaHook` | Sets cash-out weights
|
|
38
|
-
| `afterPayRecordedWith(context)` | `DefifaHook` | Processes payments
|
|
39
|
-
| `beforeCashOutRecordedWith(context)` | `DefifaHook` | Returns cash-out parameters based on game phase.
|
|
40
|
-
| `afterCashOutRecordedWith(context)` | `DefifaHook` | Burns NFTs
|
|
41
|
-
| `cashOutWeightOf(tokenIds)` | `DefifaHook` |
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `initialize(gameId, name, symbol, ...)` | `DefifaHook` | One-time initialization for a cloned hook. Sets project ID, name, symbol, store, rulesets, reporters, tiers, tier names, and default attestation delegate. Reverts if called on code origin or if already initialized. |
|
|
55
|
+
| `launchGameWith(data)` | `DefifaDeployer` | Creates a game: clones the hook, launches a Juicebox project with phased rulesets, initializes the governor, transfers hook ownership to the governor. Returns the game ID. |
|
|
56
|
+
| `fulfillCommitmentsOf(gameId)` | `DefifaDeployer` | After ratification, sends fee payouts via `sendPayoutsOf`, then queues a final ruleset enabling cash outs. |
|
|
57
|
+
| `triggerNoContestFor(gameId)` | `DefifaDeployer` | Checks safety conditions and queues a NO_CONTEST ruleset enabling full refunds. Once per game. |
|
|
58
|
+
| `currentGamePhaseOf(gameId)` | `DefifaDeployer` | Returns the current `DefifaGamePhase`. |
|
|
59
|
+
| `currentGamePotOf(gameId, includeCommitments)` | `DefifaDeployer` | Returns pot size, token address, and decimals. |
|
|
60
|
+
| `timesFor(gameId)` | `DefifaDeployer` | Returns `(start, mintPeriodDuration, refundPeriodDuration)`. |
|
|
61
|
+
| `safetyParamsOf(gameId)` | `DefifaDeployer` | Returns `(minParticipation, scorecardTimeout)`. |
|
|
62
|
+
| `nextPhaseNeedsQueueing(gameId)` | `DefifaDeployer` | True if the next phase ruleset hasn't been queued yet. |
|
|
63
|
+
| `submitScorecardFor(gameId, tierWeights)` | `DefifaGovernor` | Submits a scorecard proposal. Only during SCORING. |
|
|
64
|
+
| `attestToScorecardFrom(gameId, scorecardId)` | `DefifaGovernor` | Attests to a scorecard using tier-delegated voting power. One attestation per address per scorecard. |
|
|
65
|
+
| `ratifyScorecardFrom(gameId, tierWeights)` | `DefifaGovernor` | Ratifies a `SUCCEEDED` scorecard (50% quorum met + grace period elapsed). Executes weights on the hook, then fulfills commitments. |
|
|
66
|
+
| `initializeGame(gameId, ...)` | `DefifaGovernor` | Sets attestation start time and grace period. Called by deployer during launch. |
|
|
67
|
+
| `quorum(gameId)` | `DefifaGovernor` | Returns the quorum threshold. See Attestation & Governance for formula. |
|
|
68
|
+
| `getAttestationWeight(gameId, account, timestamp)` | `DefifaGovernor` | Returns an account's attestation power. See Attestation & Governance for formula. |
|
|
69
|
+
| `stateOf(gameId, scorecardId)` | `DefifaGovernor` | Returns scorecard state: `RATIFIED`, `PENDING`, `SUCCEEDED`, `ACTIVE`, or `DEFEATED`. |
|
|
70
|
+
| `setTierCashOutWeightsTo(tierWeights)` | `DefifaHook` | Sets cash-out weights. Weights must sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18). Owner-only (governor), SCORING phase only, one-time. |
|
|
71
|
+
| `afterPayRecordedWith(context)` | `DefifaHook` | Processes payments. Adds `msg.value != 0` check over base `JB721Hook`. |
|
|
72
|
+
| `beforeCashOutRecordedWith(context)` | `DefifaHook` | Returns cash-out parameters based on game phase. Always returns `noop: false`. |
|
|
73
|
+
| `afterCashOutRecordedWith(context)` | `DefifaHook` | Burns NFTs and tracks redemptions. During COMPLETE, also distributes fee tokens. |
|
|
74
|
+
| `cashOutWeightOf(tokenIds)` | `DefifaHook` | Cumulative cash-out weight for token IDs: `tierWeight / (minted - burned)` per token. |
|
|
75
|
+
| `totalCashOutWeight()` | `DefifaHook` | Returns `TOTAL_CASHOUT_WEIGHT` (1e18). |
|
|
76
|
+
| `setTierDelegateTo(delegatee, tierId)` | `DefifaHook` | Delegates attestation power for a tier. MINT phase only. |
|
|
77
|
+
| `setTierDelegatesTo(delegations)` | `DefifaHook` | Batch delegation. MINT phase only. |
|
|
78
|
+
| `mintReservesFor(tierId, count)` | `DefifaHook` | Mints reserved tokens. Increments `_totalMintCost` so reserved recipients share fee tokens. |
|
|
79
|
+
| `initialize(gameId, ...)` | `DefifaHook` | One-time init for cloned hook. Sets project ID, store, reporters, tiers, and default attestation delegate. |
|
|
48
80
|
|
|
49
81
|
## Integration Points
|
|
50
82
|
|
|
@@ -53,22 +85,105 @@ On-chain prediction game framework built on Juicebox V6. Players mint NFT game p
|
|
|
53
85
|
| `@bananapus/core-v6` | `IJBController`, `IJBDirectory`, `IJBRulesets`, `IJBTerminal`, `IJBMultiTerminal`, `JBRulesetConfig`, `JBSplit`, `JBConstants`, `JBMetadataResolver` | Project creation, ruleset management, terminal interactions, payout distribution, metadata encoding. |
|
|
54
86
|
| `@bananapus/721-hook-v6` | `JB721Hook`, `IJB721TiersHookStore`, `JB721TierConfig`, `JB721Tier`, `ERC721`, `JB721TiersRulesetMetadataResolver` | Hook base class, NFT tier management, tier storage, transfer pause checking. |
|
|
55
87
|
| `@bananapus/address-registry-v6` | `IJBAddressRegistry` | Hook address registration for discoverability. |
|
|
56
|
-
| `@bananapus/permission-ids-v6` | `JBPermissionIds` | Permission constants
|
|
88
|
+
| `@bananapus/permission-ids-v6` | `JBPermissionIds` | Permission constants (`SET_SPLIT_GROUPS`). |
|
|
57
89
|
| `@openzeppelin/contracts` | `Ownable`, `Clones`, `IERC721Receiver`, `SafeERC20`, `Checkpoints`, `Strings`, `IERC20` | Access control, minimal proxy cloning, safe token handling, checkpointed voting, string formatting, fee token transfers. |
|
|
58
|
-
| `@prb/math` | `mulDiv` |
|
|
90
|
+
| `@prb/math` | `mulDiv` | Fixed-point arithmetic for attestation weight and pot distribution. |
|
|
59
91
|
|
|
60
92
|
## Key Types
|
|
61
93
|
|
|
62
94
|
| Struct/Enum | Key Fields | Used In |
|
|
63
95
|
|-------------|------------|---------|
|
|
64
|
-
| `DefifaLaunchProjectData` | `name`, `tiers` (DefifaTierParams[]), `tierPrice` (uint104), `token` (JBAccountingContext), `mintPeriodDuration` (uint24), `refundPeriodDuration` (uint24), `start` (uint48), `splits` (JBSplit[]), `attestationStartTime`, `attestationGracePeriod`, `defaultAttestationDelegate`, `terminal`, `store`, `minParticipation` (uint256), `scorecardTimeout` (uint32) | `DefifaDeployer.launchGameWith` |
|
|
96
|
+
| `DefifaLaunchProjectData` | `name`, `projectUri`, `contractUri`, `baseUri`, `tiers` (DefifaTierParams[]), `tierPrice` (uint104), `token` (JBAccountingContext), `mintPeriodDuration` (uint24), `refundPeriodDuration` (uint24), `start` (uint48), `splits` (JBSplit[]), `attestationStartTime`, `attestationGracePeriod`, `defaultAttestationDelegate`, `defaultTokenUriResolver` (IJB721TokenUriResolver), `terminal`, `store`, `minParticipation` (uint256), `scorecardTimeout` (uint32) | `DefifaDeployer.launchGameWith` |
|
|
65
97
|
| `DefifaTierParams` | `name` (string), `reservedRate` (uint16), `reservedTokenBeneficiary` (address), `encodedIPFSUri` (bytes32), `shouldUseReservedTokenBeneficiaryAsDefault` (bool) | `DefifaLaunchProjectData.tiers` |
|
|
66
98
|
| `DefifaTierCashOutWeight` | `id` (uint256), `cashOutWeight` (uint256) | Scorecard proposals, `DefifaHook.setTierCashOutWeightsTo` |
|
|
67
99
|
| `DefifaOpsData` | `token` (address), `start` (uint48), `mintPeriodDuration` (uint24), `refundPeriodDuration` (uint24), `minParticipation` (uint256), `scorecardTimeout` (uint32) | Internal game state in `DefifaDeployer` |
|
|
68
100
|
| `DefifaDelegation` | `delegatee` (address), `tierId` (uint256) | `DefifaHook.setTierDelegatesTo` |
|
|
69
101
|
| `DefifaGamePhase` | `COUNTDOWN`, `MINT`, `REFUND`, `SCORING`, `COMPLETE`, `NO_CONTEST` | Phase reporting throughout |
|
|
102
|
+
| `DefifaScorecard` | `attestationsBegin` (uint48), `gracePeriodEnds` (uint48) | `DefifaGovernor._scorecardOf` |
|
|
103
|
+
| `DefifaAttestations` | `count` (uint256), `hasAttested` (mapping(address => bool)) | `DefifaGovernor._scorecardAttestationsOf` |
|
|
70
104
|
| `DefifaScorecardState` | `PENDING`, `ACTIVE`, `DEFEATED`, `SUCCEEDED`, `RATIFIED` | `DefifaGovernor.stateOf` |
|
|
71
105
|
|
|
106
|
+
## Events
|
|
107
|
+
|
|
108
|
+
| Event | Contract | Parameters |
|
|
109
|
+
|-------|----------|------------|
|
|
110
|
+
| `LaunchGame` | `DefifaDeployer` | `gameId` (indexed), `hook` (indexed), `governor` (indexed), `tokenUriResolver`, `caller` |
|
|
111
|
+
| `FulfilledCommitments` | `DefifaDeployer` | `gameId` (indexed), `pot`, `caller` |
|
|
112
|
+
| `CommitmentPayoutFailed` | `DefifaDeployer` | `gameId` (indexed), `amount`, `reason` (bytes) |
|
|
113
|
+
| `DistributeToSplit` | `DefifaDeployer` | `split` (JBSplit), `amount`, `caller` |
|
|
114
|
+
| `QueuedNoContest` | `DefifaDeployer` | `gameId` (indexed), `caller` |
|
|
115
|
+
| `QueuedRefundPhase` | `DefifaDeployer` | `gameId` (indexed), `caller` |
|
|
116
|
+
| `QueuedScoringPhase` | `DefifaDeployer` | `gameId` (indexed), `caller` |
|
|
117
|
+
| `Mint` | `DefifaHook` | `tokenId` (indexed), `tierId` (indexed), `beneficiary` (indexed), `totalAmountContributed`, `caller` |
|
|
118
|
+
| `MintReservedToken` | `DefifaHook` | `tokenId` (indexed), `tierId` (indexed), `beneficiary` (indexed), `caller` |
|
|
119
|
+
| `TierDelegateAttestationsChanged` | `DefifaHook` | `delegate` (indexed), `tierId` (indexed), `previousBalance`, `newBalance`, `caller` |
|
|
120
|
+
| `DelegateChanged` | `DefifaHook` | `delegator` (indexed), `fromDelegate` (indexed), `toDelegate` (indexed) |
|
|
121
|
+
| `ClaimedTokens` | `DefifaHook` | `beneficiary` (indexed), `defifaTokenAmount`, `baseProtocolTokenAmount`, `caller` |
|
|
122
|
+
| `TierCashOutWeightsSet` | `DefifaHook` | `tierWeights` (DefifaTierCashOutWeight[]), `caller` |
|
|
123
|
+
| `GameInitialized` | `DefifaGovernor` | `gameId` (indexed), `attestationStartTime`, `attestationGracePeriod`, `caller` |
|
|
124
|
+
| `ScorecardSubmitted` | `DefifaGovernor` | `gameId` (indexed), `scorecardId` (indexed), `tierWeights` (DefifaTierCashOutWeight[]), `isDefaultAttestationDelegate`, `caller` |
|
|
125
|
+
| `ScorecardAttested` | `DefifaGovernor` | `gameId` (indexed), `scorecardId` (indexed), `weight`, `caller` |
|
|
126
|
+
| `ScorecardRatified` | `DefifaGovernor` | `gameId` (indexed), `scorecardId` (indexed), `caller` |
|
|
127
|
+
|
|
128
|
+
## Errors
|
|
129
|
+
|
|
130
|
+
| Error | Contract | When |
|
|
131
|
+
|-------|----------|------|
|
|
132
|
+
| `DefifaDeployer_CantFulfillYet` | `DefifaDeployer` | `fulfillCommitmentsOf` called before scorecard is ratified. |
|
|
133
|
+
| `DefifaDeployer_GameOver` | `DefifaDeployer` | Attempting to queue a phase after the game is already complete. |
|
|
134
|
+
| `DefifaDeployer_InvalidFeePercent` | `DefifaDeployer` | Fee configuration is invalid. |
|
|
135
|
+
| `DefifaDeployer_InvalidGameConfiguration` | `DefifaDeployer` | Launch data fails validation (e.g., missing tiers, bad durations). |
|
|
136
|
+
| `DefifaDeployer_IncorrectDecimalAmount` | `DefifaDeployer` | Token accounting context has wrong decimal count. |
|
|
137
|
+
| `DefifaDeployer_NotNoContest` | `DefifaDeployer` | Safety conditions for no-contest are not met. |
|
|
138
|
+
| `DefifaDeployer_NoContestAlreadyTriggered` | `DefifaDeployer` | `triggerNoContestFor` called more than once for the same game. |
|
|
139
|
+
| `DefifaDeployer_TerminalNotFound` | `DefifaDeployer` | No terminal found for the game's project. |
|
|
140
|
+
| `DefifaDeployer_PhaseAlreadyQueued` | `DefifaDeployer` | The next phase ruleset has already been queued. |
|
|
141
|
+
| `DefifaDeployer_SplitsDontAddUp` | `DefifaDeployer` | Split percentages don't sum correctly. |
|
|
142
|
+
| `DefifaDeployer_UnexpectedTerminalCurrency` | `DefifaDeployer` | Terminal's accounting currency doesn't match expected currency. |
|
|
143
|
+
| `DefifaHook_BadTierOrder` | `DefifaHook` | Scorecard tier IDs are not in strict ascending order. |
|
|
144
|
+
| `DefifaHook_DelegateAddressZero` | `DefifaHook` | Attempting to delegate to the zero address. |
|
|
145
|
+
| `DefifaHook_DelegateChangesUnavailableInThisPhase` | `DefifaHook` | Delegation change attempted outside MINT phase. |
|
|
146
|
+
| `DefifaHook_GameIsntScoringYet` | `DefifaHook` | `setTierCashOutWeightsTo` called before SCORING phase. |
|
|
147
|
+
| `DefifaHook_InvalidTierId` | `DefifaHook` | Tier ID in scorecard doesn't exist. |
|
|
148
|
+
| `DefifaHook_InvalidCashoutWeights` | `DefifaHook` | Scorecard tier weights don't sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18). |
|
|
149
|
+
| `DefifaHook_NothingToClaim` | `DefifaHook` | Cash out during COMPLETE yields no ETH and no fee tokens. |
|
|
150
|
+
| `DefifaHook_NothingToMint` | `DefifaHook` | Reserved mint attempted with zero count or no available reserves. |
|
|
151
|
+
| `DefifaHook_WrongCurrency` | `DefifaHook` | Payment currency doesn't match the hook's pricing currency. |
|
|
152
|
+
| `DefifaHook_Overspending` | `DefifaHook` | Payment exceeds allowed amount for the tier. |
|
|
153
|
+
| `DefifaHook_CashoutWeightsAlreadySet` | `DefifaHook` | `setTierCashOutWeightsTo` called after weights were already set. |
|
|
154
|
+
| `DefifaHook_ReservedTokenMintingPaused` | `DefifaHook` | Reserved token minting is paused in the current ruleset. |
|
|
155
|
+
| `DefifaHook_TransfersPaused` | `DefifaHook` | Token transfers are paused in the current ruleset. |
|
|
156
|
+
| `DefifaHook_Unauthorized(tokenId, owner, caller)` | `DefifaHook` | Caller doesn't own the token being operated on. |
|
|
157
|
+
| `DefifaGovernor_AlreadyAttested` | `DefifaGovernor` | Account already attested to this scorecard. |
|
|
158
|
+
| `DefifaGovernor_AlreadyInitialized` | `DefifaGovernor` | `initializeGame` called for a game that's already initialized. |
|
|
159
|
+
| `DefifaGovernor_AlreadyRatified` | `DefifaGovernor` | Attempting to submit or ratify when a scorecard is already ratified. |
|
|
160
|
+
| `DefifaGovernor_DuplicateScorecard` | `DefifaGovernor` | Submitting a scorecard that produces the same hash as an existing one. |
|
|
161
|
+
| `DefifaGovernor_GameNotFound` | `DefifaGovernor` | Game has not been initialized (`_packedScorecardInfoOf` is 0). |
|
|
162
|
+
| `DefifaGovernor_IncorrectTierOrder` | `DefifaGovernor` | Tier weights not in ascending order. |
|
|
163
|
+
| `DefifaGovernor_NotAllowed` | `DefifaGovernor` | Operation not permitted in the current game phase or scorecard state. |
|
|
164
|
+
| `DefifaGovernor_Uint48Overflow` | `DefifaGovernor` | `attestationStartTime` or `attestationGracePeriod` exceeds uint48 max. |
|
|
165
|
+
| `DefifaGovernor_UnknownProposal` | `DefifaGovernor` | `stateOf` called with a scorecard ID that hasn't been submitted. |
|
|
166
|
+
| `DefifaGovernor_UnownedProposedCashoutValue` | `DefifaGovernor` | Scorecard assigns non-zero weight to a tier with zero minted supply. |
|
|
167
|
+
|
|
168
|
+
## Storage
|
|
169
|
+
|
|
170
|
+
| Variable | Type | Contract | Description |
|
|
171
|
+
|----------|------|----------|-------------|
|
|
172
|
+
| `_tierCashOutWeights` | `uint256[128]` | `DefifaHook` | Fixed-size array of cash-out weights per tier, set once by the governor. |
|
|
173
|
+
| `cashOutWeightIsSet` | `bool` | `DefifaHook` | Flag preventing re-setting of cash-out weights. |
|
|
174
|
+
| `amountRedeemed` | `uint256` | `DefifaHook` | Cumulative ETH redeemed from the pot (refunds not counted). |
|
|
175
|
+
| `_totalMintCost` | `uint256` | `DefifaHook` | Cumulative mint price of all live tokens. Denominator for fee token distribution. |
|
|
176
|
+
| `tokensRedeemedFrom` | `mapping(uint256 => uint256)` | `DefifaHook` | Number of tokens redeemed per tier. |
|
|
177
|
+
| `ratifiedScorecardIdOf` | `mapping(uint256 => uint256)` | `DefifaGovernor` | Maps game ID to the ratified scorecard ID (0 if none). |
|
|
178
|
+
| `_packedScorecardInfoOf` | `mapping(uint256 => uint256)` | `DefifaGovernor` | Bit-packed: attestation start time (bits 0-47), grace period (bits 48-95). |
|
|
179
|
+
| `_scorecardOf` | `mapping(uint256 => mapping(uint256 => DefifaScorecard))` | `DefifaGovernor` | Maps (gameId, scorecardId) to scorecard data. |
|
|
180
|
+
| `_scorecardAttestationsOf` | `mapping(uint256 => mapping(uint256 => DefifaAttestations))` | `DefifaGovernor` | Maps (gameId, scorecardId) to attestation data. |
|
|
181
|
+
| `defaultAttestationDelegateProposalOf` | `mapping(uint256 => uint256)` | `DefifaGovernor` | Maps game ID to the scorecard ID submitted by the default attestation delegate. |
|
|
182
|
+
| `fulfilledCommitmentsOf` | `mapping(uint256 => uint256)` | `DefifaDeployer` | Non-zero means commitments fulfilled; value of 1 is a sentinel for reentrancy guard. |
|
|
183
|
+
| `noContestTriggeredFor` | `mapping(uint256 => bool)` | `DefifaDeployer` | Whether no-contest has been triggered. Can only be set once. |
|
|
184
|
+
| `_opsOf` | `mapping(uint256 => DefifaOpsData)` | `DefifaDeployer` | Operational data (token, start, durations, safety params). |
|
|
185
|
+
| `_commitmentPercentOf` | `mapping(uint256 => uint256)` | `DefifaDeployer` | Total commitment percentage (fees + splits). |
|
|
186
|
+
|
|
72
187
|
## Constants
|
|
73
188
|
|
|
74
189
|
| Constant | Value | Location | Meaning |
|
|
@@ -92,34 +207,32 @@ During COMPLETE phase cash outs, players also receive proportional $DEFIFA and $
|
|
|
92
207
|
|
|
93
208
|
## Attestation & Governance
|
|
94
209
|
|
|
95
|
-
- Each tier contributes equal
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
- Attestation snapshots are taken at the scorecard's `attestationsBegin` timestamp, locking voting power to prevent post-submission manipulation.
|
|
210
|
+
- **Per-tier power**: `mulDiv(MAX_ATTESTATION_POWER_TIER, accountTierUnits, totalTierUnits)`. Each tier contributes equal weight regardless of supply -- a tier with 1 NFT has the same governance weight as a tier with 100.
|
|
211
|
+
- **Quorum**: `50% of (MAX_ATTESTATION_POWER_TIER * numberOfMintedTiers)`. Only tiers with at least one minted token count.
|
|
212
|
+
- Snapshots taken at the scorecard's `attestationsBegin` timestamp, locking voting power to prevent post-submission manipulation.
|
|
99
213
|
- Each address can only attest once per scorecard.
|
|
100
|
-
-
|
|
214
|
+
- Grace period (minimum 1 day) prevents instant ratification after quorum is reached.
|
|
101
215
|
|
|
102
216
|
## Gotchas
|
|
103
217
|
|
|
104
|
-
- `TOTAL_CASHOUT_WEIGHT` is 1e18. Submitted scorecard tier weights must sum to **exactly** this value or `setTierCashOutWeightsTo` reverts
|
|
105
|
-
- Tier IDs in a scorecard must be in **strict ascending order** with no duplicates
|
|
106
|
-
-
|
|
107
|
-
- `DefifaHook` is
|
|
108
|
-
- All tiers share the same price (`tierPrice` on `DefifaLaunchProjectData`).
|
|
109
|
-
- Delegation
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
- `ratifyScorecardFrom`
|
|
113
|
-
- `fulfillCommitmentsOf` uses `max(amount, 1)` as a reentrancy
|
|
114
|
-
- `_buildSplits` normalizes
|
|
115
|
-
- `_totalMintCost` tracks cumulative mint prices
|
|
116
|
-
- Cash outs during COMPLETE
|
|
117
|
-
- `minParticipation` is compared against
|
|
118
|
-
- `scorecardTimeout` counts seconds from
|
|
119
|
-
- `triggerNoContestFor` can only be called once per game
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
- Metadata IDs for pay and cashout use the **code origin address** (the uncloned implementation), not the clone address: `JBMetadataResolver.getId("pay", codeOrigin)`.
|
|
218
|
+
- `TOTAL_CASHOUT_WEIGHT` is 1e18. Submitted scorecard tier weights must sum to **exactly** this value or `setTierCashOutWeightsTo` reverts. No tolerance.
|
|
219
|
+
- Tier IDs in a scorecard must be in **strict ascending order** with no duplicates.
|
|
220
|
+
- Max 128 tiers (`uint256[128] _tierCashOutWeights`).
|
|
221
|
+
- `DefifaHook` is a **minimal proxy clone** (`Clones.cloneDeterministic`). `initialize` can only be called once.
|
|
222
|
+
- All tiers share the same price (`tierPrice` on `DefifaLaunchProjectData`).
|
|
223
|
+
- **Delegation only during MINT phase**. Other phases revert with `DefifaHook_DelegateChangesUnavailableInThisPhase`.
|
|
224
|
+
- If `totalTierUnits` is 0 for a tier (no delegations), that tier contributes no attestation power.
|
|
225
|
+
- **Dynamic quorum**: only counts tiers with minted supply. Minting new tiers changes quorum retroactively for active proposals.
|
|
226
|
+
- `ratifyScorecardFrom` uses **low-level `.call`** to execute the scorecard on the hook (necessary because `setTierCashOutWeightsTo` is `onlyOwner`).
|
|
227
|
+
- `fulfillCommitmentsOf` uses `max(amount, 1)` as a reentrancy sentinel. `sendPayoutsOf` is wrapped in try-catch: on failure, resets to sentinel (1) and emits `CommitmentPayoutFailed`.
|
|
228
|
+
- `_buildSplits` normalizes split percentages. Rounding remainder absorbed by the protocol fee split (last in array).
|
|
229
|
+
- `_totalMintCost` tracks cumulative mint prices (paid + reserved). Incremented on pay/reserve, decremented on cash out. Denominator for fee token distribution.
|
|
230
|
+
- Cash outs during COMPLETE revert with `DefifaHook_NothingToClaim` if **both** reclaimed ETH is 0 **and** no fee tokens transferred.
|
|
231
|
+
- `minParticipation` is compared against terminal surplus. Value of 0 disables the check.
|
|
232
|
+
- `scorecardTimeout` counts seconds from SCORING start. Value of 0 disables. Both enable `triggerNoContestFor` when exceeded.
|
|
233
|
+
- `triggerNoContestFor` can only be called once per game and is irreversible.
|
|
234
|
+
- Token IDs follow `JB721TiersHookStore` encoding: `tierId * 1_000_000_000 + tokenNumber`.
|
|
235
|
+
- Metadata IDs use the **code origin address** (uncloned implementation), not the clone: `JBMetadataResolver.getId("pay", codeOrigin)`.
|
|
123
236
|
|
|
124
237
|
## Example Integration
|
|
125
238
|
|
|
@@ -133,14 +246,16 @@ import {DefifaTierCashOutWeight} from "./structs/DefifaTierCashOutWeight.sol";
|
|
|
133
246
|
DefifaTierParams[] memory tiers = new DefifaTierParams[](2);
|
|
134
247
|
tiers[0] = DefifaTierParams({
|
|
135
248
|
name: "Team A",
|
|
136
|
-
reservedRate
|
|
249
|
+
// reservedRate maps to JB721's `reserveFrequency`: 1 reserved mint per N paid mints.
|
|
250
|
+
// 1001 means "1 reserve per 1001 mints" -- effectively no reserves for normal game sizes.
|
|
251
|
+
reservedRate: 1001,
|
|
137
252
|
reservedTokenBeneficiary: address(0),
|
|
138
253
|
encodedIPFSUri: bytes32(0),
|
|
139
254
|
shouldUseReservedTokenBeneficiaryAsDefault: false
|
|
140
255
|
});
|
|
141
256
|
tiers[1] = DefifaTierParams({
|
|
142
257
|
name: "Team B",
|
|
143
|
-
reservedRate: 1001,
|
|
258
|
+
reservedRate: 1001, // effectively no reserves (see above)
|
|
144
259
|
reservedTokenBeneficiary: address(0),
|
|
145
260
|
encodedIPFSUri: bytes32(0),
|
|
146
261
|
shouldUseReservedTokenBeneficiaryAsDefault: false
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
|
|
|
21
21
|
|
|
22
22
|
```solidity
|
|
23
23
|
// Contracts — pin to exact version
|
|
24
|
-
pragma solidity 0.8.26;
|
|
24
|
+
pragma solidity ^0.8.26;
|
|
25
25
|
|
|
26
26
|
// Interfaces, structs, enums — caret for forward compatibility
|
|
27
27
|
pragma solidity ^0.8.0;
|