@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.
- package/VERSION +1 -1
- package/lib/browser/marginCalculation.d.ts +79 -0
- package/lib/browser/marginCalculation.js +196 -0
- package/lib/browser/math/margin.d.ts +1 -1
- package/lib/browser/math/margin.js +8 -2
- package/lib/browser/math/spotPosition.d.ts +1 -1
- package/lib/browser/math/spotPosition.js +3 -2
- package/lib/browser/types.d.ts +10 -0
- package/lib/browser/types.js +7 -1
- package/lib/browser/user.d.ts +57 -16
- package/lib/browser/user.js +415 -46
- package/lib/node/marginCalculation.d.ts +80 -0
- package/lib/node/marginCalculation.d.ts.map +1 -0
- package/lib/node/marginCalculation.js +196 -0
- package/lib/node/math/margin.d.ts +1 -1
- package/lib/node/math/margin.d.ts.map +1 -1
- package/lib/node/math/margin.js +8 -2
- package/lib/node/math/spotPosition.d.ts +1 -1
- package/lib/node/math/spotPosition.d.ts.map +1 -1
- package/lib/node/math/spotPosition.js +3 -2
- package/lib/node/types.d.ts +10 -0
- package/lib/node/types.d.ts.map +1 -1
- package/lib/node/types.js +7 -1
- package/lib/node/user.d.ts +57 -16
- package/lib/node/user.d.ts.map +1 -1
- package/lib/node/user.js +415 -46
- package/package.json +3 -2
- package/src/marginCalculation.ts +287 -0
- package/src/math/margin.ts +15 -2
- package/src/math/spotPosition.ts +6 -2
- package/src/types.ts +12 -0
- package/src/user.ts +738 -87
- package/tests/dlob/helpers.ts +19 -0
- package/tests/user/getMarginCalculation.ts +361 -0
- package/tests/user/helpers.ts +96 -2
- package/tests/user/liquidations.ts +129 -0
package/tests/dlob/helpers.ts
CHANGED
|
@@ -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
|
+
});
|
package/tests/user/helpers.ts
CHANGED
|
@@ -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
|
+
});
|