@drift-labs/sdk 2.149.0-beta.0 → 2.149.0-beta.2

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.
@@ -45,6 +45,8 @@ export const mockPerpPosition: PerpPosition = {
45
45
  lastQuoteAssetAmountPerLp: new BN(0),
46
46
  perLpBase: 0,
47
47
  maxMarginRatio: 1,
48
+ isolatedPositionScaledBalance: new BN(0),
49
+ positionFlag: 0,
48
50
  };
49
51
 
50
52
  export const mockAMM: AMM = {
@@ -152,6 +154,8 @@ export const mockAMM: AMM = {
152
154
  mmOraclePrice: new BN(0),
153
155
  mmOracleSlot: new BN(0),
154
156
  lastFundingOracleTwap: new BN(0),
157
+ oracleSlotDelayOverride: 0,
158
+ referencePriceOffsetDeadbandPct: 0,
155
159
  };
156
160
 
157
161
  export const mockPerpMarkets: Array<PerpMarketAccount> = [
@@ -201,6 +205,11 @@ export const mockPerpMarkets: Array<PerpMarketAccount> = [
201
205
  fuelBoostTaker: 0,
202
206
  protectedMakerLimitPriceDivisor: 0,
203
207
  protectedMakerDynamicDivisor: 0,
208
+ lpPoolId: 0,
209
+ lpFeeTransferScalar: 0,
210
+ lpExchangeFeeExcluscionScalar: 0,
211
+ lpStatus: 0,
212
+ lpPausedOperations: 0,
204
213
  },
205
214
  {
206
215
  status: MarketStatus.INITIALIZED,
@@ -248,6 +257,11 @@ export const mockPerpMarkets: Array<PerpMarketAccount> = [
248
257
  fuelBoostTaker: 0,
249
258
  protectedMakerLimitPriceDivisor: 0,
250
259
  protectedMakerDynamicDivisor: 0,
260
+ lpPoolId: 0,
261
+ lpFeeTransferScalar: 0,
262
+ lpExchangeFeeExcluscionScalar: 0,
263
+ lpStatus: 0,
264
+ lpPausedOperations: 0,
251
265
  },
252
266
  {
253
267
  status: MarketStatus.INITIALIZED,
@@ -295,6 +309,11 @@ export const mockPerpMarkets: Array<PerpMarketAccount> = [
295
309
  fuelBoostTaker: 0,
296
310
  protectedMakerLimitPriceDivisor: 0,
297
311
  protectedMakerDynamicDivisor: 0,
312
+ lpPoolId: 0,
313
+ lpFeeTransferScalar: 0,
314
+ lpExchangeFeeExcluscionScalar: 0,
315
+ lpStatus: 0,
316
+ lpPausedOperations: 0,
298
317
  },
299
318
  ];
300
319
 
@@ -0,0 +1,361 @@
1
+ import {
2
+ BN,
3
+ ZERO,
4
+ User,
5
+ PublicKey,
6
+ BASE_PRECISION,
7
+ QUOTE_PRECISION,
8
+ SPOT_MARKET_BALANCE_PRECISION,
9
+ SpotBalanceType,
10
+ OPEN_ORDER_MARGIN_REQUIREMENT,
11
+ SPOT_MARKET_WEIGHT_PRECISION,
12
+ PositionFlag,
13
+ } from '../../src';
14
+ import { mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers';
15
+ import { assert } from '../../src/assert/assert';
16
+ import {
17
+ mockUserAccount as baseMockUserAccount,
18
+ makeMockUser,
19
+ } from './helpers';
20
+ import * as _ from 'lodash';
21
+
22
+ describe('getMarginCalculation snapshot', () => {
23
+ it('empty account returns zeroed snapshot', async () => {
24
+ const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
25
+ const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
26
+ const myMockUserAccount = _.cloneDeep(baseMockUserAccount);
27
+
28
+ const user: User = await makeMockUser(
29
+ myMockPerpMarkets,
30
+ myMockSpotMarkets,
31
+ myMockUserAccount,
32
+ [1, 1, 1, 1, 1, 1, 1, 1],
33
+ [1, 1, 1, 1, 1, 1, 1, 1]
34
+ );
35
+
36
+ const calc = user.getMarginCalculation('Initial');
37
+ assert(calc.totalCollateral.eq(ZERO));
38
+ assert(calc.marginRequirement.eq(ZERO));
39
+ });
40
+
41
+ it('quote deposit increases totalCollateral, no requirement', async () => {
42
+ const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
43
+ const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
44
+ const myMockUserAccount = _.cloneDeep(baseMockUserAccount);
45
+
46
+ myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
47
+ myMockUserAccount.spotPositions[0].scaledBalance = new BN(
48
+ 10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber()
49
+ );
50
+
51
+ const user: User = await makeMockUser(
52
+ myMockPerpMarkets,
53
+ myMockSpotMarkets,
54
+ myMockUserAccount,
55
+ [1, 1, 1, 1, 1, 1, 1, 1],
56
+ [1, 1, 1, 1, 1, 1, 1, 1]
57
+ );
58
+
59
+ const calc = user.getMarginCalculation('Initial');
60
+ const expected = new BN('10000000000'); // $10k
61
+ assert(calc.totalCollateral.eq(expected));
62
+ assert(calc.marginRequirement.eq(ZERO));
63
+ });
64
+
65
+ it('quote borrow increases requirement and buffer applies', async () => {
66
+ const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
67
+ const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
68
+ const myMockUserAccount = _.cloneDeep(baseMockUserAccount);
69
+
70
+ // Borrow 100 quote
71
+ myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.BORROW;
72
+ myMockUserAccount.spotPositions[0].scaledBalance = new BN(
73
+ 100 * SPOT_MARKET_BALANCE_PRECISION.toNumber()
74
+ );
75
+
76
+ const user: User = await makeMockUser(
77
+ myMockPerpMarkets,
78
+ myMockSpotMarkets,
79
+ myMockUserAccount,
80
+ [1, 1, 1, 1, 1, 1, 1, 1],
81
+ [1, 1, 1, 1, 1, 1, 1, 1]
82
+ );
83
+
84
+ const tenPercent = new BN(1000);
85
+ const calc = user.getMarginCalculation('Initial', {
86
+ liquidationBufferMap: new Map([['cross', tenPercent]]),
87
+ });
88
+ const liability = new BN(110).mul(QUOTE_PRECISION); // $110
89
+ assert(calc.totalCollateral.eq(ZERO));
90
+ assert(
91
+ calc.marginRequirement.eq(liability),
92
+ `margin requirement does not equal liability: ${calc.marginRequirement.toString()} != ${liability.toString()}`
93
+ );
94
+ assert(
95
+ calc.marginRequirementPlusBuffer.eq(
96
+ liability.div(new BN(10)).add(calc.marginRequirement) // 10% of liability + margin requirement
97
+ ),
98
+ `margin requirement plus buffer does not equal 10% of liability + margin requirement: ${calc.marginRequirementPlusBuffer.toString()} != ${liability
99
+ .div(new BN(10))
100
+ .add(calc.marginRequirement)
101
+ .toString()}`
102
+ );
103
+ });
104
+
105
+ it('non-quote spot open orders add IM', async () => {
106
+ const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
107
+ const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
108
+ const myMockUserAccount = _.cloneDeep(baseMockUserAccount);
109
+
110
+ // Market 1 (e.g., SOL) with 2 open orders
111
+ myMockUserAccount.spotPositions[1].marketIndex = 1;
112
+ myMockUserAccount.spotPositions[1].openOrders = 2;
113
+
114
+ const user: User = await makeMockUser(
115
+ myMockPerpMarkets,
116
+ myMockSpotMarkets,
117
+ myMockUserAccount,
118
+ [1, 1, 1, 1, 1, 1, 1, 1],
119
+ [1, 1, 1, 1, 1, 1, 1, 1]
120
+ );
121
+
122
+ const calc = user.getMarginCalculation('Initial');
123
+ const expectedIM = new BN(2).mul(OPEN_ORDER_MARGIN_REQUIREMENT);
124
+ assert(calc.marginRequirement.eq(expectedIM));
125
+ });
126
+
127
+ it('perp long liability reflects maintenance requirement', async () => {
128
+ const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
129
+ const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
130
+ const myMockUserAccount = _.cloneDeep(baseMockUserAccount);
131
+
132
+ // 20 base long, -$10 quote (liability)
133
+ myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul(
134
+ BASE_PRECISION
135
+ );
136
+
137
+ const user: User = await makeMockUser(
138
+ myMockPerpMarkets,
139
+ myMockSpotMarkets,
140
+ myMockUserAccount,
141
+ [1, 1, 1, 1, 1, 1, 1, 1],
142
+ [1, 1, 1, 1, 1, 1, 1, 1]
143
+ );
144
+
145
+ const calc = user.getMarginCalculation('Maintenance');
146
+ // From existing liquidation test expectations: 2_000_000
147
+ assert(calc.marginRequirement.eq(new BN('2000000')));
148
+ });
149
+
150
+ it('collateral equals maintenance requirement', async () => {
151
+ const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
152
+ const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
153
+ const myMockUserAccount = _.cloneDeep(baseMockUserAccount);
154
+
155
+ myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(200000000).mul(
156
+ BASE_PRECISION
157
+ );
158
+
159
+ myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
160
+ myMockUserAccount.spotPositions[0].scaledBalance = new BN(20000000).mul(
161
+ SPOT_MARKET_BALANCE_PRECISION
162
+ );
163
+
164
+ const user: User = await makeMockUser(
165
+ myMockPerpMarkets,
166
+ myMockSpotMarkets,
167
+ myMockUserAccount,
168
+ [1, 1, 1, 1, 1, 1, 1, 1],
169
+ [1, 1, 1, 1, 1, 1, 1, 1]
170
+ );
171
+
172
+ const calc = user.getMarginCalculation('Maintenance');
173
+ assert(
174
+ calc.marginRequirement.eq(calc.totalCollateral),
175
+ `margin requirement does not equal total collateral: ${calc.marginRequirement.toString()} != ${calc.totalCollateral.toString()}`
176
+ );
177
+ });
178
+
179
+ it('maker reducing after simulated fill: collateral equals maintenance requirement', async () => {
180
+ const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
181
+ const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
182
+
183
+ // Build maker and taker accounts
184
+ const makerAccount = _.cloneDeep(baseMockUserAccount);
185
+ const takerAccount = _.cloneDeep(baseMockUserAccount);
186
+
187
+ // Oracle price = 1 for perp and spot
188
+ const perpOracles = [1, 1, 1, 1, 1, 1, 1, 1];
189
+ const spotOracles = [1, 1, 1, 1, 1, 1, 1, 1];
190
+
191
+ // Pre-fill: maker has 21 base long at entry 1 ($21 notional), taker flat
192
+ makerAccount.perpPositions[0].baseAssetAmount = new BN(21).mul(
193
+ BASE_PRECISION
194
+ );
195
+ makerAccount.perpPositions[0].quoteEntryAmount = new BN(-21).mul(
196
+ QUOTE_PRECISION
197
+ );
198
+ makerAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-21).mul(
199
+ QUOTE_PRECISION
200
+ );
201
+ // Provide exactly $2 in quote collateral to equal 10% maintenance of 20 notional post-fill
202
+ makerAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
203
+ makerAccount.spotPositions[0].scaledBalance = new BN(2).mul(
204
+ SPOT_MARKET_BALANCE_PRECISION
205
+ );
206
+
207
+ // Simulate fill: maker sells 1 base to taker at price = oracle = 1
208
+ // Post-fill maker position: 20 base long with zero unrealized PnL
209
+ const maker: User = await makeMockUser(
210
+ myMockPerpMarkets,
211
+ myMockSpotMarkets,
212
+ makerAccount,
213
+ perpOracles,
214
+ spotOracles
215
+ );
216
+ const taker: User = await makeMockUser(
217
+ myMockPerpMarkets,
218
+ myMockSpotMarkets,
219
+ takerAccount,
220
+ perpOracles,
221
+ spotOracles
222
+ );
223
+
224
+ // Apply synthetic trade deltas to both user accounts
225
+ // Maker: base 21 -> 20; taker: base 0 -> 1. Use quote deltas consistent with price 1, fee 0
226
+ maker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(20).mul(
227
+ BASE_PRECISION
228
+ );
229
+ maker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-20).mul(
230
+ QUOTE_PRECISION
231
+ );
232
+ maker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(
233
+ -20
234
+ ).mul(QUOTE_PRECISION);
235
+ // Align quoteAssetAmount with base value so unrealized PnL = 0 at price 1
236
+ maker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-20).mul(
237
+ QUOTE_PRECISION
238
+ );
239
+
240
+ taker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(1).mul(
241
+ BASE_PRECISION
242
+ );
243
+ taker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-1).mul(
244
+ QUOTE_PRECISION
245
+ );
246
+ taker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(
247
+ -1
248
+ ).mul(QUOTE_PRECISION);
249
+ // Also set taker's quoteAssetAmount consistently
250
+ taker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-1).mul(
251
+ QUOTE_PRECISION
252
+ );
253
+
254
+ const makerCalc = maker.getMarginCalculation('Maintenance');
255
+ assert(makerCalc.marginRequirement.eq(makerCalc.totalCollateral));
256
+ assert(makerCalc.marginRequirement.gt(ZERO));
257
+ });
258
+
259
+ it('isolated position margin requirement (SDK parity)', async () => {
260
+ const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
261
+ const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
262
+ myMockSpotMarkets[0].oracle = new PublicKey(2);
263
+ myMockSpotMarkets[1].oracle = new PublicKey(5);
264
+ myMockPerpMarkets[0].amm.oracle = new PublicKey(5);
265
+
266
+ // Configure perp market 0 ratios to match on-chain test
267
+ myMockPerpMarkets[0].marginRatioInitial = 1000; // 10%
268
+ myMockPerpMarkets[0].marginRatioMaintenance = 500; // 5%
269
+
270
+ // Configure spot market 1 (e.g., SOL) weights to match on-chain test
271
+ myMockSpotMarkets[1].initialAssetWeight =
272
+ (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 8) / 10; // 0.8
273
+ myMockSpotMarkets[1].maintenanceAssetWeight =
274
+ (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 9) / 10; // 0.9
275
+ myMockSpotMarkets[1].initialLiabilityWeight =
276
+ (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 12) / 10; // 1.2
277
+ myMockSpotMarkets[1].maintenanceLiabilityWeight =
278
+ (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 11) / 10; // 1.1
279
+
280
+ // ---------- Cross margin only (spot positions) ----------
281
+ const crossAccount = _.cloneDeep(baseMockUserAccount);
282
+ // USDC deposit: $20,000
283
+ crossAccount.spotPositions[0].marketIndex = 0;
284
+ crossAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
285
+ crossAccount.spotPositions[0].scaledBalance = new BN(20000).mul(
286
+ SPOT_MARKET_BALANCE_PRECISION
287
+ );
288
+ // SOL borrow: 100 units
289
+ crossAccount.spotPositions[1].marketIndex = 1;
290
+ crossAccount.spotPositions[1].balanceType = SpotBalanceType.BORROW;
291
+ crossAccount.spotPositions[1].scaledBalance = new BN(100).mul(
292
+ SPOT_MARKET_BALANCE_PRECISION
293
+ );
294
+ // No perp exposure in cross calc
295
+ crossAccount.perpPositions[0].baseAssetAmount = new BN(
296
+ 100 * BASE_PRECISION.toNumber()
297
+ );
298
+ crossAccount.perpPositions[0].quoteAssetAmount = new BN(
299
+ -11000 * QUOTE_PRECISION.toNumber()
300
+ );
301
+ crossAccount.perpPositions[0].positionFlag = PositionFlag.IsolatedPosition;
302
+ crossAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
303
+ 100
304
+ ).mul(SPOT_MARKET_BALANCE_PRECISION);
305
+
306
+ const userCross: User = await makeMockUser(
307
+ myMockPerpMarkets,
308
+ myMockSpotMarkets,
309
+ crossAccount,
310
+ [100, 1, 1, 1, 1, 1, 1, 1], // perp oracle for market 0 = 100
311
+ [1, 100, 1, 1, 1, 1, 1, 1] // spot oracle: usdc=1, sol=100
312
+ );
313
+
314
+ const crossCalc = userCross.getMarginCalculation('Initial');
315
+ const isolatedMarginCalc = crossCalc.isolatedMarginCalculations.get(0);
316
+ // Expect: cross MR from SOL borrow: 100 * $100 = $10,000 * 1.2 = $12,000
317
+ assert(crossCalc.marginRequirement.eq(new BN('12000000000')));
318
+ // Expect: cross total collateral from USDC deposit only = $20,000
319
+ assert(crossCalc.totalCollateral.eq(new BN('20000000000')));
320
+ // Meets cross margin requirement
321
+ assert(crossCalc.marginRequirement.lte(crossCalc.totalCollateral));
322
+
323
+ assert(isolatedMarginCalc?.marginRequirement.eq(new BN('1000000000')));
324
+ assert(isolatedMarginCalc?.totalCollateral.eq(new BN('-900000000')));
325
+ // With 10% buffer
326
+ const tenPct = new BN(1000);
327
+ const crossCalcBuf = userCross.getMarginCalculation('Initial', {
328
+ liquidationBufferMap: new Map<number | 'cross', BN>([
329
+ ['cross', tenPct],
330
+ [0, new BN(100)],
331
+ ]),
332
+ });
333
+ assert(
334
+ crossCalcBuf.marginRequirementPlusBuffer.eq(new BN('14300000000')),
335
+ `margin requirement plus buffer does not equal 110% of liability + margin requirement: ${crossCalcBuf.marginRequirementPlusBuffer.toString()} != ${new BN(
336
+ '14300000000'
337
+ ).toString()}`
338
+ ); // replicate 10% buffer
339
+ const crossTotalPlusBuffer = crossCalcBuf.totalCollateral.add(
340
+ crossCalcBuf.totalCollateralBuffer
341
+ );
342
+ assert(crossTotalPlusBuffer.eq(new BN('20000000000')));
343
+
344
+ const isoPositionBuf = crossCalcBuf.isolatedMarginCalculations.get(0);
345
+ assert(
346
+ isoPositionBuf?.marginRequirementPlusBuffer.eq(new BN('1100000000')),
347
+ `margin requirement plus buffer does not equal 10% of liability + margin requirement: ${isoPositionBuf?.marginRequirementPlusBuffer.toString()} != ${new BN(
348
+ '1100000000'
349
+ ).toString()}`
350
+ );
351
+ assert(isoPositionBuf?.marginRequirement.eq(new BN('1000000000')));
352
+ assert(
353
+ isoPositionBuf?.totalCollateralBuffer
354
+ .add(isoPositionBuf?.totalCollateral)
355
+ .eq(new BN('-910000000')),
356
+ `total collateral buffer plus total collateral does not equal -$9100: ${isoPositionBuf?.totalCollateralBuffer
357
+ .add(isoPositionBuf?.totalCollateral)
358
+ .toString()} != ${new BN('-900000000').toString()}`
359
+ );
360
+ });
361
+ });
@@ -1,6 +1,13 @@
1
1
  import { PublicKey } from '@solana/web3.js';
2
2
 
3
3
  import {
4
+ BN,
5
+ User,
6
+ UserAccount,
7
+ PerpMarketAccount,
8
+ SpotMarketAccount,
9
+ PRICE_PRECISION,
10
+ OraclePriceData,
4
11
  SpotPosition,
5
12
  SpotBalanceType,
6
13
  Order,
@@ -9,12 +16,12 @@ import {
9
16
  OrderType,
10
17
  PositionDirection,
11
18
  OrderTriggerCondition,
12
- UserAccount,
13
19
  ZERO,
14
20
  MarginMode,
21
+ MMOraclePriceData,
15
22
  } from '../../src';
16
23
 
17
- import { mockPerpPosition } from '../dlob/helpers';
24
+ import { MockUserMap, mockPerpPosition } from '../dlob/helpers';
18
25
 
19
26
  export const mockOrder: Order = {
20
27
  status: OrderStatus.INIT,
@@ -92,3 +99,90 @@ export const mockUserAccount: UserAccount = {
92
99
  marginMode: MarginMode.DEFAULT,
93
100
  poolId: 0,
94
101
  };
102
+
103
+ export async function makeMockUser(
104
+ myMockPerpMarkets: Array<PerpMarketAccount>,
105
+ myMockSpotMarkets: Array<SpotMarketAccount>,
106
+ myMockUserAccount: UserAccount,
107
+ perpOraclePriceList: number[],
108
+ spotOraclePriceList: number[]
109
+ ): Promise<User> {
110
+ const umap = new MockUserMap();
111
+ const mockUser: User = await umap.mustGet('1');
112
+ mockUser._isSubscribed = true;
113
+ mockUser.driftClient._isSubscribed = true;
114
+ mockUser.driftClient.accountSubscriber.isSubscribed = true;
115
+ const getStateAccount = () =>
116
+ ({
117
+ data: {
118
+ liquidationMarginBufferRatio: 1000,
119
+ },
120
+ slot: 0,
121
+ }) as any;
122
+ mockUser.driftClient.getStateAccount = getStateAccount;
123
+
124
+ const oraclePriceMap: Record<string, number> = {};
125
+ for (let i = 0; i < myMockPerpMarkets.length; i++) {
126
+ oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] =
127
+ perpOraclePriceList[i] ?? 1;
128
+ }
129
+ for (let i = 0; i < myMockSpotMarkets.length; i++) {
130
+ oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] =
131
+ spotOraclePriceList[i] ?? 1;
132
+ }
133
+
134
+ function getMockUserAccount(): UserAccount {
135
+ return myMockUserAccount;
136
+ }
137
+ function getMockPerpMarket(marketIndex: number): PerpMarketAccount {
138
+ return myMockPerpMarkets[marketIndex];
139
+ }
140
+ function getMockSpotMarket(marketIndex: number): SpotMarketAccount {
141
+ return myMockSpotMarkets[marketIndex];
142
+ }
143
+ function getMockOracle(oracleKey: PublicKey) {
144
+ const data: OraclePriceData = {
145
+ price: new BN(
146
+ (oraclePriceMap[oracleKey.toString()] ?? 1) * PRICE_PRECISION.toNumber()
147
+ ),
148
+ slot: new BN(0),
149
+ confidence: new BN(1),
150
+ hasSufficientNumberOfDataPoints: true,
151
+ };
152
+ return { data, slot: 0 };
153
+ }
154
+ function getOracleDataForPerpMarket(marketIndex: number) {
155
+ const oracle = getMockPerpMarket(marketIndex).amm.oracle;
156
+ return getMockOracle(oracle).data;
157
+ }
158
+ function getOracleDataForSpotMarket(marketIndex: number) {
159
+ const oracle = getMockSpotMarket(marketIndex).oracle;
160
+ return getMockOracle(oracle).data;
161
+ }
162
+
163
+ function getMMOracleDataForPerpMarket(
164
+ marketIndex: number
165
+ ): MMOraclePriceData {
166
+ const oracle = getMockPerpMarket(marketIndex).amm.oracle;
167
+ return {
168
+ price: getMockOracle(oracle).data.price,
169
+ slot: getMockOracle(oracle).data.slot,
170
+ confidence: getMockOracle(oracle).data.confidence,
171
+ hasSufficientNumberOfDataPoints:
172
+ getMockOracle(oracle).data.hasSufficientNumberOfDataPoints,
173
+ isMMOracleActive: true,
174
+ };
175
+ }
176
+
177
+ mockUser.getUserAccount = getMockUserAccount;
178
+ mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any;
179
+ mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any;
180
+ mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any;
181
+ mockUser.driftClient.getOracleDataForPerpMarket =
182
+ getOracleDataForPerpMarket as any;
183
+ mockUser.driftClient.getOracleDataForSpotMarket =
184
+ getOracleDataForSpotMarket as any;
185
+ mockUser.driftClient.getMMOracleDataForPerpMarket =
186
+ getMMOracleDataForPerpMarket as any;
187
+ return mockUser;
188
+ }
@@ -0,0 +1,129 @@
1
+ import { assert } from 'chai';
2
+ import _ from 'lodash';
3
+ import { SpotBalanceType, PositionFlag } from '../../src/types';
4
+ import {
5
+ BASE_PRECISION,
6
+ QUOTE_PRECISION,
7
+ SPOT_MARKET_BALANCE_PRECISION,
8
+ } from '../../src/constants/numericConstants';
9
+ import { BN } from '../../src';
10
+ import { mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers';
11
+ import {
12
+ mockUserAccount as baseMockUserAccount,
13
+ makeMockUser,
14
+ } from './helpers';
15
+
16
+ // Helper for easy async test creation
17
+ async function makeUserWithAccount(
18
+ account,
19
+ perpOraclePrices: number[],
20
+ spotOraclePrices: number[]
21
+ ) {
22
+ const user = await makeMockUser(
23
+ _.cloneDeep(mockPerpMarkets),
24
+ _.cloneDeep(mockSpotMarkets),
25
+ account,
26
+ perpOraclePrices,
27
+ spotOraclePrices
28
+ );
29
+ return user;
30
+ }
31
+
32
+ describe('User.getLiquidationStatuses', () => {
33
+ it('isolated account: healthy, then becomes liquidatable on IM', async () => {
34
+ const isoAccount = _.cloneDeep(baseMockUserAccount);
35
+
36
+ // put full isolated perp position in the first market (marketIndex 0 = KEN)
37
+ isoAccount.perpPositions[0].baseAssetAmount = new BN(100).mul(
38
+ BASE_PRECISION
39
+ );
40
+ isoAccount.perpPositions[0].quoteAssetAmount = new BN(100).mul(
41
+ QUOTE_PRECISION
42
+ );
43
+ isoAccount.perpPositions[0].positionFlag = PositionFlag.IsolatedPosition;
44
+ isoAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
45
+ 1000
46
+ ).mul(SPOT_MARKET_BALANCE_PRECISION);
47
+
48
+ // enough deposit for margin
49
+ isoAccount.spotPositions[0].marketIndex = 0;
50
+ isoAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
51
+ isoAccount.spotPositions[0].scaledBalance = new BN(10000).mul(
52
+ SPOT_MARKET_BALANCE_PRECISION
53
+ );
54
+
55
+ const user = await makeUserWithAccount(
56
+ isoAccount,
57
+ [100, 1, 1, 1, 1, 1, 1, 1],
58
+ [1, 100, 1, 1, 1, 1, 1, 1]
59
+ );
60
+
61
+ let statuses = user.getLiquidationStatuses();
62
+ // Isolated position is not liquidatable
63
+ const cross1 = statuses.get('cross');
64
+ const iso0_1 = statuses.get(0);
65
+ assert.equal(iso0_1?.canBeLiquidated, false);
66
+ assert.equal(cross1?.canBeLiquidated, false);
67
+
68
+ // Lower spot deposit to make isolated margin not enough for IM (but still above MM)
69
+ isoAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(1).mul(
70
+ SPOT_MARKET_BALANCE_PRECISION
71
+ );
72
+
73
+ const underfundedUser = await makeUserWithAccount(
74
+ isoAccount,
75
+ [100, 1, 1, 1, 1, 1, 1, 1],
76
+ [1, 100, 1, 1, 1, 1, 1, 1]
77
+ );
78
+
79
+ statuses = underfundedUser.getLiquidationStatuses();
80
+ const cross2 = statuses.get('cross');
81
+ const iso0_2 = statuses.get(0);
82
+ assert.equal(iso0_2?.canBeLiquidated, true);
83
+ assert.equal(cross2?.canBeLiquidated, false);
84
+ });
85
+
86
+ it('isolated position becomes fully bankrupt (both margin requirements breached)', async () => {
87
+ const bankruptAccount = _.cloneDeep(baseMockUserAccount);
88
+
89
+ bankruptAccount.perpPositions[0].baseAssetAmount = new BN(100).mul(
90
+ BASE_PRECISION
91
+ );
92
+ bankruptAccount.perpPositions[0].quoteAssetAmount = new BN(-14000).mul(
93
+ QUOTE_PRECISION
94
+ );
95
+ bankruptAccount.perpPositions[0].positionFlag =
96
+ PositionFlag.IsolatedPosition;
97
+ bankruptAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
98
+ 100
99
+ ).mul(SPOT_MARKET_BALANCE_PRECISION);
100
+
101
+ bankruptAccount.spotPositions[0].marketIndex = 0;
102
+ bankruptAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
103
+ bankruptAccount.spotPositions[0].scaledBalance = new BN(1000).mul(
104
+ SPOT_MARKET_BALANCE_PRECISION
105
+ );
106
+
107
+ const user = await makeUserWithAccount(
108
+ bankruptAccount,
109
+ [100, 1, 1, 1, 1, 1, 1, 1],
110
+ [1, 100, 1, 1, 1, 1, 1, 1]
111
+ );
112
+
113
+ const statuses = user.getLiquidationStatuses();
114
+ const cross = statuses.get('cross');
115
+ const iso0 = statuses.get(0);
116
+ assert.equal(
117
+ iso0?.canBeLiquidated,
118
+ true,
119
+ 'isolated position 0 should be liquidatable'
120
+ );
121
+ // Breaches maintenance requirement if MR > total collateral
122
+ assert.ok(iso0 && iso0.marginRequirement.gt(iso0.totalCollateral));
123
+ assert.equal(
124
+ cross?.canBeLiquidated,
125
+ false,
126
+ 'cross margin should not be liquidatable'
127
+ );
128
+ });
129
+ });