@drift-labs/sdk
Version:
SDK for Drift Protocol
322 lines (281 loc) • 10.9 kB
text/typescript
import {
BN,
BASE_PRECISION,
QUOTE_PRECISION,
SPOT_MARKET_BALANCE_PRECISION,
PositionFlag,
SpotBalanceType,
ZERO,
} from '../../src';
import { assert } from '../../src/assert/assert';
import { mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers';
import {
mockUserAccount,
makeMockUser as makeMockUserFromHelpers,
} from './helpers';
import * as _ from 'lodash';
const DEFAULT_ORACLES = [1, 1, 1, 1, 1, 1, 1, 1];
describe('User margin calculations', () => {
describe('cross collateral', () => {
it('getTotalCollateral for USDC-only account matches expected value', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(10_000).mul(
SPOT_MARKET_BALANCE_PRECISION
);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
DEFAULT_ORACLES,
DEFAULT_ORACLES
);
const expectedValue = new BN(10_000).mul(QUOTE_PRECISION);
assert(mockUser.getTotalCollateral('Initial').eq(expectedValue));
assert(mockUser.getTotalCollateral('Maintenance').eq(expectedValue));
});
it('getInitialMarginRequirement is greater than or equal to maintenance', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(200).mul(
SPOT_MARKET_BALANCE_PRECISION
);
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = BASE_PRECISION;
myMockUserAccount.perpPositions[0].quoteAssetAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteEntryAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount =
QUOTE_PRECISION.neg();
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
DEFAULT_ORACLES,
DEFAULT_ORACLES
);
const initialMarginRequirement = mockUser.getInitialMarginRequirement();
const maintenanceMarginRequirement =
mockUser.getMaintenanceMarginRequirement();
assert(initialMarginRequirement.gte(maintenanceMarginRequirement));
assert(maintenanceMarginRequirement.gt(ZERO));
});
it('getFreeCollateral is positive for solvent cross account', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(10_000).mul(
SPOT_MARKET_BALANCE_PRECISION
);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
DEFAULT_ORACLES,
DEFAULT_ORACLES
);
assert(mockUser.getFreeCollateral('Initial').gt(ZERO));
});
it('getFreeCollateral is zero for undercollateralized account', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul(
BASE_PRECISION
);
myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(10)
.mul(QUOTE_PRECISION)
.neg();
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
DEFAULT_ORACLES,
DEFAULT_ORACLES
);
assert(mockUser.getFreeCollateral('Initial').eq(ZERO));
});
});
describe('isolated collateral', () => {
it('cross total collateral excludes isolated deposits', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(200).mul(
SPOT_MARKET_BALANCE_PRECISION
);
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].positionFlag =
PositionFlag.IsolatedPosition;
myMockUserAccount.perpPositions[0].baseAssetAmount = BASE_PRECISION;
myMockUserAccount.perpPositions[0].quoteAssetAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteEntryAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
DEFAULT_ORACLES,
DEFAULT_ORACLES
);
const crossTotalCollateral = mockUser.getTotalCollateral('Initial');
assert(crossTotalCollateral.eq(new BN(200).mul(QUOTE_PRECISION)));
});
it('getTotalCollateral with perpMarketIndex returns isolated collateral bucket', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].positionFlag =
PositionFlag.IsolatedPosition;
myMockUserAccount.perpPositions[0].baseAssetAmount = BASE_PRECISION;
myMockUserAccount.perpPositions[0].quoteAssetAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteEntryAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
DEFAULT_ORACLES,
DEFAULT_ORACLES
);
const isolatedCollateral = mockUser.getTotalCollateral(
'Maintenance',
false,
true,
undefined,
0
);
assert(isolatedCollateral.eq(new BN(100).mul(QUOTE_PRECISION)));
});
it('getFreeCollateral with perpMarketIndex reads isolated free collateral', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].positionFlag =
PositionFlag.IsolatedPosition;
myMockUserAccount.perpPositions[0].baseAssetAmount = BASE_PRECISION;
myMockUserAccount.perpPositions[0].quoteAssetAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteEntryAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount =
QUOTE_PRECISION.neg();
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
100
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
DEFAULT_ORACLES,
DEFAULT_ORACLES
);
assert(mockUser.getFreeCollateral('Initial', 0).gt(ZERO));
assert(mockUser.getFreeCollateral('Initial', 5).eq(ZERO));
});
});
describe('liquidationPrice', () => {
it('cross liquidationPrice returns a valid value for active position', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockPerpMarkets[0].amm.orderStepSize = BASE_PRECISION;
myMockUserAccount.spotPositions[0].marketIndex = 0;
myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT;
myMockUserAccount.spotPositions[0].scaledBalance = new BN(2).mul(
SPOT_MARKET_BALANCE_PRECISION
);
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(10).mul(
BASE_PRECISION
);
myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(10)
.mul(QUOTE_PRECISION)
.neg();
myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(10)
.mul(QUOTE_PRECISION)
.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(10)
.mul(QUOTE_PRECISION)
.neg();
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
DEFAULT_ORACLES,
DEFAULT_ORACLES
);
const liqPrice = mockUser.liquidationPrice(0);
assert(liqPrice.gte(ZERO));
assert(!liqPrice.eq(new BN(-1)));
});
it('isolated liquidationPrice returns a valid value for isolated position', async () => {
const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);
const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets);
const myMockUserAccount = _.cloneDeep(mockUserAccount);
myMockPerpMarkets[0].amm.orderStepSize = BASE_PRECISION;
myMockUserAccount.perpPositions[0].marketIndex = 0;
myMockUserAccount.perpPositions[0].positionFlag =
PositionFlag.IsolatedPosition;
myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(10).mul(
BASE_PRECISION
);
myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(10)
.mul(QUOTE_PRECISION)
.neg();
myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(10)
.mul(QUOTE_PRECISION)
.neg();
myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(10)
.mul(QUOTE_PRECISION)
.neg();
myMockUserAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(
2
).mul(SPOT_MARKET_BALANCE_PRECISION);
const mockUser = await makeMockUserFromHelpers(
myMockPerpMarkets,
myMockSpotMarkets,
myMockUserAccount,
DEFAULT_ORACLES,
DEFAULT_ORACLES
);
const liqPrice = mockUser.liquidationPrice(
0,
ZERO,
ZERO,
'Maintenance',
false,
ZERO,
'Isolated'
);
assert(liqPrice.gte(ZERO));
assert(!liqPrice.eq(new BN(-1)));
});
});
});