@reserve-protocol/dtf-rebalance-lib 0.0.1 → 0.0.3

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.
@@ -1,1448 +0,0 @@
1
- import { bn } from '../src/numbers'
2
- import {
3
- PriceControl,
4
- PriceRange,
5
- RebalanceLimits,
6
- Rebalance,
7
- WeightRange,
8
- } from '../src/types'
9
- import { getBasketDistribution } from '../src/utils'
10
- import { OpenAuctionArgs, getOpenAuction } from '../src/open-auction'
11
- import { getStartRebalance } from '../src/start-rebalance'
12
- import { describe, it, beforeEach } from 'node:test'
13
- import { strict as assert } from 'node:assert'
14
-
15
- const PRECISION = bn('1e3') // 1-part-in-1000
16
-
17
- const assertApproxEq = (
18
- a: bigint,
19
- b: bigint,
20
- precision: bigint = PRECISION
21
- ) => {
22
- const delta = a > b ? a - b : b - a
23
- // console.log('assertApproxEq', a.toString(), b.toString()) // Keep for debugging if necessary
24
- assert(a >= b / precision, `Expected ${a} to be >= ${b / precision}`) // Ensure a is not far below b
25
- assert(delta <= b / precision, `Expected delta ${delta} to be <= ${b / precision}`) // Ensure difference is small relative to b
26
- // A more robust check might be delta <= max(abs(a), abs(b)) / precision, or handle b=0
27
- if (b !== 0n) {
28
- assert(delta <= (a > b ? a : b) / precision, `Expected delta ${delta} to be <= ${(a > b ? a : b) / precision}`) // Compare delta to the larger of a or b
29
- } else {
30
- assert(delta <= precision, `Expected delta ${delta} to be <= ${precision}`) // If b is 0, delta must be small
31
- }
32
- }
33
-
34
- const assertRangesEqual = (a: WeightRange, b: WeightRange) => {
35
- assertApproxEq(a.low, b.low)
36
- assertApproxEq(a.spot, b.spot)
37
- assertApproxEq(a.high, b.high)
38
- }
39
-
40
- const assertPricesEqual = (a: PriceRange, b: PriceRange) => {
41
- assertApproxEq(a.low, b.low)
42
- assertApproxEq(a.high, b.high)
43
- }
44
-
45
- const assertRebalanceLimitsEqual = (
46
- a: RebalanceLimits,
47
- b: RebalanceLimits,
48
- precision: bigint = PRECISION
49
- ) => {
50
- assertApproxEq(a.low, b.low, precision)
51
- assertApproxEq(a.spot, b.spot, precision)
52
- assertApproxEq(a.high, b.high, precision)
53
- }
54
-
55
- const assertOpenAuctionArgsEqual = (
56
- a: OpenAuctionArgs,
57
- b: OpenAuctionArgs,
58
- precision: bigint = PRECISION
59
- ) => {
60
- assert.equal(a.rebalanceNonce, b.rebalanceNonce)
61
- assert.deepEqual(a.tokens, b.tokens)
62
-
63
- assert.equal(a.newWeights.length, b.newWeights.length)
64
- for (let i = 0; i < a.newWeights.length; i++) {
65
- assertRangesEqual(a.newWeights[i], b.newWeights[i])
66
- }
67
-
68
- assert.equal(a.newPrices.length, b.newPrices.length)
69
- for (let i = 0; i < a.newPrices.length; i++) {
70
- // assertPricesEqual uses its own default precision, which is fine.
71
- assertPricesEqual(a.newPrices[i], b.newPrices[i])
72
- }
73
-
74
- assertRebalanceLimitsEqual(a.newLimits, b.newLimits, precision)
75
- }
76
-
77
- describe('NATIVE DTFs', () => {
78
- const supply = bn('1e21') // 1000 supply
79
- const auctionPriceError = [0.01, 0.01, 0.01] // Smaller price error for getOpenAuction
80
- const finalStageAtForTest = 0.95 // Standard finalStageAt
81
-
82
- // Common expected prices for tokens [USDC (6dec), DAI (18dec), USDT (6dec)]
83
- // when market prices are [1,1,1], auctionPriceError is [0.01,0.01,0.01], and priceControl=true,
84
- // and initialPrices allow this range.
85
- const defaultExpectedPrices_USDC_DAI_USDT: PriceRange[] = [
86
- { low: bn('9.9e20'), high: bn('1.01e21') }, // USDC (D27 from $1, 6dec)
87
- { low: bn('9.9e8'), high: bn('1.01e9') }, // DAI (D27 from $1, 18dec)
88
- { low: bn('9.9e20'), high: bn('1.01e21') }, // USDT (D27 from $1, 6dec)
89
- ]
90
-
91
- describe('Rebalancing from 100% USDC to 0% USDC, 50% DAI, 50% USDT', () => {
92
- const tokens = ['USDC', 'DAI', 'USDT']
93
- const decimalsS1 = [bn('6'), bn('18'), bn('6')]
94
- const initialMarketPricesS1 = [1, 1, 1]
95
- const priceErrorStartRebalanceS1 = [0.1, 0.1, 0.1]
96
- const dtfPriceS1 = 1
97
- const initialFolioS1 = [bn('1e6'), bn('0'), bn('0')] // Represents 1 USDC, 0 DAI, 0 USDT per share (approx value)
98
- const targetBasketS1 = [bn('0'), bn('0.5e18'), bn('0.5e18')]
99
- // Folio representing mid-progress for ejection tests: ~20% USDC, ~40% DAI, ~40% USDT by value
100
- const folioMidProgressS1 = [bn('0.2e6'), bn('0.4e18'), bn('0.4e6')]
101
- // Folio representing near completion for ejection tests: ~1% USDC, ~49.5% DAI, ~49.5% USDT by value
102
- const folioNearCompletionS1 = [bn('0.01e6'), bn('0.495e18'), bn('0.495e6')]
103
- // Folio for Step 6 (negligible ejection, high relative progression): USDC almost gone, DAI/USDT balanced
104
- const folioTrueMidS1_ActuallyHighProg = [
105
- bn('0.00001e6'),
106
- bn('0.2e18'),
107
- bn('0.2e6'),
108
- ]
109
- // Folio for Step 7: shareValue ~1.0. USDC negligible. DAI 0.8 val, USDT 0.2 val.
110
- // InitialProg=0. Progression for this folio = (min(0.8,0.5)+min(0.2,0.5))/1.0 = (0.5+0.2)/1.0 = 0.7.
111
- // relativeProgression = 0.7 < 0.93 -> delta=0.05.
112
- const folioStep7S1_varied_weights = [
113
- bn('0.00001e6'),
114
- bn('0.8e18'),
115
- bn('0.2e6'),
116
- ]
117
-
118
- let mockRebalanceBaseS1: Omit<Rebalance, 'priceControl'>
119
- let initialWeightsS1: WeightRange[],
120
- initialPricesS1: PriceRange[],
121
- initialLimitsS1: RebalanceLimits
122
-
123
- beforeEach(() => {
124
- const { weights, prices, limits } = getStartRebalance(
125
- supply,
126
- tokens,
127
- decimalsS1,
128
- targetBasketS1,
129
- initialMarketPricesS1,
130
- priceErrorStartRebalanceS1,
131
- dtfPriceS1,
132
- true // weightControl: true for NATIVE-style
133
- )
134
- initialWeightsS1 = weights
135
- initialPricesS1 = prices
136
- initialLimitsS1 = limits
137
- mockRebalanceBaseS1 = {
138
- nonce: 1n,
139
- tokens: tokens,
140
- weights: initialWeightsS1, // These are the NATIVE rebalance.weights used for clamping
141
- initialPrices: initialPricesS1,
142
- inRebalance: tokens.map(() => true),
143
- limits: initialLimitsS1, // NATIVE limits are {1e18, 1e18, 1e18}, crucial for newLimits clamping
144
- startedAt: 0n,
145
- restrictedUntil: 0n,
146
- availableUntil: 0n,
147
- }
148
- })
149
-
150
- it('Step 0: Verifies initial setup from getStartRebalance', () => {
151
- assert.equal(initialWeightsS1.length, 3)
152
- assert.equal(initialPricesS1.length, 3)
153
- assertRangesEqual(initialWeightsS1[0], {
154
- low: bn('0'),
155
- spot: bn('0'),
156
- high: bn('0'),
157
- }) // USDC
158
- assertRangesEqual(initialWeightsS1[1], {
159
- low: bn('454545454545454545454545455'), // 0.5 / 1.1 * 1e27
160
- spot: bn('500000000000000000000000000'), // 0.5 * 1e27
161
- high: bn('550000000000000000000000000'), // 0.5 * 1.1 * 1e27
162
- }) // DAI
163
- assertRangesEqual(initialWeightsS1[2], {
164
- low: bn('454545454545455'), // 0.5 / 1.1 * 1e15
165
- spot: bn('500000000000000'), // 0.5 * 1e15
166
- high: bn('550000000000000'), // 0.5 * 1.1 * 1e15
167
- }) // USDT
168
- assertRebalanceLimitsEqual(initialLimitsS1, {
169
- low: bn('1e18'),
170
- spot: bn('1e18'),
171
- high: bn('1e18'),
172
- })
173
- })
174
-
175
- it('Step 1: Ejection Phase (initial folio, priceControl=true, prices=[1,1,1])', () => {
176
- const mockRebalance: Rebalance = {
177
- ...mockRebalanceBaseS1,
178
- priceControl: PriceControl.PARTIAL,
179
- }
180
- const [openAuctionArgs] = getOpenAuction(
181
- mockRebalance,
182
- supply,
183
- initialFolioS1,
184
- targetBasketS1,
185
- initialFolioS1,
186
- decimalsS1,
187
- initialMarketPricesS1,
188
- auctionPriceError,
189
- finalStageAtForTest
190
- )
191
- assertOpenAuctionArgsEqual(openAuctionArgs, {
192
- rebalanceNonce: 1n,
193
- tokens: tokens,
194
- newWeights: [
195
- initialWeightsS1[0],
196
- {
197
- low: initialWeightsS1[1].spot,
198
- spot: initialWeightsS1[1].spot,
199
- high: initialWeightsS1[1].spot,
200
- },
201
- {
202
- low: initialWeightsS1[2].spot,
203
- spot: initialWeightsS1[2].spot,
204
- high: initialWeightsS1[2].spot,
205
- },
206
- ],
207
- newPrices: defaultExpectedPrices_USDC_DAI_USDT,
208
- newLimits: initialLimitsS1,
209
- })
210
- })
211
-
212
- it('Step 2: Ejection Phase (mid-progress folio with USDC to eject)', () => {
213
- const mockRebalance: Rebalance = {
214
- ...mockRebalanceBaseS1,
215
- priceControl: PriceControl.PARTIAL,
216
- }
217
- const [openAuctionArgs] = getOpenAuction(
218
- mockRebalance,
219
- supply,
220
- initialFolioS1,
221
- targetBasketS1,
222
- folioMidProgressS1,
223
- decimalsS1,
224
- initialMarketPricesS1,
225
- auctionPriceError,
226
- finalStageAtForTest
227
- )
228
- assertOpenAuctionArgsEqual(openAuctionArgs, {
229
- rebalanceNonce: 1n,
230
- tokens: tokens,
231
- newWeights: [
232
- initialWeightsS1[0],
233
- {
234
- low: initialWeightsS1[1].spot,
235
- spot: initialWeightsS1[1].spot,
236
- high: initialWeightsS1[1].spot,
237
- },
238
- {
239
- low: initialWeightsS1[2].spot,
240
- spot: initialWeightsS1[2].spot,
241
- high: initialWeightsS1[2].spot,
242
- },
243
- ],
244
- newPrices: defaultExpectedPrices_USDC_DAI_USDT,
245
- newLimits: initialLimitsS1,
246
- })
247
- })
248
-
249
- it('Step 3: Ejection Phase (near-completion folio with USDC to eject)', () => {
250
- const mockRebalance: Rebalance = {
251
- ...mockRebalanceBaseS1,
252
- priceControl: PriceControl.PARTIAL,
253
- }
254
- const [openAuctionArgs] = getOpenAuction(
255
- mockRebalance,
256
- supply,
257
- initialFolioS1,
258
- targetBasketS1,
259
- folioNearCompletionS1,
260
- decimalsS1,
261
- initialMarketPricesS1,
262
- auctionPriceError,
263
- finalStageAtForTest
264
- )
265
- assertOpenAuctionArgsEqual(openAuctionArgs, {
266
- rebalanceNonce: 1n,
267
- tokens: tokens,
268
- newWeights: [
269
- initialWeightsS1[0],
270
- {
271
- low: initialWeightsS1[1].spot,
272
- spot: initialWeightsS1[1].spot,
273
- high: initialWeightsS1[1].spot,
274
- },
275
- {
276
- low: initialWeightsS1[2].spot,
277
- spot: initialWeightsS1[2].spot,
278
- high: initialWeightsS1[2].spot,
279
- },
280
- ],
281
- newPrices: defaultExpectedPrices_USDC_DAI_USDT,
282
- newLimits: initialLimitsS1,
283
- })
284
- })
285
-
286
- it('Step 4: Ejection Phase (initial folio, priceControl=false, prices=[1,1,1])', () => {
287
- const mockRebalance: Rebalance = {
288
- ...mockRebalanceBaseS1,
289
- priceControl: PriceControl.NONE,
290
- }
291
- const [openAuctionArgs] = getOpenAuction(
292
- mockRebalance,
293
- supply,
294
- initialFolioS1,
295
- targetBasketS1,
296
- initialFolioS1,
297
- decimalsS1,
298
- initialMarketPricesS1,
299
- auctionPriceError,
300
- finalStageAtForTest
301
- )
302
- assertOpenAuctionArgsEqual(openAuctionArgs, {
303
- rebalanceNonce: 1n,
304
- tokens: tokens,
305
- newWeights: [
306
- initialWeightsS1[0],
307
- {
308
- low: initialWeightsS1[1].spot,
309
- spot: initialWeightsS1[1].spot,
310
- high: initialWeightsS1[1].spot,
311
- },
312
- {
313
- low: initialWeightsS1[2].spot,
314
- spot: initialWeightsS1[2].spot,
315
- high: initialWeightsS1[2].spot,
316
- },
317
- ],
318
- newPrices: initialPricesS1, // from mockRebalance due to priceControl=false
319
- newLimits: initialLimitsS1,
320
- })
321
- })
322
-
323
- it('Step 5: Ejection Phase (initial folio, USDC Price Loss 0.9, priceControl=true)', () => {
324
- const mockRebalance: Rebalance = {
325
- ...mockRebalanceBaseS1,
326
- priceControl: PriceControl.PARTIAL,
327
- }
328
- const pricesS1_loss = [0.9, 1, 1]
329
- const [openAuctionArgs] = getOpenAuction(
330
- mockRebalance,
331
- supply,
332
- initialFolioS1,
333
- targetBasketS1,
334
- initialFolioS1,
335
- decimalsS1,
336
- pricesS1_loss,
337
- auctionPriceError,
338
- finalStageAtForTest
339
- )
340
- const expectedNewPricesLoss: PriceRange[] = [
341
- { low: bn('9e20'), high: bn('9.09e20') },
342
- { low: bn('9.9e8'), high: bn('1.01e9') },
343
- { low: bn('9.9e20'), high: bn('1.01e21') },
344
- ]
345
- // Ideal spots for DAI/USDT scaled by 0.9 (due to USDC price drop impacting shareValue for idealWeight calc)
346
- // DAI idealSpot was 5e26, now 5e26*0.9 = 4.5e26. USDT idealSpot was 5e14, now 5e14*0.9 = 4.5e14.
347
- // Delta is 0 (ejection). So newWeights low/spot/high are these new ideal spots, clamped by rebalance.weights.
348
- // The new ideal spots (4.5e26, 4.5e14) are exactly the .low of their respective rebalance.weights, so clamping to .low results in this value.
349
- assertOpenAuctionArgsEqual(openAuctionArgs, {
350
- rebalanceNonce: 1n,
351
- tokens: tokens,
352
- newWeights: [
353
- initialWeightsS1[0], // USDC target 0
354
- { low: bn('454545454545454545454545455'), spot: bn('454545454545454545454545455'), high: bn('454545454545454545454545455') },
355
- { low: bn('454545454545455'), spot: bn('454545454545455'), high: bn('454545454545455') },
356
- ],
357
- newPrices: expectedNewPricesLoss,
358
- newLimits: initialLimitsS1,
359
- })
360
- })
361
-
362
- it('Step 6: Test Case: Negligible Ejection, High Relative Progression -> Delta=0', () => {
363
- const mockRebalance: Rebalance = {
364
- ...mockRebalanceBaseS1,
365
- priceControl: PriceControl.PARTIAL,
366
- }
367
- const [openAuctionArgs] = getOpenAuction(
368
- mockRebalance,
369
- supply,
370
- initialFolioS1,
371
- targetBasketS1,
372
- folioTrueMidS1_ActuallyHighProg,
373
- decimalsS1,
374
- initialMarketPricesS1,
375
- auctionPriceError,
376
- finalStageAtForTest
377
- )
378
- assertOpenAuctionArgsEqual(openAuctionArgs, {
379
- rebalanceNonce: 1n,
380
- tokens: tokens,
381
- newWeights: [
382
- initialWeightsS1[0],
383
- {
384
- low: initialWeightsS1[1].low,
385
- spot: initialWeightsS1[1].low,
386
- high: initialWeightsS1[1].low,
387
- },
388
- {
389
- low: initialWeightsS1[2].low,
390
- spot: initialWeightsS1[2].low,
391
- high: initialWeightsS1[2].low,
392
- },
393
- ],
394
- newPrices: defaultExpectedPrices_USDC_DAI_USDT,
395
- newLimits: initialLimitsS1,
396
- })
397
- })
398
-
399
- it('Step 7: NATIVE Mid-Rebalance (Multi-Asset Target, Negligible Ejection, Low Relative Progression -> Varied Weights)', () => {
400
- const mockRebalance: Rebalance = {
401
- ...mockRebalanceBaseS1,
402
- priceControl: PriceControl.PARTIAL,
403
- }
404
- // Using folioStep7S1_varied_weights: shareValue ~1.0, relativeProgression ~0.7 -> delta=0.05.
405
- // ideal_DAI/USDT_whole_spot ~0.5 (since shareValue*0.5 = 0.5).
406
- // This ideal_spot is same as initialWeightsS1[i].spot_whole.
407
- const [openAuctionArgs] = getOpenAuction(
408
- mockRebalance,
409
- supply,
410
- initialFolioS1,
411
- targetBasketS1,
412
- folioStep7S1_varied_weights,
413
- decimalsS1,
414
- initialMarketPricesS1,
415
- auctionPriceError,
416
- finalStageAtForTest
417
- )
418
- assertOpenAuctionArgsEqual(openAuctionArgs, {
419
- rebalanceNonce: 1n,
420
- tokens: tokens,
421
- newWeights: [
422
- initialWeightsS1[0], // USDC target 0
423
- // DAI: ideal_whole=0.5. calc_low=0.475, calc_spot=0.5, calc_high=0.525.
424
- // initialWeight_DAI (whole): low=0.45, spot=0.5, high=~0.555. All calculated values are within this range.
425
- { low: bn('4.75e26'), spot: bn('5e26'), high: bn('5.25e26') },
426
- // USDT: ideal_whole=0.5. calc_low=0.475, calc_spot=0.5, calc_high=0.525.
427
- // initialWeight_USDT (whole): low=0.45, spot=0.5, high=~0.555. All calculated values are within this range.
428
- { low: bn('4.75e14'), spot: bn('5e14'), high: bn('5.25e14') },
429
- ],
430
- newPrices: defaultExpectedPrices_USDC_DAI_USDT,
431
- newLimits: initialLimitsS1, // newLimits will be clamped to initial flat NATIVE limits
432
- })
433
- })
434
- })
435
-
436
- describe('Rebalancing from 0% USDC, 50% DAI, 50% USDT to 100% USDC', () => {
437
- const tokens = ['USDC', 'DAI', 'USDT']
438
- const decimalsS2 = [bn('6'), bn('18'), bn('6')]
439
- const initialMarketPricesS2 = [1, 1, 1]
440
- const priceErrorStartRebalanceS2 = [0.1, 0.1, 0.1]
441
- const dtfPriceS2 = 1
442
- // initialFolioS2: approx 0 USDC, 0.5 DAI val, 0.5 USDT val (total val 1 USD for 1 share)
443
- const initialFolioS2 = [bn('0'), bn('0.5e18'), bn('0.5e6')]
444
- const targetBasketS2 = [bn('1e18'), bn('0'), bn('0')] // Target 100% USDC
445
- // Folio for mid-progress ejection tests: ~40% USDC, ~30% DAI, ~30% USDT by value
446
- const folioMidProgressS2 = [bn('0.4e6'), bn('0.3e18'), bn('0.3e6')]
447
- // Folio for near completion ejection tests: ~98% USDC, ~1% DAI, ~1% USDT by value
448
- const folioTrulyNearCompletionS2 = [
449
- bn('0.98e6'),
450
- bn('0.01e18'),
451
- bn('0.01e6'),
452
- ]
453
- // Folio for Step 6 (negligible ejection, high relative progression): DAI/USDT almost gone
454
- const folioTrueMidS2_ActuallyHighProg = [
455
- bn('0.4e6'),
456
- bn('0.00001e18'),
457
- bn('0.00001e6'),
458
- ]
459
-
460
- let mockRebalanceBaseS2: Omit<Rebalance, 'priceControl'>
461
- let initialWeightsS2: WeightRange[],
462
- initialPricesS2: PriceRange[],
463
- initialLimitsS2: RebalanceLimits
464
-
465
- beforeEach(() => {
466
- const { weights, prices, limits } = getStartRebalance(
467
- supply,
468
- tokens,
469
- decimalsS2,
470
- targetBasketS2,
471
- initialMarketPricesS2,
472
- priceErrorStartRebalanceS2,
473
- dtfPriceS2,
474
- true
475
- )
476
- initialWeightsS2 = weights
477
- initialPricesS2 = prices
478
- initialLimitsS2 = limits
479
- mockRebalanceBaseS2 = {
480
- nonce: 2n, // Different nonce for this scenario suite
481
- tokens: tokens,
482
- weights: initialWeightsS2,
483
- initialPrices: initialPricesS2,
484
- inRebalance: tokens.map(() => true),
485
- limits: initialLimitsS2,
486
- startedAt: 0n,
487
- restrictedUntil: 0n,
488
- availableUntil: 0n,
489
- }
490
- })
491
-
492
- it('Step 0: Verifies initial setup from getStartRebalance', () => {
493
- assert.equal(initialWeightsS2.length, 3)
494
- // USDC target 100%
495
- assertRangesEqual(initialWeightsS2[0], {
496
- low: bn('909090909090909'), // 1.0 / 1.1 * 1e15
497
- spot: bn('1000000000000000'), // 1.0 * 1e15
498
- high: bn('1100000000000000'), // 1.0 * 1.1 * 1e15
499
- })
500
- assertRangesEqual(initialWeightsS2[1], {
501
- low: bn('0'),
502
- spot: bn('0'),
503
- high: bn('0'),
504
- }) // DAI target 0%
505
- assertRangesEqual(initialWeightsS2[2], {
506
- low: bn('0'),
507
- spot: bn('0'),
508
- high: bn('0'),
509
- }) // USDT target 0%
510
- assertRebalanceLimitsEqual(initialLimitsS2, {
511
- low: bn('1e18'),
512
- spot: bn('1e18'),
513
- high: bn('1e18'),
514
- })
515
- })
516
-
517
- it('Step 1: Ejection Phase (initial folio, priceControl=true, prices=[1,1,1])', () => {
518
- const mockRebalance: Rebalance = {
519
- ...mockRebalanceBaseS2,
520
- priceControl: PriceControl.PARTIAL,
521
- }
522
- const [openAuctionArgs] = getOpenAuction(
523
- mockRebalance,
524
- supply,
525
- initialFolioS2,
526
- targetBasketS2,
527
- initialFolioS2,
528
- decimalsS2,
529
- initialMarketPricesS2,
530
- auctionPriceError,
531
- finalStageAtForTest
532
- )
533
- assertOpenAuctionArgsEqual(openAuctionArgs, {
534
- rebalanceNonce: 2n,
535
- tokens: tokens,
536
- newWeights: [
537
- {
538
- low: initialWeightsS2[0].spot,
539
- spot: initialWeightsS2[0].spot,
540
- high: initialWeightsS2[0].spot,
541
- },
542
- initialWeightsS2[1],
543
- initialWeightsS2[2],
544
- ],
545
- newPrices: defaultExpectedPrices_USDC_DAI_USDT,
546
- newLimits: initialLimitsS2,
547
- })
548
- })
549
-
550
- it('Step 2: Ejection Phase (mid-progress folio with DAI/USDT to eject)', () => {
551
- const mockRebalance: Rebalance = {
552
- ...mockRebalanceBaseS2,
553
- priceControl: PriceControl.PARTIAL,
554
- }
555
- const [openAuctionArgs] = getOpenAuction(
556
- mockRebalance,
557
- supply,
558
- initialFolioS2,
559
- targetBasketS2,
560
- folioMidProgressS2,
561
- decimalsS2,
562
- initialMarketPricesS2,
563
- auctionPriceError,
564
- finalStageAtForTest
565
- )
566
- assertOpenAuctionArgsEqual(openAuctionArgs, {
567
- rebalanceNonce: 2n,
568
- tokens: tokens,
569
- newWeights: [
570
- {
571
- low: initialWeightsS2[0].spot,
572
- spot: initialWeightsS2[0].spot,
573
- high: initialWeightsS2[0].spot,
574
- },
575
- initialWeightsS2[1],
576
- initialWeightsS2[2],
577
- ],
578
- newPrices: defaultExpectedPrices_USDC_DAI_USDT,
579
- newLimits: initialLimitsS2,
580
- })
581
- })
582
-
583
- it('Step 3: Ejection Phase (near-completion folio with DAI/USDT to eject)', () => {
584
- const mockRebalance: Rebalance = {
585
- ...mockRebalanceBaseS2,
586
- priceControl: PriceControl.PARTIAL,
587
- }
588
- const [openAuctionArgs] = getOpenAuction(
589
- mockRebalance,
590
- supply,
591
- initialFolioS2,
592
- targetBasketS2,
593
- folioTrulyNearCompletionS2,
594
- decimalsS2,
595
- initialMarketPricesS2,
596
- auctionPriceError,
597
- finalStageAtForTest
598
- )
599
- assertOpenAuctionArgsEqual(openAuctionArgs, {
600
- rebalanceNonce: 2n,
601
- tokens: tokens,
602
- newWeights: [
603
- {
604
- low: initialWeightsS2[0].spot,
605
- spot: initialWeightsS2[0].spot,
606
- high: initialWeightsS2[0].spot,
607
- },
608
- initialWeightsS2[1],
609
- initialWeightsS2[2],
610
- ],
611
- newPrices: defaultExpectedPrices_USDC_DAI_USDT,
612
- newLimits: initialLimitsS2,
613
- })
614
- })
615
-
616
- it('Step 4: Ejection Phase (initial folio, USDC Price Drop 0.9 - Gain for Buyer, priceControl=true)', () => {
617
- const mockRebalance: Rebalance = {
618
- ...mockRebalanceBaseS2,
619
- priceControl: PriceControl.PARTIAL,
620
- }
621
- const pricesS2_USDCdrop = [0.9, 1, 1] // USDC price drops, good for us as we target USDC
622
- const [openAuctionArgs] = getOpenAuction(
623
- mockRebalance,
624
- supply,
625
- initialFolioS2,
626
- targetBasketS2,
627
- initialFolioS2,
628
- decimalsS2,
629
- pricesS2_USDCdrop,
630
- auctionPriceError,
631
- finalStageAtForTest
632
- )
633
- // Expected: rebalanceTarget=1, delta=0. idealWeight for USDC changes due to its price drop.
634
- // shareValue (of initialFolioS2) = 0.5*1 (DAI) + 0.5*1 (USDT) = 1 (approx, using scaled folio values)
635
- // idealSpotWeight_USDC = shareValue * targetBasket_USDC[0] / actualLimits.spot / prices_USDC[0.9]
636
- // idealSpot_USDC_D27 was 1e15 at price 1. At price 0.9, idealSpot_D27 becomes 1e15 / 0.9 = 1.111...e15.
637
- const expectedNewPricesGainUSDC: PriceRange[] = [
638
- { low: bn('9e20'), high: bn('9.09e20') },
639
- { low: bn('9.9e8'), high: bn('1.01e9') },
640
- { low: bn('9.9e20'), high: bn('1.01e21') },
641
- ]
642
- // This new ideal spot (1.111...e15) is clamped by initialWeightsS2[0].high (1.11111e15).
643
- assertOpenAuctionArgsEqual(openAuctionArgs, {
644
- rebalanceNonce: 2n,
645
- tokens: tokens,
646
- newWeights: [
647
- {
648
- low: bn('1100000000000000'),
649
- spot: bn('1100000000000000'),
650
- high: bn('1100000000000000'),
651
- },
652
- initialWeightsS2[1],
653
- initialWeightsS2[2],
654
- ],
655
- newPrices: expectedNewPricesGainUSDC,
656
- newLimits: initialLimitsS2,
657
- })
658
- })
659
-
660
- it('Step 5: Ejection Phase (initial folio, USDC Price Rise 1.1 - Loss for Buyer, priceControl=true)', () => {
661
- const mockRebalance: Rebalance = {
662
- ...mockRebalanceBaseS2,
663
- priceControl: PriceControl.PARTIAL,
664
- }
665
- const pricesS2_USDCrise = [1.1, 1, 1]
666
- const [openAuctionArgs] = getOpenAuction(
667
- mockRebalance,
668
- supply,
669
- initialFolioS2,
670
- targetBasketS2,
671
- initialFolioS2,
672
- decimalsS2,
673
- pricesS2_USDCrise,
674
- auctionPriceError,
675
- finalStageAtForTest
676
- )
677
- // Expected: rebalanceTarget=1, delta=0. idealWeight for USDC changes.
678
- // idealSpot_USDC_D27 was 1e15 at price 1. At price 1.1, idealSpot_D27 becomes 1e15 / 1.1 = 9.09091e14.
679
- const expectedNewPricesLossUSDC: PriceRange[] = [
680
- { low: bn('1.089e21'), high: bn('1.1e21') },
681
- { low: bn('9.9e8'), high: bn('1.01e9') },
682
- { low: bn('9.9e20'), high: bn('1.01e21') },
683
- ]
684
- // This new ideal spot (9.09091e14) is clamped by initialWeightsS2[0].low (9e14), so becomes 9.09091e14.
685
- assertOpenAuctionArgsEqual(openAuctionArgs, {
686
- rebalanceNonce: 2n,
687
- tokens: tokens,
688
- newWeights: [
689
- {
690
- low: bn('909090909090909'),
691
- spot: bn('909090909090909'),
692
- high: bn('909090909090909'),
693
- },
694
- initialWeightsS2[1],
695
- initialWeightsS2[2],
696
- ],
697
- newPrices: expectedNewPricesLossUSDC,
698
- newLimits: initialLimitsS2,
699
- })
700
- })
701
-
702
- it('Step 6: Test Case: Negligible Ejection, High Relative Progression (Single Target Asset) -> Delta=0', () => {
703
- const mockRebalance: Rebalance = {
704
- ...mockRebalanceBaseS2,
705
- priceControl: PriceControl.PARTIAL,
706
- }
707
- const [openAuctionArgs] = getOpenAuction(
708
- mockRebalance,
709
- supply,
710
- initialFolioS2,
711
- targetBasketS2,
712
- folioTrueMidS2_ActuallyHighProg,
713
- decimalsS2,
714
- initialMarketPricesS2,
715
- auctionPriceError,
716
- finalStageAtForTest
717
- )
718
- assertOpenAuctionArgsEqual(openAuctionArgs, {
719
- rebalanceNonce: 2n,
720
- tokens: tokens,
721
- newWeights: [
722
- { low: bn('909090909090909'), spot: bn('909090909090909'), high: bn('909090909090909') },
723
- initialWeightsS2[1],
724
- initialWeightsS2[2],
725
- ],
726
- newPrices: defaultExpectedPrices_USDC_DAI_USDT,
727
- newLimits: initialLimitsS2,
728
- })
729
- })
730
- })
731
-
732
- it('volatiles: [75%, 25%]', () => {
733
- const tokens = ['USDC', 'ETH']
734
- const decimals = [bn('6'), bn('18')]
735
- const prices = [1, 3000]
736
- const priceError = [0.1, 0.1]
737
- const targetBasket = [bn('0.75e18'), bn('0.25e18')]
738
- const {
739
- weights: newWeights,
740
- prices: newPricesResult, // renamed to avoid clash
741
- limits: newLimitsResult, // renamed
742
- } = getStartRebalance(
743
- supply,
744
- tokens,
745
- decimals,
746
- targetBasket,
747
- prices,
748
- priceError,
749
- 1, // dtfPrice
750
- true // weightControl: true
751
- )
752
- assert.equal(newWeights.length, 2)
753
- assert.equal(newPricesResult.length, 2)
754
-
755
- assertRangesEqual(newWeights[0], {
756
- // USDC
757
- low: bn('681818181818182'), // 0.75 / 1.1 * 1e15
758
- spot: bn('750000000000000'), // 0.75 * 1e15
759
- high: bn('825000000000000'), // 0.75 * 1.1 * 1e15
760
- })
761
- assertRangesEqual(newWeights[1], {
762
- // ETH
763
- low: bn('75757575757575757575758'), // (0.25/3000) / 1.1 * 1e27
764
- spot: bn('83333333333333333333333'), // (0.25/3000) * 1e27
765
- high: bn('91666666666666666666667'), // (0.25/3000) * 1.1 * 1e27
766
- })
767
-
768
- assertPricesEqual(newPricesResult[0], {
769
- low: bn('9e20'), // 1 * 0.9 * 1e21
770
- high: bn('1100000000000000000000'), // 1 * 1.1 * 1e21
771
- })
772
- assertPricesEqual(newPricesResult[1], {
773
- low: bn('2700000000000'), // 3000 * 0.9 * 1e9
774
- high: bn('3300000000000'), // 3000 * 1.1 * 1e9
775
- })
776
- assertRebalanceLimitsEqual(newLimitsResult, {
777
- low: bn('1e18'),
778
- spot: bn('1e18'),
779
- high: bn('1e18'),
780
- })
781
- })
782
-
783
- it('volatiles: fuzz', () => {
784
- for (let i = 0; i < 100; i++) {
785
- // Reduced iterations for faster tests
786
- const tokensList = [
787
- ['USDC', 'DAI', 'WETH', 'WBTC'],
788
- ['SOL', 'BONK'],
789
- ]
790
- const currentTokens = tokensList[i % tokensList.length]
791
-
792
- const decimalsMap: { [key: string]: bigint } = {
793
- USDC: bn('6'),
794
- DAI: bn('18'),
795
- WETH: bn('18'),
796
- WBTC: bn('8'),
797
- SOL: bn('9'),
798
- BONK: bn('5'),
799
- }
800
- const currentDecimals = currentTokens.map((t) => decimalsMap[t])
801
-
802
- const bals = currentTokens.map(
803
- (_) => bn(Math.round(Math.random() * 1e20).toString()) // Reduced scale
804
- )
805
- const prices = currentTokens.map((_) =>
806
- Math.max(0.01, Math.random() * 1e4)
807
- ) // Ensure positive prices
808
- const priceError = currentTokens.map((_) =>
809
- Math.max(0.001, Math.min(0.5, Math.random() * 0.2))
810
- ) // Realistic price error 0.001 to 0.2
811
-
812
- const targetBasket = getBasketDistribution(bals, prices, currentDecimals)
813
-
814
- const {
815
- weights: newWeights,
816
- prices: newPricesResult,
817
- limits: newLimitsResult,
818
- } = getStartRebalance(
819
- supply,
820
- currentTokens,
821
- currentDecimals,
822
- targetBasket,
823
- prices,
824
- priceError,
825
- prices[0] || 1, // dtfPrice, use first token's price or 1
826
- true // weightControl: true
827
- )
828
- assert.equal(newWeights.length, currentTokens.length)
829
- assert.equal(newPricesResult.length, currentTokens.length)
830
- assert(newLimitsResult !== undefined, 'newLimitsResult should be defined')
831
- }
832
- })
833
- })
834
-
835
- describe('TRACKING DTF Rebalance: USDC -> DAI/USDT Sequence', () => {
836
- const supply = bn('1e21') // 1000 supply
837
- const tokens = ['USDC', 'DAI', 'USDT']
838
- const decimals = [bn('6'), bn('18'), bn('6')]
839
- const initialMarketPrices = [1, 1, 1]
840
- const priceErrorStartRebalance = [0.1, 0.1, 0.1] // For getStartRebalance limits
841
- const dtfPrice = 1
842
- const targetBasketUSDCtoDAIUST = [bn('0'), bn('5e17'), bn('5e17')] // Target 0% USDC, 50% DAI, 50% USDT
843
- const auctionPriceErrorSmall = [0.01, 0.01, 0.01] // For getOpenAuction price calcs
844
- const finalStageAtForTest = 0.95 // Standard finalStageAt
845
-
846
- // Step 0: getStartRebalance for TRACKING DTF
847
- const _folioUSDCStart = [bn('1e6'), bn('0'), bn('0')] // 100% USDC, use as initialFolio for this sequence
848
-
849
- const {
850
- weights: initialWeightsTracking,
851
- prices: initialPricesTracking,
852
- limits: initialLimitsTracking,
853
- } = getStartRebalance(
854
- supply,
855
- tokens,
856
- decimals,
857
- targetBasketUSDCtoDAIUST,
858
- initialMarketPrices,
859
- priceErrorStartRebalance,
860
- dtfPrice,
861
- false // weightControl: false for TRACKING-style weights and limits
862
- )
863
-
864
- it('Step 0: Verifies initial setup from getStartRebalance (TRACKING)', () => {
865
- // totalPortion = (0*0.1) + (0.5*0.1) + (0.5*0.1) = 0.1
866
- // expectedLowLimit = (1 / (1 + 0.1)) * 1e18 = (1/1.1) * 1e18
867
- // expectedHighLimit = (1 + 0.1) * 1e18 = 1.1 * 1e18
868
- assertRebalanceLimitsEqual(initialLimitsTracking, {
869
- low: bn('909090909090909091'), // (1/1.1) * 1e18
870
- spot: bn('1000000000000000000'),
871
- high: bn('1100000000000000000'), // 1.1 * 1e18
872
- })
873
-
874
- // For TRACKING, weights low/spot/high are identical
875
- assertRangesEqual(initialWeightsTracking[0], {
876
- low: bn('0'),
877
- spot: bn('0'),
878
- high: bn('0'),
879
- }) // USDC
880
- assertRangesEqual(initialWeightsTracking[1], {
881
- low: bn('5e26'),
882
- spot: bn('5e26'),
883
- high: bn('5e26'),
884
- }) // DAI
885
- assertRangesEqual(initialWeightsTracking[2], {
886
- low: bn('5e14'),
887
- spot: bn('5e14'),
888
- high: bn('5e14'),
889
- }) // USDT
890
-
891
- // Prices are same as NATIVE calculation initially
892
- assertPricesEqual(initialPricesTracking[0], {
893
- low: bn('9e20'), // 1 * 0.9 * 1e21
894
- high: bn('1100000000000000000000'), // 1 * 1.1 * 1e21
895
- })
896
- assertPricesEqual(initialPricesTracking[1], {
897
- low: bn('900000000'), // 1 * 0.9 * 1e9
898
- high: bn('1100000000'), // 1 * 1.1 * 1e9
899
- })
900
- assertPricesEqual(initialPricesTracking[2], {
901
- low: bn('9e20'), // 1 * 0.9 * 1e21
902
- high: bn('1100000000000000000000'), // 1 * 1.1 * 1e21
903
- })
904
- })
905
-
906
- const mockRebalanceBase: Omit<Rebalance, 'priceControl'> = {
907
- nonce: 2n, // Different nonce for this suite
908
- tokens: tokens,
909
- weights: initialWeightsTracking,
910
- initialPrices: initialPricesTracking,
911
- inRebalance: tokens.map(() => true),
912
- limits: initialLimitsTracking,
913
- startedAt: 0n,
914
- restrictedUntil: 0n,
915
- availableUntil: 0n,
916
- }
917
-
918
- it('Step 1: Auction for Ejection Phase', () => {
919
- const _folio1 = _folioUSDCStart // 100% USDC, needs ejection
920
- const currentMarketPrices1 = [1, 1, 1]
921
- const mockRebalance1: Rebalance = {
922
- ...mockRebalanceBase,
923
- priceControl: PriceControl.PARTIAL,
924
- }
925
-
926
- const [openAuctionArgs1] = getOpenAuction(
927
- mockRebalance1,
928
- supply,
929
- _folioUSDCStart, // _initialFolio
930
- targetBasketUSDCtoDAIUST,
931
- _folio1, // current _folio
932
- decimals,
933
- currentMarketPrices1,
934
- auctionPriceErrorSmall,
935
- finalStageAtForTest
936
- )
937
- // Expected: buyTarget=1 (ejection). idealSpotLimit=1. limitDelta=0.
938
- // newLimits before clamp: {1e18,1e18,1e18}. After initialLimitsTracking clamp: {1e18,1e18,1e18}
939
- assertRebalanceLimitsEqual(openAuctionArgs1.newLimits, {
940
- low: bn('1e18'),
941
- spot: bn('1e18'),
942
- high: bn('1e18'),
943
- })
944
- // For TRACKING, newWeights from getOpenAuction are clamped to initialWeightsTracking.spot values
945
- assert.deepEqual(openAuctionArgs1.newWeights, [
946
- { low: bn('0'), spot: bn('0'), high: bn('0') },
947
- { low: bn('5e26'), spot: bn('5e26'), high: bn('5e26') },
948
- { low: bn('5e14'), spot: bn('5e14'), high: bn('5e14') },
949
- ])
950
-
951
- const expectedNewPrices1: PriceRange[] = [
952
- { low: bn('9.9e20'), high: bn('1.01e21') }, // USDC
953
- { low: bn('9.9e8'), high: bn('1.01e9') }, // DAI
954
- { low: bn('9.9e20'), high: bn('1.01e21') }, // USDT
955
- ]
956
- assertPricesEqual(openAuctionArgs1.newPrices[0], expectedNewPrices1[0])
957
- assertPricesEqual(openAuctionArgs1.newPrices[1], expectedNewPrices1[1])
958
- assertPricesEqual(openAuctionArgs1.newPrices[2], expectedNewPrices1[2])
959
- })
960
-
961
- it('Step 2: Auction for Mid-Rebalance (progression < finalStageAt)', () => {
962
- // Folio: 0 USDC, 0.3 DAI (whole), 0.7 USDT (whole). shareValue = 1.
963
- // targetBasketDec = [0, 0.5, 0.5]. prices = [1,1,1].
964
- // DAI: expectedInBU = 1*0.5/1 = 0.5. actual = 0.3. balanceInBU = 0.3. value = 0.3.
965
- // USDT: expectedInBU = 1*0.5/1 = 0.5. actual = 0.7. balanceInBU = 0.5. value = 0.5.
966
- // progression = (0.3+0.5)/1 = 0.8. initialProgression (from _folioUSDCStart) = 0.
967
- // relativeProgression = (0.8 - 0) / (1 - 0) = 0.8.
968
- // finalStageAt = 0.95. threshold = 0.95 - 0.02 = 0.93. 0.8 < 0.93 is TRUE.
969
- const _folio2 = [bn('0'), bn('3e17'), bn('7e5')] // Corresponds to 0.3 DAI, 0.7 USDT, total value $1
970
- const currentMarketPrices2 = [1, 1, 1]
971
- const mockRebalance2: Rebalance = {
972
- ...mockRebalanceBase,
973
- priceControl: PriceControl.PARTIAL,
974
- }
975
-
976
- const [openAuctionArgs2] = getOpenAuction(
977
- mockRebalance2,
978
- supply,
979
- _folioUSDCStart, // _initialFolio
980
- targetBasketUSDCtoDAIUST,
981
- _folio2, // current _folio
982
- decimals,
983
- currentMarketPrices2,
984
- auctionPriceErrorSmall,
985
- finalStageAtForTest
986
- )
987
-
988
- // Expected: buyTarget=0.95 (finalStageAt). idealSpotLimit=1. limitDelta=0.05.
989
- // newLimits pre-clamp: low=0.95e18, spot=1e18, high=1.05e18.
990
- // Clamped by initialLimitsTracking (9e17,1e18,1.11111e18):
991
- // low=max(0.95e18,9e17)=9.5e17. spot=1e18. high=min(1.05e18,1.11111e18)=1.05e18.
992
- assertRebalanceLimitsEqual(openAuctionArgs2.newLimits, {
993
- low: bn('9.5e17'),
994
- spot: bn('1e18'),
995
- high: bn('1.05e18'),
996
- })
997
- assert.deepEqual(openAuctionArgs2.newWeights, [
998
- { low: bn('0'), spot: bn('0'), high: bn('0') },
999
- { low: bn('5e26'), spot: bn('5e26'), high: bn('5e26') },
1000
- { low: bn('5e14'), spot: bn('5e14'), high: bn('5e14') },
1001
- ])
1002
- // Prices same as step 1 if market prices didn't change
1003
- assertPricesEqual(openAuctionArgs2.newPrices[0], {
1004
- low: bn('9.9e20'),
1005
- high: bn('1.01e21'),
1006
- })
1007
- assertPricesEqual(openAuctionArgs2.newPrices[1], {
1008
- low: bn('9.9e8'),
1009
- high: bn('1.01e9'),
1010
- })
1011
- assertPricesEqual(openAuctionArgs2.newPrices[2], {
1012
- low: bn('9.9e20'),
1013
- high: bn('1.01e21'),
1014
- })
1015
- })
1016
-
1017
- it('Step 3: Auction for Trading to Completion (progression >= finalStageAt)', () => {
1018
- // Folio: 0 USDC, 0.48 DAI (whole), 0.52 USDT (whole). shareValue = 1.
1019
- // DAI: exp=0.5, actual=0.48, inBU=0.48. USDT: exp=0.5, actual=0.52, inBU=0.5.
1020
- // progression = (0.48+0.5)/1 = 0.98. initialProgression = 0.
1021
- // relativeProgression = (0.98 - 0) / (1-0) = 0.98.
1022
- // finalStageAt = 0.95. threshold = 0.95 - 0.02 = 0.93. 0.98 < 0.93 is FALSE.
1023
- const _folio3 = [bn('0'), bn('4.8e17'), bn('5.2e5')] // Corresponds to 0.48 DAI, 0.52 USDT, total value $1
1024
- const currentMarketPrices3 = [1, 1, 1]
1025
- const mockRebalance3: Rebalance = {
1026
- ...mockRebalanceBase,
1027
- priceControl: PriceControl.PARTIAL,
1028
- }
1029
-
1030
- const [openAuctionArgs3] = getOpenAuction(
1031
- mockRebalance3,
1032
- supply,
1033
- _folioUSDCStart, // _initialFolio
1034
- targetBasketUSDCtoDAIUST,
1035
- _folio3, // current _folio
1036
- decimals,
1037
- currentMarketPrices3,
1038
- auctionPriceErrorSmall,
1039
- finalStageAtForTest
1040
- )
1041
-
1042
- // Expected: buyTarget=1. idealSpotLimit=1. limitDelta=0.
1043
- // newLimits pre-clamp: low=1e18, spot=1e18, high=1e18.
1044
- // Clamped by initialLimitsTracking (9e17,1e18,1.11111e18) -> no change.
1045
- assertRebalanceLimitsEqual(openAuctionArgs3.newLimits, {
1046
- low: bn('1e18'),
1047
- spot: bn('1e18'),
1048
- high: bn('1e18'),
1049
- })
1050
- assert.deepEqual(openAuctionArgs3.newWeights, [
1051
- { low: bn('0'), spot: bn('0'), high: bn('0') },
1052
- { low: bn('5e26'), spot: bn('5e26'), high: bn('5e26') },
1053
- { low: bn('5e14'), spot: bn('5e14'), high: bn('5e14') },
1054
- ])
1055
- // Prices same as step 1 if market prices didn't change
1056
- assertPricesEqual(openAuctionArgs3.newPrices[0], {
1057
- low: bn('9.9e20'),
1058
- high: bn('1.01e21'),
1059
- })
1060
- assertPricesEqual(openAuctionArgs3.newPrices[1], {
1061
- low: bn('9.9e8'),
1062
- high: bn('1.01e9'),
1063
- })
1064
- assertPricesEqual(openAuctionArgs3.newPrices[2], {
1065
- low: bn('9.9e20'),
1066
- high: bn('1.01e21'),
1067
- })
1068
- })
1069
- })
1070
-
1071
- describe('Hybrid Rebalance Scenario (Manually Constructed Rebalance Object)', () => {
1072
- const supply = bn('1e21')
1073
- const tokens = ['USDC', 'DAI', 'USDT']
1074
- const decimals = [bn('6'), bn('18'), bn('6')]
1075
- const auctionPriceErrorSmall = [0.01, 0.01, 0.01]
1076
- const finalStageAtForTest = 0.95
1077
- const targetBasketHybrid = [bn('0'), bn('5e17'), bn('5e17')]
1078
- const hybridWeights: WeightRange[] = [
1079
- { low: bn('0'), spot: bn('0'), high: bn('0') },
1080
- { low: bn('4.5e26'), spot: bn('5e26'), high: bn('5.55556e26') },
1081
- { low: bn('4.5e14'), spot: bn('5e14'), high: bn('5.55556e14') },
1082
- ]
1083
- const hybridInitialPrices: PriceRange[] = [
1084
- { low: bn('9e20'), high: bn('1.11111e21') },
1085
- { low: bn('9e8'), high: bn('1.11111e9') },
1086
- { low: bn('9e20'), high: bn('1.11111e21') },
1087
- ]
1088
- const hybridLimits_veryWide: RebalanceLimits = {
1089
- low: bn('1'),
1090
- spot: bn('1e18'),
1091
- high: bn('1e36'),
1092
- }
1093
- const mockRebalanceHybridBase: Omit<Rebalance, 'priceControl'> = {
1094
- nonce: 3n,
1095
- tokens: tokens,
1096
- weights: hybridWeights,
1097
- initialPrices: hybridInitialPrices,
1098
- limits: hybridLimits_veryWide,
1099
- inRebalance: tokens.map(() => true),
1100
- startedAt: 0n,
1101
- restrictedUntil: 0n,
1102
- availableUntil: 0n,
1103
- }
1104
- const currentMarketPrices_Hybrid = [1, 1, 1] // Defined for this scope
1105
-
1106
- const defaultPricesHybridScope: PriceRange[] = [
1107
- { low: bn('9.9e20'), high: bn('1.01e21') }, // USDC
1108
- { low: bn('9.9e8'), high: bn('1.01e9') }, // DAI
1109
- { low: bn('9.9e20'), high: bn('1.01e21') }, // USDT
1110
- ]
1111
-
1112
- it('Hybrid Scenario 1: Mid-Rebalance (progression < finalStageAt)', () => {
1113
- const _folio = [bn('0'), bn('3e17'), bn('7e5')]
1114
- const mockRebalanceHybrid: Rebalance = {
1115
- ...mockRebalanceHybridBase,
1116
- priceControl: PriceControl.PARTIAL,
1117
- }
1118
- const [openAuctionArgs] = getOpenAuction(
1119
- mockRebalanceHybrid,
1120
- supply,
1121
- _folio,
1122
- targetBasketHybrid,
1123
- _folio,
1124
- decimals,
1125
- currentMarketPrices_Hybrid,
1126
- auctionPriceErrorSmall,
1127
- finalStageAtForTest
1128
- )
1129
- assertRebalanceLimitsEqual(openAuctionArgs.newLimits, {
1130
- low: bn('9.5e17'),
1131
- spot: bn('1e18'),
1132
- high: bn('1.05e18'),
1133
- })
1134
- assert.deepEqual(openAuctionArgs.newWeights, [
1135
- { low: bn('0'), spot: bn('0'), high: bn('0') },
1136
- { low: bn('5e26'), spot: bn('5e26'), high: bn('5e26') },
1137
- { low: bn('5e14'), spot: bn('5e14'), high: bn('5e14') },
1138
- ])
1139
- assertPricesEqual(openAuctionArgs.newPrices[0], defaultPricesHybridScope[0])
1140
- assertPricesEqual(openAuctionArgs.newPrices[1], defaultPricesHybridScope[1])
1141
- assertPricesEqual(openAuctionArgs.newPrices[2], defaultPricesHybridScope[2])
1142
- })
1143
-
1144
- it('Hybrid Scenario 2: Near Completion (progression >= finalStageAt)', () => {
1145
- const _folio = [bn('0'), bn('4.8e17'), bn('5.2e5')]
1146
- const mockRebalanceHybrid: Rebalance = {
1147
- ...mockRebalanceHybridBase,
1148
- priceControl: PriceControl.PARTIAL,
1149
- }
1150
- const [openAuctionArgs] = getOpenAuction(
1151
- mockRebalanceHybrid,
1152
- supply,
1153
- _folio,
1154
- targetBasketHybrid,
1155
- _folio,
1156
- decimals,
1157
- currentMarketPrices_Hybrid,
1158
- auctionPriceErrorSmall,
1159
- finalStageAtForTest
1160
- )
1161
- assertRebalanceLimitsEqual(openAuctionArgs.newLimits, {
1162
- low: bn('9.5e17'),
1163
- spot: bn('1e18'),
1164
- high: bn('1.05e18'),
1165
- })
1166
- assert.deepEqual(openAuctionArgs.newWeights, [
1167
- { low: bn('0'), spot: bn('0'), high: bn('0') },
1168
- { low: bn('5e26'), spot: bn('5e26'), high: bn('5e26') },
1169
- { low: bn('5e14'), spot: bn('5e14'), high: bn('5e14') },
1170
- ])
1171
- assertPricesEqual(openAuctionArgs.newPrices[0], defaultPricesHybridScope[0])
1172
- assertPricesEqual(openAuctionArgs.newPrices[1], defaultPricesHybridScope[1])
1173
- assertPricesEqual(openAuctionArgs.newPrices[2], defaultPricesHybridScope[2])
1174
- })
1175
-
1176
- it('Hybrid Scenario 3: Custom finalStageAt (0.8) - Round 1 & Round 2', () => {
1177
- const finalStageAtCustom = 0.8
1178
- const mockRebalanceHybridCustom: Rebalance = {
1179
- ...mockRebalanceHybridBase,
1180
- priceControl: PriceControl.PARTIAL,
1181
- }
1182
- const _folioRound1 = [bn('0'), bn('2e17'), bn('8e5')]
1183
- const [openAuctionArgsCustomRound1] = getOpenAuction(
1184
- mockRebalanceHybridCustom,
1185
- supply,
1186
- _folioRound1,
1187
- targetBasketHybrid,
1188
- _folioRound1,
1189
- decimals,
1190
- currentMarketPrices_Hybrid,
1191
- auctionPriceErrorSmall,
1192
- finalStageAtCustom
1193
- )
1194
- assertRebalanceLimitsEqual(openAuctionArgsCustomRound1.newLimits, {
1195
- low: bn('8e17'),
1196
- spot: bn('1e18'),
1197
- high: bn('1.2e18'),
1198
- })
1199
- assert.deepEqual(openAuctionArgsCustomRound1.newWeights, [
1200
- { low: bn('0'), spot: bn('0'), high: bn('0') },
1201
- { low: bn('5e26'), spot: bn('5e26'), high: bn('5e26') },
1202
- { low: bn('5e14'), spot: bn('5e14'), high: bn('5e14') },
1203
- ])
1204
- assertPricesEqual(
1205
- openAuctionArgsCustomRound1.newPrices[0],
1206
- defaultPricesHybridScope[0]
1207
- )
1208
- assertPricesEqual(
1209
- openAuctionArgsCustomRound1.newPrices[1],
1210
- defaultPricesHybridScope[1]
1211
- )
1212
- assertPricesEqual(
1213
- openAuctionArgsCustomRound1.newPrices[2],
1214
- defaultPricesHybridScope[2]
1215
- )
1216
-
1217
- const _folioRound2 = [bn('0'), bn('4e17'), bn('6e5')]
1218
- const [openAuctionArgsCustomRound2] = getOpenAuction(
1219
- mockRebalanceHybridCustom,
1220
- supply,
1221
- _folioRound1,
1222
- targetBasketHybrid,
1223
- _folioRound2,
1224
- decimals,
1225
- currentMarketPrices_Hybrid,
1226
- auctionPriceErrorSmall,
1227
- finalStageAtCustom
1228
- )
1229
- assertRebalanceLimitsEqual(openAuctionArgsCustomRound2.newLimits, {
1230
- low: bn('8e17'),
1231
- spot: bn('1e18'),
1232
- high: bn('1.2e18'),
1233
- })
1234
- assert.deepEqual(openAuctionArgsCustomRound2.newWeights, [
1235
- { low: bn('0'), spot: bn('0'), high: bn('0') },
1236
- { low: bn('5e26'), spot: bn('5e26'), high: bn('5e26') },
1237
- { low: bn('5e14'), spot: bn('5e14'), high: bn('5e14') },
1238
- ])
1239
- assertPricesEqual(
1240
- openAuctionArgsCustomRound2.newPrices[0],
1241
- defaultPricesHybridScope[0]
1242
- )
1243
- assertPricesEqual(
1244
- openAuctionArgsCustomRound2.newPrices[1],
1245
- defaultPricesHybridScope[1]
1246
- )
1247
- assertPricesEqual(
1248
- openAuctionArgsCustomRound2.newPrices[2],
1249
- defaultPricesHybridScope[2]
1250
- )
1251
- })
1252
-
1253
- it('Hybrid Scenario 4: Delta split between Limits and Weights', () => {
1254
- const _folioForS4 = [bn('0'), bn('2e17'), bn('8e5')] // 0 USDC, 0.2 DAI val, 0.8 USDT val; shareVal=1.0
1255
- const scenario4Limits: RebalanceLimits = {
1256
- low: bn('0.98e18'),
1257
- spot: bn('1e18'),
1258
- high: bn('1.02e18'),
1259
- }
1260
- const mockRebalanceHybrid4: Rebalance = {
1261
- ...mockRebalanceHybridBase,
1262
- nonce: 4n,
1263
- limits: scenario4Limits,
1264
- priceControl: PriceControl.PARTIAL,
1265
- }
1266
- const [openAuctionArgs] = getOpenAuction(
1267
- mockRebalanceHybrid4,
1268
- supply,
1269
- _folioForS4,
1270
- targetBasketHybrid,
1271
- _folioForS4,
1272
- decimals,
1273
- currentMarketPrices_Hybrid,
1274
- auctionPriceErrorSmall,
1275
- finalStageAtForTest
1276
- )
1277
-
1278
- assertRebalanceLimitsEqual(openAuctionArgs.newLimits, scenario4Limits)
1279
- assertPricesEqual(openAuctionArgs.newPrices[0], defaultPricesHybridScope[0])
1280
- assertPricesEqual(openAuctionArgs.newPrices[1], defaultPricesHybridScope[1])
1281
- assertPricesEqual(openAuctionArgs.newPrices[2], defaultPricesHybridScope[2])
1282
-
1283
- const spotDAI_D27 = hybridWeights[1].spot // bn('5e26')
1284
- const spotUSDT_D27 = hybridWeights[2].spot // bn('5e14')
1285
-
1286
- assert.equal(openAuctionArgs.newWeights.length, 3)
1287
- assertRangesEqual(openAuctionArgs.newWeights[0], hybridWeights[0])
1288
- assertRangesEqual(openAuctionArgs.newWeights[1], {
1289
- low: bn('4.847e26'),
1290
- spot: spotDAI_D27,
1291
- high: bn('5.147e26'),
1292
- })
1293
- assertRangesEqual(openAuctionArgs.newWeights[2], {
1294
- low: bn('4.847e14'),
1295
- spot: spotUSDT_D27,
1296
- high: bn('5.147e14'),
1297
- })
1298
- })
1299
-
1300
- it('Hybrid Scenario 5: Already Balanced Folio (rebalanceTarget=1, delta=0)', () => {
1301
- const _folioAlreadyBalancedAndShareValue1 = [bn('0'), bn('5e17'), bn('5e5')]
1302
- const mockRebalanceHybrid5: Rebalance = {
1303
- ...mockRebalanceHybridBase,
1304
- nonce: 5n,
1305
- priceControl: PriceControl.PARTIAL,
1306
- }
1307
- const [openAuctionArgs] = getOpenAuction(
1308
- mockRebalanceHybrid5,
1309
- supply,
1310
- _folioAlreadyBalancedAndShareValue1,
1311
- targetBasketHybrid,
1312
- _folioAlreadyBalancedAndShareValue1,
1313
- decimals,
1314
- currentMarketPrices_Hybrid,
1315
- auctionPriceErrorSmall,
1316
- finalStageAtForTest
1317
- )
1318
-
1319
- assertRebalanceLimitsEqual(openAuctionArgs.newLimits, {
1320
- low: bn('1e18'),
1321
- spot: bn('1e18'),
1322
- high: bn('1e18'),
1323
- })
1324
-
1325
- assertPricesEqual(openAuctionArgs.newPrices[0], defaultPricesHybridScope[0])
1326
- assertPricesEqual(openAuctionArgs.newPrices[1], defaultPricesHybridScope[1])
1327
- assertPricesEqual(openAuctionArgs.newPrices[2], defaultPricesHybridScope[2])
1328
-
1329
- // For USDT (index 2), hybridWeights[2].spot is bn('5e14').
1330
- // As deduced, all components of openAuctionArgs.newWeights[2] should also be bn('5e14').
1331
- // Making the assertion explicit with hardcoded values to test exactness.
1332
- assert.equal(openAuctionArgs.newWeights.length, 3)
1333
- assertRangesEqual(openAuctionArgs.newWeights[0], hybridWeights[0]) // USDC {0,0,0}
1334
- assertRangesEqual(openAuctionArgs.newWeights[1], {
1335
- low: hybridWeights[1].spot,
1336
- spot: hybridWeights[1].spot,
1337
- high: hybridWeights[1].spot,
1338
- })
1339
- assertRangesEqual(openAuctionArgs.newWeights[2], {
1340
- low: bn('5e14'),
1341
- spot: bn('5e14'),
1342
- high: bn('5e14'),
1343
- })
1344
- })
1345
- })
1346
-
1347
- describe('Price Edge Cases in getOpenAuction', () => {
1348
- const supply = bn('1e21')
1349
- const tokens = ['USDC', 'DAI']
1350
- const decimals = [bn('6'), bn('18')]
1351
- const auctionPriceErrorSmall = [0.01, 0.01]
1352
- const targetBasketSimple = [bn('5e17'), bn('5e17')] // 50% USDC, 50% DAI
1353
- const folioSimple = [bn('5e5'), bn('5e17')] // Example folio, value $1
1354
-
1355
- it('should throw "spot price out of bounds!" when market price is outside initial price bounds', () => {
1356
- // Set up narrow initial price bounds that are below the market price
1357
- const initialPricesNarrowUSDC: PriceRange[] = [
1358
- { low: bn('8e20'), high: bn('8.5e20') }, // USDC: Range 0.8 - 0.85 USD
1359
- { low: bn('9e8'), high: bn('1.11111e9') }, // DAI: Normal range
1360
- ]
1361
-
1362
- const mockRebalanceEdge: Rebalance = {
1363
- nonce: 4n,
1364
- tokens: tokens,
1365
- weights: [
1366
- { low: bn('4.5e14'), spot: bn('5e14'), high: bn('5.5e14') },
1367
- { low: bn('4.5e26'), spot: bn('5e26'), high: bn('5.5e26') },
1368
- ],
1369
- initialPrices: initialPricesNarrowUSDC,
1370
- inRebalance: tokens.map(() => true),
1371
- limits: { low: bn('1'), spot: bn('1e18'), high: bn('1e36') }, // Wide limits
1372
- startedAt: 0n,
1373
- restrictedUntil: 0n,
1374
- availableUntil: 0n,
1375
- priceControl: PriceControl.PARTIAL,
1376
- }
1377
-
1378
- // Market price is $1.0, but initial bounds are only 0.8-0.85
1379
- // spotPrice = 1.0 * 1e27 / 1e6 = 1e21
1380
- // initialPrice.high = 8.5e20
1381
- // Since 1e21 > 8.5e20, this triggers "spot price out of bounds!"
1382
- const currentMarketPrices = [1.0, 1.0] // USDC at $1.0 is above the 0.8-0.85 range
1383
-
1384
- assert.throws(() => {
1385
- getOpenAuction(
1386
- mockRebalanceEdge,
1387
- supply,
1388
- folioSimple, // _initialFolio
1389
- targetBasketSimple,
1390
- folioSimple, // current _folio
1391
- decimals,
1392
- currentMarketPrices,
1393
- auctionPriceErrorSmall,
1394
- 0.95
1395
- )
1396
- }, {
1397
- message: 'spot price out of bounds! auction launcher MUST closeRebalance to prevent loss!'
1398
- })
1399
- })
1400
-
1401
- it('should throw "no price range" when price clamping results in identical low and high prices', () => {
1402
- // Set up initial price bounds where low == high (degenerate range)
1403
- const initialPricesSameValue: PriceRange[] = [
1404
- { low: bn('1e21'), high: bn('1e21') }, // USDC: Exactly $1.0 (no range)
1405
- { low: bn('9e8'), high: bn('1.11111e9') }, // DAI: Normal range
1406
- ]
1407
-
1408
- const mockRebalanceEdge: Rebalance = {
1409
- nonce: 5n,
1410
- tokens: tokens,
1411
- weights: [
1412
- { low: bn('4.5e14'), spot: bn('5e14'), high: bn('5.5e14') },
1413
- { low: bn('4.5e26'), spot: bn('5e26'), high: bn('5.5e26') },
1414
- ],
1415
- initialPrices: initialPricesSameValue,
1416
- inRebalance: tokens.map(() => true),
1417
- limits: { low: bn('1'), spot: bn('1e18'), high: bn('1e36') }, // Wide limits
1418
- startedAt: 0n,
1419
- restrictedUntil: 0n,
1420
- availableUntil: 0n,
1421
- priceControl: PriceControl.PARTIAL,
1422
- }
1423
-
1424
- // Market price is $1.0, which matches the initial price bounds exactly
1425
- // spotPrice = 1.0 * 1e27 / 1e6 = 1e21 == initialPrice.low == initialPrice.high ✓
1426
- // But when we calculate pricesD27 with price error:
1427
- // pricesD27.low = 1.0 * 0.99 * 1e27 / 1e6 = 9.9e20, gets clamped to 1e21
1428
- // pricesD27.high = 1.0 / 0.99 * 1e27 / 1e6 ≈ 1.0101e21, gets clamped to 1e21
1429
- // Result: pricesD27.low == pricesD27.high == 1e21, triggering "no price range"
1430
- const currentMarketPrices = [1.0, 1.0]
1431
-
1432
- assert.throws(() => {
1433
- getOpenAuction(
1434
- mockRebalanceEdge,
1435
- supply,
1436
- folioSimple, // _initialFolio
1437
- targetBasketSimple,
1438
- folioSimple, // current _folio
1439
- decimals,
1440
- currentMarketPrices,
1441
- auctionPriceErrorSmall,
1442
- 0.95
1443
- )
1444
- }, {
1445
- message: 'no price range'
1446
- })
1447
- })
1448
- })