@ballkidz/defifa 0.0.12 → 0.0.14

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.
Files changed (44) hide show
  1. package/ADMINISTRATION.md +3 -3
  2. package/ARCHITECTURE.md +3 -2
  3. package/AUDIT_INSTRUCTIONS.md +5 -5
  4. package/CHANGE_LOG.md +62 -5
  5. package/CRYPTO_ECON.md +506 -271
  6. package/CRYPTO_ECON.pdf +0 -0
  7. package/CRYPTO_ECON.tex +438 -241
  8. package/RISKS.md +13 -1
  9. package/SKILLS.md +5 -3
  10. package/USER_JOURNEYS.md +4 -3
  11. package/package.json +6 -6
  12. package/src/DefifaDeployer.sol +128 -130
  13. package/src/DefifaGovernor.sol +304 -83
  14. package/src/DefifaHook.sol +184 -171
  15. package/src/enums/DefifaScorecardState.sol +1 -0
  16. package/src/interfaces/IDefifaGovernor.sol +42 -2
  17. package/src/libraries/DefifaHookLib.sol +69 -62
  18. package/src/structs/DefifaAttestations.sol +3 -3
  19. package/src/structs/DefifaLaunchProjectData.sol +1 -0
  20. package/src/structs/DefifaScorecard.sol +2 -0
  21. package/test/BWAFunctionComparison.t.sol +1320 -0
  22. package/test/DefifaAdversarialQuorum.t.sol +52 -37
  23. package/test/DefifaAuditLowGuards.t.sol +9 -5
  24. package/test/DefifaFeeAccounting.t.sol +2 -1
  25. package/test/DefifaGovernanceHardening.t.sol +1315 -0
  26. package/test/DefifaGovernor.t.sol +8 -4
  27. package/test/DefifaHookRegressions.t.sol +2 -1
  28. package/test/DefifaMintCostInvariant.t.sol +2 -1
  29. package/test/DefifaNoContest.t.sol +3 -2
  30. package/test/DefifaSecurity.t.sol +55 -47
  31. package/test/DefifaUSDC.t.sol +3 -2
  32. package/test/Fork.t.sol +37 -32
  33. package/test/TestAuditGaps.sol +6 -4
  34. package/test/TestQALastMile.t.sol +6 -3
  35. package/test/audit/{CodexAttestationDoubleCount.t.sol → AttestationDoubleCount.t.sol} +3 -2
  36. package/test/audit/FixPendingReserveDilution.t.sol +366 -0
  37. package/test/audit/PendingReserveDilution.t.sol +298 -0
  38. package/test/audit/PendingReserveQuorumGrief.t.sol +355 -0
  39. package/test/audit/PendingReserveSnapshotBypass.t.sol +279 -0
  40. package/test/regression/AttestationDelegateBeneficiary.t.sol +2 -1
  41. package/test/regression/FulfillmentBlocksRatification.t.sol +2 -1
  42. package/test/regression/GracePeriodBypass.t.sol +2 -1
  43. package/test/SVG.t.sol +0 -164
  44. package/test/deployScript.t.sol +0 -144
package/RISKS.md CHANGED
@@ -12,7 +12,7 @@
12
12
  ## 2. Economic Risks
13
13
 
14
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.
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
+ - **Dynamic quorum from live supply (mitigated).** `quorum()` counts tiers with circulating supply (`currentSupplyOfTier > 0`) OR pending reserves (`numberOfPendingReservesFor > 0`). No snapshot is needed because during SCORING, supply is frozen (no new paid mints, no burns) and reserve minting doesn't change which tiers are counted — tiers with pending reserves are already included. Pending reserves also dilute attestation power — `getAttestationWeight` includes them in the denominator so every token holder's voting power already accounts for reserves that will eventually be minted. When the reserve beneficiary mints later, power redistributes smoothly (no shift). Consistent with the cash-out path which also dilutes by pending reserves.
16
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).
17
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%.
18
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.
@@ -67,3 +67,15 @@ Cash-out weights set via `ratifyScorecardFrom` cannot be updated or corrected. T
67
67
  ### 8.4 ratifyScorecardFrom reentrancy is double-guarded
68
68
 
69
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.
70
+
71
+ ### 8.5 Attestation snapshot uses attestationsBegin - 1 (Codex R2 fix)
72
+
73
+ `attestToScorecardFrom` snapshots attestation weight at `attestationsBegin - 1` instead of `attestationsBegin`. This prevents same-block transfer manipulation where a holder attests, transfers the NFT, and the recipient also attests in the same block. The trade-off is that NFTs minted in the same block as `attestationsBegin` have zero weight for that attestation call. This is acceptable because attestation typically happens well after minting, and the delay is negligible.
74
+
75
+ ### 8.6 Pending reserves snapshotted at submission and included in denominators (Codex R2 fix)
76
+
77
+ Two related protections:
78
+
79
+ **Governance:** `submitScorecardFor` snapshots `numberOfPendingReservesFor()` per tier into `_pendingReservesSnapshotOf`. `getBWAAttestationWeight` reads from this snapshot instead of live state. This prevents reserve minting between submission and attestation from inflating a holder's voting power by removing pending-reserve dilution. The trade-off is that if reserves are minted after submission, the dilution persists even though the reserves are no longer pending. This is conservative but correct -- it locks governance power at submission time.
80
+
81
+ **Cash-out (fee tokens):** `afterCashOutRecordedWith` includes `_pendingReserveMintCost()` (sum of `pendingReserves * tier.price` across all tiers) in the fee token claim denominator. This prevents paid holders from claiming a disproportionate share of $DEFIFA/$NANA tokens before reserves are minted. The trade-off is that if reserve NFTs are never minted (e.g., the reserve beneficiary is set to address(0) and minting reverts), those shares of fee tokens remain locked in the contract. This is acceptable because: (1) it prevents paid holders from front-running reserve minting to extract the reserves' share, and (2) reserve beneficiaries are set at deployment and should always be valid.
package/SKILLS.md CHANGED
@@ -99,7 +99,7 @@ On-chain prediction game framework built on Juicebox V6. Players mint NFT game p
99
99
  | `DefifaOpsData` | `token` (address), `start` (uint48), `mintPeriodDuration` (uint24), `refundPeriodDuration` (uint24), `minParticipation` (uint256), `scorecardTimeout` (uint32) | Internal game state in `DefifaDeployer` |
100
100
  | `DefifaDelegation` | `delegatee` (address), `tierId` (uint256) | `DefifaHook.setTierDelegatesTo` |
101
101
  | `DefifaGamePhase` | `COUNTDOWN`, `MINT`, `REFUND`, `SCORING`, `COMPLETE`, `NO_CONTEST` | Phase reporting throughout |
102
- | `DefifaScorecard` | `attestationsBegin` (uint48), `gracePeriodEnds` (uint48) | `DefifaGovernor._scorecardOf` |
102
+ | `DefifaScorecard` | `attestationsBegin` (uint48), `gracePeriodEnds` (uint48) — set at submission time | `DefifaGovernor._scorecardOf` |
103
103
  | `DefifaAttestations` | `count` (uint256), `hasAttested` (mapping(address => bool)) | `DefifaGovernor._scorecardAttestationsOf` |
104
104
  | `DefifaScorecardState` | `PENDING`, `ACTIVE`, `DEFEATED`, `SUCCEEDED`, `RATIFIED` | `DefifaGovernor.stateOf` |
105
105
 
@@ -209,7 +209,7 @@ During COMPLETE phase cash outs, players also receive proportional $DEFIFA and $
209
209
 
210
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
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.
212
+ - Attestation weight checkpointed at `attestationsBegin - 1` (one second before the attestation window opens), preventing same-block transfer manipulation. Pending reserve counts are snapshotted at submission time to prevent reserve minting from inflating attestation power.
213
213
  - Each address can only attest once per scorecard.
214
214
  - Grace period (minimum 1 day) prevents instant ratification after quorum is reached.
215
215
 
@@ -222,7 +222,9 @@ During COMPLETE phase cash outs, players also receive proportional $DEFIFA and $
222
222
  - All tiers share the same price (`tierPrice` on `DefifaLaunchProjectData`).
223
223
  - **Delegation only during MINT phase**. Other phases revert with `DefifaHook_DelegateChangesUnavailableInThisPhase`.
224
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.
225
+ - **Quorum stability via pending reserves**: `quorum()` counts tiers with either minted supply (`currentSupplyOfTier > 0`) OR pending reserves (`numberOfPendingReservesFor > 0`). No snapshot is needed because during SCORING, supply is frozen (no new paid mints, no burns) and reserve minting doesn't change which tiers are counted tiers with pending reserves are already included. The pending reserves check matters when all paid tokens in a tier were burned during REFUND: `currentSupplyOfTier` drops to 0 but pending reserves persist.
226
+ - **Pending reserves dilute attestation power (snapshotted)**: `getBWAAttestationWeight` includes pending reserves in the denominator (total attestation units) but NOT the numerator (individual account units). These pending reserve counts are **snapshotted at scorecard submission time** (`_pendingReservesSnapshotOf`), so minting reserves after submission does not inflate attestation power. The live `getAttestationWeight` still reads live pending reserves. Consistent with the cash-out path (`computeCashOutWeight`) which also includes pending reserves in `totalTokensForCashoutInTier`.
227
+ - **Fee token claim includes pending reserve cost**: During COMPLETE cash-outs, fee token distribution uses `_totalMintCost + _pendingReserveMintCost()` as the denominator. This prevents paid holders from claiming a disproportionate share of $DEFIFA/$NANA tokens before reserves are minted.
226
228
  - `ratifyScorecardFrom` uses **low-level `.call`** to execute the scorecard on the hook (necessary because `setTierCashOutWeightsTo` is `onlyOwner`).
227
229
  - `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
230
  - `_buildSplits` normalizes split percentages. Rounding remainder absorbed by the protocol fee split (last in array).
package/USER_JOURNEYS.md CHANGED
@@ -349,7 +349,8 @@ uint256 scorecardId = governor.submitScorecardFor(gameId, tierWeights);
349
349
 
350
350
  1. `DefifaGovernor._scorecardOf[gameId][scorecardId].attestationsBegin` -- Set to `max(block.timestamp, attestationStartTime)`.
351
351
  2. `DefifaGovernor._scorecardOf[gameId][scorecardId].gracePeriodEnds` -- Set to `attestationsBegin + attestationGracePeriod`.
352
- 3. `DefifaGovernor.defaultAttestationDelegateProposalOf[gameId]` -- Set to `scorecardId` if sender is the default attestation delegate.
352
+ 3. `DefifaGovernor._pendingReservesSnapshotOf[gameId][scorecardId][tierId]` -- Snapshots `numberOfPendingReservesFor()` for every tier. Used by `getBWAAttestationWeight()` to prevent reserve minting from inflating attestation power.
353
+ 4. `DefifaGovernor.defaultAttestationDelegateProposalOf[gameId]` -- Set to `scorecardId` if sender is the default attestation delegate.
353
354
 
354
355
  ### Events
355
356
 
@@ -370,7 +371,7 @@ uint256 scorecardId = governor.submitScorecardFor(gameId, tierWeights);
370
371
 
371
372
  **Entry point:** `DefifaGovernor.attestToScorecardFrom(uint256 gameId, uint256 scorecardId) external returns (uint256 weight)`
372
373
 
373
- **Who can call:** Anyone. However, attestation weight is zero unless the caller (or their delegate) held NFTs at the `attestationsBegin` snapshot timestamp.
374
+ **Who can call:** Anyone. However, attestation weight is zero unless the caller (or their delegate) held NFTs at the `attestationsBegin - 1` checkpoint timestamp (one second before the attestation window opens).
374
375
 
375
376
  **Actor:** Attestor (NFT holder or delegate)
376
377
  **Phase:** SCORING
@@ -408,7 +409,7 @@ uint256 weight = governor.attestToScorecardFrom(gameId, scorecardId);
408
409
  - `DefifaGovernor_NotAllowed` -- Game not in SCORING phase, or scorecard not in ACTIVE/SUCCEEDED state.
409
410
  - `DefifaGovernor_AlreadyAttested` -- Account already attested to this scorecard.
410
411
  - `DefifaGovernor_UnknownProposal` -- Scorecard ID has no submission record.
411
- - Attestation weight is computed at `attestationsBegin` timestamp using checkpointed values (snapshot, not live).
412
+ - Attestation weight is computed at `attestationsBegin - 1` timestamp using checkpointed values (snapshot, not live). This prevents same-block transfer manipulation.
412
413
  - Each tier caps at `MAX_ATTESTATION_POWER_TIER` (1e9) regardless of how many tokens exist in that tier.
413
414
 
414
415
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ballkidz/defifa",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -13,14 +13,14 @@
13
13
  "url": "https://github.com/BallKidz/defifa-collection-deployer"
14
14
  },
15
15
  "dependencies": {
16
- "@bananapus/721-hook-v6": "^0.0.21",
17
- "@bananapus/address-registry-v6": "^0.0.15",
18
- "@bananapus/core-v6": "^0.0.27",
16
+ "@bananapus/721-hook-v6": "^0.0.22",
17
+ "@bananapus/address-registry-v6": "^0.0.16",
18
+ "@bananapus/core-v6": "^0.0.28",
19
19
  "@bananapus/permission-ids-v6": "^0.0.14",
20
- "@croptop/core-v6": "^0.0.22",
20
+ "@croptop/core-v6": "^0.0.23",
21
21
  "@openzeppelin/contracts": "^5.6.1",
22
22
  "@prb/math": "^4.1.1",
23
- "@rev-net/core-v6": "^0.0.17",
23
+ "@rev-net/core-v6": "^0.0.21",
24
24
  "scripty.sol": "^2.1.1"
25
25
  },
26
26
  "devDependencies": {