@reserve-protocol/dtf-rebalance-lib 0.0.1
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/.github/workflows/publish.yml +15 -0
- package/LICENSE.md +55 -0
- package/README.md +3 -0
- package/package.json +37 -0
- package/src/index.ts +5 -0
- package/src/numbers.ts +17 -0
- package/src/open-auction.ts +495 -0
- package/src/start-rebalance.ts +150 -0
- package/src/types.ts +35 -0
- package/src/utils.ts +36 -0
- package/test/rebalancing.test.ts +1448 -0
- package/tsconfig.json +14 -0
- package/tsconfig.test.json +9 -0
@@ -0,0 +1,1448 @@
|
|
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
|
+
})
|