@bananapus/distributor-v6 0.0.3 → 0.0.4
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/foundry.lock +5 -0
- package/package.json +1 -1
- package/src/JB721Distributor.sol +236 -22
- package/src/JBDistributor.sol +293 -213
- package/src/JBTokenDistributor.sol +32 -27
- package/src/interfaces/IJBDistributor.sol +37 -24
- package/test/AuditFixes.t.sol +429 -0
- package/test/JB721Distributor.t.sol +232 -163
- package/test/JBTokenDistributor.t.sol +92 -13
- package/test/audit/H26VotingPowerCap.t.sol +338 -0
- package/test/invariant/JB721DistributorInvariant.t.sol +11 -12
|
@@ -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.
|
|
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
|
|
297
|
-
vm.
|
|
342
|
+
function test_currentRound_afterWarping() public {
|
|
343
|
+
vm.warp(block.timestamp + ROUND_DURATION);
|
|
298
344
|
assertEq(distributor.currentRound(), 1);
|
|
299
345
|
|
|
300
|
-
vm.
|
|
346
|
+
vm.warp(block.timestamp + ROUND_DURATION * 3);
|
|
301
347
|
assertEq(distributor.currentRound(), 4);
|
|
302
348
|
}
|
|
303
349
|
|
|
304
|
-
function
|
|
305
|
-
assertEq(distributor.
|
|
306
|
-
assertEq(distributor.
|
|
307
|
-
assertEq(distributor.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
594
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
603
|
-
|
|
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
|
-
//
|
|
609
|
-
//
|
|
610
|
-
|
|
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
|
-
//
|
|
624
|
-
|
|
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
|
-
//
|
|
644
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
756
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1226
|
+
_advanceToRound(r);
|
|
1093
1227
|
vm.prank(alice);
|
|
1094
1228
|
distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
|
|
1095
1229
|
}
|
|
1096
1230
|
|
|
1097
|
-
//
|
|
1098
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1376
|
+
_advanceToRound(1);
|
|
1240
1377
|
_fundHook(400 ether);
|
|
1241
1378
|
distributor.beginVesting(address(hook), tokenIds, tokens);
|
|
1242
1379
|
|
|
1243
1380
|
// Round 2: vest
|
|
1244
|
-
|
|
1381
|
+
_advanceToRound(2);
|
|
1245
1382
|
_fundHook(200 ether);
|
|
1246
1383
|
distributor.beginVesting(address(hook), tokenIds, tokens);
|
|
1247
1384
|
|
|
1248
|
-
//
|
|
1249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1443
|
+
_advanceToRound(6);
|
|
1309
1444
|
vm.prank(alice);
|
|
1310
1445
|
distributor.collectVestedRewards(address(hook), tokenIds, tokens, alice);
|
|
1311
1446
|
|
|
1312
|
-
//
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1463
|
+
_advanceToRound(1);
|
|
1332
1464
|
_fundHook(400 ether);
|
|
1333
1465
|
distributor.beginVesting(address(hook), tokenIds, tokens);
|
|
1334
1466
|
|
|
1335
|
-
|
|
1467
|
+
_advanceToRound(2);
|
|
1336
1468
|
_fundHook(200 ether);
|
|
1337
1469
|
distributor.beginVesting(address(hook), tokenIds, tokens);
|
|
1338
1470
|
|
|
1339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1511
|
+
_advanceToRound(4);
|
|
1379
1512
|
distributor.releaseForfeitedRewards(address(hook), tokenIds, tokens, address(0));
|
|
1380
1513
|
|
|
1381
|
-
//
|
|
1382
|
-
assertEq(distributor.totalVestingAmountOf(address(hook), IERC20(address(rewardToken))),
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
1649
|
+
assertEq(snap0.vestingAmount, 0);
|
|
1525
1650
|
|
|
1526
1651
|
// Round 1: snapshot should reflect vesting from round 0.
|
|
1527
|
-
|
|
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);
|
|
1533
|
-
assertEq(snap1.vestingAmount, 250 ether);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1747
|
+
_advanceToRound(1);
|
|
1626
1748
|
_fundHook(fund2);
|
|
1627
1749
|
distributor.beginVesting(address(hook), tokenIds, tokens);
|
|
1628
1750
|
|
|
1629
1751
|
// Full vest.
|
|
1630
|
-
|
|
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
|
-
|
|
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;
|
|
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 {}
|