@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
@@ -531,9 +531,10 @@ contract DefifaGovernorTest is JBTest, TestBaseWorkflow {
531
531
  // 0 = Against
532
532
  // 1 = For
533
533
  // 2 = Abstain
534
+ // BWA may reduce beneficiaries' power to zero; skip those gracefully.
534
535
  for (uint256 i = 0; i < _users.length; i++) {
535
536
  vm.prank(_users[i]);
536
- _governor.attestToScorecardFrom(_gameId, _proposalId);
537
+ try _governor.attestToScorecardFrom(_gameId, _proposalId) {} catch {}
537
538
  }
538
539
  // each block is of 12 secs
539
540
  vm.warp(block.timestamp + _governor.attestationGracePeriodOf(_gameId));
@@ -871,9 +872,10 @@ contract DefifaGovernorTest is JBTest, TestBaseWorkflow {
871
872
  // 0 = Against
872
873
  // 1 = For
873
874
  // 2 = Abstain
875
+ // BWA may reduce beneficiaries' power to zero; skip those gracefully.
874
876
  for (uint256 i = 0; i < _users.length; i++) {
875
877
  vm.prank(_users[i]);
876
- _governor.attestToScorecardFrom(_gameId, _proposalId);
878
+ try _governor.attestToScorecardFrom(_gameId, _proposalId) {} catch {}
877
879
  }
878
880
  }
879
881
 
@@ -977,7 +979,8 @@ contract DefifaGovernorTest is JBTest, TestBaseWorkflow {
977
979
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
978
980
  terminal: jbMultiTerminal(),
979
981
  minParticipation: 0,
980
- scorecardTimeout: 0
982
+ scorecardTimeout: 0,
983
+ timelockDuration: 0
981
984
  });
982
985
  (uint256 _projectId, DefifaHook _nft,) = createDefifaProject(_launchData);
983
986
  // Wait until the phase 1 start
@@ -1236,7 +1239,8 @@ contract DefifaGovernorTest is JBTest, TestBaseWorkflow {
1236
1239
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
1237
1240
  terminal: jbMultiTerminal(),
1238
1241
  minParticipation: 0,
1239
- scorecardTimeout: 0
1242
+ scorecardTimeout: 0,
1243
+ timelockDuration: 0
1240
1244
  });
1241
1245
  }
1242
1246
 
@@ -383,7 +383,8 @@ contract DefifaHookRegressions is JBTest, TestBaseWorkflow {
383
383
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
384
384
  terminal: jbMultiTerminal(),
385
385
  minParticipation: 0,
386
- scorecardTimeout: 0
386
+ scorecardTimeout: 0,
387
+ timelockDuration: 0
387
388
  });
388
389
  }
389
390
 
@@ -270,7 +270,8 @@ contract DefifaMintCostInvariantTest is JBTest, TestBaseWorkflow {
270
270
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
271
271
  terminal: jbMultiTerminal(),
272
272
  minParticipation: 0,
273
- scorecardTimeout: 0
273
+ scorecardTimeout: 0,
274
+ timelockDuration: 0
274
275
  });
275
276
 
276
277
  uint256 pid = deployer.launchGameWith(d);
@@ -826,7 +826,8 @@ contract DefifaNoContestTest is JBTest, TestBaseWorkflow {
826
826
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
827
827
  terminal: jbMultiTerminal(),
828
828
  minParticipation: minParticipation,
829
- scorecardTimeout: scorecardTimeout
829
+ scorecardTimeout: scorecardTimeout,
830
+ timelockDuration: 0
830
831
  });
831
832
  }
832
833
 
@@ -886,7 +887,7 @@ contract DefifaNoContestTest is JBTest, TestBaseWorkflow {
886
887
  vm.warp(block.timestamp + _gov.attestationStartTimeOf(_gameId) + 1);
887
888
  for (uint256 i; i < _users.length; i++) {
888
889
  vm.prank(_users[i]);
889
- _gov.attestToScorecardFrom(_gameId, pid);
890
+ try _gov.attestToScorecardFrom(_gameId, pid) {} catch {}
890
891
  }
891
892
  vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
892
893
  }
@@ -219,20 +219,25 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
219
219
 
220
220
  // =========================================================================
221
221
  // ROUNDING: extreme weights at 1000 ETH per tier
222
+ // With BWA + HHI, highly concentrated scorecards on fewer than 4 tiers
223
+ // cannot reach quorum (total BWA = MAX*(n-1) < adjusted quorum for HHI~1).
224
+ // Using 5 tiers ensures total BWA (4*MAX) exceeds the adjusted quorum.
222
225
  // =========================================================================
223
226
  function testRounding_extremeWeights() external {
224
- _setupGame(3, 1000 ether);
227
+ _setupGame(5, 1000 ether);
225
228
  _toScoring();
226
229
 
227
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(3);
230
+ DefifaTierCashOutWeight[] memory sc = _buildScorecard(5);
228
231
  sc[0].cashOutWeight = 1;
229
- sc[1].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() - 2;
232
+ sc[1].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() - 4;
230
233
  sc[2].cashOutWeight = 1;
234
+ sc[3].cashOutWeight = 1;
235
+ sc[4].cashOutWeight = 1;
231
236
 
232
237
  _attestAndRatify(sc);
233
238
  uint256 pot = _surplus();
234
239
  uint256 out = _cashOutAllUsers();
235
- assertApproxEqAbs(out + _surplus(), pot, 3, "fund conservation");
240
+ assertApproxEqAbs(out + _surplus(), pot, 5, "fund conservation");
236
241
  assertGt(_users[1].balance, pot * 99 / 100, "tier 2 > 99%");
237
242
  }
238
243
 
@@ -287,7 +292,9 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
287
292
  // FUZZ: fund conservation across varying tier/player counts
288
293
  // =========================================================================
289
294
  function testFuzz_fundConservation(uint8 rawTiers, uint8 rawPlayers) external {
290
- uint8 nTiers = uint8(bound(rawTiers, 2, 12));
295
+ // Minimum 3 tiers: with BWA + HHI-adjusted quorum, 2-tier games with equal scorecards
296
+ // can never reach quorum (total BWA = MAX*(n-1) < adjusted quorum when n < 3).
297
+ uint8 nTiers = uint8(bound(rawTiers, 3, 12));
291
298
  uint8 nPpt = uint8(bound(rawPlayers, 1, 3));
292
299
 
293
300
  _setupMultiN(nTiers, nPpt, 1 ether);
@@ -342,13 +349,17 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
342
349
 
343
350
  // =========================================================================
344
351
  // C-D3: reserved minters get proportional fee tokens ($DEFIFA/$NANA)
352
+ // With BWA + HHI, 2-tier games cannot reach quorum (total BWA power for
353
+ // n tiers = MAX*(n-1) which is always less than HHI-adjusted quorum for n=2).
354
+ // We use 4 tiers with equal weight, all having reserveRate=1. This ensures
355
+ // enough attestation power from all participants to meet the adjusted quorum.
345
356
  // =========================================================================
346
357
  function testC_D3_reservedMintersGetFeeTokens() external {
347
- // Setup: 2 tiers, reservedRate=1 (1 reserve per paid mint), reserveBeneficiary = _reserveAddr
358
+ // Setup: 4 tiers, reservedRate=1, reserveBeneficiary = _reserveAddr
348
359
  address _reserveAddr = address(bytes20(keccak256("reserveBeneficiary")));
349
360
 
350
- DefifaTierParams[] memory tp = new DefifaTierParams[](2);
351
- for (uint256 i; i < 2; i++) {
361
+ DefifaTierParams[] memory tp = new DefifaTierParams[](4);
362
+ for (uint256 i; i < 4; i++) {
352
363
  tp[i] = DefifaTierParams({
353
364
  reservedRate: 1, // 1 reserve per 1 paid mint
354
365
  reservedTokenBeneficiary: _reserveAddr,
@@ -376,33 +387,34 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
376
387
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
377
388
  terminal: jbMultiTerminal(),
378
389
  minParticipation: 0,
379
- scorecardTimeout: 0
390
+ scorecardTimeout: 0,
391
+ timelockDuration: 0
380
392
  });
381
393
  (_pid, _nft, _gov) = _launch(d);
382
394
  vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
383
395
 
384
- // Paid mints: user0 mints tier 1, user1 mints tier 2
385
- _users = new address[](2);
386
- _users[0] = _addr(0);
387
- _users[1] = _addr(1);
388
- _mint(_users[0], 1, 1 ether);
389
- _delegateSelf(_users[0], 1);
390
- vm.warp(_tsReader.timestamp() + 1);
391
- _mint(_users[1], 2, 1 ether);
392
- _delegateSelf(_users[1], 2);
393
- vm.warp(_tsReader.timestamp() + 1);
396
+ // Paid mints: 1 user per tier
397
+ _users = new address[](4);
398
+ for (uint256 i; i < 4; i++) {
399
+ _users[i] = _addr(i);
400
+ _mint(_users[i], i + 1, 1 ether);
401
+ _delegateSelf(_users[i], i + 1);
402
+ vm.warp(_tsReader.timestamp() + 1);
403
+ }
394
404
 
395
405
  // Move to scoring phase (reserves can only be minted here)
396
406
  _toScoring();
397
407
 
398
408
  // Mint reserved tokens (1 per tier since reserveFrequency=1)
399
- JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](2);
409
+ JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](4);
400
410
  reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
401
411
  reserveConfigs[1] = JB721TiersMintReservesConfig({tierId: 2, count: 1});
412
+ reserveConfigs[2] = JB721TiersMintReservesConfig({tierId: 3, count: 1});
413
+ reserveConfigs[3] = JB721TiersMintReservesConfig({tierId: 4, count: 1});
402
414
  _nft.mintReservesFor(reserveConfigs);
403
415
 
404
- // Reserve beneficiary should hold 2 NFTs (mintReservesFor auto-delegates to self)
405
- assertEq(_nft.balanceOf(_reserveAddr), 2, "reserve beneficiary holds 2 NFTs");
416
+ // Reserve beneficiary should hold 4 NFTs (mintReservesFor auto-delegates to self)
417
+ assertEq(_nft.balanceOf(_reserveAddr), 4, "reserve beneficiary holds 4 NFTs");
406
418
 
407
419
  // Seed fee tokens into the hook (simulating protocol fee distribution)
408
420
  uint256 defifaAmount = 1000 ether;
@@ -410,29 +422,28 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
410
422
  deal(address(IERC20(_defifaProjectTokenAccount)), address(_nft), defifaAmount);
411
423
  deal(address(IERC20(_protocolFeeProjectTokenAccount)), address(_nft), nanaAmount);
412
424
 
413
- // Scorecard: equal weight
425
+ // Scorecard: equal weight across all 4 tiers
414
426
  uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
415
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(2);
416
- sc[0].cashOutWeight = tw / 2;
417
- sc[1].cashOutWeight = tw / 2;
418
-
419
- // Need _reserveAddr to attest too
420
- address[] memory allUsers = new address[](3);
421
- allUsers[0] = _users[0];
422
- allUsers[1] = _users[1];
423
- allUsers[2] = _reserveAddr;
424
-
427
+ DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
428
+ sc[0].cashOutWeight = tw / 4;
429
+ sc[1].cashOutWeight = tw / 4;
430
+ sc[2].cashOutWeight = tw / 4;
431
+ sc[3].cashOutWeight = tw / 4;
432
+
433
+ // Only paid minters attest -- reserve beneficiary has zero attestation weight at the
434
+ // snapshot (attestationsBegin - 1) because they received NFTs via reserve minting after
435
+ // the snapshot timestamp.
425
436
  uint256 pid = _gov.submitScorecardFor(_gameId, sc);
426
437
  vm.warp(_tsReader.timestamp() + _gov.attestationStartTimeOf(_gameId) + 1);
427
- for (uint256 i; i < allUsers.length; i++) {
428
- vm.prank(allUsers[i]);
438
+ for (uint256 i; i < _users.length; i++) {
439
+ vm.prank(_users[i]);
429
440
  _gov.attestToScorecardFrom(_gameId, pid);
430
441
  }
431
442
  vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
432
443
  _gov.ratifyScorecardFrom(_gameId, sc);
433
444
  vm.warp(_tsReader.timestamp() + 1);
434
445
 
435
- // Cash out paid minters
446
+ // Cash out paid minters from tiers 1 and 2
436
447
  _cashOut(_users[0], 1, 1);
437
448
  _cashOut(_users[1], 2, 1);
438
449
 
@@ -442,8 +453,7 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
442
453
  assertGt(user0Defifa, 0, "paid minter got DEFIFA tokens");
443
454
  assertGt(user0Nana, 0, "paid minter got NANA tokens");
444
455
 
445
- // Cash out reserved minter (tier 1, token #2 and tier 2, token #2)
446
- // Reserved tokens are the 2nd minted in each tier
456
+ // Cash out reserved minter's tokens from tiers 1 and 2 (token #2 in each tier)
447
457
  bytes memory meta1 = _cashOutMeta(1, 2);
448
458
  vm.prank(_reserveAddr);
449
459
  JBMultiTerminal(address(jbMultiTerminal()))
@@ -470,20 +480,17 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
470
480
  metadata: meta2
471
481
  });
472
482
 
473
- // Reserved minter should have gotten fee tokens too
483
+ // Reserved minter should have gotten fee tokens from tiers 1+2 cash-outs
474
484
  uint256 reserveDefifa = IERC20(_defifaProjectTokenAccount).balanceOf(_reserveAddr);
475
485
  uint256 reserveNana = IERC20(_protocolFeeProjectTokenAccount).balanceOf(_reserveAddr);
476
486
  assertGt(reserveDefifa, 0, "reserved minter got DEFIFA tokens");
477
487
  assertGt(reserveNana, 0, "reserved minter got NANA tokens");
478
488
 
479
- // All 4 tokens had equal tier.price (1 ether), so each should get 25% of fee tokens
480
- // (paid and reserved mints are treated equally in _totalMintCost)
489
+ // Each tier has 2 tokens (1 paid + 1 reserve), all at 1 ether.
490
+ // Paid minter (1 token in 1 tier) vs reserved minter (2 tokens in 2 tiers).
491
+ // Reserved minter gets 2x fee tokens relative to paid minter.
481
492
  assertApproxEqAbs(user0Defifa, reserveDefifa / 2, 1, "reserved gets 2x (2 tokens) vs paid (1 token)");
482
493
  assertApproxEqAbs(user0Nana, reserveNana / 2, 1, "NANA distribution matches");
483
-
484
- // All fee tokens distributed (none left in hook)
485
- assertEq(IERC20(_defifaProjectTokenAccount).balanceOf(address(_nft)), 0, "no DEFIFA left");
486
- assertEq(IERC20(_protocolFeeProjectTokenAccount).balanceOf(address(_nft)), 0, "no NANA left");
487
494
  }
488
495
 
489
496
  // =========================================================================
@@ -616,7 +623,8 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
616
623
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
617
624
  terminal: jbMultiTerminal(),
618
625
  minParticipation: 0,
619
- scorecardTimeout: 0
626
+ scorecardTimeout: 0,
627
+ timelockDuration: 0
620
628
  });
621
629
  }
622
630
 
@@ -672,7 +680,7 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
672
680
  vm.warp(block.timestamp + _gov.attestationStartTimeOf(_gameId) + 1);
673
681
  for (uint256 i; i < _users.length; i++) {
674
682
  vm.prank(_users[i]);
675
- _gov.attestToScorecardFrom(_gameId, pid);
683
+ try _gov.attestToScorecardFrom(_gameId, pid) {} catch {}
676
684
  }
677
685
  vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
678
686
  }
@@ -211,7 +211,8 @@ contract DefifaUSDCTest is JBTest, TestBaseWorkflow {
211
211
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
212
212
  terminal: jbMultiTerminal(),
213
213
  minParticipation: minParticipation,
214
- scorecardTimeout: scorecardTimeout
214
+ scorecardTimeout: scorecardTimeout,
215
+ timelockDuration: 0
215
216
  });
216
217
  }
217
218
 
@@ -277,7 +278,7 @@ contract DefifaUSDCTest is JBTest, TestBaseWorkflow {
277
278
  vm.warp((attestStart > current ? attestStart : current) + 1);
278
279
  for (uint256 i; i < _users.length; i++) {
279
280
  vm.prank(_users[i]);
280
- _gov.attestToScorecardFrom(_gameId, pid);
281
+ try _gov.attestToScorecardFrom(_gameId, pid) {} catch {}
281
282
  }
282
283
  vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
283
284
  _gov.ratifyScorecardFrom(_gameId, sc);
package/test/Fork.t.sol CHANGED
@@ -694,28 +694,36 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
694
694
  // =========================================================================
695
695
 
696
696
  function test_fork_singleTierGame() external {
697
- DefifaLaunchProjectData memory d = _launchData(1, 1 ether);
697
+ // BWA prevents a sole beneficiary (100% weight) from self-attesting.
698
+ // Use 2 tiers: tier 1 gets all weight, tier 2 provides neutral attestation.
699
+ DefifaLaunchProjectData memory d = _launchData(2, 1 ether);
698
700
  (_pid, _nft, _gov) = _launch(d);
699
701
  vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
700
702
 
701
- _users = new address[](1);
703
+ _users = new address[](2);
702
704
  _users[0] = _addr(0);
703
705
  _mint(_users[0], 1, 1 ether);
704
706
  _delegateSelf(_users[0], 1);
705
707
  vm.warp(_tsReader.timestamp() + 1);
706
708
 
709
+ _users[1] = _addr(1);
710
+ _mint(_users[1], 2, 1 ether);
711
+ _delegateSelf(_users[1], 2);
712
+ vm.warp(_tsReader.timestamp() + 1);
713
+
707
714
  _toScoring();
708
715
 
709
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(1);
716
+ DefifaTierCashOutWeight[] memory sc = _buildScorecard(2);
710
717
  sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
718
+ sc[1].cashOutWeight = 0;
711
719
 
712
720
  _attestAndRatify(sc);
713
721
 
714
- // Cash out the single player.
722
+ // Cash out the winner (tier 1).
715
723
  uint256 bb = _users[0].balance;
716
724
  _cashOut(_users[0], 1, 1);
717
725
  uint256 received = _users[0].balance - bb;
718
- assertGt(received, 0, "single player receives ETH");
726
+ assertGt(received, 0, "winner receives ETH");
719
727
  }
720
728
 
721
729
  // =========================================================================
@@ -969,7 +977,8 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
969
977
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
970
978
  terminal: jbMultiTerminal(),
971
979
  minParticipation: 0,
972
- scorecardTimeout: 0
980
+ scorecardTimeout: 0,
981
+ timelockDuration: 0
973
982
  });
974
983
  (_pid, _nft, _gov) = _launch(d);
975
984
  vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
@@ -1003,15 +1012,13 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
1003
1012
  sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 2;
1004
1013
  sc[1].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 2;
1005
1014
 
1006
- address[] memory allUsers = new address[](3);
1007
- allUsers[0] = _users[0];
1008
- allUsers[1] = _users[1];
1009
- allUsers[2] = reserveAddr;
1010
-
1015
+ // Only paid minters attest -- reserve beneficiary has zero attestation weight at the
1016
+ // snapshot (attestationsBegin - 1) because they received NFTs via reserve minting after
1017
+ // the snapshot timestamp.
1011
1018
  uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1012
1019
  vm.warp(_tsReader.timestamp() + _gov.attestationStartTimeOf(_gameId) + 1);
1013
- for (uint256 i; i < allUsers.length; i++) {
1014
- vm.prank(allUsers[i]);
1020
+ for (uint256 i; i < _users.length; i++) {
1021
+ vm.prank(_users[i]);
1015
1022
  _gov.attestToScorecardFrom(_gameId, pid);
1016
1023
  }
1017
1024
  vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
@@ -1354,15 +1361,10 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
1354
1361
  uint256 current = _tsReader.timestamp();
1355
1362
  vm.warp((attestStart > current ? attestStart : current) + 1);
1356
1363
 
1357
- // Non-holder attests — should succeed but add 0 weight.
1364
+ // Non-holder attests — should revert because BWA weight is 0.
1358
1365
  address stranger = _addr(999);
1359
1366
  vm.prank(stranger);
1360
- uint256 weight = _gov.attestToScorecardFrom(_gameId, pid);
1361
- assertEq(weight, 0, "non-holder has 0 attestation power");
1362
-
1363
- // But they can't attest again.
1364
- vm.prank(stranger);
1365
- vm.expectRevert(DefifaGovernor.DefifaGovernor_AlreadyAttested.selector);
1367
+ vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
1366
1368
  _gov.attestToScorecardFrom(_gameId, pid);
1367
1369
  }
1368
1370
 
@@ -1632,7 +1634,7 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
1632
1634
  uint256 expectedQuorum = (3 * _gov.MAX_ATTESTATION_POWER_TIER()) / 2;
1633
1635
  assertEq(q, expectedQuorum, "quorum = floor(3 * 1e9 / 2)");
1634
1636
 
1635
- // 2 of 3 tiers attesting should exceed quorum.
1637
+ // All 3 tiers attesting should exceed quorum (BWA reduces each holder's power by their tier share).
1636
1638
  DefifaTierCashOutWeight[] memory sc = _evenScorecard(3);
1637
1639
  uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1638
1640
 
@@ -1640,13 +1642,12 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
1640
1642
  uint256 current = _tsReader.timestamp();
1641
1643
  vm.warp((attestStart > current ? attestStart : current) + 1);
1642
1644
 
1643
- // Only users 0 and 1 attest (2 of 3 tiers).
1644
- vm.prank(_users[0]);
1645
- _gov.attestToScorecardFrom(_gameId, pid);
1646
- vm.prank(_users[1]);
1647
- _gov.attestToScorecardFrom(_gameId, pid);
1645
+ // All 3 users attest. BWA reduces each to ~2/3 power; 3 * 2/3 * MAX = 2*MAX > quorum.
1646
+ for (uint256 i; i < 3; i++) {
1647
+ vm.prank(_users[i]);
1648
+ _gov.attestToScorecardFrom(_gameId, pid);
1649
+ }
1648
1650
 
1649
- // 2e9 > 1.5e9 → quorum met.
1650
1651
  vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
1651
1652
  assertEq(uint256(_gov.stateOf(_gameId, pid)), uint256(DefifaScorecardState.SUCCEEDED));
1652
1653
  }
@@ -1866,7 +1867,8 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
1866
1867
  // =========================================================================
1867
1868
 
1868
1869
  function test_fork_fuzz_fundConservation(uint8 rawTiers, uint8 rawPlayers) external {
1869
- uint8 nTiers = uint8(bound(rawTiers, 2, 12));
1870
+ // N≥3 avoids BWA rounding shortfall with N=2 even split + multiple holders per tier.
1871
+ uint8 nTiers = uint8(bound(rawTiers, 3, 12));
1870
1872
  uint8 nPpt = uint8(bound(rawPlayers, 1, 3));
1871
1873
 
1872
1874
  _setupMultiN(nTiers, nPpt, 1 ether);
@@ -2068,7 +2070,8 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
2068
2070
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
2069
2071
  terminal: jbMultiTerminal(),
2070
2072
  minParticipation: 0,
2071
- scorecardTimeout: 0
2073
+ scorecardTimeout: 0,
2074
+ timelockDuration: 0
2072
2075
  });
2073
2076
  (_pid, _nft, _gov) = _launch(d);
2074
2077
 
@@ -2201,7 +2204,8 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
2201
2204
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
2202
2205
  terminal: jbMultiTerminal(),
2203
2206
  minParticipation: minParticipation,
2204
- scorecardTimeout: scorecardTimeout
2207
+ scorecardTimeout: scorecardTimeout,
2208
+ timelockDuration: 0
2205
2209
  });
2206
2210
  }
2207
2211
 
@@ -2233,7 +2237,8 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
2233
2237
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
2234
2238
  terminal: jbMultiTerminal(),
2235
2239
  minParticipation: 0,
2236
- scorecardTimeout: 0
2240
+ scorecardTimeout: 0,
2241
+ timelockDuration: 0
2237
2242
  });
2238
2243
  }
2239
2244
 
@@ -2319,7 +2324,7 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow {
2319
2324
  vm.warp((attestStart > current ? attestStart : current) + 1);
2320
2325
  for (uint256 i; i < _users.length; i++) {
2321
2326
  vm.prank(_users[i]);
2322
- _gov.attestToScorecardFrom(_gameId, pid);
2327
+ try _gov.attestToScorecardFrom(_gameId, pid) {} catch {}
2323
2328
  }
2324
2329
  vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
2325
2330
  }
@@ -210,7 +210,8 @@ contract TestAuditGapsERC20Games is JBTest, TestBaseWorkflow {
210
210
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
211
211
  terminal: jbMultiTerminal(),
212
212
  minParticipation: minParticipation,
213
- scorecardTimeout: scorecardTimeout
213
+ scorecardTimeout: scorecardTimeout,
214
+ timelockDuration: 0
214
215
  });
215
216
  }
216
217
 
@@ -276,7 +277,7 @@ contract TestAuditGapsERC20Games is JBTest, TestBaseWorkflow {
276
277
  vm.warp((attestStart > current ? attestStart : current) + 1);
277
278
  for (uint256 i; i < _users.length; i++) {
278
279
  vm.prank(_users[i]);
279
- _gov.attestToScorecardFrom(_gameId, pid);
280
+ try _gov.attestToScorecardFrom(_gameId, pid) {} catch {}
280
281
  }
281
282
  vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
282
283
  _gov.ratifyScorecardFrom(_gameId, sc);
@@ -656,7 +657,8 @@ contract TestAuditGapsMultiGameIsolation is JBTest, TestBaseWorkflow {
656
657
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
657
658
  terminal: jbMultiTerminal(),
658
659
  minParticipation: 0,
659
- scorecardTimeout: 0
660
+ scorecardTimeout: 0,
661
+ timelockDuration: 0
660
662
  });
661
663
  }
662
664
 
@@ -782,7 +784,7 @@ contract TestAuditGapsMultiGameIsolation is JBTest, TestBaseWorkflow {
782
784
  vm.warp((attestStart > current ? attestStart : current) + 1);
783
785
  for (uint256 i; i < users.length; i++) {
784
786
  vm.prank(users[i]);
785
- governor.attestToScorecardFrom(gameId, pid);
787
+ try governor.attestToScorecardFrom(gameId, pid) {} catch {}
786
788
  }
787
789
  vm.warp(_tsReader.timestamp() + governor.attestationGracePeriodOf(gameId) + 1);
788
790
  governor.ratifyScorecardFrom(gameId, sc);
@@ -195,7 +195,8 @@ contract TestQACashOutDoSDuringFulfillmentWindow is JBTest, TestBaseWorkflow {
195
195
 
196
196
  uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
197
197
  vm.warp(_tsReader.timestamp() + _governor.attestationStartTimeOf(_gameId) + 1);
198
- for (uint256 i = 0; i < _users.length; i++) {
198
+ // Start from i=1: user 0 holds tier 1 which gets 100% weight, so BWA reduces their power to 0.
199
+ for (uint256 i = 1; i < _users.length; i++) {
199
200
  vm.prank(_users[i]);
200
201
  _governor.attestToScorecardFrom(_gameId, _proposalId);
201
202
  }
@@ -283,7 +284,8 @@ contract TestQACashOutDoSDuringFulfillmentWindow is JBTest, TestBaseWorkflow {
283
284
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
284
285
  terminal: jbMultiTerminal(),
285
286
  minParticipation: 0,
286
- scorecardTimeout: 0
287
+ scorecardTimeout: 0,
288
+ timelockDuration: 0
287
289
  });
288
290
  }
289
291
 
@@ -505,7 +507,8 @@ contract TestQAGameIdPredictionRace is JBTest, TestBaseWorkflow {
505
507
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
506
508
  terminal: jbMultiTerminal(),
507
509
  minParticipation: 0,
508
- scorecardTimeout: 0
510
+ scorecardTimeout: 0,
511
+ timelockDuration: 0
509
512
  });
510
513
  }
511
514
  }
@@ -30,7 +30,7 @@ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
30
30
  import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
31
31
  import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
32
32
 
33
- contract CodexAttestationDoubleCount is JBTest, TestBaseWorkflow {
33
+ contract AttestationDoubleCountTest is JBTest, TestBaseWorkflow {
34
34
  using JBRulesetMetadataResolver for JBRuleset;
35
35
 
36
36
  uint256 internal _protocolFeeProjectId;
@@ -193,7 +193,8 @@ contract CodexAttestationDoubleCount is JBTest, TestBaseWorkflow {
193
193
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
194
194
  terminal: jbMultiTerminal(),
195
195
  minParticipation: 0,
196
- scorecardTimeout: 0
196
+ scorecardTimeout: 0,
197
+ timelockDuration: 0
197
198
  });
198
199
 
199
200
  _mintPhaseStart = d.start - d.mintPeriodDuration - d.refundPeriodDuration;