@@ -8,10 +8,26 @@ const contract = contracts.sip031;
8
8
const constants = contract . constants ;
9
9
const indirectContract = contracts . sip031Indirect ;
10
10
11
+ /**
12
+ * "Mint" STX to the contract
13
+ */
11
14
function mint ( amount : number | bigint ) {
12
15
txOk ( indirectContract . transferStx ( amount , contract . identifier ) , accounts . wallet_4 . address ) ;
13
16
}
14
17
18
+ // Helper function to mint the initial 200M STX to the contract
19
+ function mintInitial ( ) {
20
+ // First make sure wallet_4 has enough STX to mint the initial amount
21
+ txOk ( indirectContract . transferStx ( constants . INITIAL_MINT_AMOUNT / 2n , accounts . wallet_4 . address ) , accounts . wallet_5 . address ) ;
22
+ txOk ( indirectContract . transferStx ( constants . INITIAL_MINT_AMOUNT / 2n , accounts . wallet_4 . address ) , accounts . wallet_6 . address ) ;
23
+ // Mint the entire INITIAL_MINT_AMOUNT to the vesting contract
24
+ mint ( constants . INITIAL_MINT_AMOUNT ) ;
25
+ }
26
+
27
+ function months ( n : number ) {
28
+ return n * Number ( constants . INITIAL_MINT_VESTING_ITERATION_BLOCKS ) ;
29
+ }
30
+
15
31
test ( 'initial recipient should be the deployer' , ( ) => {
16
32
const value = rov ( contract . getRecipient ( ) ) ;
17
33
expect ( value ) . toBe ( accounts . deployer . address ) ;
@@ -49,27 +65,29 @@ test('errors if claiming as a non-recipient', () => {
49
65
} ) ;
50
66
51
67
test ( 'initial recipient can claim' , ( ) => {
52
- mint ( 100000000 ) ;
53
- const receipt = txOk ( contract . claim ( ) , accounts . deployer . address )
54
- expect ( receipt . value ) . toBe ( 100000000n ) ;
68
+ mintInitial ( ) ;
69
+ const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
70
+ expect ( receipt . value ) . toBe ( constants . INITIAL_MINT_IMMEDIATE_AMOUNT ) ;
71
+
55
72
const [ event ] = filterEvents ( receipt . events , CoreNodeEventType . StxTransferEvent ) ;
56
- expect ( event . data . amount ) . toBe ( `${ 100000000n } ` ) ;
73
+ expect ( event . data . amount ) . toBe ( `${ constants . INITIAL_MINT_IMMEDIATE_AMOUNT } ` ) ;
57
74
expect ( event . data . recipient ) . toBe ( accounts . deployer . address ) ;
58
75
expect ( event . data . sender ) . toBe ( contract . identifier ) ;
59
76
} ) ;
60
77
78
+ // Mint full initial amount first
61
79
test ( 'updated recipient can claim' , ( ) => {
62
- mint ( 100000000 ) ;
80
+ mintInitial ( ) ;
63
81
const balance = rov ( indirectContract . getBalance ( contract . identifier ) ) ;
64
- expect ( balance ) . toBe ( 100000000n ) ;
82
+ expect ( balance ) . toBe ( constants . INITIAL_MINT_AMOUNT ) ;
65
83
66
- txOk ( contract . updateRecipient ( accounts . wallet_1 . address ) , accounts . deployer . address )
67
- const receipt = txOk ( contract . claim ( ) , accounts . wallet_1 . address )
68
- expect ( receipt . value ) . toBe ( 100000000n ) ;
84
+ txOk ( contract . updateRecipient ( accounts . wallet_1 . address ) , accounts . deployer . address ) ;
85
+ const receipt = txOk ( contract . claim ( ) , accounts . wallet_1 . address ) ;
86
+ expect ( receipt . value ) . toBe ( constants . INITIAL_MINT_IMMEDIATE_AMOUNT ) ;
69
87
70
88
expect ( receipt . events . length ) . toBe ( 1 ) ;
71
89
const [ event ] = filterEvents ( receipt . events , CoreNodeEventType . StxTransferEvent ) ;
72
- expect ( event . data . amount ) . toBe ( `${ 100000000n } ` ) ;
90
+ expect ( event . data . amount ) . toBe ( `${ constants . INITIAL_MINT_IMMEDIATE_AMOUNT } ` ) ;
73
91
expect ( event . data . recipient ) . toBe ( accounts . wallet_1 . address ) ;
74
92
expect ( event . data . sender ) . toBe ( contract . identifier ) ;
75
93
} ) ;
@@ -79,7 +97,6 @@ test('calculating vested amounts at a block height', () => {
79
97
80
98
const initialMintAmount = 200_000_000n * 1000000n ; // 200,000,000 STX
81
99
const immediateAmount = 100_000_000n * 1000000n ; // 100,000,000 STX
82
- const vestingAmount = initialMintAmount - immediateAmount ;
83
100
84
101
function expectedAmount ( burnHeight : bigint ) {
85
102
const diff = burnHeight - deployBlockHeight ;
@@ -95,30 +112,172 @@ test('calculating vested amounts at a block height', () => {
95
112
const burnHeight = deployBlockHeight + month * 4383n ;
96
113
expect ( rovOk ( contract . calcVestedAmount ( burnHeight ) ) ) . toBe ( expectedAmount ( burnHeight ) ) ;
97
114
}
98
- expectAmount ( 1n ) ;
99
- expectAmount ( 2n ) ;
100
- expectAmount ( 3n ) ;
101
- expectAmount ( 4n ) ;
102
- expectAmount ( 5n ) ;
103
- expectAmount ( 6n ) ;
104
- expectAmount ( 7n ) ;
105
- expectAmount ( 8n ) ;
106
- expectAmount ( 9n ) ;
107
- expectAmount ( 10n ) ;
108
- expectAmount ( 11n ) ;
109
- expectAmount ( 12n ) ;
110
- expectAmount ( 13n ) ;
111
- expectAmount ( 14n ) ;
112
- expectAmount ( 15n ) ;
113
- expectAmount ( 16n ) ;
114
- expectAmount ( 17n ) ;
115
- expectAmount ( 18n ) ;
116
- expectAmount ( 19n ) ;
117
- expectAmount ( 20n ) ;
118
- expectAmount ( 21n ) ;
119
- expectAmount ( 22n ) ;
120
- expectAmount ( 23n ) ;
121
- expectAmount ( 24n ) ;
122
115
116
+ for ( let i = 1n ; i < 24n ; i ++ ) {
117
+ expectAmount ( i ) ;
118
+ }
119
+ // At 24+ months, the entire vesting bucket should be unlocked
120
+ expect ( rovOk ( contract . calcVestedAmount ( deployBlockHeight + 24n * 4383n ) ) ) . toBe ( initialMintAmount ) ;
123
121
expect ( rovOk ( contract . calcVestedAmount ( deployBlockHeight + 25n * 4383n ) ) ) . toBe ( initialMintAmount ) ;
122
+ } ) ;
123
+
124
+ // -----------------------------------------------------------------------------
125
+ // Claim scenario 1:
126
+ // - contract gets 100 STX after initial mint
127
+ // - claim after 1 month
128
+ // - recipient should get 100M + vested + 100 STX
129
+ // -----------------------------------------------------------------------------
130
+ test ( 'claim scenario 1' , ( ) => {
131
+ mintInitial ( ) ;
132
+ mint ( 100n * 1000000n ) ;
133
+ simnet . mineEmptyBlocks ( months ( 1 ) ) ;
134
+ const receipt = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
135
+ const expected = constants . INITIAL_MINT_IMMEDIATE_AMOUNT + constants . INITIAL_MINT_VESTING_AMOUNT / 24n + 100n * 1000000n ;
136
+ expect ( receipt . value ) . toBe ( expected ) ;
137
+
138
+ const [ event ] = filterEvents ( receipt . events , CoreNodeEventType . StxTransferEvent ) ;
139
+ expect ( event . data . amount ) . toBe ( expected . toString ( ) ) ;
140
+ expect ( event . data . recipient ) . toBe ( accounts . deployer . address ) ;
141
+ expect ( event . data . sender ) . toBe ( contract . identifier ) ;
142
+
143
+ // wait 4 months, also the contract gets 500 STX
144
+ mint ( 500n * 1000000n ) ;
145
+ simnet . mineEmptyBlocks ( months ( 4 ) ) ;
146
+ const receipt2 = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
147
+ const expected2 = constants . INITIAL_MINT_VESTING_AMOUNT / 24n * 4n + 500n * 1000000n ;
148
+ expect ( receipt2 . value ) . toBe ( expected2 ) ;
149
+
150
+ const [ event2 ] = filterEvents ( receipt2 . events , CoreNodeEventType . StxTransferEvent ) ;
151
+ expect ( event2 . data . amount ) . toBe ( expected2 . toString ( ) ) ;
152
+ expect ( event2 . data . recipient ) . toBe ( accounts . deployer . address ) ;
153
+
154
+ // wait until end of vesting (20 more months), with an extra 1500 STX
155
+ // calc remainder of unvested, to deal with integer division
156
+ const vestedAlready = constants . INITIAL_MINT_VESTING_AMOUNT / 24n * 5n ;
157
+ const unvested = constants . INITIAL_MINT_VESTING_AMOUNT - vestedAlready ;
158
+ const expected3 = unvested + 1500n * 1000000n ;
159
+ mint ( 1500n * 1000000n ) ;
160
+ simnet . mineEmptyBlocks ( months ( 20 ) ) ;
161
+ const receipt3 = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
162
+ expect ( receipt3 . value ) . toBe ( expected3 ) ;
163
+
164
+ const [ event3 ] = filterEvents ( receipt3 . events , CoreNodeEventType . StxTransferEvent ) ;
165
+ expect ( event3 . data . amount ) . toBe ( expected3 . toString ( ) ) ;
166
+ expect ( event3 . data . recipient ) . toBe ( accounts . deployer . address ) ;
167
+
168
+ // wait 1 more month, with an extra 1000 STX
169
+ // there is no more vested amount, so the extra 1000 STX should be claimed
170
+ const expected4 = 1000n * 1000000n ;
171
+ mint ( 1000n * 1000000n ) ;
172
+ simnet . mineEmptyBlocks ( months ( 1 ) ) ;
173
+ const receipt4 = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
174
+ expect ( receipt4 . value ) . toBe ( expected4 ) ;
175
+
176
+ const [ event4 ] = filterEvents ( receipt4 . events , CoreNodeEventType . StxTransferEvent ) ;
177
+ expect ( event4 . data . amount ) . toBe ( expected4 . toString ( ) ) ;
178
+ expect ( event4 . data . recipient ) . toBe ( accounts . deployer . address ) ;
179
+ expect ( rov ( indirectContract . getBalance ( contract . identifier ) ) ) . toBe ( 0n ) ;
180
+ } )
181
+
182
+ // -----------------------------------------------------------------------------
183
+ // Edge-case: Claim when the contract holds *zero* balance should revert
184
+ // -----------------------------------------------------------------------------
185
+ test ( 'claim with zero balance should error with ERR_NOTHING_TO_CLAIM' , ( ) => {
186
+ // No minting has happened, contract balance == 0
187
+ const receipt = txErr ( contract . claim ( ) , accounts . deployer . address ) ;
188
+ expect ( receipt . value ) . toBe ( constants . ERR_NOTHING_TO_CLAIM ) ;
189
+ } ) ;
190
+
191
+ // -----------------------------------------------------------------------------
192
+ // Edge-case: Calling `claim` twice in the same block – second should fail
193
+ // -----------------------------------------------------------------------------
194
+ test ( 'double claim in the same block reverts on second call' , ( ) => {
195
+ mintInitial ( ) ;
196
+
197
+ // First claim succeeds and drains the immediate bucket
198
+ const first = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
199
+ expect ( first . value ) . toBe ( constants . INITIAL_MINT_IMMEDIATE_AMOUNT ) ;
200
+
201
+ // Second claim in the *same* block should have nothing left
202
+ const second = txErr ( contract . claim ( ) , accounts . deployer . address ) ;
203
+ expect ( second . value ) . toBe ( constants . ERR_NOTHING_TO_CLAIM ) ;
204
+ } ) ;
205
+
206
+ // -----------------------------------------------------------------------------
207
+ // Edge-case: Deposit exactly the amount that is still un-vested ("reserved")
208
+ // -> nothing should be claimable.
209
+ // -----------------------------------------------------------------------------
210
+ test ( 'deposit equal to reserved (unvested) amount is NOT claimable' , ( ) => {
211
+ // `reserved` at deployment time equals the total unvested part (100 M STX)
212
+ const reserved = constants . INITIAL_MINT_VESTING_AMOUNT ;
213
+
214
+ // Deposit *only* the reserved amount, without the initial 200 M mint
215
+ mint ( reserved ) ;
216
+
217
+ // No portion of this deposit is vested, so claim must revert
218
+ const receipt = txErr ( contract . claim ( ) , accounts . deployer . address ) ;
219
+ expect ( receipt . value ) . toBe ( constants . ERR_NOTHING_TO_CLAIM ) ;
220
+ } ) ;
221
+
222
+ // -----------------------------------------------------------------------------
223
+ // Edge-case: Integer-division rounding – last vesting iteration flushes the
224
+ // remainder so that total withdrawn == 200 M STX.
225
+ // -----------------------------------------------------------------------------
226
+ test ( 'final vesting iteration flushes rounding remainder' , ( ) => {
227
+ mintInitial ( ) ;
228
+
229
+ // Advance 23 of 24 months
230
+ simnet . mineEmptyBlocks ( months ( 23 ) ) ;
231
+
232
+ // First claim: immediate bucket + 23/24 of vesting bucket
233
+ const perIteration = constants . INITIAL_MINT_VESTING_AMOUNT / constants . INITIAL_MINT_VESTING_ITERATIONS ;
234
+ const expectedFirst =
235
+ constants . INITIAL_MINT_IMMEDIATE_AMOUNT + perIteration * 23n ;
236
+ const first = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
237
+ expect ( first . value ) . toBe ( expectedFirst ) ;
238
+
239
+ // Advance the final month
240
+ simnet . mineEmptyBlocks ( months ( 1 ) ) ;
241
+
242
+ // Second claim: should transfer *exactly* the remainder
243
+ const expectedSecond = constants . INITIAL_MINT_AMOUNT - expectedFirst ;
244
+ expect ( expectedSecond + expectedFirst ) . toBe ( constants . INITIAL_MINT_AMOUNT ) ;
245
+ const second = txOk ( contract . claim ( ) , accounts . deployer . address ) ;
246
+ expect ( second . value ) . toBe ( expectedSecond ) ;
247
+
248
+ // Contract should now hold zero STX (no extras were ever deposited)
249
+ expect ( rov ( indirectContract . getBalance ( contract . identifier ) ) ) . toBe ( 0n ) ;
250
+ } ) ;
251
+
252
+ // -----------------------------------------------------------------------------
253
+ // Edge-case #5: Recipient change between deposits – new recipient should receive
254
+ // the next vested tranche *plus* freshly deposited STX.
255
+ // -----------------------------------------------------------------------------
256
+ test ( 'new recipient claims vested tranche plus extra deposit' , ( ) => {
257
+ mintInitial ( ) ;
258
+
259
+ // Deployer immediately claims the instantaneous 100 M
260
+ txOk ( contract . claim ( ) , accounts . deployer . address ) ;
261
+
262
+ // Mine one vesting iteration (1 month)
263
+ simnet . mineEmptyBlocks ( months ( 1 ) ) ;
264
+
265
+ // Update recipient to wallet_1
266
+ txOk ( contract . updateRecipient ( accounts . wallet_1 . address ) , accounts . deployer . address ) ;
267
+
268
+ // External party deposits 500 STX
269
+ const extraDeposit = 500n * 1000000n ;
270
+ mint ( extraDeposit ) ;
271
+
272
+ // Wallet_1 claims: should receive 1/24 of vesting bucket + 500 STX
273
+ const perIteration = constants . INITIAL_MINT_VESTING_AMOUNT / constants . INITIAL_MINT_VESTING_ITERATIONS ;
274
+ const expected = perIteration + extraDeposit ;
275
+ const receipt = txOk ( contract . claim ( ) , accounts . wallet_1 . address ) ;
276
+ expect ( receipt . value ) . toBe ( expected ) ;
277
+
278
+ // Validate transfer event
279
+ const [ evt ] = filterEvents ( receipt . events , CoreNodeEventType . StxTransferEvent ) ;
280
+ expect ( evt . data . amount ) . toBe ( expected . toString ( ) ) ;
281
+ expect ( evt . data . recipient ) . toBe ( accounts . wallet_1 . address ) ;
282
+ expect ( evt . data . sender ) . toBe ( contract . identifier ) ;
124
283
} ) ;
0 commit comments