@@ -562,6 +562,40 @@ test('vesting calculation one block before boundary', () => {
562
562
}
563
563
} ) ;
564
564
565
+ test ( 'vesting calculation one block after boundary' , ( ) => {
566
+ mintInitial ( ) ;
567
+ const deployBlockHeight = rov ( contract . getDeployBlockHeight ( ) ) ;
568
+
569
+ // Test one block after each iteration boundary
570
+ for ( let i = 1n ; i < constants . INITIAL_MINT_VESTING_ITERATIONS ; i ++ ) {
571
+ const oneBlockAfter =
572
+ deployBlockHeight +
573
+ i * constants . INITIAL_MINT_VESTING_ITERATION_BLOCKS +
574
+ 1n ;
575
+ const claimable = rov ( contract . calcClaimableAmount ( oneBlockAfter ) ) ;
576
+
577
+ // Should still be the current iteration's amount
578
+ const expectedVested =
579
+ i *
580
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
581
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) ;
582
+ const expected = constants . INITIAL_MINT_IMMEDIATE_AMOUNT + expectedVested ;
583
+ expect ( claimable ) . toBe ( expected ) ;
584
+ }
585
+
586
+ // Special case for the last iteration
587
+ const oneBlockAfter =
588
+ deployBlockHeight +
589
+ constants . INITIAL_MINT_VESTING_ITERATION_BLOCKS *
590
+ constants . INITIAL_MINT_VESTING_ITERATIONS +
591
+ 1n ;
592
+ const claimable = rov ( contract . calcClaimableAmount ( oneBlockAfter ) ) ;
593
+
594
+ // Should still be the current iteration's amount
595
+ const expectedVested = constants . INITIAL_MINT_AMOUNT ;
596
+ expect ( claimable ) . toBe ( expectedVested ) ;
597
+ } ) ;
598
+
565
599
test ( 'contract balance exactly equals vested amount (no extra funds)' , ( ) => {
566
600
// Only mint the exact initial amount, no extra
567
601
mintInitial ( ) ;
@@ -636,3 +670,223 @@ test('recipient change during vesting period preserves vested amounts', () => {
636
670
const secondClaim = txOk ( contract . claim ( ) , accounts . wallet_1 . address ) ;
637
671
expect ( secondClaim . value ) . toBe ( expectedVested ) ;
638
672
} ) ;
673
+
674
+ // -----------------------------------------------------------------------------
675
+ // Fuzz Testing - Randomized Tests
676
+ // -----------------------------------------------------------------------------
677
+
678
+ test ( 'fuzz: random extra deposits at random times' , ( ) => {
679
+ mintInitial ( ) ;
680
+
681
+ let totalExtraDeposited = 0n ;
682
+ let totalMonthsElapsed = 0 ;
683
+
684
+ // Generate 10 random deposit/time combinations
685
+ for ( let i = 0 ; i < 10 ; i ++ ) {
686
+ // Random deposit between 1 and 1000 STX
687
+ const randomDeposit =
688
+ BigInt ( Math . floor ( Math . random ( ) * 1000 ) + 1 ) * 1000000n ;
689
+ mint ( randomDeposit ) ;
690
+ totalExtraDeposited += randomDeposit ;
691
+
692
+ // Random time advancement between 0 and 3 months
693
+ const randomMonths = Math . floor ( Math . random ( ) * 4 ) ;
694
+ if ( randomMonths > 0 ) {
695
+ simnet . mineEmptyBlocks ( months ( randomMonths ) ) ;
696
+ totalMonthsElapsed += randomMonths ;
697
+ }
698
+ }
699
+
700
+ // Cap at 24 months for vesting calculation
701
+ const effectiveMonths = Math . min ( totalMonthsElapsed , 24 ) ;
702
+ const expectedVested =
703
+ effectiveMonths < 24
704
+ ? ( constants . INITIAL_MINT_VESTING_AMOUNT /
705
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
706
+ BigInt ( effectiveMonths )
707
+ : constants . INITIAL_MINT_VESTING_AMOUNT ;
708
+
709
+ const expectedTotal =
710
+ constants . INITIAL_MINT_IMMEDIATE_AMOUNT +
711
+ expectedVested +
712
+ totalExtraDeposited ;
713
+
714
+ const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
715
+ expect ( receipt . value ) . toBe ( expectedTotal ) ;
716
+
717
+ // Verify balance consistency
718
+ const remainingBalance = rov (
719
+ indirectContract . getBalance ( contract . identifier ) ,
720
+ ) ;
721
+ const expectedRemaining =
722
+ effectiveMonths < 24
723
+ ? constants . INITIAL_MINT_VESTING_AMOUNT - expectedVested
724
+ : 0n ;
725
+ expect ( remainingBalance ) . toBe ( expectedRemaining ) ;
726
+ } ) ;
727
+
728
+ test ( 'fuzz: random recipient changes during vesting' , ( ) => {
729
+ mintInitial ( ) ;
730
+
731
+ const wallets = [
732
+ accounts . deployer . address ,
733
+ accounts . wallet_1 . address ,
734
+ accounts . wallet_2 . address ,
735
+ accounts . wallet_3 . address ,
736
+ ] ;
737
+
738
+ let currentRecipient : string = accounts . deployer . address ;
739
+ let currentRecipientIndex = 0 ;
740
+ let totalMonthsElapsed = 0 ;
741
+
742
+ // Perform random recipient changes over time
743
+ for ( let i = 0 ; i < 8 ; i ++ ) {
744
+ // Random time advancement
745
+ const monthsToAdvance = Math . floor ( Math . random ( ) * 4 ) + 1 ; // 1-4 months
746
+ simnet . mineEmptyBlocks ( months ( monthsToAdvance ) ) ;
747
+ totalMonthsElapsed += monthsToAdvance ;
748
+
749
+ if ( totalMonthsElapsed >= 24 ) break ;
750
+
751
+ // Random recipient change
752
+ const newRecipientIndex = Math . floor ( Math . random ( ) * wallets . length ) ;
753
+ if ( newRecipientIndex !== currentRecipientIndex ) {
754
+ txOk (
755
+ contract . updateRecipient ( wallets [ newRecipientIndex ] ) ,
756
+ currentRecipient ,
757
+ ) ;
758
+ currentRecipient = wallets [ newRecipientIndex ] ;
759
+ currentRecipientIndex = newRecipientIndex ;
760
+ }
761
+ }
762
+
763
+ // Final recipient should be able to claim all vested funds
764
+ const expectedVested =
765
+ totalMonthsElapsed < 24
766
+ ? ( constants . INITIAL_MINT_VESTING_AMOUNT /
767
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
768
+ BigInt ( totalMonthsElapsed )
769
+ : constants . INITIAL_MINT_VESTING_AMOUNT ;
770
+
771
+ // If balance is sufficient, we can claim the vested amount
772
+ const receipt = txOk ( contract . claim ( ) , currentRecipient ) ;
773
+ expect ( receipt . value ) . toBe (
774
+ expectedVested + constants . INITIAL_MINT_IMMEDIATE_AMOUNT ,
775
+ ) ;
776
+ } ) ;
777
+
778
+ test ( 'fuzz: random tiny deposits' , ( ) => {
779
+ mintInitial ( ) ;
780
+
781
+ let totalTinyDeposits = 0n ;
782
+
783
+ // Add many tiny deposits (1-10 micro-STX each)
784
+ for ( let i = 0 ; i < 100 ; i ++ ) {
785
+ const tinyAmount = BigInt ( Math . floor ( Math . random ( ) * 10 ) + 1 ) ; // 1-10 micro-STX
786
+ mint ( tinyAmount ) ;
787
+ totalTinyDeposits += tinyAmount ;
788
+ }
789
+
790
+ // Advance some time
791
+ simnet . mineEmptyBlocks ( months ( 6 ) ) ;
792
+
793
+ const expectedVested =
794
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
795
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
796
+ 6n ;
797
+ const expected =
798
+ constants . INITIAL_MINT_IMMEDIATE_AMOUNT +
799
+ expectedVested +
800
+ totalTinyDeposits ;
801
+
802
+ const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
803
+ expect ( receipt . value ) . toBe ( expected ) ;
804
+
805
+ // Verify the expected remaining balance
806
+ const remainingBalance = rov (
807
+ indirectContract . getBalance ( contract . identifier ) ,
808
+ ) ;
809
+ expect ( remainingBalance ) . toBe (
810
+ constants . INITIAL_MINT_AMOUNT -
811
+ constants . INITIAL_MINT_IMMEDIATE_AMOUNT -
812
+ expectedVested ,
813
+ ) ;
814
+ } ) ;
815
+
816
+ test ( 'fuzz: stress test claim at random intervals' , ( ) => {
817
+ mintInitial ( ) ;
818
+
819
+ let totalClaimed = 0n ;
820
+ let currentBlock = 0n ;
821
+ let currentMonth = 0n ;
822
+ let initialBalance = rov (
823
+ indirectContract . getBalance ( accounts . deployer . address ) ,
824
+ ) ;
825
+ let totalExtraDeposited = 0n ;
826
+
827
+ // Perform claims at random intervals with random extra deposits
828
+ while ( currentMonth < 24 ) {
829
+ const balance = rov ( indirectContract . getBalance ( contract . identifier ) ) ;
830
+ console . log ( `Current balance: ${ balance } ` ) ;
831
+
832
+ // Random extra deposit
833
+ const extraDeposit = BigInt ( Math . floor ( Math . random ( ) * 10000000000 ) ) ;
834
+ if ( extraDeposit > 0n ) {
835
+ mint ( extraDeposit ) ;
836
+ totalExtraDeposited += extraDeposit ;
837
+ }
838
+
839
+ // Random time advancement, up to 5 months
840
+ const blocksToAdvance =
841
+ Math . floor (
842
+ Math . random ( ) *
843
+ Number ( constants . INITIAL_MINT_VESTING_ITERATION_BLOCKS ) *
844
+ 5 ,
845
+ ) + 1 ;
846
+ simnet . mineEmptyBlocks ( blocksToAdvance ) ;
847
+ currentBlock += BigInt ( blocksToAdvance ) ;
848
+ currentMonth =
849
+ currentBlock / constants . INITIAL_MINT_VESTING_ITERATION_BLOCKS ;
850
+
851
+ const totalClaimable =
852
+ currentMonth < 24
853
+ ? ( constants . INITIAL_MINT_VESTING_AMOUNT /
854
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
855
+ currentMonth +
856
+ constants . INITIAL_MINT_IMMEDIATE_AMOUNT
857
+ : constants . INITIAL_MINT_AMOUNT ;
858
+ const expectedClaimable =
859
+ totalClaimable + totalExtraDeposited - totalClaimed ;
860
+
861
+ console . log (
862
+ `Claiming at month ${ currentMonth } (block ${ currentBlock } )\n extra deposit: ${ extraDeposit } \n expected claimable: ${ expectedClaimable } ` ,
863
+ ) ;
864
+
865
+ // Claim whatever is available
866
+ const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
867
+ totalClaimed += receipt . value ;
868
+
869
+ // Verify claim amount is correct
870
+ expect ( receipt . value ) . toBe ( expectedClaimable ) ;
871
+
872
+ // Verify the account balance after claim
873
+ const newBalance = rov (
874
+ indirectContract . getBalance ( accounts . deployer . address ) ,
875
+ ) ;
876
+ expect ( newBalance ) . toBe ( initialBalance + totalClaimed ) ;
877
+ }
878
+
879
+ // After all claims, verify the final balance
880
+ const finalBalance = rov ( indirectContract . getBalance ( contract . identifier ) ) ;
881
+ const expectedFinalBalance =
882
+ currentMonth >= 24
883
+ ? 0n
884
+ : constants . INITIAL_MINT_AMOUNT -
885
+ constants . INITIAL_MINT_IMMEDIATE_AMOUNT -
886
+ currentMonth *
887
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
888
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) ;
889
+
890
+ // Total claimed + final balance should account for all deposited funds
891
+ expect ( finalBalance ) . toBe ( expectedFinalBalance ) ;
892
+ } ) ;
0 commit comments