@@ -148,7 +148,9 @@ test('calculating vested amounts at a block height', () => {
148
148
function expectedAmount ( burnHeight : bigint ) {
149
149
const diff = burnHeight - deployBlockHeight ;
150
150
const iterations = diff / 4383n ;
151
- const stxPerIteration = ( initialMintAmount - immediateAmount ) / 24n ;
151
+ const stxPerIteration =
152
+ ( initialMintAmount - immediateAmount ) /
153
+ constants . INITIAL_MINT_VESTING_ITERATIONS ;
152
154
const vestingAmount = stxPerIteration * iterations ;
153
155
return immediateAmount + vestingAmount ;
154
156
}
@@ -164,12 +166,16 @@ test('calculating vested amounts at a block height', () => {
164
166
) ;
165
167
}
166
168
167
- for ( let i = 1n ; i < 24n ; i ++ ) {
169
+ for ( let i = 1n ; i < constants . INITIAL_MINT_VESTING_ITERATIONS ; i ++ ) {
168
170
expectAmount ( i ) ;
169
171
}
170
172
// At 24+ months, the entire vesting bucket should be unlocked
171
173
expect (
172
- rov ( contract . calcClaimableAmount ( deployBlockHeight + 24n * 4383n ) ) ,
174
+ rov (
175
+ contract . calcClaimableAmount (
176
+ deployBlockHeight + constants . INITIAL_MINT_VESTING_ITERATIONS * 4383n ,
177
+ ) ,
178
+ ) ,
173
179
) . toBe ( initialMintAmount ) ;
174
180
expect (
175
181
rov ( contract . calcClaimableAmount ( deployBlockHeight + 25n * 4383n ) ) ,
@@ -189,7 +195,8 @@ test('claim scenario 1', () => {
189
195
const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
190
196
const expected =
191
197
constants . INITIAL_MINT_IMMEDIATE_AMOUNT +
192
- constants . INITIAL_MINT_VESTING_AMOUNT / 24n +
198
+ constants . INITIAL_MINT_VESTING_AMOUNT /
199
+ constants . INITIAL_MINT_VESTING_ITERATIONS +
193
200
100n * 1000000n ;
194
201
expect ( receipt . value ) . toBe ( expected ) ;
195
202
@@ -206,7 +213,10 @@ test('claim scenario 1', () => {
206
213
simnet . mineEmptyBlocks ( months ( 4 ) ) ;
207
214
const receipt2 = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
208
215
const expected2 =
209
- ( constants . INITIAL_MINT_VESTING_AMOUNT / 24n ) * 4n + 500n * 1000000n ;
216
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
217
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
218
+ 4n +
219
+ 500n * 1000000n ;
210
220
expect ( receipt2 . value ) . toBe ( expected2 ) ;
211
221
212
222
const [ event2 ] = filterEvents (
@@ -218,7 +228,10 @@ test('claim scenario 1', () => {
218
228
219
229
// wait until end of vesting (20 more months), with an extra 1500 STX
220
230
// calc remainder of unvested, to deal with integer division
221
- const vestedAlready = ( constants . INITIAL_MINT_VESTING_AMOUNT / 24n ) * 5n ;
231
+ const vestedAlready =
232
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
233
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
234
+ 5n ;
222
235
const unvested = constants . INITIAL_MINT_VESTING_AMOUNT - vestedAlready ;
223
236
const expected3 = unvested + 1500n * 1000000n ;
224
237
mint ( 1500n * 1000000n ) ;
@@ -406,7 +419,8 @@ test('claiming after waiting more than 1 month', () => {
406
419
const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
407
420
expect ( receipt . value ) . toBe (
408
421
constants . INITIAL_MINT_IMMEDIATE_AMOUNT +
409
- constants . INITIAL_MINT_VESTING_AMOUNT / 24n ,
422
+ constants . INITIAL_MINT_VESTING_AMOUNT /
423
+ constants . INITIAL_MINT_VESTING_ITERATIONS ,
410
424
) ;
411
425
} ) ;
412
426
@@ -431,3 +445,194 @@ test('recipient can be set to a contract', () => {
431
445
txOk ( contract . updateRecipient ( contractAddr ) , accounts . deployer . address ) ;
432
446
expect ( rov ( contract . getRecipient ( ) ) ) . toBe ( contractAddr ) ;
433
447
} ) ;
448
+
449
+ test ( 'multiple recipient changes in same block work correctly' , ( ) => {
450
+ // Initial recipient is deployer
451
+ expect ( rov ( contract . getRecipient ( ) ) ) . toBe ( accounts . deployer . address ) ;
452
+
453
+ // Change to wallet_1
454
+ txOk (
455
+ contract . updateRecipient ( accounts . wallet_1 . address ) ,
456
+ accounts . deployer . address ,
457
+ ) ;
458
+ expect ( rov ( contract . getRecipient ( ) ) ) . toBe ( accounts . wallet_1 . address ) ;
459
+
460
+ // Change back to deployer in same block
461
+ txOk (
462
+ contract . updateRecipient ( accounts . deployer . address ) ,
463
+ accounts . wallet_1 . address ,
464
+ ) ;
465
+ expect ( rov ( contract . getRecipient ( ) ) ) . toBe ( accounts . deployer . address ) ;
466
+ } ) ;
467
+
468
+ test ( 'large extra deposit does not break vesting calculations' , ( ) => {
469
+ mintInitial ( ) ;
470
+
471
+ // Add a large amount of extra STX (100M STX)
472
+ const largeAmount = 100000000n * 1000000n ; // 100 million STX
473
+ mint ( largeAmount ) ;
474
+
475
+ // Move forward 12 months
476
+ simnet . mineEmptyBlocks ( months ( 12 ) ) ;
477
+
478
+ // Calculate expected: immediate + half vesting + large amount
479
+ const expected =
480
+ constants . INITIAL_MINT_IMMEDIATE_AMOUNT +
481
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
482
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
483
+ 12n +
484
+ largeAmount ;
485
+
486
+ const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
487
+ expect ( receipt . value ) . toBe ( expected ) ;
488
+ } ) ;
489
+
490
+ test ( 'previous recipient cannot update the recipient' , ( ) => {
491
+ txOk (
492
+ contract . updateRecipient ( accounts . wallet_1 . address ) ,
493
+ accounts . deployer . address ,
494
+ ) ;
495
+ expect ( rov ( contract . getRecipient ( ) ) ) . toBe ( accounts . wallet_1 . address ) ;
496
+
497
+ txErr (
498
+ contract . updateRecipient ( accounts . wallet_2 . address ) ,
499
+ accounts . deployer . address ,
500
+ ) ;
501
+ expect ( rov ( contract . getRecipient ( ) ) ) . toBe ( accounts . wallet_1 . address ) ;
502
+ } ) ;
503
+
504
+ test ( 'previous recipient cannot claim' , ( ) => {
505
+ mintInitial ( ) ;
506
+
507
+ txOk (
508
+ contract . updateRecipient ( accounts . wallet_1 . address ) ,
509
+ accounts . deployer . address ,
510
+ ) ;
511
+
512
+ txErr ( contract . claim ( ) , accounts . deployer . address ) ;
513
+
514
+ // Contract should still have the unvested portion
515
+ const remainingBalance = rov (
516
+ indirectContract . getBalance ( contract . identifier ) ,
517
+ ) ;
518
+ expect ( remainingBalance ) . toBe ( constants . INITIAL_MINT_AMOUNT ) ;
519
+ } ) ;
520
+
521
+ test ( 'vesting calculation at exact boundary blocks' , ( ) => {
522
+ mintInitial ( ) ;
523
+ const deployBlockHeight = rov ( contract . getDeployBlockHeight ( ) ) ;
524
+
525
+ // Test at exact iteration boundaries
526
+ for ( let i = 1n ; i <= constants . INITIAL_MINT_VESTING_ITERATIONS ; i ++ ) {
527
+ const exactBoundary =
528
+ deployBlockHeight + i * constants . INITIAL_MINT_VESTING_ITERATION_BLOCKS ;
529
+ const claimable = rov ( contract . calcClaimableAmount ( exactBoundary ) ) ;
530
+
531
+ const expectedVested =
532
+ i < constants . INITIAL_MINT_VESTING_ITERATIONS
533
+ ? ( constants . INITIAL_MINT_VESTING_AMOUNT /
534
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
535
+ i
536
+ : constants . INITIAL_MINT_VESTING_AMOUNT ;
537
+
538
+ const expected = constants . INITIAL_MINT_IMMEDIATE_AMOUNT + expectedVested ;
539
+ expect ( claimable ) . toBe ( expected ) ;
540
+ }
541
+ } ) ;
542
+
543
+ test ( 'vesting calculation one block before boundary' , ( ) => {
544
+ mintInitial ( ) ;
545
+ const deployBlockHeight = rov ( contract . getDeployBlockHeight ( ) ) ;
546
+
547
+ // Test one block before each iteration boundary
548
+ for ( let i = 1n ; i <= constants . INITIAL_MINT_VESTING_ITERATIONS ; i ++ ) {
549
+ const oneBlockBefore =
550
+ deployBlockHeight +
551
+ i * constants . INITIAL_MINT_VESTING_ITERATION_BLOCKS -
552
+ 1n ;
553
+ const claimable = rov ( contract . calcClaimableAmount ( oneBlockBefore ) ) ;
554
+
555
+ // Should still be the previous iteration's amount
556
+ const expectedVested =
557
+ ( i - 1n ) *
558
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
559
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) ;
560
+ const expected = constants . INITIAL_MINT_IMMEDIATE_AMOUNT + expectedVested ;
561
+ expect ( claimable ) . toBe ( expected ) ;
562
+ }
563
+ } ) ;
564
+
565
+ test ( 'contract balance exactly equals vested amount (no extra funds)' , ( ) => {
566
+ // Only mint the exact initial amount, no extra
567
+ mintInitial ( ) ;
568
+
569
+ // Move to middle of vesting period (12 months)
570
+ simnet . mineEmptyBlocks ( months ( 12 ) ) ;
571
+
572
+ const vested =
573
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
574
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
575
+ 12n ;
576
+ // Should be able to claim immediate + half of vesting
577
+ const expected = constants . INITIAL_MINT_IMMEDIATE_AMOUNT + vested ;
578
+ const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
579
+ expect ( receipt . value ) . toBe ( expected ) ;
580
+
581
+ // Contract should still have the unvested portion
582
+ const remainingBalance = rov (
583
+ indirectContract . getBalance ( contract . identifier ) ,
584
+ ) ;
585
+ const expectedRemaining = constants . INITIAL_MINT_VESTING_AMOUNT - vested ;
586
+ expect ( remainingBalance ) . toBe ( expectedRemaining ) ;
587
+ } ) ;
588
+
589
+ test ( 'multiple small extra deposits accumulate correctly' , ( ) => {
590
+ mintInitial ( ) ;
591
+
592
+ // Add multiple small deposits over time
593
+ mint ( 100n * 1000000n ) ; // 100 STX
594
+ simnet . mineEmptyBlocks ( months ( 1 ) ) ;
595
+
596
+ mint ( 200n * 1000000n ) ; // 200 STX
597
+ simnet . mineEmptyBlocks ( months ( 1 ) ) ;
598
+
599
+ mint ( 300n * 1000000n ) ; // 300 STX
600
+ simnet . mineEmptyBlocks ( months ( 1 ) ) ;
601
+
602
+ // Total extra: 600 STX, Total time: 3 months
603
+ const extraAmount = 600n * 1000000n ;
604
+ const vestedAfter3Months =
605
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
606
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
607
+ 3n ;
608
+ const expected =
609
+ constants . INITIAL_MINT_IMMEDIATE_AMOUNT + vestedAfter3Months + extraAmount ;
610
+
611
+ const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
612
+ expect ( receipt . value ) . toBe ( expected ) ;
613
+ } ) ;
614
+
615
+ test ( 'recipient change during vesting period preserves vested amounts' , ( ) => {
616
+ mintInitial ( ) ;
617
+
618
+ // Original recipient claims immediate amount
619
+ const firstClaim = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
620
+ expect ( firstClaim . value ) . toBe ( constants . INITIAL_MINT_IMMEDIATE_AMOUNT ) ;
621
+
622
+ // Wait 6 months
623
+ simnet . mineEmptyBlocks ( months ( 6 ) ) ;
624
+
625
+ // Change recipient
626
+ txOk (
627
+ contract . updateRecipient ( accounts . wallet_1 . address ) ,
628
+ accounts . deployer . address ,
629
+ ) ;
630
+
631
+ // New recipient should be able to claim 6 months of vesting
632
+ const expectedVested =
633
+ ( constants . INITIAL_MINT_VESTING_AMOUNT /
634
+ constants . INITIAL_MINT_VESTING_ITERATIONS ) *
635
+ 6n ;
636
+ const secondClaim = txOk ( contract . claim ( ) , accounts . wallet_1 . address ) ;
637
+ expect ( secondClaim . value ) . toBe ( expectedVested ) ;
638
+ } ) ;
0 commit comments