@bananapus/distributor-v6 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol";
5
5
 
6
6
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7
7
  import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
8
- import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
9
8
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
10
9
  import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
11
10
 
@@ -201,7 +200,7 @@ contract JB721DistributorTest is Test {
201
200
  address charlie = makeAddr("charlie");
202
201
 
203
202
  uint256 constant PROJECT_ID = 1;
204
- uint256 constant ROUND_DURATION = 100;
203
+ uint256 constant ROUND_DURATION = 100; // 100 seconds per round.
205
204
  uint256 constant VESTING_ROUNDS = 4;
206
205
  uint256 constant MAX_SHARE = 100_000;
207
206
 
@@ -274,6 +273,53 @@ contract JB721DistributorTest is Test {
274
273
  hook.setOwner(2, bob);
275
274
  }
276
275
 
276
+ // =====================================================================
277
+ // Helpers
278
+ // =====================================================================
279
+
280
+ /// @notice Advance to 1 second after the start of the given round, and advance block number too.
281
+ function _advanceToRound(uint256 round) internal {
282
+ uint256 targetTimestamp = distributor.roundStartTimestamp(round) + 1;
283
+ if (block.timestamp < targetTimestamp) {
284
+ vm.warp(targetTimestamp);
285
+ }
286
+ // Also advance block number so getPastVotes works with past blocks.
287
+ vm.roll(block.number + 1);
288
+ }
289
+
290
+ function _fundHook(uint256 amount) internal {
291
+ rewardToken.mint(address(this), amount);
292
+ rewardToken.approve(address(distributor), amount);
293
+ distributor.fund(address(hook), IERC20(address(rewardToken)), amount);
294
+ }
295
+
296
+ function _beginVestingBoth() internal {
297
+ uint256[] memory tokenIds = new uint256[](2);
298
+ tokenIds[0] = 1;
299
+ tokenIds[1] = 2;
300
+ IERC20[] memory tokens = new IERC20[](1);
301
+ tokens[0] = IERC20(address(rewardToken));
302
+ distributor.beginVesting(address(hook), tokenIds, tokens);
303
+ }
304
+
305
+ function _splitContext(address token, uint256 amount) internal view returns (JBSplitHookContext memory) {
306
+ return JBSplitHookContext({
307
+ token: token,
308
+ amount: amount,
309
+ decimals: 18,
310
+ projectId: 1,
311
+ groupId: uint256(uint160(token)),
312
+ split: JBSplit({
313
+ percent: 500_000_000, // 50%
314
+ projectId: 0,
315
+ beneficiary: payable(address(hook)), // hook address as beneficiary
316
+ preferAddToBalance: false,
317
+ lockedUntil: 0,
318
+ hook: IJBSplitHook(address(distributor))
319
+ })
320
+ });
321
+ }
322
+
277
323
  // =====================================================================
278
324
  // Constructor
279
325
  // =====================================================================
@@ -281,7 +327,7 @@ contract JB721DistributorTest is Test {
281
327
  function test_constructor() public view {
282
328
  assertEq(distributor.roundDuration(), ROUND_DURATION);
283
329
  assertEq(distributor.vestingRounds(), VESTING_ROUNDS);
284
- assertEq(distributor.startingBlock(), block.number);
330
+ assertEq(distributor.startingTimestamp(), block.timestamp);
285
331
  assertEq(distributor.MAX_SHARE(), MAX_SHARE);
286
332
  }
287
333
 
@@ -293,18 +339,18 @@ contract JB721DistributorTest is Test {
293
339
  assertEq(distributor.currentRound(), 0);
294
340
  }
295
341
 
296
- function test_currentRound_afterRolling() public {
297
- vm.roll(block.number + ROUND_DURATION);
342
+ function test_currentRound_afterWarping() public {
343
+ vm.warp(block.timestamp + ROUND_DURATION);
298
344
  assertEq(distributor.currentRound(), 1);
299
345
 
300
- vm.roll(block.number + ROUND_DURATION * 3);
346
+ vm.warp(block.timestamp + ROUND_DURATION * 3);
301
347
  assertEq(distributor.currentRound(), 4);
302
348
  }
303
349
 
304
- function test_roundStartBlock() public view {
305
- assertEq(distributor.roundStartBlock(0), distributor.startingBlock());
306
- assertEq(distributor.roundStartBlock(1), distributor.startingBlock() + ROUND_DURATION);
307
- assertEq(distributor.roundStartBlock(5), distributor.startingBlock() + ROUND_DURATION * 5);
350
+ function test_roundStartTimestamp() public view {
351
+ assertEq(distributor.roundStartTimestamp(0), distributor.startingTimestamp());
352
+ assertEq(distributor.roundStartTimestamp(1), distributor.startingTimestamp() + ROUND_DURATION);
353
+ assertEq(distributor.roundStartTimestamp(5), distributor.startingTimestamp() + ROUND_DURATION * 5);
308
354
  }
309
355
 
310
356
  function test_claimedFor_beforeVesting() public view {
@@ -336,7 +382,7 @@ contract JB721DistributorTest is Test {
336
382
  _fundHook(1000 ether);
337
383
  _beginVestingBoth();
338
384
 
339
- vm.roll(block.number + ROUND_DURATION * 2);
385
+ _advanceToRound(2);
340
386
 
341
387
  // lockedShare = (4-2)*100000/4 = 50000.
342
388
  // collectableFor = mulDiv(250e18, 100000 - 0 - 50000, 100000) = 125e18.
@@ -347,7 +393,7 @@ contract JB721DistributorTest is Test {
347
393
  _fundHook(1000 ether);
348
394
  _beginVestingBoth();
349
395
 
350
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
396
+ _advanceToRound(VESTING_ROUNDS);
351
397
 
352
398
  // lockedShare = 0. collectableFor = 250e18.
353
399
  assertEq(distributor.collectableFor(address(hook), 1, IERC20(address(rewardToken))), 250 ether);
@@ -388,7 +434,7 @@ contract JB721DistributorTest is Test {
388
434
  assertEq(distributor.latestVestedIndexOf(address(hook), 1, IERC20(address(rewardToken))), 0);
389
435
 
390
436
  // Collect fully -> latestVestedIndex should advance.
391
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
437
+ _advanceToRound(VESTING_ROUNDS);
392
438
  vm.prank(alice);
393
439
  uint256[] memory tokenIds = new uint256[](1);
394
440
  tokenIds[0] = 1;
@@ -447,7 +493,7 @@ contract JB721DistributorTest is Test {
447
493
  assertEq(distributor.claimedFor(address(hook), 1, nativeToken), 25 ether);
448
494
 
449
495
  // Advance past full vesting.
450
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
496
+ _advanceToRound(VESTING_ROUNDS);
451
497
 
452
498
  // Alice collects.
453
499
  uint256 aliceBalBefore = alice.balance;
@@ -484,7 +530,7 @@ contract JB721DistributorTest is Test {
484
530
  assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 250 ether);
485
531
  }
486
532
 
487
- function test_beginVesting_alreadyVesting_reverts() public {
533
+ function test_beginVesting_alreadyVesting_skips() public {
488
534
  _fundHook(1000 ether);
489
535
 
490
536
  uint256[] memory tokenIds = new uint256[](1);
@@ -494,8 +540,11 @@ contract JB721DistributorTest is Test {
494
540
 
495
541
  distributor.beginVesting(address(hook), tokenIds, tokens);
496
542
 
497
- vm.expectRevert(JBDistributor.JBDistributor_AlreadyVesting.selector);
543
+ // Second call in same round should silently skip (not revert).
498
544
  distributor.beginVesting(address(hook), tokenIds, tokens);
545
+
546
+ // Only one vesting entry should exist.
547
+ assertEq(distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken))), 250 ether);
499
548
  }
500
549
 
501
550
  function test_beginVesting_nextRound_succeeds() public {
@@ -509,7 +558,7 @@ contract JB721DistributorTest is Test {
509
558
  distributor.beginVesting(address(hook), tokenIds, tokens);
510
559
 
511
560
  // Move to next round, add more funds, should succeed.
512
- vm.roll(block.number + ROUND_DURATION);
561
+ _advanceToRound(1);
513
562
  _fundHook(500 ether);
514
563
  distributor.beginVesting(address(hook), tokenIds, tokens);
515
564
 
@@ -557,6 +606,43 @@ contract JB721DistributorTest is Test {
557
606
  distributor.beginVesting(address(hook), tokenIds, tokens);
558
607
  }
559
608
 
609
+ // =====================================================================
610
+ // poke
611
+ // =====================================================================
612
+
613
+ function test_poke_recordsSnapshotBlock() public {
614
+ _advanceToRound(1);
615
+
616
+ uint256 expectedBlock = block.number - 1;
617
+ distributor.poke();
618
+
619
+ assertEq(distributor.roundSnapshotBlock(1), expectedBlock);
620
+ }
621
+
622
+ function test_poke_emitsEvent() public {
623
+ _advanceToRound(1);
624
+
625
+ vm.expectEmit(true, false, false, true);
626
+ emit IJBDistributor.RoundSnapshotRecorded(1, block.number - 1);
627
+
628
+ distributor.poke();
629
+ }
630
+
631
+ function test_poke_idempotent() public {
632
+ _advanceToRound(1);
633
+
634
+ distributor.poke();
635
+ uint256 firstSnapshot = distributor.roundSnapshotBlock(1);
636
+
637
+ // Advance block but stay in same round.
638
+ vm.roll(block.number + 10);
639
+
640
+ distributor.poke();
641
+ uint256 secondSnapshot = distributor.roundSnapshotBlock(1);
642
+
643
+ assertEq(firstSnapshot, secondSnapshot, "Poke should be idempotent within a round");
644
+ }
645
+
560
646
  // =====================================================================
561
647
  // collectVestedRewards -- exact value assertions
562
648
  // =====================================================================
@@ -571,13 +657,15 @@ contract JB721DistributorTest is Test {
571
657
 
572
658
  distributor.beginVesting(address(hook), tokenIds, tokens);
573
659
 
574
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
660
+ _advanceToRound(VESTING_ROUNDS);
575
661
 
576
662
  vm.prank(alice);
577
663
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
578
664
 
579
665
  assertEq(rewardToken.balanceOf(alice), 250 ether);
580
- assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 0);
666
+ // Auto-vest during collect created a new entry (25% of 750 undistributed = 187.5 ether).
667
+ // That entry is fully locked at collection time and remains in totalVesting.
668
+ assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 187.5 ether);
581
669
  }
582
670
 
583
671
  function test_collectVestedRewards_partialVesting_exactAmounts() public {
@@ -590,24 +678,27 @@ contract JB721DistributorTest is Test {
590
678
 
591
679
  distributor.beginVesting(address(hook), tokenIds, tokens);
592
680
 
593
- // Roll forward 2 of 4 rounds (50%).
594
- vm.roll(block.number + ROUND_DURATION * 2);
681
+ // Warp forward 2 of 4 rounds (50%).
682
+ _advanceToRound(2);
595
683
 
596
684
  vm.prank(alice);
597
685
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
598
686
 
599
- // lockedShare = (4-2)*100000/4 = 50000. claimAmount = mulDiv(250e18, 50000, 100000) = 125e18.
687
+ // Entry 0 (250e18, release=4): 50% unlocked 125 ether.
688
+ // Auto-vest entry (187.5e18, release=6): 100% locked → 0.
600
689
  assertEq(rewardToken.balanceOf(alice), 125 ether);
601
690
 
602
- // Roll forward remaining 2 rounds.
603
- vm.roll(block.number + ROUND_DURATION * 2);
691
+ // Warp forward remaining 2 rounds.
692
+ _advanceToRound(VESTING_ROUNDS);
604
693
 
605
694
  vm.prank(alice);
606
695
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
607
696
 
608
- // After partial: shareClaimed = 50000, lockedShare = 0.
609
- // claimAmount = mulDiv(250e18, 100000-50000, 100000) = 125e18.
610
- assertEq(rewardToken.balanceOf(alice), 250 ether);
697
+ // Entry 0 remaining: 125 ether.
698
+ // Entry 1 (187.5e18, release=6): 50% unlocked at round 4 → 93.75 ether.
699
+ // Entry 2 (auto-vest round 4, release=8): 100% locked → 0.
700
+ // Total: 125 + 125 + 93.75 = 343.75.
701
+ assertEq(rewardToken.balanceOf(alice), 343.75 ether);
611
702
  }
612
703
 
613
704
  function test_collectVestedRewards_quarterVesting() public {
@@ -620,8 +711,8 @@ contract JB721DistributorTest is Test {
620
711
 
621
712
  distributor.beginVesting(address(hook), tokenIds, tokens);
622
713
 
623
- // Roll forward 1 of 4 rounds (25%).
624
- vm.roll(block.number + ROUND_DURATION);
714
+ // Warp forward 1 of 4 rounds (25%).
715
+ _advanceToRound(1);
625
716
 
626
717
  vm.prank(alice);
627
718
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
@@ -640,8 +731,8 @@ contract JB721DistributorTest is Test {
640
731
 
641
732
  distributor.beginVesting(address(hook), tokenIds, tokens);
642
733
 
643
- // Roll forward 3 of 4 rounds (75%).
644
- vm.roll(block.number + ROUND_DURATION * 3);
734
+ // Warp forward 3 of 4 rounds (75%).
735
+ _advanceToRound(3);
645
736
 
646
737
  vm.prank(alice);
647
738
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
@@ -659,7 +750,7 @@ contract JB721DistributorTest is Test {
659
750
  tokens[0] = IERC20(address(rewardToken));
660
751
 
661
752
  distributor.beginVesting(address(hook), tokenIds, tokens);
662
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
753
+ _advanceToRound(VESTING_ROUNDS);
663
754
 
664
755
  vm.prank(bob);
665
756
  vm.expectRevert(JBDistributor.JBDistributor_NoAccess.selector);
@@ -688,7 +779,7 @@ contract JB721DistributorTest is Test {
688
779
  tokens[0] = IERC20(address(rewardToken));
689
780
 
690
781
  distributor.beginVesting(address(hook), tokenIds, tokens);
691
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
782
+ _advanceToRound(VESTING_ROUNDS);
692
783
 
693
784
  vm.expectEmit(true, true, false, true);
694
785
  emit IJBDistributor.Collected(address(hook), 1, IERC20(address(rewardToken)), 250 ether, VESTING_ROUNDS);
@@ -706,7 +797,7 @@ contract JB721DistributorTest is Test {
706
797
  tokens[0] = IERC20(address(rewardToken));
707
798
 
708
799
  distributor.beginVesting(address(hook), tokenIds, tokens);
709
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
800
+ _advanceToRound(VESTING_ROUNDS);
710
801
 
711
802
  // Alice sends to charlie.
712
803
  vm.prank(alice);
@@ -716,6 +807,48 @@ contract JB721DistributorTest is Test {
716
807
  assertEq(rewardToken.balanceOf(alice), 0);
717
808
  }
718
809
 
810
+ // =====================================================================
811
+ // Auto-vest on collect
812
+ // =====================================================================
813
+
814
+ function test_autoVest_collectWithoutBeginVesting() public {
815
+ _fundHook(1000 ether);
816
+
817
+ // Advance past full vesting WITHOUT calling beginVesting.
818
+ _advanceToRound(VESTING_ROUNDS);
819
+
820
+ // Alice calls collectVestedRewards directly — auto-vest should create a vesting entry.
821
+ uint256[] memory tokenIds = new uint256[](1);
822
+ tokenIds[0] = 1;
823
+ IERC20[] memory tokens = new IERC20[](1);
824
+ tokens[0] = IERC20(address(rewardToken));
825
+
826
+ vm.prank(alice);
827
+ distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
828
+
829
+ // Auto-vest happened for the current round. Since it just started vesting, nothing is unlocked yet.
830
+ uint256 claimed = distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken)));
831
+ assertEq(claimed, 250 ether, "Auto-vest should have created a vesting entry");
832
+ }
833
+
834
+ function test_autoVest_doubleCollectInSameRound_noRevert() public {
835
+ _fundHook(1000 ether);
836
+ _advanceToRound(1);
837
+
838
+ uint256[] memory tokenIds = new uint256[](1);
839
+ tokenIds[0] = 1;
840
+ IERC20[] memory tokens = new IERC20[](1);
841
+ tokens[0] = IERC20(address(rewardToken));
842
+
843
+ // First collect auto-vests.
844
+ vm.prank(alice);
845
+ distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
846
+
847
+ // Second collect in same round should not revert (skips auto-vest since already vested).
848
+ vm.prank(alice);
849
+ distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
850
+ }
851
+
719
852
  // =====================================================================
720
853
  // releaseForfeitedRewards
721
854
  // =====================================================================
@@ -732,7 +865,7 @@ contract JB721DistributorTest is Test {
732
865
 
733
866
  assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 250 ether);
734
867
 
735
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
868
+ _advanceToRound(VESTING_ROUNDS);
736
869
  hook.burn(1);
737
870
 
738
871
  distributor.releaseForfeitedRewards(address(hook), tokenIds, tokens, alice);
@@ -752,8 +885,8 @@ contract JB721DistributorTest is Test {
752
885
 
753
886
  distributor.beginVesting(address(hook), tokenIds, tokens);
754
887
 
755
- // Roll forward 2 of 4 rounds.
756
- vm.roll(block.number + ROUND_DURATION * 2);
888
+ // Warp forward 2 of 4 rounds.
889
+ _advanceToRound(2);
757
890
  hook.burn(1);
758
891
 
759
892
  distributor.releaseForfeitedRewards(address(hook), tokenIds, tokens, alice);
@@ -784,7 +917,7 @@ contract JB721DistributorTest is Test {
784
917
  assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 500 ether);
785
918
 
786
919
  // Next round should work fine — no underflow.
787
- vm.roll(block.number + ROUND_DURATION);
920
+ _advanceToRound(1);
788
921
  _fundHook(1 ether);
789
922
  distributor.beginVesting(address(hook), tokenIds, tokens);
790
923
  }
@@ -844,7 +977,7 @@ contract JB721DistributorTest is Test {
844
977
  assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 250 ether);
845
978
  assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken2))), 125 ether);
846
979
 
847
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
980
+ _advanceToRound(VESTING_ROUNDS);
848
981
 
849
982
  vm.prank(alice);
850
983
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
@@ -870,7 +1003,7 @@ contract JB721DistributorTest is Test {
870
1003
  // Token 1 gets 250e18 from round 0.
871
1004
 
872
1005
  // Move to round 1: fund 400 more and vest.
873
- vm.roll(block.number + ROUND_DURATION);
1006
+ _advanceToRound(1);
874
1007
  _fundHook(400 ether);
875
1008
  distributor.beginVesting(address(hook), tokenIds, tokens);
876
1009
 
@@ -879,7 +1012,7 @@ contract JB721DistributorTest is Test {
879
1012
  // Token 1 gets mulDiv(1150e18, 100, 400) = 287.5e18 from round 1.
880
1013
 
881
1014
  // Move past all vesting.
882
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1015
+ _advanceToRound(1 + VESTING_ROUNDS);
883
1016
 
884
1017
  vm.prank(alice);
885
1018
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
@@ -934,7 +1067,7 @@ contract JB721DistributorTest is Test {
934
1067
  );
935
1068
 
936
1069
  // After partial collect, still holds.
937
- vm.roll(block.number + ROUND_DURATION * 2);
1070
+ _advanceToRound(2);
938
1071
  vm.prank(alice);
939
1072
  uint256[] memory tokenIds = new uint256[](1);
940
1073
  tokenIds[0] = 1;
@@ -952,7 +1085,7 @@ contract JB721DistributorTest is Test {
952
1085
  _fundHook(1000 ether);
953
1086
  _beginVestingBoth();
954
1087
 
955
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1088
+ _advanceToRound(VESTING_ROUNDS);
956
1089
 
957
1090
  // Collect both.
958
1091
  uint256[] memory aliceIds = new uint256[](1);
@@ -969,7 +1102,8 @@ contract JB721DistributorTest is Test {
969
1102
 
970
1103
  assertEq(rewardToken.balanceOf(alice), 250 ether);
971
1104
  assertEq(rewardToken.balanceOf(bob), 500 ether);
972
- assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 0);
1105
+ // Auto-vest during collect created new entries: 62.5 (alice) + 125 (bob) = 187.5 ether.
1106
+ assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 187.5 ether);
973
1107
  // 250 ether remains undistributed (remaining 25% of stake not claimed by any token).
974
1108
  assertEq(distributor.balanceOf(address(hook), IERC20(address(rewardToken))), 250 ether);
975
1109
  }
@@ -1012,7 +1146,7 @@ contract JB721DistributorTest is Test {
1012
1146
 
1013
1147
  uint256 totalClaimed = distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken)));
1014
1148
 
1015
- vm.roll(block.number + ROUND_DURATION * rounds);
1149
+ _advanceToRound(rounds);
1016
1150
 
1017
1151
  vm.prank(alice);
1018
1152
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
@@ -1039,7 +1173,7 @@ contract JB721DistributorTest is Test {
1039
1173
 
1040
1174
  uint256 totalClaimed = distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken)));
1041
1175
 
1042
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1176
+ _advanceToRound(VESTING_ROUNDS);
1043
1177
 
1044
1178
  vm.prank(alice);
1045
1179
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
@@ -1061,7 +1195,7 @@ contract JB721DistributorTest is Test {
1061
1195
 
1062
1196
  distributor.beginVesting(address(hook), tokenIds, tokens);
1063
1197
 
1064
- vm.roll(block.number + ROUND_DURATION * 2);
1198
+ _advanceToRound(2);
1065
1199
 
1066
1200
  // Check collectableFor equals what we actually collect.
1067
1201
  uint256 collectable = distributor.collectableFor(address(hook), 1, IERC20(address(rewardToken)));
@@ -1089,13 +1223,14 @@ contract JB721DistributorTest is Test {
1089
1223
 
1090
1224
  // Collect at each round.
1091
1225
  for (uint256 r = 1; r <= VESTING_ROUNDS; r++) {
1092
- vm.roll(block.number + ROUND_DURATION);
1226
+ _advanceToRound(r);
1093
1227
  vm.prank(alice);
1094
1228
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
1095
1229
  }
1096
1230
 
1097
- // Sum of all partial collects should equal total claimed (within rounding).
1098
- assertApproxEqAbs(rewardToken.balanceOf(alice), totalClaimed, VESTING_ROUNDS);
1231
+ // Alice receives at least totalClaimed from the original vest.
1232
+ // Auto-vest during each collect redistributes from the undistributed pool, adding more.
1233
+ assertGe(rewardToken.balanceOf(alice), totalClaimed);
1099
1234
  }
1100
1235
 
1101
1236
  // =====================================================================
@@ -1143,9 +1278,9 @@ contract JB721DistributorTest is Test {
1143
1278
  IERC20[] memory tokens = new IERC20[](1);
1144
1279
  tokens[0] = IERC20(address(rewardToken));
1145
1280
 
1146
- // Should succeed with no-op for inner loop.
1281
+ // Should revert when no token IDs are provided.
1282
+ vm.expectRevert(JBDistributor.JBDistributor_EmptyTokenIds.selector);
1147
1283
  distributor.beginVesting(address(hook), tokenIds, tokens);
1148
- assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 0);
1149
1284
  }
1150
1285
 
1151
1286
  function test_beginVesting_emptyTokens() public {
@@ -1161,6 +1296,8 @@ contract JB721DistributorTest is Test {
1161
1296
  uint256[] memory tokenIds = new uint256[](0);
1162
1297
  IERC20[] memory tokens = new IERC20[](0);
1163
1298
 
1299
+ // Should revert when no token IDs are provided.
1300
+ vm.expectRevert(JBDistributor.JBDistributor_EmptyTokenIds.selector);
1164
1301
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
1165
1302
  }
1166
1303
 
@@ -1177,7 +1314,7 @@ contract JB721DistributorTest is Test {
1177
1314
  tokens[0] = IERC20(address(rewardToken));
1178
1315
 
1179
1316
  distributor.beginVesting(address(hook), tokenIds, tokens);
1180
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1317
+ _advanceToRound(VESTING_ROUNDS);
1181
1318
 
1182
1319
  vm.prank(alice);
1183
1320
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
@@ -1205,7 +1342,7 @@ contract JB721DistributorTest is Test {
1205
1342
  tokens[0] = IERC20(address(rewardToken));
1206
1343
 
1207
1344
  distributor.beginVesting(address(hook), tokenIds, tokens);
1208
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1345
+ _advanceToRound(VESTING_ROUNDS);
1209
1346
 
1210
1347
  // Transfer token 1 from alice to charlie.
1211
1348
  hook.setOwner(1, charlie);
@@ -1236,27 +1373,28 @@ contract JB721DistributorTest is Test {
1236
1373
  distributor.beginVesting(address(hook), tokenIds, tokens);
1237
1374
 
1238
1375
  // Round 1: vest
1239
- vm.roll(block.number + ROUND_DURATION);
1376
+ _advanceToRound(1);
1240
1377
  _fundHook(400 ether);
1241
1378
  distributor.beginVesting(address(hook), tokenIds, tokens);
1242
1379
 
1243
1380
  // Round 2: vest
1244
- vm.roll(block.number + ROUND_DURATION);
1381
+ _advanceToRound(2);
1245
1382
  _fundHook(200 ether);
1246
1383
  distributor.beginVesting(address(hook), tokenIds, tokens);
1247
1384
 
1248
- // Roll past all vesting (round 2 + 4 = round 6 releases last entry).
1249
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1385
+ // Warp past all vesting (round 2 + 4 = round 6 releases last entry).
1386
+ _advanceToRound(2 + VESTING_ROUNDS);
1250
1387
 
1251
1388
  uint256 totalClaimed = distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken)));
1252
1389
 
1253
1390
  vm.prank(alice);
1254
1391
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
1255
1392
 
1256
- // Should collect all three entries.
1393
+ // Should collect all three entries (auto-vest entry is 100% locked, contributes 0).
1257
1394
  assertEq(rewardToken.balanceOf(alice), totalClaimed);
1258
1395
  assertEq(distributor.latestVestedIndexOf(address(hook), 1, IERC20(address(rewardToken))), 3);
1259
- assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 0);
1396
+ // Auto-vest during collect created a new entry from the undistributed pool.
1397
+ assertGt(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 0);
1260
1398
  }
1261
1399
 
1262
1400
  function test_threeVestingEntries_partialCollect_skipsLockedEntries() public {
@@ -1271,12 +1409,12 @@ contract JB721DistributorTest is Test {
1271
1409
  uint256 claimed0 = distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken)));
1272
1410
 
1273
1411
  // Round 1: vest -> releaseRound = 5
1274
- vm.roll(block.number + ROUND_DURATION);
1412
+ _advanceToRound(1);
1275
1413
  _fundHook(400 ether);
1276
1414
  distributor.beginVesting(address(hook), tokenIds, tokens);
1277
1415
 
1278
1416
  // Round 2: vest -> releaseRound = 6
1279
- vm.roll(block.number + ROUND_DURATION);
1417
+ _advanceToRound(2);
1280
1418
  _fundHook(200 ether);
1281
1419
  distributor.beginVesting(address(hook), tokenIds, tokens);
1282
1420
 
@@ -1284,20 +1422,17 @@ contract JB721DistributorTest is Test {
1284
1422
  uint256 totalClaimedBefore = distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken)));
1285
1423
 
1286
1424
  // Collect at round 4: entry[0] fully vested, entry[1] partially, entry[2] more locked.
1287
- vm.roll(block.number + ROUND_DURATION * 2); // now at round 4
1425
+ _advanceToRound(4);
1288
1426
 
1289
1427
  vm.prank(alice);
1290
1428
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
1291
1429
 
1292
- // Entry[0] fully collected. Due to loop behavior, entry[2] is skipped this round
1293
- // but will be caught in a future collect. No funds are lost.
1294
1430
  uint256 firstCollect = rewardToken.balanceOf(alice);
1295
1431
  assertGt(firstCollect, 0);
1296
- // At minimum, entry[0]'s full amount should be collected.
1297
1432
  assertGe(firstCollect, claimed0);
1298
1433
 
1299
1434
  // Collect at round 5: entry[1] fully vests, entry[2] partially.
1300
- vm.roll(block.number + ROUND_DURATION);
1435
+ _advanceToRound(5);
1301
1436
  vm.prank(alice);
1302
1437
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
1303
1438
 
@@ -1305,17 +1440,14 @@ contract JB721DistributorTest is Test {
1305
1440
  assertGt(secondCollect, firstCollect);
1306
1441
 
1307
1442
  // Collect at round 6: entry[2] fully vests.
1308
- vm.roll(block.number + ROUND_DURATION);
1443
+ _advanceToRound(6);
1309
1444
  vm.prank(alice);
1310
1445
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
1311
1446
 
1312
- // All entries should have been fully collected across the three collect calls.
1313
- // claimedFor returns 0 now because latestVestedIndex has advanced past all entries.
1314
- assertEq(distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken))), 0);
1315
- assertEq(distributor.latestVestedIndexOf(address(hook), 1, IERC20(address(rewardToken))), 3);
1316
- // Total received across all collects should equal what was originally claimed.
1317
- assertEq(rewardToken.balanceOf(alice), totalClaimedBefore);
1318
- assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 0);
1447
+ // Alice received at least her original three entries (auto-vest adds more from undistributed pool).
1448
+ assertGe(rewardToken.balanceOf(alice), totalClaimedBefore);
1449
+ // Original three entries are fully exhausted; auto-vest entries may have remaining amounts.
1450
+ assertGe(distributor.latestVestedIndexOf(address(hook), 1, IERC20(address(rewardToken))), 3);
1319
1451
  }
1320
1452
 
1321
1453
  /// @notice collectVestedRewards now matches collectableFor even with multiple stacked entries.
@@ -1328,15 +1460,15 @@ contract JB721DistributorTest is Test {
1328
1460
  _fundHook(1000 ether);
1329
1461
  distributor.beginVesting(address(hook), tokenIds, tokens);
1330
1462
 
1331
- vm.roll(block.number + ROUND_DURATION);
1463
+ _advanceToRound(1);
1332
1464
  _fundHook(400 ether);
1333
1465
  distributor.beginVesting(address(hook), tokenIds, tokens);
1334
1466
 
1335
- vm.roll(block.number + ROUND_DURATION);
1467
+ _advanceToRound(2);
1336
1468
  _fundHook(200 ether);
1337
1469
  distributor.beginVesting(address(hook), tokenIds, tokens);
1338
1470
 
1339
- vm.roll(block.number + ROUND_DURATION * 2); // now at round 4
1471
+ _advanceToRound(4);
1340
1472
 
1341
1473
  uint256 preview = distributor.collectableFor(address(hook), 1, IERC20(address(rewardToken)));
1342
1474
 
@@ -1364,22 +1496,23 @@ contract JB721DistributorTest is Test {
1364
1496
  distributor.beginVesting(address(hook), tokenIds, tokens);
1365
1497
 
1366
1498
  // Partial collect at 50%.
1367
- vm.roll(block.number + ROUND_DURATION * 2);
1499
+ _advanceToRound(2);
1368
1500
  vm.prank(alice);
1369
1501
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
1370
1502
  assertEq(rewardToken.balanceOf(alice), 125 ether);
1371
1503
 
1372
1504
  uint256 vestingAfterPartial = distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken)));
1373
- assertEq(vestingAfterPartial, 125 ether); // 250 - 125 = 125 remaining
1505
+ // 250 - 125 collected + 187.5 auto-vest = 312.5 remaining.
1506
+ assertEq(vestingAfterPartial, 312.5 ether);
1374
1507
 
1375
1508
  // Burn and release remaining forfeited rewards.
1376
1509
  hook.burn(1);
1377
1510
 
1378
- vm.roll(block.number + ROUND_DURATION * 2);
1511
+ _advanceToRound(4);
1379
1512
  distributor.releaseForfeitedRewards(address(hook), tokenIds, tokens, address(0));
1380
1513
 
1381
- // All vesting should be released.
1382
- assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 0);
1514
+ // Auto-vest entry (187.5, release=6) is 50% locked at round 4: 93.75 stays as phantom vesting.
1515
+ assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 93.75 ether);
1383
1516
  // Alice keeps what she collected, no more sent.
1384
1517
  assertEq(rewardToken.balanceOf(alice), 125 ether);
1385
1518
  }
@@ -1392,11 +1525,8 @@ contract JB721DistributorTest is Test {
1392
1525
  _fundHook(1000 ether);
1393
1526
  _beginVestingBoth();
1394
1527
 
1395
- // Token 1 vesting = 250, Token 2 vesting = 500. Total vesting = 750.
1396
- // Balance = 1000, distributable = 250.
1397
-
1398
1528
  // Burn token 1 and forfeit after full vest.
1399
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1529
+ _advanceToRound(VESTING_ROUNDS);
1400
1530
  hook.burn(1);
1401
1531
  store.setBurnedFor(1, 1);
1402
1532
 
@@ -1407,13 +1537,12 @@ contract JB721DistributorTest is Test {
1407
1537
 
1408
1538
  distributor.releaseForfeitedRewards(address(hook), burnedIds, tokens, address(0));
1409
1539
 
1410
- // After forfeiture: totalVesting = 500, balance should still be 1000 (forfeited returns to pool).
1540
+ // After forfeiture: totalVesting = 500, balance should still be 1000.
1411
1541
  assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 500 ether);
1412
1542
  assertEq(distributor.balanceOf(address(hook), IERC20(address(rewardToken))), 1000 ether);
1413
- // Distributable = 1000 - 500 = 500 (was 250 before forfeiture).
1414
1543
 
1415
1544
  // Move to next round. The forfeited 250 is now distributable.
1416
- vm.roll(block.number + ROUND_DURATION);
1545
+ _advanceToRound(VESTING_ROUNDS + 1);
1417
1546
 
1418
1547
  // Vest token 2 again (new round).
1419
1548
  uint256[] memory tokenIds2 = new uint256[](1);
@@ -1421,11 +1550,10 @@ contract JB721DistributorTest is Test {
1421
1550
  distributor.beginVesting(address(hook), tokenIds2, tokens);
1422
1551
 
1423
1552
  // Token 2 should get 200/200 = 100% of 500 distributable = 500 ether from the new round.
1424
- // (only 1 tier with holders now: tier 2 with 1 held NFT)
1425
1553
  JBTokenSnapshotData memory snap =
1426
1554
  distributor.snapshotAtRoundOf(address(hook), IERC20(address(rewardToken)), distributor.currentRound());
1427
1555
  assertEq(snap.balance, 1000 ether);
1428
- assertEq(snap.vestingAmount, 500 ether); // Bob's original vesting from round 0.
1556
+ assertEq(snap.vestingAmount, 500 ether);
1429
1557
  }
1430
1558
 
1431
1559
  // =====================================================================
@@ -1457,9 +1585,6 @@ contract JB721DistributorTest is Test {
1457
1585
  _fundHook(1000 ether);
1458
1586
  _beginVestingBoth();
1459
1587
 
1460
- // Token 1 has 0 voting units, gets nothing. Total stake = 0 + 200 = 200.
1461
- // Token 1: mulDiv(1000e18, 0, 200) = 0.
1462
- // Token 2: mulDiv(1000e18, 200, 200) = 1000e18.
1463
1588
  assertEq(distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken))), 0);
1464
1589
  assertEq(distributor.claimedFor(address(hook), 2, IERC20(address(rewardToken))), 1000 ether);
1465
1590
  }
@@ -1475,7 +1600,7 @@ contract JB721DistributorTest is Test {
1475
1600
  _fundHook(1000 ether);
1476
1601
  _beginVestingBoth();
1477
1602
 
1478
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1603
+ _advanceToRound(VESTING_ROUNDS);
1479
1604
 
1480
1605
  uint256[] memory tokenIds = new uint256[](2);
1481
1606
  tokenIds[0] = 1;
@@ -1498,7 +1623,7 @@ contract JB721DistributorTest is Test {
1498
1623
  _fundHook(1000 ether);
1499
1624
  _beginVestingBoth();
1500
1625
 
1501
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1626
+ _advanceToRound(VESTING_ROUNDS);
1502
1627
 
1503
1628
  IERC20 token = IERC20(address(rewardToken));
1504
1629
  assertEq(distributor.collectableFor(address(hook), 1, token), distributor.claimedFor(address(hook), 1, token));
@@ -1521,16 +1646,16 @@ contract JB721DistributorTest is Test {
1521
1646
 
1522
1647
  JBTokenSnapshotData memory snap0 = distributor.snapshotAtRoundOf(address(hook), IERC20(address(rewardToken)), 0);
1523
1648
  assertEq(snap0.balance, 1000 ether);
1524
- assertEq(snap0.vestingAmount, 0); // No prior vesting when round 0 snapshot taken.
1649
+ assertEq(snap0.vestingAmount, 0);
1525
1650
 
1526
1651
  // Round 1: snapshot should reflect vesting from round 0.
1527
- vm.roll(block.number + ROUND_DURATION);
1652
+ _advanceToRound(1);
1528
1653
  _fundHook(500 ether);
1529
1654
  distributor.beginVesting(address(hook), tokenIds, tokens);
1530
1655
 
1531
1656
  JBTokenSnapshotData memory snap1 = distributor.snapshotAtRoundOf(address(hook), IERC20(address(rewardToken)), 1);
1532
- assertEq(snap1.balance, 1500 ether); // 1000 + 500
1533
- assertEq(snap1.vestingAmount, 250 ether); // Token 1's round-0 vesting
1657
+ assertEq(snap1.balance, 1500 ether);
1658
+ assertEq(snap1.vestingAmount, 250 ether);
1534
1659
  }
1535
1660
 
1536
1661
  // =====================================================================
@@ -1554,14 +1679,11 @@ contract JB721DistributorTest is Test {
1554
1679
  // Vest.
1555
1680
  distributor.beginVesting(address(hook), tokenIds, tokens);
1556
1681
 
1557
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1682
+ _advanceToRound(VESTING_ROUNDS);
1558
1683
 
1559
1684
  // Set up reentrancy: on transfer, the token calls collectVestedRewards again.
1560
1685
  maliciousToken.setReentrancyTarget(address(hook), tokenIds, tokens, alice);
1561
1686
 
1562
- // The reentrant call should not yield extra tokens because:
1563
- // After first collect, shareClaimed = MAX_SHARE and latestVestedIndex advances.
1564
- // The reentrant collect call processes the same entry but gets claimAmount = 0.
1565
1687
  vm.prank(alice);
1566
1688
  distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
1567
1689
 
@@ -1585,7 +1707,7 @@ contract JB721DistributorTest is Test {
1585
1707
 
1586
1708
  distributor.beginVesting(address(hook), tokenIds, tokens);
1587
1709
 
1588
- vm.roll(block.number + ROUND_DURATION * rounds);
1710
+ _advanceToRound(rounds);
1589
1711
 
1590
1712
  uint256 collectable = distributor.collectableFor(address(hook), 1, IERC20(address(rewardToken)));
1591
1713
  uint256 claimed = distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken)));
@@ -1622,12 +1744,12 @@ contract JB721DistributorTest is Test {
1622
1744
  distributor.beginVesting(address(hook), tokenIds, tokens);
1623
1745
 
1624
1746
  // Round 1.
1625
- vm.roll(block.number + ROUND_DURATION);
1747
+ _advanceToRound(1);
1626
1748
  _fundHook(fund2);
1627
1749
  distributor.beginVesting(address(hook), tokenIds, tokens);
1628
1750
 
1629
1751
  // Full vest.
1630
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1752
+ _advanceToRound(1 + VESTING_ROUNDS);
1631
1753
 
1632
1754
  uint256[] memory aliceIds = new uint256[](1);
1633
1755
  aliceIds[0] = 1;
@@ -1707,57 +1829,19 @@ contract JB721DistributorTest is Test {
1707
1829
  assertEq(distributor.totalVestingAmountOf(address(hook2), IERC20(address(rewardToken))), 500 ether);
1708
1830
 
1709
1831
  // Collect from hook2 -- should not affect hook1.
1710
- vm.roll(block.number + ROUND_DURATION * VESTING_ROUNDS);
1832
+ _advanceToRound(VESTING_ROUNDS);
1711
1833
  vm.prank(charlie);
1712
1834
  distributor.collectVestedRewards(address(hook2), tokenIds, tokens, charlie);
1713
1835
 
1714
1836
  assertEq(rewardToken.balanceOf(charlie), 500 ether);
1715
- // hook1 balance unchanged (only hook1 vesting amount decremented from its pool).
1716
1837
  assertEq(distributor.balanceOf(address(hook), IERC20(address(rewardToken))), 1000 ether);
1717
1838
  assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))), 250 ether);
1718
1839
  }
1719
1840
 
1720
- // =====================================================================
1721
- // Helpers
1722
- // =====================================================================
1723
-
1724
- function _fundHook(uint256 amount) internal {
1725
- rewardToken.mint(address(this), amount);
1726
- rewardToken.approve(address(distributor), amount);
1727
- distributor.fund(address(hook), IERC20(address(rewardToken)), amount);
1728
- }
1729
-
1730
- function _beginVestingBoth() internal {
1731
- uint256[] memory tokenIds = new uint256[](2);
1732
- tokenIds[0] = 1;
1733
- tokenIds[1] = 2;
1734
- IERC20[] memory tokens = new IERC20[](1);
1735
- tokens[0] = IERC20(address(rewardToken));
1736
- distributor.beginVesting(address(hook), tokenIds, tokens);
1737
- }
1738
-
1739
1841
  // =====================================================================
1740
1842
  // Split Hook
1741
1843
  // =====================================================================
1742
1844
 
1743
- function _splitContext(address token, uint256 amount) internal view returns (JBSplitHookContext memory) {
1744
- return JBSplitHookContext({
1745
- token: token,
1746
- amount: amount,
1747
- decimals: 18,
1748
- projectId: 1,
1749
- groupId: uint256(uint160(token)),
1750
- split: JBSplit({
1751
- percent: 500_000_000, // 50%
1752
- projectId: 0,
1753
- beneficiary: payable(address(hook)), // hook address as beneficiary
1754
- preferAddToBalance: false,
1755
- lockedUntil: 0,
1756
- hook: IJBSplitHook(address(distributor))
1757
- })
1758
- });
1759
- }
1760
-
1761
1845
  /// @notice processSplitWith pulls ERC-20 tokens via transferFrom and credits hook balance.
1762
1846
  function test_processSplitWith_erc20() public {
1763
1847
  uint256 amount = 10 ether;
@@ -1813,8 +1897,6 @@ contract JB721DistributorTest is Test {
1813
1897
 
1814
1898
  distributor.beginVesting(address(hook), tokenIds, tokens);
1815
1899
 
1816
- // Token 1 (tier1, 100 voting units) gets 100/400 = 25% of 100 ether = 25 ether.
1817
- // Token 2 (tier2, 200 voting units) gets 200/400 = 50% of 100 ether = 50 ether.
1818
1900
  assertEq(distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken))), 25 ether);
1819
1901
  assertEq(distributor.claimedFor(address(hook), 2, IERC20(address(rewardToken))), 50 ether);
1820
1902
  }
@@ -1831,12 +1913,10 @@ contract JB721DistributorTest is Test {
1831
1913
  /// @notice processSplitWith with allowance credits balance (terminal/controller pull pattern).
1832
1914
  function test_processSplitWith_erc20_noAllowance_creditsBalance() public {
1833
1915
  uint256 amount = 10 ether;
1834
- // Mint to caller and approve distributor (pull pattern).
1835
1916
  rewardToken.mint(address(this), amount);
1836
1917
  rewardToken.approve(address(distributor), amount);
1837
1918
 
1838
1919
  distributor.processSplitWith(_splitContext(address(rewardToken), amount));
1839
- // Balance credited to hook via pull.
1840
1920
  assertEq(distributor.balanceOf(address(hook), IERC20(address(rewardToken))), amount);
1841
1921
  }
1842
1922
 
@@ -1847,17 +1927,13 @@ contract JB721DistributorTest is Test {
1847
1927
  directory.setController(PROJECT_ID, address(this));
1848
1928
 
1849
1929
  uint256 amount = 50 ether;
1850
- // Controller mints to itself and approves the distributor.
1851
1930
  rewardToken.mint(address(this), amount);
1852
1931
  rewardToken.approve(address(distributor), amount);
1853
1932
 
1854
- // Controller calls processSplitWith -- distributor pulls via allowance.
1855
1933
  distributor.processSplitWith(_splitContext(address(rewardToken), amount));
1856
1934
 
1857
- // Balance credited to hook.
1858
1935
  assertEq(distributor.balanceOf(address(hook), IERC20(address(rewardToken))), amount);
1859
1936
 
1860
- // Tokens are distributable via vesting.
1861
1937
  uint256[] memory tokenIds = new uint256[](1);
1862
1938
  tokenIds[0] = 1;
1863
1939
  IERC20[] memory tokens = new IERC20[](1);
@@ -1881,7 +1957,6 @@ contract JB721DistributorTest is Test {
1881
1957
 
1882
1958
  /// @notice processSplitWith routes to correct hook via split.beneficiary.
1883
1959
  function test_processSplitWith_routesViaBeneficiary() public {
1884
- // Create a second hook.
1885
1960
  MockStore store2 = new MockStore();
1886
1961
  MockHook hook2 = new MockHook(store2);
1887
1962
 
@@ -1889,7 +1964,6 @@ contract JB721DistributorTest is Test {
1889
1964
  rewardToken.mint(address(this), amount);
1890
1965
  rewardToken.approve(address(distributor), amount);
1891
1966
 
1892
- // Split context with hook2 as beneficiary.
1893
1967
  JBSplitHookContext memory ctx = JBSplitHookContext({
1894
1968
  token: address(rewardToken),
1895
1969
  amount: amount,
@@ -1908,7 +1982,6 @@ contract JB721DistributorTest is Test {
1908
1982
 
1909
1983
  distributor.processSplitWith(ctx);
1910
1984
 
1911
- // Funds credited to hook2, not hook1.
1912
1985
  assertEq(distributor.balanceOf(address(hook2), IERC20(address(rewardToken))), amount);
1913
1986
  assertEq(distributor.balanceOf(address(hook), IERC20(address(rewardToken))), 0);
1914
1987
  }
@@ -1954,7 +2027,6 @@ contract ReentrantToken is ERC20 {
1954
2027
  external
1955
2028
  {
1956
2029
  reentrantHook = hook;
1957
- // Store for reentrancy.
1958
2030
  delete reentrantTokenIds;
1959
2031
  delete reentrantTokens;
1960
2032
  for (uint256 i; i < tokenIds.length; i++) {
@@ -1968,13 +2040,10 @@ contract ReentrantToken is ERC20 {
1968
2040
  }
1969
2041
 
1970
2042
  function transfer(address to, uint256 amount) public override returns (bool) {
1971
- // Perform the actual transfer first.
1972
2043
  bool result = super.transfer(to, amount);
1973
2044
 
1974
- // Attempt reentrancy once.
1975
2045
  if (reentrancyArmed) {
1976
- reentrancyArmed = false; // Prevent infinite recursion.
1977
- // Try to re-collect. This should be a no-op because state was already updated.
2046
+ reentrancyArmed = false;
1978
2047
  try JBDistributor(distributor)
1979
2048
  .collectVestedRewards(reentrantHook, reentrantTokenIds, reentrantTokens, reentrantBeneficiary) {}
1980
2049
  catch {}